aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/InkStrokeProperties.ts
diff options
context:
space:
mode:
authorNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2024-10-03 02:47:23 -0400
committerNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2024-10-03 02:47:23 -0400
commitb21d8cc9d1b25ccd2e0103f47ad132dd6f04612d (patch)
treede6de9f556dd647f132a1f707a93c77a38946f1c /src/client/views/InkStrokeProperties.ts
parentb013de245c77e40487e9842650239eff20c0e361 (diff)
parent7af6d22556467c115809222db77d4056b42f390f (diff)
Merge branch 'nathan-starter' of https://github.com/brown-dash/Dash-Web into nathan-starter
Diffstat (limited to 'src/client/views/InkStrokeProperties.ts')
-rw-r--r--src/client/views/InkStrokeProperties.ts180
1 files changed, 105 insertions, 75 deletions
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 3920ecc2a..358274f0e 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,7 +1,9 @@
import { Bezier } from 'bezier-js';
+import * as fitCurve from 'fit-curve';
import * as _ from 'lodash';
import { action, makeObservable, observable, reaction, runInAction } from 'mobx';
import { Doc, NumListCast, Opt } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
@@ -9,7 +11,7 @@ import { Cast, NumCast } from '../../fields/Types';
import { PointData } from '../../pen-gestures/GestureTypes';
import { Point } from '../../pen-gestures/ndollar';
import { DocumentType } from '../documents/DocumentTypes';
-import { undoBatch } from '../util/UndoManager';
+import { undoable } from '../util/UndoManager';
import { FitOneCurve } from '../util/bezierFit';
import { InkingStroke } from './InkingStroke';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
@@ -89,8 +91,7 @@ export class InkStrokeProperties {
* @param i index of first control point of segment being split
* @param control The list of all control points of the ink.
*/
- @undoBatch
- addPoints = (inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
+ addPoints = undoable((inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
this.applyFunction(inkView, (view: DocumentView /* , ink: InkData */) => {
const doc = view.Document;
const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]];
@@ -106,7 +107,7 @@ export class InkStrokeProperties {
return controls;
});
- };
+ }, 'add ink points');
/**
* Scales a handle point of a control point that is adjacent to a newly added one.
@@ -161,8 +162,7 @@ export class InkStrokeProperties {
/**
* Deletes the current control point of the selected ink instance.
*/
- @undoBatch
- deletePoints = (inkView: DocumentView, preserve: boolean) =>
+ deletePoints = undoable((inkView: DocumentView, preserve: boolean) => {
this.applyFunction(
inkView,
(view: DocumentView, ink: InkData) => {
@@ -201,6 +201,7 @@ export class InkStrokeProperties {
},
true
);
+ }, 'delete ink points');
/**
* Rotates ink stroke(s) about a point
@@ -208,8 +209,7 @@ export class InkStrokeProperties {
* @param angle The angle at which to rotate the ink in radians.
* @param scrpt The center point of the rotation in screen coordinates
*/
- @undoBatch
- rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
+ rotateInk = undoable((inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number /* , inkStrokeWidth: number */) => {
const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
return !inkCenterPt
@@ -221,7 +221,7 @@ export class InkStrokeProperties {
return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
});
});
- };
+ }, 'rotate ink');
/**
* Rotates ink stroke(s) about a point
@@ -229,8 +229,7 @@ export class InkStrokeProperties {
* @param angle The angle at which to rotate the ink in radians.
* @param scrpt The center point of the rotation in screen coordinates
*/
- @undoBatch
- stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
+ stretchInk = undoable((inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData) => {
const ptFromScreen = view.ComponentView?.ptFromScreen;
const ptToScreen = view.ComponentView?.ptToScreen;
@@ -244,77 +243,77 @@ export class InkStrokeProperties {
return ptFromScreen(newscrpt);
});
});
- };
+ }, 'stretch ink');
/**
* Handles the movement/scaling of a control point.
*/
- @undoBatch
- moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
+ moveControlPtHandle = undoable((inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) => {
inkView &&
- this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
- const order = controlIndex % 4;
- const closed = InkingStroke.IsClosed(ink);
- const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
- if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
- const cptBefore = ink[controlIndex];
- const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
- 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);
- if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice();
- const samplesLeft: Point[] = [];
- const samplesRight: Point[] = [];
- let startDir = { x: 0, y: 0 };
- let endDir = { x: 0, y: 0 };
- for (let 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(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
- if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
- for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.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));
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ const order = controlIndex % 4;
+ const closed = InkingStroke.IsClosed(ink);
+ const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
+ if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
+ const cptBefore = ink[controlIndex];
+ const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
+ 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);
+ if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice();
+ const samplesLeft: Point[] = [];
+ const samplesRight: Point[] = [];
+ let startDir = { x: 0, y: 0 };
+ let endDir = { x: 0, y: 0 };
+ for (let 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(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
+ if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
+ for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.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));
+ }
}
- }
- let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
- for (let 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(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
- for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.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));
+ let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ for (let 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(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
+ for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.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 { 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;
- }
- 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) ||
- ((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))
- ) {
- return { X: pt.X + deltaX, Y: pt.Y + deltaY };
- }
- return pt;
+ 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) ||
+ ((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))
+ ) {
+ return { X: pt.X + deltaX, Y: pt.Y + deltaY };
+ }
+ return pt;
+ });
});
- });
+ }, 'move ink ctrl pt');
public static nearestPtToStroke(ctrlPoints: { X: number; Y: number }[], refInkSpacePt: { X: number; Y: number }, excludeSegs?: number[]) {
let distance = Number.MAX_SAFE_INTEGER;
@@ -322,7 +321,6 @@ export class InkStrokeProperties {
let nearestSeg = -1;
let nearestPt = { X: 0, Y: 0 };
for (let i = 0; i < ctrlPoints.length - 3; i += 4) {
- // eslint-disable-next-line no-continue
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: refInkSpacePt.X, y: refInkSpacePt.Y });
@@ -467,8 +465,7 @@ export class InkStrokeProperties {
/**
* Handles the movement/scaling of a handle point.
*/
- @undoBatch
- moveTangentHandle = (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
+ moveTangentHandle = undoable((inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => {
this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
const doc = view.Document;
const closed = InkingStroke.IsClosed(ink);
@@ -487,4 +484,37 @@ export class InkStrokeProperties {
}
return inkCopy;
});
+ }, 'move ink tangent');
+
+ sampleBezier = (curves: InkData) => {
+ const polylinePoints = [{ x: curves[0].X, y: curves[0].Y }];
+ for (let i = 0; i < curves.length / 4; i++) {
+ const bez = new Bezier(curves.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ for (let t = 0.05; t < 1; t += 0.05) {
+ polylinePoints.push(bez.compute(t));
+ }
+ polylinePoints.push(bez.points[3]);
+ }
+ return polylinePoints.length > 2 ? polylinePoints : undefined;
+ };
+ /**
+ * Function that "smooths" ink strokes by sampling the curve, then fitting it with new bezier curves, subject to a
+ * maximum pixel error tolerance
+ * @param inkDocs
+ * @param tolerance how many pixels of error are allowed
+ */
+ smoothInkStrokes = undoable((inkDocs: Doc[], tolerance = 5) => {
+ inkDocs.forEach(inkDoc => {
+ const inkView = DocumentView.getDocumentView(inkDoc);
+ const inkStroke = inkView?.ComponentView as InkingStroke;
+ const polylinePoints = this.sampleBezier(inkStroke?.inkScaledData().inkData ?? [])?.map(pt => [pt.x, pt.y]);
+ if (polylinePoints) {
+ inkDoc[DocData].stroke = new InkField(
+ fitCurve.default(polylinePoints, tolerance)
+ .reduce((cpts, bez) =>
+ ({n: cpts.push(...bez.map(cpt => ({X:cpt[0], Y:cpt[1]}))), cpts}).cpts,
+ [] as {X:number, Y:number}[])); // prettier-ignore
+ }
+ });
+ }, 'smooth ink stroke');
}