aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/InkStrokeProperties.ts
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-12-10 13:36:12 -0500
committerbobzel <zzzman@gmail.com>2021-12-10 13:36:12 -0500
commite54c1ef16b4ce0a324fac3747defdc6501834de5 (patch)
treee956e5bbe07e53a36e5ead3d637e6f7c2b01671b /src/client/views/InkStrokeProperties.ts
parent8176b94970b86bd3c1669130f6fef2ccd70d0b84 (diff)
parentf8ce34c8ed42646691d1e392effe79bc27daf810 (diff)
Merge branch 'master' into ink_v1
Diffstat (limited to 'src/client/views/InkStrokeProperties.ts')
-rw-r--r--src/client/views/InkStrokeProperties.ts133
1 files changed, 109 insertions, 24 deletions
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 02288bbb5..cab4e1216 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,26 +1,30 @@
import { Bezier } from "bezier-js";
+import { Normalize, Distance } from "../util/bezierFit";
import { action, observable, reaction } from "mobx";
-import { Doc, Opt, DocListCast } from "../../fields/Doc";
+import { Doc, NumListCast, Opt } from "../../fields/Doc";
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 { Point } from "../../pen-gestures/ndollar";
import { DocumentType } from "../documents/DocumentTypes";
+import { FitOneCurve } from "../util/bezierFit";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
+import { DocumentManager } from "../util/DocumentManager";
import { undoBatch } from "../util/UndoManager";
import { InkingStroke } from "./InkingStroke";
import { DocumentView } from "./nodes/DocumentView";
-import { DocumentManager } from "../util/DocumentManager";
export class InkStrokeProperties {
- static Instance: InkStrokeProperties | undefined;
+ static _Instance: InkStrokeProperties | undefined;
+ public static get Instance() { return this._Instance || new InkStrokeProperties(); }
@observable _lock = false;
@observable _controlButton = false;
@observable _currentPoint = -1;
constructor() {
- InkStrokeProperties.Instance = this;
+ InkStrokeProperties._Instance = this;
reaction(() => this._controlButton, button => button && (CurrentUserUtils.SelectedTool = InkTool.None));
reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false));
}
@@ -139,18 +143,35 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- deletePoints = (inkView: DocumentView) => this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ deletePoints = (inkView: DocumentView, preserve: boolean) => this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
const doc = view.rootDoc;
- 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 });
+ const newPoints = ink.slice();
+ const brokenIndices = NumListCast(doc.brokenInkIndices);
+ if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
+ newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
+ } else {
+ const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+ const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+ const samples: Point[] = [];
+ var startDir = { x: 0, y: 0 };
+ var endDir = { x: 0, y: 0 };
+ for (var i = 0; i < splicedPoints.length / 4; i++) {
+ const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ if (i === 0) startDir = bez.derivative(0);
+ if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+ for (var t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
+ const pt = bez.compute(t);
+ samples.push(new Point(pt.x, pt.y));
+ }
+ }
+ const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ if (error < 100) {
+ newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
+ } else {
+ newPoints.splice(this._currentPoint - 2, 4);
}
}
- 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);
+ doc.brokenInkIndices = new List(brokenIndices.map(control => control >= this._currentPoint ? control - 4 : control));
this._currentPoint = -1;
return newPoints.length < 4 ? undefined : newPoints;
}, true)
@@ -163,10 +184,10 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: { x: number, y: number }) => {
+ rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
view.rootDoc.rotation = NumCast(view.rootDoc.rotation) + angle;
- const inkCenterPt = view.ComponentView?.ptFromScreen?.({ X: scrpt.x, Y: scrpt.y });
+ const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
return !inkCenterPt ? ink :
ink.map(i => {
const pt = { X: i.X - inkCenterPt.X, Y: i.Y - inkCenterPt.Y };
@@ -178,19 +199,84 @@ export class InkStrokeProperties {
}
/**
+ * Rotates ink stroke(s) about a point
+ * @param inkStrokes set of ink documentViews to rotate
+ * @param angle The angle at which to rotate the ink in radians.
+ * @param scrpt The center point of the rotation in screen coordinates
+ */
+ @undoBatch
+ @action
+ stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
+ this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
+ const ptFromScreen = view.ComponentView?.ptFromScreen;
+ const ptToScreen = view.ComponentView?.ptToScreen;
+ return !ptToScreen || !ptFromScreen ? ink :
+ ink.map(ptToScreen).map(i => {
+ const pvec = { X: i.X - scrpt.X, Y: i.Y - scrpt.Y };
+ const svec = pvec.X * scrVec.X * scaling + pvec.Y * scrVec.Y * scaling;
+ const ovec = -pvec.X * scrVec.Y * (scaleUniformly ? scaling : 1) + pvec.Y * scrVec.X * (scaleUniformly ? scaling : 1);
+ const newscrpt = { X: scrpt.X + svec * scrVec.X - ovec * scrVec.Y, Y: scrpt.Y + svec * scrVec.Y + ovec * scrVec.X };
+ return ptFromScreen(newscrpt);
+ });
+ });
+ }
+
+ /**
* Handles the movement/scaling of a control point.
*/
@undoBatch
@action
- moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number) =>
+ moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => {
const order = controlIndex % 4;
const closed = InkingStroke.IsClosed(ink);
+ if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1) {
+ const cpt_before = ink[controlIndex];
+ const cpt = { X: cpt_before.X + deltaX, Y: cpt_before.Y + deltaY };
+ if (true) {
+ const newink = origInk.slice();
+ const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+ const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+ const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
+ const samplesLeft: Point[] = [];
+ const samplesRight: Point[] = [];
+ var startDir = { x: 0, y: 0 };
+ var endDir = { x: 0, y: 0 };
+ for (var i = 0; i < nearestSeg / 4 + 1; i++) {
+ const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ if (i === 0) startDir = bez.derivative(0);
+ if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
+ for (var t = 0; t < (i === nearestSeg / 4 ? nearestT + .05 : 1); t += 0.05) {
+ const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
+ samplesLeft.push(new Point(pt.x, pt.y));
+ }
+ }
+ var { finalCtrls, error } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ for (var i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
+ const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
+ if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+ for (var t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + .05 + 1e-7 : 1 + 1e-7); t += 0.05) {
+ const pt = bez.compute(Math.min(1, t));
+ samplesRight.push(new Point(pt.x, pt.y));
+ }
+ }
+ const { finalCtrls: rightCtrls, error: errorRight } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ finalCtrls = finalCtrls.concat(rightCtrls);
+ newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
+ return newink;
+ }
+ }
- const newpts = ink.map((pt, i) => {
+ return ink.map((pt, i) => {
const leftHandlePoint = order === 0 && i === controlIndex + 1;
const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
if (controlIndex === i ||
+ (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
+ (order === 3 && i === controlIndex - 1)) {
+ return ({ X: pt.X + deltaX, Y: pt.Y + deltaY });
+ }
+ if (controlIndex === i ||
leftHandlePoint ||
rightHandlePoint ||
(order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
@@ -203,7 +289,6 @@ export class InkStrokeProperties {
}
return pt;
});
- return newpts;
})
@@ -243,7 +328,7 @@ export class InkStrokeProperties {
if (snapData.distance < 10) {
const deltaX = (snapData.nearestPt.X - ink[controlIndex].X);
const deltaY = (snapData.nearestPt.Y - ink[controlIndex].Y);
- const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex);
+ const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice());
console.log("X = " + snapData.nearestPt.X + " " + snapData.nearestPt.Y);
return res;
}
@@ -296,7 +381,7 @@ export class InkStrokeProperties {
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 angleDifference = InkStrokeProperties.angleChange(handleB, oppositeHandleA, controlPoint);
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;
@@ -320,7 +405,7 @@ export class InkStrokeProperties {
*
* α = arccos(a·b / |a|·|b|), where a and b are both vectors.
*/
- angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => {
+ public static 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;
@@ -333,14 +418,14 @@ export class InkStrokeProperties {
/**
* Finds the angle difference (in radians) between two vectors relative to an arbitrary origin.
*/
- angleChange = (a: PointData, b: PointData, origin: PointData) => {
+ public static angleChange(a: PointData, b: PointData, origin: PointData) {
// Finding vector representation of inputted points relative to new origin.
const vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y };
const vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y };
const crossProduct = vectorB.X * vectorA.Y - vectorB.Y * vectorA.X;
// Determining whether rotation is clockwise or counterclockwise.
const sign = crossProduct < 0 ? 1 : -1;
- const theta = this.angleBetweenTwoVectors(vectorA, vectorB);
+ const theta = InkStrokeProperties.angleBetweenTwoVectors(vectorA, vectorB);
return sign * theta;
}
@@ -364,7 +449,7 @@ export class InkStrokeProperties {
// 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) && !brokenIndices?.includes(equivIndex))) &&
(closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
- const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
+ const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
}
return inkCopy;