aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json10
-rw-r--r--package.json2
-rw-r--r--src/client/util/InteractionUtils.tsx65
-rw-r--r--src/client/views/InkControls.tsx46
-rw-r--r--src/client/views/InkStrokeProperties.ts116
-rw-r--r--src/client/views/InkingStroke.tsx50
6 files changed, 138 insertions, 151 deletions
diff --git a/package-lock.json b/package-lock.json
index 7810e3120..c1dd8506f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -616,6 +616,11 @@
"integrity": "sha1-TN2WtJKTs5MhIuS34pVD415rrlg=",
"dev": true
},
+ "@types/bezier-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@types/bezier-js/-/bezier-js-4.1.0.tgz",
+ "integrity": "sha512-ElU16s8E6Pr6magp8ihwH1O8pbUJASbMND/qgUc9RsLmP3lMLHiDMRXdjtaObwW5GPtOVYOsXDUIhTIluT+yaw=="
+ },
"@types/bluebird": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.32.tgz",
@@ -2628,6 +2633,11 @@
"resolved": "https://registry.npmjs.org/bezier-curve/-/bezier-curve-1.0.0.tgz",
"integrity": "sha1-o9+v6rEqlMRicw1QeYxSqEBdc3k="
},
+ "bezier-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-4.1.1.tgz",
+ "integrity": "sha512-oVOS6SSFFFlfnZdzC+lsfvhs/RRcbxJ47U04M4s5QIBaJmr3YWmTIL3qmrOK9uW+nUUcl9Jccmo/xpTrG+bBoQ=="
+ },
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
diff --git a/package.json b/package.json
index 6cbde9e1f..5d10c0d54 100644
--- a/package.json
+++ b/package.json
@@ -126,6 +126,7 @@
"@hig/theme-data": "^2.16.1",
"@material-ui/core": "^4.11.0",
"@react-three/fiber": "^6.0.16",
+ "@types/bezier-js": "^4.1.0",
"@types/cors": "^2.8.8",
"@types/d3-axis": "^2.0.0",
"@types/d3-color": "^2.0.1",
@@ -145,6 +146,7 @@
"babel-runtime": "^6.26.0",
"bcrypt-nodejs": "0.0.3",
"bezier-curve": "^1.0.0",
+ "bezier-js": "^4.1.1",
"bluebird": "^3.7.2",
"body-parser": "^1.18.3",
"bootstrap": "^4.5.0",
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 66afc849e..f748188d7 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,10 +1,8 @@
import React = require("react");
import * as beziercurve from 'bezier-curve';
import * as fitCurve from 'fit-curve';
-import "./InteractionUtils.scss";
import { Utils } from "../../Utils";
-import { CurrentUserUtils } from "./CurrentUserUtils";
-import { InkTool } from "../../fields/InkField";
+import "./InteractionUtils.scss";
export namespace InteractionUtils {
export const MOUSETYPE = "mouse";
@@ -93,70 +91,13 @@ export namespace InteractionUtils {
return myTouches;
}
- export function CreatePoints(points: { X: number, Y: number }[], left: number, top: number,
- color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
- dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) {
- let pts: { X: number; Y: number; }[] = [];
- if (shape) { //if any of the shape are true
- pts = makePolygon(shape, points);
- }
- else if ((points.length >= 5 && points[3].X === points[4].X) || (points.length === 4)) {
- for (var i = 0; i < points.length - 3; i += 4) {
- const array = [[points[i].X, points[i].Y], [points[i + 1].X, points[i + 1].Y], [points[i + 2].X, points[i + 2].Y], [points[i + 3].X, points[i + 3].Y]];
- for (var t = 0; t < 1; t += 0.01) {
- const point = beziercurve(t, array);
- pts.push({ X: point[0], Y: point[1] });
- }
- }
- }
- else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) {
- //pointer is up (first and last points are the same)
- const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]);
- newPoints.pop();
-
- const bezierCurves = fitCurve(newPoints, parseInt(bezier));
- for (const curve of bezierCurves) {
- for (var t = 0; t < 1; t += 0.01) {
- const point = beziercurve(t, curve);
- pts.push({ X: point[0], Y: point[1] });
- }
- }
- } else {
- pts = points.slice();
- // bcz: Ugh... this is ugly, but shapes apprently have an extra point added that is = (p[0].x,p[0].y+1) as some sort of flag. need to remove it here.
- if (pts.length > 2 && pts[pts.length - 2].X === pts[0].X && pts[pts.length - 2].Y === pts[0].Y) {
- pts.pop();
- }
- }
- if (isNaN(scalex)) {
- scalex = 1;
- }
- if (isNaN(scaley)) {
- scaley = 1;
- }
- return pts;
- }
-
-
-
export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean) {
let pts: { X: number; Y: number; }[] = [];
if (shape) { //if any of the shape are true
- pts = makePolygon(shape, points);
- }
- else if (((points.length >= 5 && points[3].X === points[4].X) || (points.length === 4)) && !bezier) {
- for (var i = 0; i < points.length - 3; i += 4) {
- const array = [[points[i].X, points[i].Y], [points[i + 1].X, points[i + 1].Y], [points[i + 2].X, points[i + 2].Y], [points[i + 3].X, points[i + 3].Y]];
- for (var t = 0; t < 1; t += 0.01) {
- const point = beziercurve(t, array);
- pts.push({ X: point[0], Y: point[1] });
- }
- }
- } else {
- pts = points.slice();
- }
+ const pts = shape ? makePolygon(shape, points) : points;
+
if (isNaN(scalex)) scalex = 1;
if (isNaN(scaley)) scaley = 1;
diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx
index 7e685288d..5fe0c0f8a 100644
--- a/src/client/views/InkControls.tsx
+++ b/src/client/views/InkControls.tsx
@@ -5,7 +5,7 @@ import { Doc } from "../../fields/Doc";
import { ControlPoint, InkData, PointData } from "../../fields/InkField";
import { listSpec } from "../../fields/Schema";
import { Cast } from "../../fields/Types";
-import { setupMoveUpEvents } from "../../Utils";
+import { setupMoveUpEvents, Utils } from "../../Utils";
import { Transform } from "../util/Transform";
import { UndoManager } from "../util/UndoManager";
import { Colors } from "./global/globalEnums";
@@ -15,17 +15,16 @@ export interface InkControlProps {
inkDoc: Doc;
inkCtrlPoints: InkData;
screenCtrlPoints: InkData;
- inkStrokeSamplePts: PointData[];
- screenStrokeSamplePoints: PointData[];
format: number[];
ScreenToLocalTransform: () => Transform;
+ nearestScreenPt: () => PointData | undefined;
}
@observer
export class InkControls extends React.Component<InkControlProps> {
+
@observable private _overControl = -1;
@observable private _overAddPoint = -1;
-
/**
* Handles the movement of a selected control point when the user clicks and drags.
* @param controlIndex The index of the currently selected control point.
@@ -57,6 +56,14 @@ export class InkControls extends React.Component<InkControlProps> {
}));
}
}
+ /**
+ * Updates whether a user has hovered over a particular control point or point that could be added
+ * on click.
+ */
+ @action onEnterControl = (i: number) => { this._overControl = i; };
+ @action onLeaveControl = () => { this._overControl = -1; };
+ @action onEnterAddPoint = (i: number) => { this._overAddPoint = i; };
+ @action onLeaveAddPoint = () => { this._overAddPoint = -1; };
/**
* Deletes the currently selected point.
@@ -79,19 +86,11 @@ export class InkControls extends React.Component<InkControlProps> {
}
}
- /**
- * Updates whether a user has hovered over a particular control point or point that could be added
- * on click.
- */
- @action onEnterControl = (i: number) => { this._overControl = i; };
- @action onLeaveControl = () => { this._overControl = -1; };
- @action onEnterAddPoint = (i: number) => { this._overAddPoint = i; };
- @action onLeaveAddPoint = () => { this._overAddPoint = -1; };
-
render() {
const formatInstance = InkStrokeProperties.Instance;
if (!formatInstance) return (null);
+
// Accessing the current ink's data and extracting all control points.
const scrData = this.props.screenCtrlPoints;
const sreenCtrlPoints: ControlPoint[] = [];
@@ -109,24 +108,23 @@ export class InkControls extends React.Component<InkControlProps> {
const [left, top, scaleX, scaleY, strokeWidth, screenSpaceLineWidth] = this.props.format;
const rectHdlSize = (i: number) => this._overControl === i ? screenSpaceLineWidth * 6 : screenSpaceLineWidth * 4;
+
+ const nearestScreenPt = this.props.nearestScreenPt();
return (<svg>
{/* should really have just one circle here that represents the neqraest point on the stroke to the users hover point.
This points should be passed as a prop from InkingStroke's UI which should set it in its onPointerOver method */}
- {this.props.screenStrokeSamplePoints.map((pts, i) =>
- <circle key={i}
- cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
- cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
+ {!nearestScreenPt ? (null) :
+ <circle key={"npt"}
+ cx={(nearestScreenPt.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
+ cy={(nearestScreenPt.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
r={screenSpaceLineWidth * 4}
- fill={this._overAddPoint === i ? "#00007777" : "transparent"}
- stroke={this._overAddPoint === i ? "#00007777" : "transparent"}
+ fill={"#00007777"}
+ stroke={"#00007777"}
strokeWidth={0}
- onPointerDown={() => formatInstance?.addPoints(this.props.inkStrokeSamplePts[i].X, this.props.inkStrokeSamplePts[i].Y, this.props.inkStrokeSamplePts, i, inkCtrlPts)}
- onMouseEnter={() => this.onEnterAddPoint(i)}
- onMouseLeave={this.onLeaveAddPoint}
- pointerEvents="all"
+ pointerEvents="none"
cursor="all-scroll"
/>
- )}
+ }
{sreenCtrlPoints.map((control, i) =>
<rect key={i}
x={(control.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2 - rectHdlSize(i) / 2}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 2073497b9..03946bb60 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,3 +1,4 @@
+import { Bezier } from "bezier-js";
import { action, computed, observable, reaction } from "mobx";
import { Doc, Field, Opt } from "../../fields/Doc";
import { Document } from "../../fields/documentSchemas";
@@ -70,67 +71,70 @@ export class InkStrokeProperties {
/**
* Adds a new control point to the ink instance when editing its format.
- * @param index The index of the new point.
+ * @param t T-Value of new control point
+ * @param i index of first control point of segment being split
* @param control The list of all control points of the ink.
*/
@undoBatch
@action
- addPoints = (x: number, y: number, points: InkData, index: number, controls: { X: number, Y: number }[]) => {
- this.applyFunction((doc: Doc, ink: InkData) => {
- const newControl = { X: x, Y: y };
- const newPoints: InkData = [];
- let [counter, start, end] = [0, 0, 0];
- for (let k = 0; k < points.length; k++) {
- if (end === 0) {
- controls.forEach((control) => {
- if (control.X === points[k].X && control.Y === points[k].Y) {
- if (k < index) {
- counter++;
- start = k;
- } else if (k > index) {
- end = k;
- }
- }
- });
- }
- }
- if (end === 0) end = points.length - 1;
- // Index of new control point with regards to the ink data.
- const newIndex = Math.floor(counter / 2) * 4 + 2;
- // Creating new ink data with the new control point and handle points inputted.
- for (let i = 0; i < ink.length; i++) {
- if (i === newIndex) {
- const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index + 1), points.slice(index, end), newControl);
- newPoints.push(handleA, newControl, newControl, handleB);
- // Adjusting the magnitude of the left handle line of the right neighboring control point.
- const [rightControl, rightHandle] = [points[end], ink[i]];
- const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle);
- rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y });
- } else if (i === newIndex - 1) {
- // Adjusting the magnitude of the right handle line of the left neighboring control point.
- const [leftControl, leftHandle] = [points[start], ink[i]];
- const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle);
- leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y });
- } else {
- ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y });
- }
+ addPoints = (t: number, i: number, controls: { X: number, Y: number }[]) => {
+ const array = [{ x: controls[i].X, y: controls[i].Y }, { x: controls[i + 1].X, y: controls[i + 1].Y }, { x: controls[i + 2].X, y: controls[i + 2].Y }, { x: controls[i + 3].X, y: controls[i + 3].Y }];
+ const newsegs = new Bezier(array).split(t);
+ // this.applyFunction((doc: Doc, ink: InkData) => {
+ // const newControl = { X: x, Y: y };
+ // const newPoints: InkData = [];
+ // let [counter, start, end] = [0, 0, 0];
+ // for (let k = 0; k < points.length; k++) {
+ // if (end === 0) {
+ // controls.forEach((control) => {
+ // if (control.X === points[k].X && control.Y === points[k].Y) {
+ // if (k < index) {
+ // counter++;
+ // start = k;
+ // } else if (k > index) {
+ // end = k;
+ // }
+ // }
+ // });
+ // }
+ // }
+ // if (end === 0) end = points.length - 1;
+ // // Index of new control point with regards to the ink data.
+ // const newIndex = Math.floor(counter / 2) * 4 + 2;
+ // // Creating new ink data with the new control point and handle points inputted.
+ // for (let i = 0; i < ink.length; i++) {
+ // if (i === newIndex) {
+ // const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index + 1), points.slice(index, end), newControl);
+ // newPoints.push(handleA, newControl, newControl, handleB);
+ // // Adjusting the magnitude of the left handle line of the right neighboring control point.
+ // const [rightControl, rightHandle] = [points[end], ink[i]];
+ // const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle);
+ // rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y });
+ // } else if (i === newIndex - 1) {
+ // // Adjusting the magnitude of the right handle line of the left neighboring control point.
+ // const [leftControl, leftHandle] = [points[start], ink[i]];
+ // const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle);
+ // leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y });
+ // } else {
+ // ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y });
+ // }
- }
- let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
- // Updating the indices of the control points whose handle tangency has been broken.
- if (brokenIndices) {
- brokenIndices = new List(brokenIndices.map((control) => {
- if (control >= newIndex) {
- return control + 4;
- } else {
- return control;
- }
- }));
- }
- doc.brokenInkIndices = brokenIndices;
- this._currentPoint = -1;
- return newPoints;
- });
+ // }
+ // let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
+ // // Updating the indices of the control points whose handle tangency has been broken.
+ // if (brokenIndices) {
+ // brokenIndices = new List(brokenIndices.map((control) => {
+ // if (control >= newIndex) {
+ // return control + 4;
+ // } else {
+ // return control;
+ // }
+ // }));
+ // }
+ // doc.brokenInkIndices = brokenIndices;
+ // this._currentPoint = -1;
+ // return newPoints;
+ // });
}
/**
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a518bf07b..efe2e5f66 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -16,6 +16,7 @@ import { ViewBoxBaseComponent } from "./DocComponent";
import { Colors } from "./global/globalEnums";
import { InkControls } from "./InkControls";
import { InkHandles } from "./InkHandles";
+import { Bezier } from "bezier-js";
import "./InkStroke.scss";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { FieldView, FieldViewProps } from "./nodes/FieldView";
@@ -82,6 +83,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
this._properties._controlButton = true;
InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1);
this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
+ } else {
+ this._nearestT && this._nearestSeg && InkStrokeProperties.Instance?.addPoints(this._nearestT, this._nearestSeg, this.inkScaledData().inkData);
}
}), this._properties?._controlButton, this._properties?._controlButton
);
@@ -129,6 +132,39 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
inkScaleY: inkWidth === inkStrokeWidth ? 1 : (this.props.PanelHeight() - inkStrokeWidth) / (inkHeight - inkStrokeWidth)
};
}
+ @observable _nearestT: number | undefined;
+ @observable _nearestSeg: number | undefined;
+ @observable _nearestScrPt: { X: number, Y: number } | undefined;
+ @observable _inkSamplePts: { X: number, Y: number }[] | undefined;
+ nearestScreenPt = () => this._nearestScrPt;
+
+ @action
+ onPointerOver = (e: React.PointerEvent) => {
+ const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ const screenOrigin = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ const screenInkWidth = this.props.ScreenToLocalTransform().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth)[0];
+ const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(point.X, point.Y)).map(p => ({ X: p[0], Y: p[1] }));
+ const screenTop = Math.min(...screenPts.map(p => p.Y)) - screenInkWidth / 2;
+ const screenLeft = Math.min(...screenPts.map(p => p.X)) - screenInkWidth / 2;
+ var nearest = Number.MAX_SAFE_INTEGER;
+ this._nearestT = -1;
+ this._nearestSeg = -1;
+ this._nearestScrPt = { X: 0, Y: 0 };
+ for (var i = 0; i < screenPts.length - 3; i += 4) {
+ const array = [{ x: screenPts[i].X, y: screenPts[i].Y }, { x: screenPts[i + 1].X, y: screenPts[i + 1].Y }, { x: screenPts[i + 2].X, y: screenPts[i + 2].Y }, { x: screenPts[i + 3].X, y: screenPts[i + 3].Y }];
+ const point = new Bezier(array).project({ x: e.clientX + screenLeft - screenOrigin[0], y: e.clientY + screenTop - screenOrigin[1] });
+ if (point.t) {
+ const dist = (point.x - screenLeft - e.clientX + screenOrigin[0]) * (point.x - screenLeft - e.clientX + screenOrigin[0]) +
+ (point.y - screenTop - e.clientY + screenOrigin[1]) * (point.y - screenTop - e.clientY + screenOrigin[1]);
+ if (dist < nearest) {
+ nearest = dist;
+ this._nearestT = point.t;
+ this._nearestSeg = i;
+ this._nearestScrPt = { X: point.x, Y: point.y };
+ }
+ }
+ }
+ }
componentUI = (boundsLeft: number, boundsTop: number) => {
const inkDoc = this.props.Document;
@@ -141,13 +177,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const screenLeft = Math.min(...screenPts.map(p => p.X)) - screenInkWidth[0] / 2;
const screenOrigin = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- const screenSpaceSamplePoints = InteractionUtils.CreatePoints(screenPts, screenLeft, screenTop, StrCast(inkDoc.strokeColor, "none"), screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
- StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
- StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", this.props.isSelected() && inkStrokeWidth <= 5, false);
- const inkSpaceSamplePoints = InteractionUtils.CreatePoints(inkData, inkLeft, inkTop, StrCast(inkDoc.strokeColor, "none"), inkStrokeWidth, screenSpaceCenterlineStrokeWidth,
- StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
- StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), 1, 1, "", "none", this.props.isSelected() && inkStrokeWidth <= 5, false);
-
return <div className="inkstroke-UI" style={{
left: screenOrigin[0],
top: screenOrigin[1],
@@ -163,8 +192,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
inkDoc={inkDoc}
inkCtrlPoints={inkData}
screenCtrlPoints={screenPts}
- inkStrokeSamplePts={inkSpaceSamplePoints}
- screenStrokeSamplePoints={screenSpaceSamplePoints}
+ nearestScreenPt={this.nearestScreenPt}
format={[screenLeft, screenTop, inkScaleX, inkScaleY, screenInkWidth[0], screenSpaceCenterlineStrokeWidth]}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
<InkHandles
@@ -205,6 +233,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
overflow: "visible",
}}
+ onPointerLeave={action(e =>
+ this._nearestScrPt = undefined
+ )}
+ onPointerMove={this.onPointerOver}
onPointerDown={this.onPointerDown}
onClick={this.onClick}
onContextMenu={() => {