aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/InkStrokeProperties.ts
diff options
context:
space:
mode:
authorgeireann <geireann.lindfield@gmail.com>2021-10-14 15:01:19 -0400
committergeireann <geireann.lindfield@gmail.com>2021-10-14 15:01:19 -0400
commit5bbd1b35d2c3855eae8405e26deb0c6679cc7c26 (patch)
treec9d999f36b078d7fd8f55a74c94ce495c9fa8d4e /src/client/views/InkStrokeProperties.ts
parentbe4fd2492ad706f30af28f33133a4df0e8049e12 (diff)
parented68bbec549dedeb89bcb584151b097863b52d0d (diff)
Merge branch 'master' into schema-view-En-Hua
Diffstat (limited to 'src/client/views/InkStrokeProperties.ts')
-rw-r--r--src/client/views/InkStrokeProperties.ts229
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