diff options
| author | geireann <geireann.lindfield@gmail.com> | 2021-10-14 15:01:19 -0400 |
|---|---|---|
| committer | geireann <geireann.lindfield@gmail.com> | 2021-10-14 15:01:19 -0400 |
| commit | 5bbd1b35d2c3855eae8405e26deb0c6679cc7c26 (patch) | |
| tree | c9d999f36b078d7fd8f55a74c94ce495c9fa8d4e /src/client/views/InkStrokeProperties.ts | |
| parent | be4fd2492ad706f30af28f33133a4df0e8049e12 (diff) | |
| parent | ed68bbec549dedeb89bcb584151b097863b52d0d (diff) | |
Merge branch 'master' into schema-view-En-Hua
Diffstat (limited to 'src/client/views/InkStrokeProperties.ts')
| -rw-r--r-- | src/client/views/InkStrokeProperties.ts | 229 |
1 files changed, 116 insertions, 113 deletions
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index d527b2a05..ee30caa3d 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,13 +1,16 @@ -import { action, computed, observable } from "mobx"; -import { Doc, DocListCast, Field, Opt } from "../../fields/Doc"; +import { Bezier } from "bezier-js"; +import { action, computed, observable, reaction } from "mobx"; +import { Doc } from "../../fields/Doc"; import { Document } from "../../fields/documentSchemas"; -import { InkField, InkData, PointData, ControlPoint } from "../../fields/InkField"; +import { InkData, InkField, InkTool, PointData } from "../../fields/InkField"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; import { DocumentType } from "../documents/DocumentTypes"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; +import { InkingStroke } from "./InkingStroke"; export class InkStrokeProperties { static Instance: InkStrokeProperties | undefined; @@ -18,6 +21,8 @@ export class InkStrokeProperties { constructor() { InkStrokeProperties.Instance = this; + reaction(() => this._controlButton, button => button && (CurrentUserUtils.SelectedTool = InkTool.None)); + reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false)); } @computed get selectedInk() { @@ -25,11 +30,6 @@ export class InkStrokeProperties { return inks.length ? inks : undefined; } - getField(key: string) { - return this.selectedInk?.reduce((p, i) => - (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); - } - /** * Helper function that enables other functions to be applied to a particular ink instance. * @param func The inputted function. @@ -45,14 +45,14 @@ export class InkStrokeProperties { if (ink) { const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); - const ptsXscale = NumCast(doc._width) / (oldXrange.max - oldXrange.min); - const ptsYscale = NumCast(doc._height) / (oldYrange.max - oldYrange.min); + const ptsXscale = ((NumCast(doc._width) - NumCast(doc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1; + const ptsYscale = ((NumCast(doc._height) - NumCast(doc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1; const newPoints = func(doc, ink, ptsXscale, ptsYscale); if (newPoints) { const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X)); const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y)); - doc._width = (newXrange.max - newXrange.min) * ptsXscale; - doc._height = (newYrange.max - newYrange.min) * ptsYscale; + doc._width = (newXrange.max - newXrange.min) * ptsXscale + NumCast(doc.strokeWidth); + doc._height = (newYrange.max - newYrange.min) * ptsYscale + NumCast(doc.strokeWidth); doc.x = (oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale); doc.y = (oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale); Doc.GetProto(doc).data = new InkField(newPoints); @@ -67,66 +67,24 @@ 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 }[]) => { + addPoints = (t: number, i: 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 }); - } + const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]]; + const newsegs = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).split(t); + const splicepts = [...newsegs.left.points, ...newsegs.right.points]; + controls.splice(i, 4, ...splicepts.map(p => ({ X: p.x, Y: p.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; + doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec("number"), []).map(control => control > i ? control + 4 : control)); this._currentPoint = -1; - return newPoints; + + return controls; }); } @@ -140,8 +98,7 @@ export class InkStrokeProperties { const prevSize = end - start; const newSize = isLeft ? index - start : end - index; const handleVector = { X: control.X - handle.X, Y: control.Y - handle.Y }; - const scaledVector = { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) }; - return scaledVector; + return { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) }; } /** @@ -189,22 +146,16 @@ export class InkStrokeProperties { deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => { const newPoints: { X: number, Y: number }[] = []; const toRemove = Math.floor(((this._currentPoint + 2) / 4)); + const last = this._currentPoint === ink.length - 1; for (let i = 0; i < ink.length; i++) { if (Math.floor((i + 2) / 4) !== toRemove && (toRemove !== 0 || i > 3)) { newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } } + doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec("number"), []).map(control => control >= toRemove * 4 ? control - 4 : control)); + if (last) newPoints.splice(newPoints.length - 3, 2); this._currentPoint = -1; - if (newPoints.length < 4) return undefined; - if (newPoints.length === 4) { - const newerPoints: { X: number, Y: number }[] = []; - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - return newerPoints; - } - return newPoints; + return newPoints.length < 4 ? undefined : newPoints; }, true) /** @@ -218,11 +169,11 @@ export class InkStrokeProperties { const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); const centerPoint = { X: (oldXrange.min + oldXrange.max) / 2, Y: (oldYrange.min + oldYrange.max) / 2 }; - const newPoints: { X: number, Y: number }[] = []; - ink.map(i => ({ X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y })).forEach(i => { - const newX = Math.cos(angle) * i.X - Math.sin(angle) * i.Y; - const newY = Math.sin(angle) * i.X + Math.cos(angle) * i.Y; - newPoints.push({ X: newX + centerPoint.X, Y: newY + centerPoint.Y }); + const newPoints = ink.map(i => { + const pt = { X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y }; + const newX = Math.cos(angle) * pt.X - Math.sin(angle) * pt.Y * yScale / xScale; + const newY = Math.sin(angle) * pt.X * xScale / yScale + Math.cos(angle) * pt.Y; + return { X: newX + centerPoint.X, Y: newY + centerPoint.Y }; }); doc.rotation = NumCast(doc.rotation) + angle; return newPoints; @@ -234,29 +185,85 @@ export class InkStrokeProperties { */ @undoBatch @action - moveControl = (deltaX: number, deltaY: number, controlIndex: number) => + moveControlPtHandle = (deltaX: number, deltaY: number, controlIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { - const newPoints: { X: number, Y: number }[] = []; const order = controlIndex % 4; - for (var i = 0; i < ink.length; i++) { + const closed = InkingStroke.IsClosed(ink); + + const newpts = ink.map((pt, i) => { const leftHandlePoint = order === 0 && i === controlIndex + 1; const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; if (controlIndex === i || leftHandlePoint || rightHandlePoint || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || + ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) || (order === 3 && i === controlIndex - 1) || (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) { - newPoints.push({ X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }); - } else { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + return ({ X: pt.X + deltaX / xScale, Y: pt.Y + deltaY / yScale }); } - } - return newPoints; + return pt; + }); + return newpts; }) + + public static nearestPtToStroke(ctrlPoints: { X: number, Y: number }[], refPt: { X: number, Y: number }, excludeSegs?: number[]) { + var distance = Number.MAX_SAFE_INTEGER; + var nearestT = -1; + var nearestSeg = -1; + var nearestPt = { X: 0, Y: 0 }; + for (var i = 0; i < ctrlPoints.length - 3; i += 4) { + if (excludeSegs?.includes(i)) continue; + const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refPt.X, y: refPt.Y }); + if (point.t !== undefined) { + const dist = Math.sqrt((point.x - refPt.X) * (point.x - refPt.X) + (point.y - refPt.Y) * (point.y - refPt.Y)); + if (dist < distance) { + distance = dist; + nearestT = point.t; + nearestSeg = i; + nearestPt = { X: point.x, Y: point.y }; + } + } + } + return { distance, nearestT, nearestSeg, nearestPt }; + } + + /** + * Handles the movement/scaling of a control point. + */ + snapControl = (inkDoc: Doc, controlIndex: number) => { + const ink = Cast(inkDoc.data, InkField)?.inkData; + if (ink) { + const closed = InkingStroke.IsClosed(ink); + + // figure out which segments we don't want to snap to - avoid the dragged control point's segment and the next and prev segments (when they exist -- ie not for endpoints of unclosed curve) + const thisseg = Math.floor(controlIndex / 4) * 4; + const which = controlIndex % 4; + const nextseg = which > 1 && (closed || controlIndex < ink.length - 1) ? (thisseg + 4) % ink.length : -1; + const prevseg = which < 2 && (closed || controlIndex > 0) ? (thisseg - 4 + ink.length) % ink.length : -1; + const refPt = ink[controlIndex]; + const { nearestPt } = InkStrokeProperties.nearestPtToStroke(ink, refPt, [thisseg, prevseg, nextseg]); + + // nearestPt is in inkDoc coordinates -- we need to compute the distance in screen coordinates. + // so we scale the X & Y distances by the internal ink scale factor and then transform the final distance by the ScreenToLocal.Scale of the inkDoc itself. + const oldXrange = (xs => ({ coord: NumCast(inkDoc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); + const oldYrange = (ys => ({ coord: NumCast(inkDoc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); + const ptsXscale = ((NumCast(inkDoc._width) - NumCast(inkDoc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1; + const ptsYscale = ((NumCast(inkDoc._height) - NumCast(inkDoc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1; + const near = Math.sqrt((nearestPt.X - refPt.X) * (nearestPt.X - refPt.X) * ptsXscale * ptsXscale + + (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale); + + if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) { + return this.moveControlPtHandle((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex); + } + } + return false; + } + /** * Snaps a control point with broken tangency back to synced rotation. * @param handleIndexA The handle point that retains its current position. @@ -264,21 +271,16 @@ export class InkStrokeProperties { */ snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => { this.applyFunction((doc: Doc, ink: InkData) => { - const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); - if (brokenIndices) { - const newBrokenIndices = new List; - brokenIndices.forEach(brokenIndex => { - if (brokenIndex !== controlIndex) { - newBrokenIndices.push(brokenIndex); - } - }); - doc.brokenInkIndices = newBrokenIndices; + const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"), []); + const ind = brokenIndices.findIndex(value => value === controlIndex); + if (ind !== -1) { + brokenIndices.splice(ind, 1); const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]]; const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI); const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint); - const newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference); - ink[handleIndexB] = newHandleB; - return ink; + const inkCopy = ink.slice(); // have to make a new copy of the array to keep from corrupting undo/redo. without slicing, the same array will be stored in each undo step meaning earlier undo steps will be inadvertently updated to store the latest value. + inkCopy[handleIndexB] = this.rotatePoint(handleB, controlPoint, angleDifference); + return inkCopy; } }); } @@ -291,9 +293,7 @@ export class InkStrokeProperties { const rotatedTarget = { X: target.X - origin.X, Y: target.Y - origin.Y }; const newX = Math.cos(angle) * rotatedTarget.X - Math.sin(angle) * rotatedTarget.Y; const newY = Math.sin(angle) * rotatedTarget.X + Math.cos(angle) * rotatedTarget.Y; - rotatedTarget.X = newX + origin.X; - rotatedTarget.Y = newY + origin.Y; - return rotatedTarget; + return { X: newX + origin.X, Y: newY + origin.Y }; } /** @@ -304,11 +304,11 @@ export class InkStrokeProperties { angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => { const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y); const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y); + if (magnitudeA === 0 || magnitudeB === 0) return 0; // Normalizing the vectors. vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA }; vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB }; - const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y; - return Math.acos(dotProduct); + return Math.acos(vectorB.X * vectorA.X + vectorB.Y * vectorA.Y); } /** @@ -330,20 +330,23 @@ export class InkStrokeProperties { */ @undoBatch @action - moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => + moveTangentHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { + const closed = InkingStroke.IsClosed(ink); const oldHandlePoint = ink[handleIndex]; - let oppositeHandlePoint = ink[oppositeHandleIndex]; + const oppositeHandlePoint = ink[oppositeHandleIndex]; const controlPoint = ink[controlIndex]; const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; - ink[handleIndex] = newHandlePoint; + const inkCopy = ink.slice(); + inkCopy[handleIndex] = newHandlePoint; const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); + const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1; // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). - if ((!brokenIndices || !brokenIndices?.includes(controlIndex)) && handleIndex !== 1 && handleIndex !== ink.length - 2) { + if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && + (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) { const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); - oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); - ink[oppositeHandleIndex] = oppositeHandlePoint; + inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); } - return ink; + return inkCopy; }) }
\ No newline at end of file |
