diff options
-rw-r--r-- | package-lock.json | 10 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/client/util/InteractionUtils.tsx | 65 | ||||
-rw-r--r-- | src/client/views/InkControls.tsx | 46 | ||||
-rw-r--r-- | src/client/views/InkStrokeProperties.ts | 116 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 50 |
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={() => { |