From 39ca6f5ea22c99f3e7f530559d0b08c00aa19cb4 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 27 Oct 2021 00:57:47 -0400 Subject: fixed warnings/errors. --- src/client/views/InkStrokeProperties.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/views/InkStrokeProperties.ts') diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 7c91a3323..6687b2bc7 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -239,7 +239,7 @@ export class InkStrokeProperties { if (ink) { const screenDragPt = inkView.ComponentView?.ptToScreen?.(ink[controlIndex]); if (screenDragPt) { - var snapData = this.snapToAllCurves(screenDragPt, inkView, { nearestPt: { X: 0, Y: 0 }, distance: 10 }, ink, controlIndex); + const snapData = this.snapToAllCurves(screenDragPt, inkView, { nearestPt: { X: 0, Y: 0 }, distance: 10 }, ink, controlIndex); if (snapData.distance < 10) { const deltaX = (snapData.nearestPt.X - ink[controlIndex].X); const deltaY = (snapData.nearestPt.Y - ink[controlIndex].Y); -- cgit v1.2.3-70-g09d2 From 061dab5285d3a334a258d8097a6e95b065b30de3 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 29 Nov 2021 14:09:17 -0500 Subject: added moving ink stroke segments. added stretching and rotating ink strokes about opposite end point. --- src/client/util/InteractionUtils.tsx | 5 +- src/client/views/InkControlPtHandles.tsx | 77 +++++++++++++++++++++++++++---- src/client/views/InkStrokeProperties.ts | 24 ++++++++++ src/client/views/InkingStroke.tsx | 79 ++++++++++++++++++++++---------- 4 files changed, 150 insertions(+), 35 deletions(-) (limited to 'src/client/views/InkStrokeProperties.ts') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index a32a8eecc..4eb0be320 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -91,7 +91,8 @@ export namespace InteractionUtils { export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean) { + dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean, + downHdlr?: ((e: React.PointerEvent) => void)) { const pts = shape ? makePolygon(shape, points) : points; if (isNaN(scalex)) scalex = 1; @@ -107,7 +108,7 @@ export namespace InteractionUtils { const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements; const makerStrokeWidth = strokeWidth / 2; - return ( {/* setting the svg fill sets the arrowStart fill */} + return ( {/* setting the svg fill sets the arrowStart fill */} {nodefs ? (null) : {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index f24dab949..df803ba31 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -6,7 +6,7 @@ import { ControlPoint, InkData, PointData, InkField } from "../../fields/InkFiel import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; -import { setupMoveUpEvents } from "../../Utils"; +import { setupMoveUpEvents, returnFalse } from "../../Utils"; import { Transform } from "../util/Transform"; import { UndoManager } from "../util/UndoManager"; import { Colors } from "./global/globalEnums"; @@ -133,7 +133,6 @@ export class InkControlPtHandles extends React.Component { inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 }); } - const screenSpaceLineWidth = this.props.screenSpaceLineWidth; const closed = InkingStroke.IsClosed(inkData); const nearestScreenPt = this.props.nearestScreenPt(); const TagType = (broken?: boolean) => broken ? "rect" : "circle"; @@ -141,18 +140,18 @@ export class InkControlPtHandles extends React.Component { const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I); const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements; return this.onControlDown(e, control.I)} + onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)} onMouseEnter={() => this.onEnterControl(control.I)} onMouseLeave={this.onLeaveControl} pointerEvents="all" @@ -164,7 +163,7 @@ export class InkControlPtHandles extends React.Component { { ); } +} + + +export interface InkEndProps { + inkDoc: Doc; + inkView: DocumentView; + screenSpaceLineWidth: number; + startPt: PointData; + endPt: PointData; +} +@observer +export class InkEndPtHandles extends React.Component { + @observable controlUndo: UndoManager.Batch | undefined; + @observable _overStart: boolean = false; + @observable _overEnd: boolean = false; + + @action + dragRotate = (e: React.PointerEvent, p1: () => { X: number, Y: number }, p2: () => { X: number, Y: number }) => { + setupMoveUpEvents(this, e, (e) => { + if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink"); + // compute stretch factor by finding scaling along axis between start and end points + const v1 = { x: p1().X - p2().X, y: p1().Y - p2().Y }; + const v2 = { x: e.clientX - p2().X, y: e.clientY - p2().Y }; + const v1len = Math.sqrt(v1.x * v1.x + v1.y * v1.y); + const v2len = Math.sqrt(v2.x * v2.x + v2.y * v2.y); + const scaling = v2len / v1len; + const v1n = { x: v1.x / v1len, y: v1.y / v1len }; + const v2n = { x: v2.x / v2len, y: v2.y / v2len }; + const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y) + InkStrokeProperties.Instance?.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n); + InkStrokeProperties.Instance?.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y }); + return false; + }, action(() => { + this.controlUndo?.end(); + this.controlUndo = undefined; + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }), returnFalse); + } + + render() { + const hdl = (pt: PointData, dragFunc: (e: React.PointerEvent) => void) => this._overStart = false)} + onPointerEnter={action(() => this._overStart = true)} + onPointerDown={e => dragFunc(e)} + pointerEvents="all" + />; + return ( + {hdl(this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))} + {hdl(this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))} + + ); + } } \ No newline at end of file diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 02288bbb5..8ad4864f9 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -177,6 +177,30 @@ 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: { x: number, y: number }, scrVec: { x: number, y: number }) => { + 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(i => ptToScreen(i)).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 + pvec.Y * (scrVec.x); + const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x }; + const newpt = ptFromScreen!(newscrpt); + return newpt; + }); + }); + } + /** * Handles the movement/scaling of a control point. */ diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 59efb36dd..ecc82a580 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,28 +1,29 @@ import React = require("react"); import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, HeightSym, WidthSym } from "../../fields/Doc"; +import { Doc, WidthSym } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; -import { emptyFunction, returnFalse, setupMoveUpEvents, OmitKeys } from "../../Utils"; +import { OmitKeys, returnFalse, setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; import { SnappingManager } from "../util/SnappingManager"; +import { Transform } from "../util/Transform"; +import { UndoManager } from "../util/UndoManager"; import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; import { Colors } from "./global/globalEnums"; -import { InkControlPtHandles } from "./InkControlPtHandles"; +import { InkControlPtHandles, InkEndPtHandles } from "./InkControlPtHandles"; import "./InkStroke.scss"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { InkTangentHandles } from "./InkTangentHandles"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import Color = require("color"); -import { Transform } from "../util/Transform"; import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox"; +import Color = require("color"); type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -89,21 +90,49 @@ export class InkingStroke extends ViewBoxBaseComponent { + const ptFromScreen = this.ptFromScreen; this._handledClick = false; - if (this.props.isSelected(true)) { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, + if (InkStrokeProperties.Instance && ptFromScreen) { + const inkView = this.props.docViewPath().lastElement(); + const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); + const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint( + (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, + (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); + const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); + const controlIndex = nearestSeg; + const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex; + var controlUndo: UndoManager.Batch | undefined; + const isEditing = this._properties?._controlButton && this.props.isSelected(); + setupMoveUpEvents(this, e, + !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => { + if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); + const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] }); + const inkMoveStart = ptFromScreen({ X: 0, Y: 0 }); + InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); + InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3); + return false; + }), + !isEditing ? returnFalse : action(() => { + if (controlUndo) { + InkStrokeProperties.Instance?.snapControl(inkView, controlIndex); + InkStrokeProperties.Instance?.snapControl(inkView, controlIndex + 3); + } + controlUndo?.end(); + controlUndo = undefined; + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }), action((e: PointerEvent, doubleTap: boolean | undefined) => { doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick; if (doubleTap && this._properties) { 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 if (this._properties?._controlButton) { + } else if (isEditing) { this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); } - }), this._properties?._controlButton, this._properties?._controlButton - ); + }), isEditing, isEditing, () => wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1)); } } @@ -185,7 +214,6 @@ export class InkingStroke extends ViewBoxBaseComponent this._nearestScrPt; componentUI = (boundsLeft: number, boundsTop: number) => { const inkDoc = this.props.Document; @@ -200,11 +228,17 @@ export class InkingStroke extends ViewBoxBaseComponent - {!this._properties?._controlButton ? (null) : - <> + return SnappingManager.GetIsDragging() ? (null) : + !this._properties?._controlButton ? + (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) : +
+
) : +
{InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth, StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier), "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)} @@ -222,8 +256,7 @@ export class InkingStroke extends ViewBoxBaseComponent - } -
; + ; } render() { @@ -246,11 +279,12 @@ export class InkingStroke extends ViewBoxBaseComponent void) => InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor, + inkStrokeWidth, inkStrokeWidth + (CurrentUserUtils.SelectedTool === InkTool.Eraser ? 0 : highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15), StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker, - undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false); + undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, + false, downHdlr); // Set of points rendered upon the ink that can be added if a user clicks on one. return
@@ -266,7 +300,6 @@ export class InkingStroke extends ViewBoxBaseComponent this._nearestScrPt = undefined)} onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined} - onPointerDown={this.onPointerDown} onClick={e => this._handledClick && e.stopPropagation()} onContextMenu={() => { const cm = ContextMenu.Instance; @@ -275,7 +308,7 @@ export class InkingStroke extends ViewBoxBaseComponent { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" }); }} > - {clickableLine} + {clickableLine(this.onPointerDown)} {inkLine} {!closed ? (null) : -- cgit v1.2.3-70-g09d2 From f313cfa5ae644eadb57d936bc81bd355e0c88e17 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 30 Nov 2021 16:15:06 -0500 Subject: change delete points for ink to try to preserve the shape as much as possible. Shift + backspace deletes the point without preserving geometry --- src/client/util/bezierFit.ts | 1446 ++++++++++++++++++++++++++++++ src/client/views/InkControlPtHandles.tsx | 2 +- src/client/views/InkStrokeProperties.ts | 41 +- src/client/views/InkingStroke.tsx | 2 +- 4 files changed, 1478 insertions(+), 13 deletions(-) create mode 100644 src/client/util/bezierFit.ts (limited to 'src/client/views/InkStrokeProperties.ts') diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts new file mode 100644 index 000000000..57c6dbbde --- /dev/null +++ b/src/client/util/bezierFit.ts @@ -0,0 +1,1446 @@ +import { Point } from "../../pen-gestures/ndollar"; +import { max } from "lodash"; + +class SmartRect { + minx: number = 0; + miny: number = 0; + maxx: number = 0; + maxy: number = 0; + + constructor(mix: number = 0, miy: number = 0, max: number = 0, may: number = 0) { this.minx = mix; this.miny = miy; this.maxx = max; this.maxy = may; } + + public get Center() { return new Point((this.maxx + this.minx) / 2.0, (this.maxy + this.miny) / 2.0); } + public get TopLeft() { return new Point(this.minx, this.miny); } + public get TopRight() { return new Point(this.maxx, this.miny); } + public get BotLeft() { return new Point(this.minx, this.maxy); } + public get BotRight() { return new Point(this.maxx, this.maxy); } + public get Width() { return this.maxx - this.minx; } + public get Height() { return this.maxy - this.miny; } + public static Intersect(a: SmartRect, b: SmartRect) { return a.Intersect(b); } + public Intersect(b: SmartRect) { return !((this.minx > b.maxx) || (this.miny > b.maxy) || (b.minx > this.maxx) || (b.miny > this.maxy)); } + + public ContainsPercentage(other: SmartRect, axis: Point) { + var ret = 0; + var minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y); + var maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y); + ret = maxx > minx ? (maxx - minx) / (axis == new Point(1, 0) ? other.Width : other.Height) : 0; + return ret; + } + public static Bounds(p: Point[]) { + var r = new SmartRect(); + if (p.length > 0) { + r.minx = p[0].X; // These are the most likely to be extremal + r.maxx = p.lastElement().X; + r.miny = p[0].Y; + r.maxy = p.lastElement().Y; + + if (r.minx > r.maxx) { + var tmp = r.minx; + r.minx = r.maxx; + r.maxx = tmp; + } + if (r.miny > r.maxy) { + var tmp = r.miny; + r.miny = r.maxy; + r.maxy = tmp; + } + + for (var pt of p) { + if (pt.X < r.minx) + r.minx = pt.X; + else if (pt.X > r.maxx) + r.maxx = pt.X; + + if (pt.Y < r.miny) + r.miny = pt.Y; + else if (pt.Y > r.maxy) + r.maxy = pt.Y; + } + } + return r; + } +}; + +function Normalize(p: Point) { + const len = Math.sqrt(p.X * p.X + p.Y * p.Y); + return new Point(p.X / len, p.Y / len); +} + +function ReparameterizeBezier(d: Point[], first: number, last: number, u: number[], bezCurve: Point[]) { + var uPrime = new Array(last - first + 1); // New parameter values + + for (var i = first; i <= last; i++) { + uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]); + } + return uPrime; +} +function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Point[], u: number[]) { + var maxError = 0; // Maximum error + + var splitPoint2D = (last - first + 1) / 2; + for (var i = first + 1; i < last; i++) { + var P = [0, 0]; // point on curve + EvalBezierFast(bezCurve, u[i - first], P); + var dx = P[0] - d[i].X;// offset from point to curve + var dy = P[1] - d[i].Y; + var dist = Math.sqrt(dx * dx + dy * dy); // Current error + if (dist >= maxError) { + maxError = dist; + if (splitPoint2D) + splitPoint2D = i; + } + } + return { maxError, splitPoint2D }; +} +function ChordLengthParameterize(d: Point[], first: number, last: number) { + var u = new Array(last - first + 1);// Parameterization + + var prev = 0.0; + u[0] = prev; + for (var i = first + 1; i <= last; i++) { + var lastd = d[i - 1]; + var curd = d[i]; + var dx = lastd.X - curd.X; + var dy = lastd.Y - curd.Y; + prev = u[i - first] = prev + Math.sqrt(dx * dx + dy * dy); + } + + var ulastfirst = u[last - first]; + for (var i = first + 1; i <= last; i++) { + u[i - first] /= ulastfirst; + } + + return u; +} +/* +* B0, B1, B2, B3 : +* Bezier multipliers +*/ +function B0(u: number) { var tmp = 1.0 - u; return tmp * tmp * tmp; } +function B1(u: number) { var tmp = 1.0 - u; return 3 * u * tmp * tmp; } +function B2(u: number) { var tmp = 1.0 - u; return 3 * u * u * tmp; } +function B3(u: number) { return u * u * u; } +function bounds(p: Point[]) { + var r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal + + if (r.minx > r.maxx) (r.minx, r.maxx); + if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; // swap min & max + + for (var i = 1; i < 3; i++) { + if (p[i].X < r.minx) r.minx = p[i].X; + else if (p[i].X > r.maxx) r.maxx = p[i].X; + + if (p[i].Y < r.miny) r.miny = p[i].Y; + else if (p[i].Y > r.maxy) r.maxy = p[i].Y; + } + return r; +} + + + +function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) { + var sz = 4; + var Vtemp = new Array>(4); + for (var i = 0; i < 4; i++) Vtemp[i] = new Array(4); + + /* Copy control points */ + // std::copy(p.begin(), p.end(), Vtemp[0]); + for (var i = 0; i < sz; i++) { + Vtemp[0][i].X = p[i].X; + Vtemp[0][i].Y = p[i].Y; + } + + /* Triangle computation */ + for (var i = 1; i < sz; i++) { + for (var j = 0; j < sz - i; j++) { + var a = Vtemp[i - 1][j]; + var b = Vtemp[i - 1][j + 1]; + Vtemp[i][j].X = b.X * t + a.X * (1 - t); + Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t); // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t); + } + } + + if (left) { + for (var j = 0; j < sz; j++) { + left[j].X = Vtemp[j][0].X; + left[j].Y = Vtemp[j][0].Y; + } + } + if (right) { + for (var j = 0; j < sz; j++) { + right[j].X = Vtemp[sz - 1 - j][j].X; + right[j].Y = Vtemp[sz - 1 - j][j].Y; + } + } +} + +/* +* Recursively intersect two curves keeping track of their real parameters +* and depths of intersection. +* The results are returned in a 2-D array of doubles indicating the parameters +* for which intersections are found. The parameters are in the order the +* intersections were found, which is probably not in sorted order. +* When an intersection is found, the parameter value for each of the two +* is stored in the index elements array, and the index is incremented. +* +* If either of the curves has subdivisions left before it is straight +* (depth > 0) +* that curve (possibly both) is (are) subdivided at its (their) midpoint(s). +* the depth(s) is (are) decremented, and the parameter value(s) corresponding +* to the midpoints(s) is (are) computed. +* Then each of the subcurves of one curve is intersected with each of the +* subcurves of the other curve, first by testing the bounding boxes for +* interference. If there is any bounding box interference, the corresponding +* subcurves are recursively intersected. +* +* If neither curve has subdivisions left, the line segments from the first +* to last control point of each segment are intersected. (Actually the +* only the parameter value corresponding to the intersection point is found). +* +* The apriori flatness test is probably more efficient than testing at each +* level of recursion, although a test after three or four levels would +* probably be worthwhile, since many curves become flat faster than their +* asymptotic rate for the first few levels of recursion. +* +* The bounding box test fails much more frequently than it succeeds, providing +* substantial pruning of the search space. +* +* Each (sub)curve is subdivided only once, hence it is not possible that for +* one final line intersection test the subdivision was at one level, while +* for another final line intersection test the subdivision (of the same curve) +* was at another. Since the line segments share endpoints, the intersection +* is robust: a near-tangential intersection will yield zero or two +* intersections. +*/ +function recursively_intersect(a: Point[], t0: number, t1: number, deptha: number, b: Point[], u0: number, u1: number, depthb: number, parameters: number[][]) { + if (deptha > 0) { + var a1 = new Array(4), a2 = new Array(4); + splitCubic(a, 0.5, a1, a2); + var tmid = (t0 + t1) * 0.5; + deptha--; + if (depthb > 0) { + var b1 = new Array(4), b2 = new Array(4); + splitCubic(b, 0.5, b1, b2); + var umid = (u0 + u1) * 0.5; + depthb--; + if (SmartRect.Intersect(bounds(a1), bounds(b1))) { + recursively_intersect(a1, t0, tmid, deptha, b1, u0, umid, depthb, parameters); + } + if (SmartRect.Intersect(bounds(a2), bounds(b1))) { + recursively_intersect(a2, tmid, t1, deptha, b1, u0, umid, depthb, parameters); + } + if (SmartRect.Intersect(bounds(a1), bounds(b2))) { + recursively_intersect(a1, t0, tmid, deptha, b2, umid, u1, depthb, parameters); + } + if (SmartRect.Intersect(bounds(a2), bounds(b2))) { + recursively_intersect(a2, tmid, t1, deptha, b2, umid, u1, depthb, parameters); + } + } + else { + if (SmartRect.Intersect(bounds(a1), bounds(b))) { + recursively_intersect(a1, t0, tmid, deptha, b, u0, u1, depthb, parameters); + } + if (SmartRect.Intersect(bounds(a2), bounds(b))) { + recursively_intersect(a2, tmid, t1, deptha, b, u0, u1, depthb, parameters); + } + } + } + else { + if (depthb > 0) { + var b1 = new Array(4), b2 = new Array(4); + splitCubic(b, 0.5, b1, b2); + var umid = (u0 + u1) * 0.5; + depthb--; + if (SmartRect.Intersect(bounds(a), bounds(b1))) { + recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters); + } + if (SmartRect.Intersect(bounds(a), bounds(b2))) { + recursively_intersect(a, t0, t1, deptha, b2, umid, u1, depthb, parameters); + } + } + else // Both segments are fully subdivided; now do line segments + { + var xlk = a[3].X - a[0].X; + var ylk = a[3].Y - a[0].Y; + var xnm = b[3].X - b[0].X; + var ynm = b[3].Y - b[0].Y; + var xmk = b[0].X - a[0].X; + var ymk = b[0].Y - a[0].Y; + var det = xnm * ylk - ynm * xlk; + if (1.0 + det == 1.0) { + return; + } + else { + var detinv = 1.0 / det; + var s = (xnm * ymk - ynm * xmk) * detinv; + var t = (xlk * ymk - ylk * xmk) * detinv; + if ((s < 0.0) || (s > 1.0) || (t < 0.0) || (t > 1.0) || Number.isNaN(s) || Number.isNaN(t)) { + return; + } + parameters.push([t0 + s * (t1 - t0), u0 + t * (u1 - u0)]); + } + } + } +} + +/* +* EvalBezier : +* Evaluate a Bezier curve at a particular parameter value +* +*/ +const MAX_DEGREE = 5; +function EvalBezier(V: Point[], degree: number, t: number, result: number[]) { + if (degree + 1 > MAX_DEGREE) { + result[0] = V[0].X; + result[1] = V[0].Y; + return; + } + + var Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points + + /* Copy array */ + for (var i = 0; i <= degree; i++) { + Vtemp[i].X = V[i].X; + Vtemp[i].Y = V[i].Y; + } + + /* Triangle computation */ + for (var i = 1; i <= degree; i++) { + for (var j = 0; j <= degree - i; j++) { + Vtemp[j].X = (1.0 - t) * Vtemp[j].X + t * Vtemp[j + 1].X; + Vtemp[j].Y = (1.0 - t) * Vtemp[j].Y + t * Vtemp[j + 1].Y; + } + } + + result[0] = Vtemp[0].X; + result[1] = Vtemp[0].Y;// Point on curve at parameter t +} + +function EvalBezierFast(p: Point[], t: number, result: number[]) { + var n = 3; + var u: number, bc: number, tn: number, tmpX: number, tmpY: number; + u = 1.0 - t; + bc = 1; + tn = 1; + + tmpX = p[0].X * u; + tmpY = p[0].Y * u; + tn = tn * t; + bc = bc * (n - 1 + 1) / 1; + tmpX = (tmpX + tn * bc * p[1].X) * u; + tmpY = (tmpY + tn * bc * p[1].Y) * u; + tn = tn * t; + bc = bc * (n - 2 + 1) / 2; + tmpX = (tmpX + tn * bc * p[2].X) * u; + tmpY = (tmpY + tn * bc * p[2].Y) * u; + + result[0] = tmpX + tn * t * p[3].X; + result[1] = tmpY + tn * t * p[3].Y; +} +/* +* ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent : +*Approximate unit tangents at endpoints and "center" of digitized curve +*/ +function ComputeLeftTangent(d: Point[], end: number) { + var use = 1; + var tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y); + return Normalize(tHat1); +} +function ComputeRightTangent(d: Point[], end: number) { + var available = end; + var use = 1; + var tHat2 = new Point(d[end - use].X - d[end].X, d[end - use].Y - d[end].Y); + return Normalize(tHat2); +} +function ComputeCenterTangent(d: Point[], center: number) { + if (center == 0) { + return ComputeLeftTangent(d, center); + } + var V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1]; + var V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1]; + var tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0); + if (tHatCenter === new Point(0, 0)) { + tHatCenter = new Point(-V1.Y, -V1.X);// V1.Perp(); + } + return Normalize(tHatCenter); +} +function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[], tHat1: Point, tHat2: Point, result: Point[] /* must be prealloacted to size 4 */) { + var nPts = last - first + 1; // Number of pts in sub-curve + var Ax = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); + var Ay = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); + + /* Compute the A's */ + for (var i = 0; i < nPts; i++) { + var uprime = uPrime[i]; + var b1 = B1(uprime); + var b2 = B2(uprime); + Ax[i] = tHat1.X * b1; + Ay[i] = tHat1.Y * b1; + Ax[i + 1 * nPts] = tHat2.X * b2; + Ay[i + 1 * nPts] = tHat2.Y * b2; + } + + /* Create the C and X matrices */ + var C = [[0, 0], [0, 0]]; + var df = d[first]; + var dl = d[last]; + + var X = [0, 0]; // Matrix X + for (var i = 0; i < nPts; i++) { + C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i]; //A[i+0*nPts].Dot(A[i+0*nPts]); + C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts];//A[i+0*nPts].Dot(A[i+1*nPts]); + C[1][0] = C[0][1]; + C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts];// A[i+1*nPts].Dot(A[i+1*nPts]); + var uprime = uPrime[i]; + var b0plb1 = B0(uprime) + B1(uprime); + var b2plb3 = B2(uprime) + B3(uprime); + var df1 = d[first + i]; + var tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3)); + var tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3)); + + X[0] += Ax[i] * tmpX + Ay[i] * tmpY; // A[i+0*nPts].Dot(tmp) + X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; //A[i+1*nPts].Dot(tmp) + } + + /* Compute the determinants of C and X */ + var det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1]; + var det_C0_X = C[0][0] * X[1] - C[0][1] * X[0]; + var det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1]; + + /* Finally, derive alpha values */ + if (det_C0_C1 == 0.0) { + det_C0_C1 = (C[0][0] * C[1][1]) * 10e-12; + } + var alpha_l = (det_C0_C1 == 0) ? 0.0 : det_X_C1 / det_C0_C1; + var alpha_r = (det_C0_C1 == 0) ? 0.0 : det_C0_X / det_C0_C1; + + /* If alpha negative, use the Wu/Barsky heuristic (see text) */ + /* (if alpha is 0, you get coincident control points that lead to + * divide by zero in any subsequent NewtonRaphsonRootFind() call. */ + var segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y)); + var epsilon = 1.0e-6 * segLength; + if (alpha_l < epsilon || alpha_r < epsilon) { + /* fall back on standard (probably inaccurate) formula, and subdivide further if needed. */ + alpha_l = alpha_r = segLength / 3.0; + } + + /* First and last control points of the Bezier curve are */ + /* positioned exactly at the first and last data points */ + /* Control points 1 and 2 are positioned an alpha distance out */ + /* on the tangent vectors, left and right, respectively */ + result[0] = df;// RETURN bezier curve ctl pts + result[3] = dl; + result[1] = new Point(df.X + (tHat1.X * alpha_l), df.Y + (tHat1.Y * alpha_l)); + result[2] = new Point(dl.X + (tHat2.X * alpha_r), dl.Y + (tHat2.Y * alpha_r)); + +} +/* + * NewtonRaphsonRootFind : + * Use Newton-Raphson iteration to find better root. + */ +function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) { + var Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; // Q' and Q'' + var Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q'' + + /* Compute Q(u) */ + var uPrime: number; // Improved u + EvalBezierFast(Q, u, Q_u); + + /* Generate control vertices for Q' */ + for (var i = 0; i <= 2; i++) { + Q1[i].X = (Q[i + 1].X - Q[i].X) * 3.0; + Q1[i].Y = (Q[i + 1].Y - Q[i].Y) * 3.0; + } + + /* Generate control vertices for Q'' */ + for (var i = 0; i <= 1; i++) { + Q2[i].X = (Q1[i + 1].X - Q1[i].X) * 2.0; + Q2[i].Y = (Q1[i + 1].Y - Q1[i].Y) * 2.0; + } + + /* Compute Q'(u) and Q''(u) */ + EvalBezier(Q1, 2, u, Q1_u); + EvalBezier(Q2, 1, u, Q2_u); + + /* Compute f(u)/f'(u) */ + var numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]); + var denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]); + if (denominator == 0.0) + uPrime = u; + else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */ + + return uPrime; +} +function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: Point, error: number, result: Point[]) { + var bezCurve = new Array(4); // Control points of fitted Bezier curve + var splitPoint2D: number; // Point2D to split point set at + var maxIterations = 4; // Max times to try iterating + + var iterationError = error * error; // Error below which you try iterating + var nPts = last - first + 1; // Number of points in subset + + /* Use heuristic if region only has two points in it */ + if (nPts == 2) { + var dist = Math.sqrt((d[first].X - d[last].X) * (d[first].X - d[last].X) + (d[first].Y - d[last].Y) * (d[first].Y - d[last].Y)) / 3; + + bezCurve[0] = d[first]; + bezCurve[3] = d[last]; + bezCurve[1] = new Point(bezCurve[0].X + (tHat1.X * dist), bezCurve[0].Y + (tHat1.Y * dist)); + bezCurve[2] = new Point(bezCurve[3].X + (tHat2.X * dist), bezCurve[3].Y + (tHat2.Y * dist)); + + result.push(bezCurve[1]); + result.push(bezCurve[2]); + result.push(bezCurve[3]); + return; + } + + /* Parameterize points, and attempt to fit curve */ + var u = ChordLengthParameterize(d, first, last); + GenerateBezier(d, first, last, u, tHat1, tHat2, bezCurve); + + /* Find max deviation of points to fitted curve */ + var { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u); // Maximum fitting error + if (maxError < Math.abs(error)) { + result.push(bezCurve[1]); + result.push(bezCurve[2]); + result.push(bezCurve[3]); + return; + } + + /* If error not too large, try some reparameterization */ + /* and iteration */ + if (maxError < iterationError) { + for (var i = 0; i < maxIterations; i++) { + var uPrime = ReparameterizeBezier(d, first, last, u, bezCurve); // Improved parameter values + GenerateBezier(d, first, last, uPrime, tHat1, tHat2, bezCurve); + var { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, uPrime); + if (maxError < error) { + result.push(bezCurve[1]); + result.push(bezCurve[2]); + result.push(bezCurve[3]); + return; + } + u = uPrime; + } + } + + /* Fitting failed -- split at max error point and fit recursively */ + var tHatCenter = splitPoint2D >= last - 1 ? ComputeRightTangent(d, splitPoint2D) : ComputeCenterTangent(d, splitPoint2D); + FitCubic(d, first, splitPoint2D, tHat1, tHatCenter, error, result); + var negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y); + FitCubic(d, splitPoint2D, last, negThatCenter, tHat2, error, result); +} +export function FitCurve(d: Point[], error: number) { + var tHat1 = ComputeLeftTangent(d, 0); // Unit tangent vectors at endpoints + var tHat2 = ComputeRightTangent(d, d.length - 1); + var result: Point[] = []; + result.push(d[0]); + FitCubic(d, 0, d.length - 1, tHat1, tHat2, error, result); + return result; +} +export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) { + tHat1 = tHat1 ?? Normalize(ComputeLeftTangent(d, 0)); + tHat2 = tHat2 ?? Normalize(ComputeRightTangent(d, d.length - 1)); + tHat2 = new Point(-tHat2.X, -tHat2.Y); + var u = ChordLengthParameterize(d, 0, d.length - 1); + var bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; + GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls); /* Find max deviation of points to fitted curve */ + var finalCtrls = bezCurveCtrls.slice(); + var { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u); + for (var i = 0; i < 10; i++) { + var uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls); // Improved parameter values + GenerateBezier(d, 0, d.length - 1, uPrime, tHat1, tHat2, bezCurveCtrls); + var { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime); + if (maxError < error) { + error = maxError; + finalCtrls = bezCurveCtrls.slice(); + } + u = uPrime; + } + return { finalCtrls, error }; +} + +/* +static double GetTValueFromSValue (const BezierRep &parent, double t, double endT, bool left, double influenceDistance, double &excess) { +double dist = 0; +double step = 0.01; +double spliceT = t; +for (spliceT = t+(left?-1:1)*step; dist < influenceDistance && (left ? (spliceT > endT) : (spliceT < endT)); spliceT += step * (left ? -1 : 1)) { +dist += (parent[spliceT]-parent[spliceT-step*(left ? -1:1)]).Length(); +} +if ((left && spliceT < endT) || (!left && spliceT > endT)) +spliceT = endT; +excess = influenceDistance - dist; +return spliceT; +} +static BezierRep::BezierLock FindSplitIndex (const BezierRep &parent, double t, bool left, std::vector &locked) +{ +BezierRep::BezierLock cuspIndex = { left ? 0.0 : 1.0*parent.MaxIndex(), true}; +double tprev = t; +for (int tstep = (left ? std::floor(t) : std::ceil(t)) * 3; left ? (tstep >= 0) : (tstep < parent.p.size()); tstep += (left ? -1 : 1)) +{ +double near = HUGE_VAL; +for (auto &l : locked) { +if ((( left && tprev > l.T && tstep <= l.T) || +(!left && tprev < l.T && tstep >= l.T)) && std::abs(tprev-l.T) < near) { +near = std::abs(tprev-l.T); +cuspIndex = l; +} +} +if (near != HUGE_VAL) +break; +} +return cuspIndex; +} +size_t SampleBezier (const BezierRep &bez, Point2D *&multiSegmentSamplePts, size_t numMultiSegmentSamples, size_t samplesPerSegment) +{ +auto numSamples = bez.MaxIndex() * samplesPerSegment + 1; +if (numSamples > numMultiSegmentSamples) { +if (numMultiSegmentSamples) +delete [] multiSegmentSamplePts; +multiSegmentSamplePts = new Point2D[numSamples]; +} +for (auto seg = 0; seg < bez.MaxIndex(); seg++) +{ + double result[2]; +Point2D tmp[4] = { bez.p[seg * 3], bez.p[seg * 3 +1], bez.p[seg * 3 +2], bez.p[seg * 3 +3] }; +for (auto index = 0; index < samplesPerSegment; index++) { +EvalBezierFast(tmp, 1.0 * index / samplesPerSegment, result); +multiSegmentSamplePts[seg * samplesPerSegment + index].X = result[0]; +multiSegmentSamplePts[seg * samplesPerSegment + index].Y = result[1]; +} +} +multiSegmentSamplePts[numSamples-1] = bez.p.back(); +return numSamples; +} +static double GetSpliceCurve (const BezierRep &parent, double t, BezierRep::BezierLock * isCusp, BezierRep::BezierLock tEnd, std::vector &locked, const Vector2D &v, Point2D singleSegmentSpliceCurve[4], double errorTolerance, double influenceDistance, double &excess) +{ +Point2D *multiSegmentSamplePts = NULL; +size_t numMultiSegmentSamples = 0; +double spliceT = tEnd.T; +bool left = tEnd.T < t; +auto parTangent = parent.Tangent(t + (left ? -1e-7:1e-7)); +if (_isnan(parTangent.X)) +parTangent = Vector2D(); +for (auto &l : locked) { +if (l.T == t && l.Cusp) { +parTangent = Vector2D(); +if (left && (l.Side == 2) && t<= parent.MaxIndex()) +parTangent = (parent[t+1]-(parent[t]+v)).Normal(); +else if (!left && (l.Side == 1) && t >= 1) +parTangent = (parent[t]+v - parent[t-1]).Normal(); +} +} + +if (_isnan(influenceDistance) && isCusp && abs(tEnd.T - t) <= 1 && tEnd.Cusp && (((tEnd.Side & 2) && left) || ((tEnd.Side & 1) && !left))) { +singleSegmentSpliceCurve[0] = parent[ left ? tEnd.T : t]; +singleSegmentSpliceCurve[2] = parent[!left ? tEnd.T : t]; +singleSegmentSpliceCurve[1] = singleSegmentSpliceCurve[0]; +singleSegmentSpliceCurve[3] = singleSegmentSpliceCurve[2]; +return spliceT; +} + +for (auto startSample = t, endSample = tEnd.T; !((left && startSample < endSample+1e-5) || (!left && startSample > endSample-1e-5)); spliceT = (endSample + startSample)/2) +{ +if (!_isnan(influenceDistance)) // if influenceDistance has been set, we just use it without subdividing. +endSample = startSample = spliceT = GetTValueFromSValue(parent, t, tEnd.T, left, influenceDistance, excess); + +bool endCusp = spliceT == tEnd.T && tEnd.Cusp && tEnd.Side == 3; +auto multiSegmentSplitCurve = BezierRep(parent.Split(left ? spliceT : t, left ? t : spliceT) ); +double singleToMultiSegmentError = 0; +if (multiSegmentSplitCurve.p.size() == 4) { // if split curve is a single-segment bezier, then we it should be 100% accurate +singleSegmentSpliceCurve[0] = multiSegmentSplitCurve.p[0]; +singleSegmentSpliceCurve[3] = multiSegmentSplitCurve.p[3]; +singleSegmentSpliceCurve[1] = !left && isCusp && isCusp->Side == 3 ? singleSegmentSpliceCurve[0] : multiSegmentSplitCurve.p[1]; +singleSegmentSpliceCurve[2] = left && isCusp && isCusp->Side == 3 ? singleSegmentSpliceCurve[3] : multiSegmentSplitCurve.p[2]; +if (spliceT == endSample) +break; +} else { +const size_t SAMPLES_PER_SEGMENT = 20; +numMultiSegmentSamples = SampleBezier(multiSegmentSplitCurve, multiSegmentSamplePts, numMultiSegmentSamples, SAMPLES_PER_SEGMENT); + +auto endTan = (endSample == tEnd.T && tEnd.Cusp && tEnd.Side == (left ? 1 : 2)) ? parent.Tangent(endSample + (left ? -0.001 : 0.001)) : multiSegmentSplitCurve.Tangent(left ? 0.0 : 1.0*multiSegmentSplitCurve.MaxIndex()); +auto tHat1 = endCusp && left ? Vector2D() : !left ? parTangent : endTan; +auto tHat2 = endCusp && !left ? Vector2D() : left ? -parTangent : -endTan; +auto u = BezierRep::ChordLengthParameterize(multiSegmentSamplePts, 0, numMultiSegmentSamples-1); +GenerateBezier(multiSegmentSamplePts, 0, numMultiSegmentSamples-1, u, tHat1, tHat2, singleSegmentSpliceCurve); + +singleToMultiSegmentError = BezierRep::ComputeMaxError(multiSegmentSamplePts, 0, numMultiSegmentSamples-1, singleSegmentSpliceCurve, u); +} +if (singleToMultiSegmentError > (endCusp ? 5 : 1) * errorTolerance) +endSample = spliceT; +else startSample = spliceT; +} + +if (numMultiSegmentSamples) +delete [] multiSegmentSamplePts; +return spliceT; +} + +static void MoveCurveSplice(double t, Point2D splice[4], BezierRep::BezierLock &stepLock, double &extra, bool left, BezierRep::BezierLock *moveLock, const Vector2D &v, double influenceDistance, const Vector2D &smoothParTangent, double ctrlPtScale, double ctrlPtRotate) +{ +if (!_isnan(influenceDistance) && influenceDistance < 0) { +splice[left ? 2 : 1] = (splice[left ? 3 : 0] += v); +if (moveLock) { +moveLock->Side |= (left ? 1 : 2); +moveLock->Cusp = true; +} +} +else { +auto tan = (splice[left?2:1]-splice[left?3:0]); +splice[left?3:0] += v; +splice[left?2:1] = splice[left?3:0] + Mat::Rotate(ctrlPtRotate) * tan * ctrlPtScale; +if (influenceDistance > 0 && t <= stepLock.T ) { +LnSeg tangent(splice[left?3:0], tan == Vector2D() ? (splice[left?3:0]+smoothParTangent):splice[left?2:1]), otherTangent(splice[left?0:3], splice[left?1:2]); +auto inter = otherTangent.LnIntersection(tangent); +auto seglen = (splice[0] - splice[3]).Length(); +if (inter == Point2D::Null()) { +if (otherTangent.Length() == 0) { +auto ang = tangent.Direction().UnsignedAngle(splice[left?0:3]-splice[left?3:0]) / M_PI; +auto target = splice[left?3:0] + tangent.Direction() * .5519 * (ang < 0.01 ? 0 : 1) * seglen; + +splice[left?2:1] = (target * std::min(1.0, extra/25) + splice[left?2:1] * std::max(0.0, 1-extra/25)); +} +} else { +bool behind = tangent.ClosestFraction(inter) <= 0 && otherTangent.ClosestFraction(inter) <= 0; +auto tandir = tangent.ClosestFraction(inter) <=0 ? -tangent.Direction() : tangent.Direction(); +auto leglen = std::max(seglen/4, (splice[left?3:0]-inter).Length()); +auto aspect = std::sqrt(leglen / seglen / .7071); +auto modinter = splice[left?3:0] + tandir * leglen*std::min(1.0,.5519/aspect); +if (leglen / seglen > 2) { +if (tangent.Direction().Dot(otherTangent.Direction()) < 0) +modinter = splice[left?3:0] + tandir * seglen*(.5519); +else modinter = splice[left?3:0] + tandir * seglen*.7071; +} +if (behind && tangent.Direction().Dot(otherTangent.Direction()) < 0) +modinter = (splice[0] + splice[3])/2; +auto targetFrac = (modinter-splice[left?3:0]).Length(); +auto target = splice[left?3:0] + targetFrac * tangent.Direction(); +splice[left?2:1] = (target * std::min(1.0, extra/25) + splice[left?2:1] * std::max(0.0, 1-extra/25)); +if (extra> 25) { +//LnSeg tangent(splice[left?3:0], splice[left?2:1]), otherTangent(splice[left?0:3], splice[left?1:2]); +auto oextra = extra - 25; +auto otandir = otherTangent.ClosestFraction(inter) <=0 ? -otherTangent.Direction() : otherTangent.Direction(); +auto oleglen = std::max(seglen/4, (splice[!left?3:0]-inter).Length()); +auto oaspect = std::sqrt(oleglen / seglen / .7071); +auto omodinter = splice[!left?3:0] + otandir * oleglen*std::min(1.0,.5519/oaspect); +if (oleglen/ seglen > 2) { +if (tangent.Direction().Dot(otherTangent.Direction()) < 0) +omodinter = splice[!left?3:0] + tandir * seglen*(.5519); +else omodinter = splice[!left?3:0] + otandir * seglen *.7071; +} +if (behind && tangent.Direction().Dot(otherTangent.Direction()) < 0) +omodinter = (splice[0] + splice[3])/2; +auto otargetFrac = (omodinter-splice[!left?3:0]).Length(); +auto otarget = splice[!left?3:0] + otargetFrac * otherTangent.Direction(); +splice[!left?2:1] = (otarget * std::min(1.0, oextra/25) + splice[!left?2:1] * std::max(0.0, 1-oextra/25)); +} +} +} +} +} +static void MoveTAux (BezierRep &curve, double tMove, const Vector2D &v, bool moveEnds) +{ +auto &p = curve.p; +auto tstart = static_cast(tMove); +auto tend = static_cast(ceil(tMove)); +if (tend == tstart) +{ +if (tend == 0) +{ +tend = 1; +} +else +{ +tstart = tend - 1; +} +} +auto t = tMove - tstart; + +auto b0 = pow(1 - t, 3); +auto b1 = 3 * t * pow(1 - t, 2); +auto b2 = 3 * t * t * (1 - t); +auto b3 = t * t * t; + +auto ind = t < 0.4 ? 1 : t > 0.6 ? -1 : 0; +if (ind == 0) { +auto norm = (b1 + b2); +p[tstart * 3 + 1] += (b1/norm * v)/b1; +p[tend * 3 - 1] += (b2/norm * v)/b2; +} +else if (ind == 1 && b1 != 0) { +auto pt = curve[tMove] + v; +p[tstart * 3 +1].X = (pt.X - b0 * p[tstart*3].X - b3 * p[tend*3].X - b2 * p[tend*3-1].X) / b1; +p[tstart * 3 +1].Y = (pt.Y - b0 * p[tstart*3].Y - b3 * p[tend*3].Y - b2 * p[tend*3-1].Y) / b1; +} +else if (ind == -1 && b2 != 0) { +auto pt = curve[tMove] + v; +p[tend * 3 -1].X = (pt.X - b0 * p[tstart*3].X - b3 * p[tend*3].X - b1 * p[tstart*3+1].X) / b2; +p[tend * 3 -1].Y = (pt.Y - b0 * p[tstart*3].Y - b3 * p[tend*3].Y - b1 * p[tstart*3+1].Y) / b2; +} + +if (moveEnds) { +p[tstart * 3] += b0 * v; +p[tend * 3] += b3 * v; +} + +//p[tstart * 3 + 1] += b1 * v; +//p[tend * 3 - 1] += b2 * v; +//p[tstart*3] += v * b0; +//p[tend*3] += v * b3; + +// fx(t):=(1−t)3p1x+3t(1−t)2p2x+3t2(1−t)p3x+t3p4x +//fy(t):=(1−t)3p1y+3t(1−t)2p2y+3t2(1−t)p3y+t3p4y + +//Call the curve C(t) = b0(t) P0 + b1(t) P1 + b2(t) P2 + b3(t) P3. The user clicks at some point Q and drags to a new point R. +// 3. Compute c0 = b0(s); c1 = b1(s), c2 = b2(s), and c3 = b3(s), the coefficients of the control points at parameter s. + +//4. Adjust the Ps like this: + +//P0 += c0 * v +//P1 += c1 * v; +//P2 += c2 * v; +//P3 += c3 * v. +} +static void MoveTAdaptive (BezierRep &curve, double tMove, const Vector2D &v, std::vector &locked, bool rangeDrag, double errorTolerance, double influenceLDistance, double influenceRDistance, double ctrlPtLRotate, double ctrlPtRRotate, double ctrlPtScale, bool moveEnds) +{ +auto tleftMove = rangeDrag ? (static_cast(tMove) == tMove ? tMove-1 : static_cast(tMove)) : tMove; +auto trightMove = rangeDrag ? (static_cast(tMove) == tMove ? tMove+1 : static_cast(tMove)+1) : tMove; +auto leftStep = FindSplitIndex(curve,tleftMove, true, locked); +auto rightStep = FindSplitIndex(curve, trightMove, false, locked); +auto leftTan = curve.Tangent(std::max(0.0, tleftMove-1e-5)); +auto rightTan = curve.Tangent(std::min(curve.MaxIndex() * 1.0, trightMove + 1e-5)); +auto smoothParTangent = (leftTan + rightTan)/2; +auto smoothParDist = (curve[std::max(0.0, tMove-1)] - curve[std::min(curve.MaxIndex() * 1.0, tMove+1)]).Length()/4; + +BezierRep::BezierLock *isCusp = NULL, *moveLock = NULL; +for (auto &lck : locked) { +if (lck.T == tMove) { +moveLock = &lck; +if (influenceLDistance > 0 && moveLock && moveLock->Cusp) { +moveLock->Cusp = false; +if (moveLock->T*3 - 1 >= 0) +curve.p[moveLock->T * 3 - 1] = curve.p[moveLock->T * 3] - smoothParTangent*smoothParDist; +if (moveLock->T*3 + 1 < curve.p.size()) +curve.p[moveLock->T * 3 + 1] = curve.p[moveLock->T * 3] + smoothParTangent*smoothParDist; +} +if (lck.Cusp) +isCusp = &lck; +break; +} +} +if (moveLock && moveLock->T * 3 -1 >= 0 && moveLock->T*3+1 < curve.p.size() && +curve.p[moveLock->T*3-1] == curve.p[moveLock->T*3+1]) +leftTan = rightTan = smoothParTangent; +// splice the left side of the point that is moved +Point2D spliceL[4], spliceR[4]; +double lextra=0, rextra= 0; +auto l = GetSpliceCurve(curve, tleftMove, isCusp, leftStep, locked, v, spliceL, errorTolerance, abs(influenceLDistance), lextra); +auto r = GetSpliceCurve(curve, trightMove, isCusp, rightStep, locked, v, spliceR, errorTolerance, abs(influenceRDistance), rextra); + +BezierRep splicedCurve; +if (tMove != 0) { +if (l == -1) +return; + +MoveCurveSplice(l, spliceL, leftStep, lextra, true, moveLock, v, influenceLDistance, -rightTan, ctrlPtScale, ctrlPtLRotate); + +// add on the remaining left side of the curve +if (l != 0) +splicedCurve = curve.Split(0,l); + +// add the spliced left side of the curve +for (auto i = l == 0 ? 0 : 1; i < 4; i++) +splicedCurve.p.push_back(spliceL[i]); + +if (tleftMove != tMove) +{ +auto fixedL = curve.Split(tleftMove, tMove); +for (auto i = 1; i < 4; i++) +splicedCurve.p.push_back(fixedL[i] + v); +} +} + +auto moveIndex = splicedCurve.p.size(); +auto insertEnd = moveIndex; + +// splice the right side of the point that is moved +if (tMove != curve.MaxIndex()) { +if (r == -1) +return; + +if (trightMove != tMove) +{ +auto fixedL = curve.Split(tMove, trightMove); +for (auto i = 1; i < 4; i++) +splicedCurve.p.push_back(fixedL[i] + v); +} +MoveCurveSplice(r, spliceR, rightStep, rextra, false, moveLock, v, influenceRDistance, leftTan, ctrlPtScale, ctrlPtRRotate); + +// add the spliced right side of the curve +for (auto i = splicedCurve.p.size() == 0 ? 0 : 1; i < (r != curve.MaxIndex() ? 3 :4); i++) { +insertEnd++; +splicedCurve.p.push_back(spliceR[i]); +} +if (r != curve.MaxIndex()) { +insertEnd++; +for (auto & p : curve.Split(r, 1.0* curve.MaxIndex())) // add on the remaining right side of the curve +splicedCurve.p.push_back(p); +} +} + +for (auto & pt : splicedCurve.p) { +if (_isnan(pt.X)) +break; +} + +// adjust all lock t-values based on the size of the inserted splice segments +for (auto i = 0; i < locked.size(); i++) { +if (locked[i].T == tMove) +locked[i].T = moveIndex ==0 ? 0.0 : (moveIndex*1.0-1)/3; +else if (locked[i].T == l) +locked[i].T = std::ceil(l); +else if (locked[i].T == r) +locked[i].T = (insertEnd*1.0-1)/3; +else +locked[i].T = splicedCurve.NearestT(curve[locked[i].T]); +} +curve.p = splicedCurve.p; +} + + +BezierRep BezierRep::Rotate(const BezierRep &bez, const double angle, const Point2D ¢er) +{ +auto rot = Mat::Rotate(angle); +auto tri = Mat::Translate(-center); +auto tr = Mat::Translate( center); +BezierRep moved; +for (auto &p : bez.p) { +moved.p.push_back(tr * (rot * (tri *p))); +} +return moved; +} +BezierRep BezierRep::Move(const BezierRep &bez, const Vector2D &move) +{ +BezierRep moved; +for (auto &p : bez.p) { +moved.p.push_back(p+move); +} +return moved; +} +BezierRep BezierRep::Interpolate(const BezierRep &start, const BezierRep &end, double t) +{ +BezierRep interpolated; +for (auto p=0; p < start.p.size() && p < end.p.size(); p++) { +interpolated.p.push_back(start.p[p] + (end.p[p]-start.p[p])*t); +} +return interpolated; +} +std::vector> BezierRep::Find_intersections(const BezierRep & a, const BezierRep & b, size_t t_a_off, size_t t_b_off) +{ +auto ints = std::vector>(); +if (a.p.size() == 0 || b.p.size() == 0) +return ints; +if (a.p.size() == 4 && b.p.size() == 4) +{ +std::vector> parameters; +if (SmartRect::Intersect(a.Bounds(), b.Bounds())) +{ +const int depth = 6; +Point2D ap[4], bp[4]; +ap[0] = a.p[0]; +ap[1] = a.p[1]; +ap[2] = a.p[2]; +ap[3] = a.p[3]; +bp[0] = b.p[0]; +bp[1] = b.p[1]; +bp[2] = b.p[2]; +bp[3] = b.p[3]; +recursively_intersect(ap, 0, 1, depth, bp, 0, 1, depth, parameters); +} + +std::vector> modParameters; +for (size_t i = 0; i < parameters.size(); i++) { +modParameters.push_back(std::tuple(std::get<0>(parameters[i]) + t_a_off, std::get<1>(parameters[i]) + t_b_off)); +} +return modParameters; +} +for (size_t i = 0; i <= a.p.size() - 4; i += 3) +{ +for (size_t j = 0; j <= b.p.size() - 4; j += 3) +{ +std::vector tempVector2(4); +tempVector2[0] = a.p[i]; +tempVector2[1] = a.p[i + 1]; +tempVector2[2] = a.p[i + 2]; +tempVector2[3] = a.p[i + 3]; +std::vector tempVector3(4); +tempVector3[0] = b.p[j]; +tempVector3[1] = b.p[j + 1]; +tempVector3[2] = b.p[j + 2]; +tempVector3[3] = b.p[j + 3]; +auto fints = Find_intersections(BezierRep(tempVector2), BezierRep(tempVector3), t_a_off + i / 3, t_b_off + j / 3); +for (auto inter = 0; inter < fints.size(); inter++) { +bool newinter = true; +for (auto & oint : ints) +if (std::get<0>(oint) == std::get<0>(fints[inter]) && +std::get<1>(oint) == std::get<1>(fints[inter])) { +newinter = false; +break; +} +if (newinter) +ints.push_back(fints[inter]); +} +} +} +return ints; +} +std::vector > BezierRep::FitCurveSet( const Point2D d[], size_t dSize, double error, bool & isLoop) { +std::vector> fitSet; +fitSet.push_back(::FitCurve(d, dSize, error)); +return fitSet; +} +std::vector BezierRep::FitCurve( const std::vector &d, double error) +{ +return ::FitCurve(d.data(), d.size(), error); +} +std::vector BezierRep::FitOneCurve(const std::vector &d) +{ +return::FitOneCurve(d.data(), d.size()); +} + +std::vector BezierRep::Reparameterize( const Point2D d[], size_t first, size_t last, const std::vector &u, const Point2D bezCurve[4]) +{ +std::vector uPrime(last - first + 1); // New parameter values + +for (auto i = first; i <= last; i++) +{ +uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]); +} +return uPrime; +} +double BezierRep::ComputeMaxError(const Point2D d[], size_t first, size_t last, const Point2D bezCurve[4], const std::vector &u, size_t *splitPoint2D) +{ +double maxDist; // Maximum error + +if (splitPoint2D) +*splitPoint2D = (last - first + 1) / 2; +maxDist = 0.0; +for (auto i = first + 1; i < last; i++) +{ + double P[2]; // point on curve +EvalBezierFast(bezCurve, u[i-first], P); +double dx = P[0] - d[i].X;// offset from point to curve +double dy = P[1] - d[i].Y; +auto dist = sqrt(dx*dx+dy*dy); // Current error +if (dist >= maxDist) +{ +maxDist = dist; +if (splitPoint2D) +*splitPoint2D = i; +} +} +return maxDist; +} +std::vector BezierRep::ChordLengthParameterize(const Point2D d[], size_t first, size_t last) +{ +std::vector u(last-first+1);// Parameterization + +double prev = 0.0; +u[0] = prev; +for (auto i = first + 1; i <= last; i++) +{ +auto & lastd = d[i-1]; +auto & curd = d[i]; +auto dx = lastd.X - curd.X; +auto dy = lastd.Y - curd.Y; +prev = u[i - first] = prev + sqrt(dx*dx+dy*dy); +} + +double ulastfirst = u[last-first]; +for (auto i = first + 1; i <= last; i++) +{ +u[i - first] /= ulastfirst; +} + +return u; +} + +void BezierRep::InsertCpt(double tstart) +{ auto &allPts = p; + auto t_start_base = (size_t)tstart; + if (t_start_base >= MaxIndex()) + t_start_base = MaxIndex() - 1; + + Point2D left[4], right[4]; + splitCubic(&allPts[t_start_base*3], tstart - t_start_base, left, right); +std::vector newP; +for (size_t i = 0; i < t_start_base*3; i++) +newP.push_back(allPts[i]); +for (size_t i = 0; i < 4; i++) +newP.push_back(left[i]); +for (size_t i = 1; i < 4; i++) +newP.push_back(right[i]); +for (size_t i = t_start_base*3+4; i < allPts.size(); i++) +newP.push_back(allPts[i]); +p = newP; +} +std::vector BezierRep::Split(double tstart, double tend) const +{ + auto t_start_base = static_cast(tstart); + auto t_end_base = static_cast(tend); + auto maxIndex = MaxIndex(); + if (t_start_base >= maxIndex) + t_start_base = maxIndex - 1; + if (t_end_base >= maxIndex) + t_end_base = maxIndex - 1; + + Point2D split[4]; + std::vector splitPts(4); + if (t_start_base != t_end_base) + { +bool used4 = true; +if (tstart - t_start_base == 0) { +splitPts[0] = p[t_start_base*3]; +splitPts[1] = p[t_start_base*3+1]; +splitPts[2] = p[t_start_base*3+2]; +splitPts[3] = p[t_start_base*3+3]; +} else { +splitCubic(&(p[t_start_base*3]), tstart - t_start_base, NULL, split); +if (!(split[0].X == split[1].X && split[1].X == split[2].X && split[2].X == split[3].X && + split[0].Y == split[1].Y && split[1].Y == split[2].Y && split[2].Y == split[3].Y)) +for (size_t i = 0; i < 4; i++) +splitPts[i] = split[i]; +else { +splitPts[0] = split[0]; +used4 = false; +} +} + for (auto i = (t_start_base + 1) * 3; i < t_end_base * 3; i += 3) { +if (!used4) { +used4 = true; +splitPts[1] = p[i+1]; +splitPts[2] = p[i+2]; +splitPts[3] = p[i+3]; +} else { +splitPts.push_back(p[i+1]); +splitPts.push_back(p[i+2]); +splitPts.push_back(p[i+3]); +} +} + if (t_end_base * 3 < p.size() - 1 && tend - t_end_base != 0) + { +splitCubic(&(p[t_end_base *3]), tend - t_end_base, split, NULL); +if (!(split[0].X == split[1].X && split[1].X == split[2].X && split[2].X == split[3].X && +split[0].Y == split[1].Y && split[1].Y == split[2].Y && split[2].Y == split[3].Y)) +{ +if (!used4) { +splitPts[1] = split[1]; +splitPts[2] = split[2]; +splitPts[3] = split[3]; +} else { +splitPts.push_back(split[1]); +splitPts.push_back(split[2]); +splitPts.push_back(split[3]); +} + +} + } + } + else + { +Point2D tmp[4]; +splitCubic(&(p[t_end_base *3]), tend-t_end_base, tmp, NULL); +splitCubic(tmp, tstart==tend ? 0 : (tstart-t_end_base) / (tend-t_end_base), NULL, split); +for (auto i = 0; i < 4; i++) +splitPts[i] = split[i]; + } +return splitPts; +} +void BezierRep::MoveT(double tMove, const Vector2D & v, bool moveEnds, std::vector &locked, bool adaptive, bool rangeDrag, double errorTolerance, double influenceLDistance, double influenceRDistance, double ctrlPtLRotate, double ctrlPtRRotate, double ctrlPtScale) { +if (adaptive) +MoveTAdaptive(*this, tMove, v, locked, rangeDrag, errorTolerance, influenceLDistance, influenceRDistance, ctrlPtLRotate, ctrlPtRRotate, ctrlPtScale, moveEnds); +else MoveTAux(*this, tMove, v, moveEnds); +} +void BezierRep::GetPoint(const std::vector &p, double t, Point2D &result) +{ +while (t < 0) { +t += (p.size()-1)/3; +} +while (t > (p.size()-1)/3) { +t -= (p.size()-1)/3; +} +if (p.size() == 0) +return; +size_t t_base = 0; +if (p.size() > 4) +{ +t_base = static_cast(t); +if (t_base * 3 + 1 >= p.size() - 2) { +result.X = p.back().X; +result.Y = p.back().Y; +return; +} +t = t- t_base; +} + +Point2D bez[4] = { p[t_base * 3 + 0], p[t_base * 3 + 1], p[t_base * 3 + 2], p[t_base * 3 + 3] }; +double res[2]; +EvalBezierFast(bez, t, res); +result.X = res[0]; +result.Y = res[1]; +} +double BezierRep::NearestT(const Point2D &Pt) const +{ +if (p.size() < 1) +return 0; + +double closest = DBL_MAX; +double tclosest = -1; +for (size_t i = 0; i< MaxIndex(); i++) { +std::vector tmppts; +tmppts.push_back(p[i*3]); +tmppts.push_back(p[i*3+1]); +tmppts.push_back(p[i*3+2]); +tmppts.push_back(p[i*3+3]); +double tc; +auto nrst = NearestPointOnCurve(Pt, tmppts, &tc); +if ((nrst-Pt).Length() < closest) { +closest = (nrst-Pt).Length(); +tclosest = tc+i; +} +} +return tclosest; +} +Vector2D BezierRep::Tangent(double T) const +{ +while (T < 0) { +T += (p.size()-1)/3; +} +while (T > (p.size()-1)/3) { +T -= (p.size()-1)/3; +} +if (T == 0) +{ +for (auto i = 1; i < p.size(); i++) +if (p[i] != p[0]) +return (p[i]-p[0]).Normal(); +//else return Vector2D(); +return Vector2D(); +} +if (T == MaxIndex()) +{ +for (int i = static_cast(p.size())-2; i >= 0; i--) +if (p[i] != p.back()) +return (p.back()-p[i]).Normal(); +//else return Vector2D(); +return Vector2D(); +} + + +int segStart = 3 * (static_cast(T)); +auto t = T - static_cast(T); +auto A = p[segStart] - p[segStart]; +auto B = p[segStart + 1] - p[segStart]; +// if (B == Vector2D() && segStart > 0 && (p[segStart-1] - p[segStart]) == Vector2D()) +// return Vector2D(); +auto C = p[segStart + 2] - p[segStart]; +auto D = p[segStart + 3] - p[segStart]; +// note that abcd are aka x0 x1 x2 x3 + +auto tan = -3*A*(1-t)*(1-t) + B*(3*(1-t)*(1-t) - 6*(1 - t)*t) + C*(6*(1 - t)*t - 3*t*t) + 3*D*t*t; +return tan.Normal(); + +// the four coefficients .. +// A = x3 - 3 * x2 + 3 * x1 - x0 +// B = 3 * x2 - 6 * x1 + 3 * x0 +// C = 3 * x1 - 3 * x0 +// D = x0 +// +// and then... +// Vx = 3At2 + 2Bt + C + +// first calcuate what are usually know as the coeffients, +// they are trivial based on the four control points: + +//double C1x = (D.X - (3.0 * C.X) + (3.0 * B.X) - A.X); +//double C2x = ((3.0 * C.X) - (6.0 * B.X) + (3.0 * A.X)); +//double C3x = ((3.0 * B.X) - (3.0 * A.X)); +//double C4x = (A.X); // (not needed for this calculation) + +//double C1y = (D.Y - (3.0 * C.Y) + (3.0 * B.Y) - A.Y); +//double C2y = ((3.0 * C.Y) - (6.0 * B.Y) + (3.0 * A.Y)); +//double C3y = ((3.0 * B.Y) - (3.0 * A.Y)); +//double C4y = (A.Y); // (not needed for this calculation) + +// finally it is easy to calculate the slope element, using those coefficients: + +//Vector2D vec(((3.0 * C1x * t * t) + (2.0 * C2x * t) + C3x), ((3.0 * C1y * t * t) + (2.0 * C2y * t) + C3y)); + +//vec.Normalize(); +//return vec; +// note that this routine works for both the x and y side; +// simply run this routine twice, once for x once for y +// note that there are sometimes said to be 8 (not 4) coefficients, +// these are simply the four for x and four for y, calculated as above in each case. +} +bool BezierRep::IsDiscontinuity(int t) const +{ +if (t == 0 || t == MaxIndex()) { +if (p.front() != p.back()) +return true; + +auto inTan = (p[1]-p[0]).Normal(); +auto outTan = (p[p.size()-2]-p[p.size()-1]).Normal(); +if (_isnan(inTan.X) || _isnan(outTan.X) || inTan.Dot(outTan) > -0.998) +return true; +} + +return false; +} +Point2D BezierRep::Reflect(const Point2D &srcPt) const +{ +auto nrstT = NearestT(srcPt); +auto nrst = (*this)[nrstT]; +if (nrstT < 1e-4) +nrstT = 0; +if ((MaxIndex()-nrstT) < 1e-4) +nrstT = static_cast(MaxIndex()); +if (nrstT == 0 || nrstT == MaxIndex() || (p.size()== 4 && p[0]==p[1] && p[2]==p[3])) { +LnSeg seg(nrst, Tangent(nrstT)); +nrst = seg.LnClosestPoint(srcPt); +} +auto normal = Normal(nrstT); +auto offset = (nrst - srcPt).Length(); +if (normal.Dot(srcPt-nrst) > 0) +normal = -normal; +return nrst + normal * offset; +} +BezierRep BezierRep::Reflect(const BezierRep &b) const { +std::vector reflected; +for (auto &p : b.p) { +reflected.push_back(Reflect(p)); +} +return BezierRep(reflected); +} + +// +// ReflectAndClip - Clips one curve against another, then reflects the clipped segments. +// This returns two lists of reflected segments corresponding to reflections of segments which were on the same side as the +// initial point of the stroke (relative to the reflection axis) and those which which were on the opposite side. +// +std::vector>> BezierRep::ReflectAndClip(const BezierRep &b) const +{ +BezierRep testRep = *this; +if (MaxIndex() == 1 && p[0]==p[1] && p[2]==p[3]) { +Vector2D dir = p[3]-p[0]; +std::vector pts; +pts.push_back(p[0] - 10000 * dir); +pts.push_back(p[0] - 10000 * dir); +pts.push_back(p[3] + 10000 * dir); +pts.push_back(p[3] + 10000 * dir); +testRep = BezierRep(pts); +} +auto ints = Find_intersections(testRep, b); + +std::vector> flipSets; +std::vector fragments[2]; +if (ints.size() == 0) { +fragments[0].push_back(b); +flipSets.push_back(fragments[0]); +} else { +double start = 0; +int which = 0; +for (auto &i: ints) { +auto split = b.Split(start, std::get<1>(i)); +fragments[which++%2].push_back(split); +start = std::get<1>(i); +} +fragments[which++%2].push_back(b.Split(start, static_cast(b.MaxIndex()))); + +} + +std::vector>> mirroredSides; +for (auto &side: fragments) { +std::vector> mirrors; +for (auto &f : side) +mirrors.push_back(std::tuple(f, Reflect(f))); +mirroredSides.push_back(mirrors); +} +return mirroredSides; +} +#ifdef later +if (!_isnan(influenceDistance) && influenceDistance < 0) { +spliceL[2] = (spliceL[3] += v); +if (moveLock) { +moveLock->Side |= 1; +moveLock->Cusp = true; +} +} +else if (influenceDistance > 0 && l <= leftStep.T && spliceL[2] != spliceL[3]) { +auto lTan = (spliceL[2]-spliceL[3]); +spliceL[2] = spliceL[3] + Mat::Rotate(ctrlPtRotate) * lTan * ctrlPtScale + v; +spliceL[3] += v; + +LnSeg tangent(spliceL[3], spliceL[2]), otherTangent(spliceL[0], spliceL[1]); +auto inter = otherTangent.LnIntersection(tangent); +if (inter != Point2D::Null()) { +auto tandir = tangent.ClosestFraction(inter) <=0 ? -tangent.Direction() : tangent.Direction(); +auto aspect = (spliceL[3]-inter).Length() / (spliceL[0]-spliceL[3]).Length() / .7071; +auto modinter = spliceL[3] + tandir * (spliceL[3]-inter).Length()*std::min(1.0,.5519/aspect); + +auto targetFrac = (modinter-spliceL[3]).Length(); +auto target = spliceL[3] + targetFrac * tangent.Direction(); +spliceL[2] = (target * std::min(1.0, lextra/25) + spliceL[2] * std::max(0.0, 1-lextra/25)); +} +} else { +auto lTan = (spliceL[2]-spliceL[3]); +if (lTan == Vector2D() && influenceDistance > 0) { +if (moveLock) +moveLock->Cusp = false; +spliceL[2] = spliceL[3] + v - smoothParTangent.Normal()*lextra; +} else +spliceL[2] = spliceL[3] + Mat::Rotate(ctrlPtRotate) * lTan * ctrlPtScale + v; +spliceL[3] += v; +} +#endif +#if 0 +if (!_isnan(influenceDistance) && influenceDistance < 0) { +spliceR[1] = (spliceR[0] += v); +if (moveLock) { +moveLock->Side |= 2; +moveLock->Cusp = true; +} +} +else +if (influenceDistance > 0 && r>=rightStep.T && spliceR[1] != spliceR[0]) { + +auto rTan = (spliceR[1]-spliceR[0]); +spliceR[1] = spliceR[0] + Mat::Rotate(ctrlPtRotate) * rTan* ctrlPtScale + v; +spliceR[0] += v; + +LnSeg tangent(spliceR[0], spliceR[1]), otherTangent(spliceR[3], spliceR[2]); +auto inter = otherTangent.LnIntersection(tangent); +if (inter != Point2D::Null()) { +auto tandir = tangent.ClosestFraction(inter) <=0 ? -tangent.Direction() : tangent.Direction(); +//auto aspect = (spliceR[0]-inter).Length() / (spliceR[3]-inter).Length(); +auto aspect = (spliceR[0]-inter).Length() / (spliceR[0]-spliceR[3]).Length() / .7071; +auto modinter = spliceR[0] + tandir * (spliceR[0]-inter).Length()*std::min(1.0,.5519/aspect); + +auto targetFrac = (modinter-spliceR[0]).Length(); +auto target = spliceR[0] + targetFrac * tangent.Direction(); +spliceR[1] = (target * std::min(1.0, rextra/25) + spliceR[1] * std::max(0.0, 1-rextra/25)); +} +} else { +auto rTan = (spliceR[1]-spliceR[0]); +if (rTan == Vector2D() && influenceDistance > 0) { +if (moveLock) +moveLock->Cusp = false; +spliceR[1] = spliceR[0] + v + smoothParTangent.Normal()*rextra; +} else +spliceR[1] = spliceR[0] + Mat::Rotate(ctrlPtRotate) * rTan* ctrlPtScale + v; +spliceR[0] += v; +} +#endif + +*/ \ No newline at end of file diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index df803ba31..a91e74c44 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -102,7 +102,7 @@ export class InkControlPtHandles extends React.Component { @action onDelete = (e: KeyboardEvent) => { if (["-", "Backspace", "Delete"].includes(e.key)) { - InkStrokeProperties.Instance?.deletePoints(this.props.inkView); + InkStrokeProperties.Instance?.deletePoints(this.props.inkView, e.shiftKey); e.stopPropagation(); } } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 8ad4864f9..4808bbc77 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,16 +1,18 @@ import { Bezier } from "bezier-js"; 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; @@ -139,18 +141,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)); + var samples: Point[] = []; + var startDir = { x: 0, y: 0 }; + var endDir = { x: 0, y: 0 }; + for (var i = 0; i < splicedPoints.length / 4; i++) { + var 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) { + var 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) diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index ecc82a580..8ff080f81 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -132,7 +132,7 @@ export class InkingStroke extends ViewBoxBaseComponent wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1)); + }), isEditing, isEditing, action(() => wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1))); } } -- cgit v1.2.3-70-g09d2 From 91247d583e5e4c7205a1ed764dd0e3a12af3be25 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 30 Nov 2021 23:37:49 -0500 Subject: fixed warnings/errors. added inkingStroke comments. need to double-click now to add a point to an ink stroke. --- src/client/util/bezierFit.ts | 242 ++++++++++----------- src/client/views/DocumentDecorations.tsx | 8 +- src/client/views/GlobalKeyHandler.ts | 2 +- src/client/views/InkControlPtHandles.tsx | 34 ++- src/client/views/InkStroke.scss | 5 +- src/client/views/InkStrokeProperties.ts | 15 +- src/client/views/InkTangentHandles.tsx | 28 +-- src/client/views/InkingStroke.tsx | 174 ++++++++------- src/client/views/MainView.tsx | 1 - src/client/views/PropertiesView.tsx | 21 +- src/client/views/StyleProvider.tsx | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- src/client/views/search/SearchBox.tsx | 18 +- 13 files changed, 269 insertions(+), 283 deletions(-) (limited to 'src/client/views/InkStrokeProperties.ts') diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 57c6dbbde..784bb2e18 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -1,5 +1,4 @@ import { Point } from "../../pen-gestures/ndollar"; -import { max } from "lodash"; class SmartRect { minx: number = 0; @@ -21,45 +20,38 @@ class SmartRect { public ContainsPercentage(other: SmartRect, axis: Point) { var ret = 0; - var minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y); - var maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y); - ret = maxx > minx ? (maxx - minx) / (axis == new Point(1, 0) ? other.Width : other.Height) : 0; + const minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y); + const maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y); + ret = maxx > minx ? (maxx - minx) / (axis === new Point(1, 0) ? other.Width : other.Height) : 0; return ret; } public static Bounds(p: Point[]) { - var r = new SmartRect(); + const r = new SmartRect(); if (p.length > 0) { r.minx = p[0].X; // These are the most likely to be extremal r.maxx = p.lastElement().X; r.miny = p[0].Y; r.maxy = p.lastElement().Y; - if (r.minx > r.maxx) { - var tmp = r.minx; - r.minx = r.maxx; - r.maxx = tmp; - } - if (r.miny > r.maxy) { - var tmp = r.miny; - r.miny = r.maxy; - r.maxy = tmp; - } + if (r.minx > r.maxx) [r.minx, r.maxx] = [r.maxx, r.minx]; + if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; - for (var pt of p) { - if (pt.X < r.minx) + for (const pt of p) { + if (pt.X < r.minx) { r.minx = pt.X; - else if (pt.X > r.maxx) + } else if (pt.X > r.maxx) { r.maxx = pt.X; - - if (pt.Y < r.miny) + } + if (pt.Y < r.miny) { r.miny = pt.Y; - else if (pt.Y > r.maxy) + } else if (pt.Y > r.maxy) { r.maxy = pt.Y; + } } } return r; } -}; +} function Normalize(p: Point) { const len = Math.sqrt(p.X * p.X + p.Y * p.Y); @@ -67,7 +59,7 @@ function Normalize(p: Point) { } function ReparameterizeBezier(d: Point[], first: number, last: number, u: number[], bezCurve: Point[]) { - var uPrime = new Array(last - first + 1); // New parameter values + const uPrime = new Array(last - first + 1); // New parameter values for (var i = first; i <= last; i++) { uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]); @@ -76,36 +68,36 @@ function ReparameterizeBezier(d: Point[], first: number, last: number, u: number } function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Point[], u: number[]) { var maxError = 0; // Maximum error - var splitPoint2D = (last - first + 1) / 2; for (var i = first + 1; i < last; i++) { - var P = [0, 0]; // point on curve + const P = [0, 0]; // point on curve EvalBezierFast(bezCurve, u[i - first], P); - var dx = P[0] - d[i].X;// offset from point to curve - var dy = P[1] - d[i].Y; - var dist = Math.sqrt(dx * dx + dy * dy); // Current error + const dx = P[0] - d[i].X;// offset from point to curve + const dy = P[1] - d[i].Y; + const dist = Math.sqrt(dx * dx + dy * dy); // Current error if (dist >= maxError) { maxError = dist; - if (splitPoint2D) + if (splitPoint2D) { splitPoint2D = i; + } } } return { maxError, splitPoint2D }; } function ChordLengthParameterize(d: Point[], first: number, last: number) { - var u = new Array(last - first + 1);// Parameterization + const u = new Array(last - first + 1);// Parameterization var prev = 0.0; u[0] = prev; for (var i = first + 1; i <= last; i++) { - var lastd = d[i - 1]; - var curd = d[i]; - var dx = lastd.X - curd.X; - var dy = lastd.Y - curd.Y; + const lastd = d[i - 1]; + const curd = d[i]; + const dx = lastd.X - curd.X; + const dy = lastd.Y - curd.Y; prev = u[i - first] = prev + Math.sqrt(dx * dx + dy * dy); } - var ulastfirst = u[last - first]; + const ulastfirst = u[last - first]; for (var i = first + 1; i <= last; i++) { u[i - first] /= ulastfirst; } @@ -116,12 +108,12 @@ function ChordLengthParameterize(d: Point[], first: number, last: number) { * B0, B1, B2, B3 : * Bezier multipliers */ -function B0(u: number) { var tmp = 1.0 - u; return tmp * tmp * tmp; } -function B1(u: number) { var tmp = 1.0 - u; return 3 * u * tmp * tmp; } -function B2(u: number) { var tmp = 1.0 - u; return 3 * u * u * tmp; } +function B0(u: number) { const tmp = 1.0 - u; return tmp * tmp * tmp; } +function B1(u: number) { const tmp = 1.0 - u; return 3 * u * tmp * tmp; } +function B2(u: number) { const tmp = 1.0 - u; return 3 * u * u * tmp; } function B3(u: number) { return u * u * u; } function bounds(p: Point[]) { - var r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal + const r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal if (r.minx > r.maxx) (r.minx, r.maxx); if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; // swap min & max @@ -137,10 +129,9 @@ function bounds(p: Point[]) { } - function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) { - var sz = 4; - var Vtemp = new Array>(4); + const sz = 4; + const Vtemp = new Array>(4); for (var i = 0; i < 4; i++) Vtemp[i] = new Array(4); /* Copy control points */ @@ -153,8 +144,8 @@ function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) { /* Triangle computation */ for (var i = 1; i < sz; i++) { for (var j = 0; j < sz - i; j++) { - var a = Vtemp[i - 1][j]; - var b = Vtemp[i - 1][j + 1]; + const a = Vtemp[i - 1][j]; + const b = Vtemp[i - 1][j + 1]; Vtemp[i][j].X = b.X * t + a.X * (1 - t); Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t); // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t); } @@ -214,14 +205,14 @@ function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) { */ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: number, b: Point[], u0: number, u1: number, depthb: number, parameters: number[][]) { if (deptha > 0) { - var a1 = new Array(4), a2 = new Array(4); + const a1 = new Array(4), a2 = new Array(4); splitCubic(a, 0.5, a1, a2); - var tmid = (t0 + t1) * 0.5; + const tmid = (t0 + t1) * 0.5; deptha--; if (depthb > 0) { - var b1 = new Array(4), b2 = new Array(4); + const b1 = new Array(4), b2 = new Array(4); splitCubic(b, 0.5, b1, b2); - var umid = (u0 + u1) * 0.5; + const umid = (u0 + u1) * 0.5; depthb--; if (SmartRect.Intersect(bounds(a1), bounds(b1))) { recursively_intersect(a1, t0, tmid, deptha, b1, u0, umid, depthb, parameters); @@ -247,9 +238,9 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe } else { if (depthb > 0) { - var b1 = new Array(4), b2 = new Array(4); + const b1 = new Array(4), b2 = new Array(4); splitCubic(b, 0.5, b1, b2); - var umid = (u0 + u1) * 0.5; + const umid = (u0 + u1) * 0.5; depthb--; if (SmartRect.Intersect(bounds(a), bounds(b1))) { recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters); @@ -260,20 +251,20 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe } else // Both segments are fully subdivided; now do line segments { - var xlk = a[3].X - a[0].X; - var ylk = a[3].Y - a[0].Y; - var xnm = b[3].X - b[0].X; - var ynm = b[3].Y - b[0].Y; - var xmk = b[0].X - a[0].X; - var ymk = b[0].Y - a[0].Y; - var det = xnm * ylk - ynm * xlk; - if (1.0 + det == 1.0) { + const xlk = a[3].X - a[0].X; + const ylk = a[3].Y - a[0].Y; + const xnm = b[3].X - b[0].X; + const ynm = b[3].Y - b[0].Y; + const xmk = b[0].X - a[0].X; + const ymk = b[0].Y - a[0].Y; + const det = xnm * ylk - ynm * xlk; + if (1.0 + det === 1.0) { return; } else { - var detinv = 1.0 / det; - var s = (xnm * ymk - ynm * xmk) * detinv; - var t = (xlk * ymk - ylk * xmk) * detinv; + const detinv = 1.0 / det; + const s = (xnm * ymk - ynm * xmk) * detinv; + const t = (xlk * ymk - ylk * xmk) * detinv; if ((s < 0.0) || (s > 1.0) || (t < 0.0) || (t > 1.0) || Number.isNaN(s) || Number.isNaN(t)) { return; } @@ -296,7 +287,7 @@ function EvalBezier(V: Point[], degree: number, t: number, result: number[]) { return; } - var Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points + const Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points /* Copy array */ for (var i = 0; i <= degree; i++) { @@ -317,14 +308,11 @@ function EvalBezier(V: Point[], degree: number, t: number, result: number[]) { } function EvalBezierFast(p: Point[], t: number, result: number[]) { - var n = 3; - var u: number, bc: number, tn: number, tmpX: number, tmpY: number; - u = 1.0 - t; - bc = 1; - tn = 1; - - tmpX = p[0].X * u; - tmpY = p[0].Y * u; + const n = 3; + const u = 1.0 - t; + var bc = 1, tn = 1; + var tmpX = p[0].X * u; + var tmpY = p[0].Y * u; tn = tn * t; bc = bc * (n - 1 + 1) / 1; tmpX = (tmpX + tn * bc * p[1].X) * u; @@ -342,22 +330,21 @@ function EvalBezierFast(p: Point[], t: number, result: number[]) { *Approximate unit tangents at endpoints and "center" of digitized curve */ function ComputeLeftTangent(d: Point[], end: number) { - var use = 1; - var tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y); + const use = 1; + const tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y); return Normalize(tHat1); } function ComputeRightTangent(d: Point[], end: number) { - var available = end; - var use = 1; - var tHat2 = new Point(d[end - use].X - d[end].X, d[end - use].Y - d[end].Y); + const use = 1; + const tHat2 = new Point(d[end - use].X - d[end].X, d[end - use].Y - d[end].Y); return Normalize(tHat2); } function ComputeCenterTangent(d: Point[], center: number) { - if (center == 0) { + if (center === 0) { return ComputeLeftTangent(d, center); } - var V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1]; - var V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1]; + const V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1]; + const V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1]; var tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0); if (tHatCenter === new Point(0, 0)) { tHatCenter = new Point(-V1.Y, -V1.X);// V1.Perp(); @@ -365,15 +352,15 @@ function ComputeCenterTangent(d: Point[], center: number) { return Normalize(tHatCenter); } function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[], tHat1: Point, tHat2: Point, result: Point[] /* must be prealloacted to size 4 */) { - var nPts = last - first + 1; // Number of pts in sub-curve - var Ax = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); - var Ay = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); + const nPts = last - first + 1; // Number of pts in sub-curve + const Ax = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); + const Ay = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); /* Compute the A's */ for (var i = 0; i < nPts; i++) { - var uprime = uPrime[i]; - var b1 = B1(uprime); - var b2 = B2(uprime); + const uprime = uPrime[i]; + const b1 = B1(uprime); + const b2 = B2(uprime); Ax[i] = tHat1.X * b1; Ay[i] = tHat1.Y * b1; Ax[i + 1 * nPts] = tHat2.X * b2; @@ -381,44 +368,41 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[ } /* Create the C and X matrices */ - var C = [[0, 0], [0, 0]]; - var df = d[first]; - var dl = d[last]; + const C = [[0, 0], [0, 0]]; + const df = d[first]; + const dl = d[last]; - var X = [0, 0]; // Matrix X + const X = [0, 0]; // Matrix X for (var i = 0; i < nPts; i++) { C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i]; //A[i+0*nPts].Dot(A[i+0*nPts]); C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts];//A[i+0*nPts].Dot(A[i+1*nPts]); C[1][0] = C[0][1]; C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts];// A[i+1*nPts].Dot(A[i+1*nPts]); - var uprime = uPrime[i]; - var b0plb1 = B0(uprime) + B1(uprime); - var b2plb3 = B2(uprime) + B3(uprime); - var df1 = d[first + i]; - var tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3)); - var tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3)); + const uprime = uPrime[i]; + const b0plb1 = B0(uprime) + B1(uprime); + const b2plb3 = B2(uprime) + B3(uprime); + const df1 = d[first + i]; + const tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3)); + const tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3)); X[0] += Ax[i] * tmpX + Ay[i] * tmpY; // A[i+0*nPts].Dot(tmp) X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; //A[i+1*nPts].Dot(tmp) } /* Compute the determinants of C and X */ - var det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1]; - var det_C0_X = C[0][0] * X[1] - C[0][1] * X[0]; - var det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1]; + const det_C0_C1 = (C[0][0] * C[1][1] - C[1][0] * C[0][1]) || (C[0][0] * C[1][1]) * 10e-12; + const det_C0_X = C[0][0] * X[1] - C[0][1] * X[0]; + const det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1]; /* Finally, derive alpha values */ - if (det_C0_C1 == 0.0) { - det_C0_C1 = (C[0][0] * C[1][1]) * 10e-12; - } - var alpha_l = (det_C0_C1 == 0) ? 0.0 : det_X_C1 / det_C0_C1; - var alpha_r = (det_C0_C1 == 0) ? 0.0 : det_C0_X / det_C0_C1; + var alpha_l = (det_C0_C1 === 0) ? 0.0 : det_X_C1 / det_C0_C1; + var alpha_r = (det_C0_C1 === 0) ? 0.0 : det_C0_X / det_C0_C1; /* If alpha negative, use the Wu/Barsky heuristic (see text) */ /* (if alpha is 0, you get coincident control points that lead to * divide by zero in any subsequent NewtonRaphsonRootFind() call. */ - var segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y)); - var epsilon = 1.0e-6 * segLength; + const segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y)); + const epsilon = 1.0e-6 * segLength; if (alpha_l < epsilon || alpha_r < epsilon) { /* fall back on standard (probably inaccurate) formula, and subdivide further if needed. */ alpha_l = alpha_r = segLength / 3.0; @@ -432,15 +416,15 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[ result[3] = dl; result[1] = new Point(df.X + (tHat1.X * alpha_l), df.Y + (tHat1.Y * alpha_l)); result[2] = new Point(dl.X + (tHat2.X * alpha_r), dl.Y + (tHat2.Y * alpha_r)); - } + /* * NewtonRaphsonRootFind : * Use Newton-Raphson iteration to find better root. */ function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) { - var Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; // Q' and Q'' - var Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q'' + const Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; // Q' and Q'' + const Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q'' /* Compute Q(u) */ var uPrime: number; // Improved u @@ -463,25 +447,24 @@ function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) { EvalBezier(Q2, 1, u, Q2_u); /* Compute f(u)/f'(u) */ - var numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]); - var denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]); - if (denominator == 0.0) + const numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]); + const denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]); + if (denominator === 0.0) { uPrime = u; - else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */ + } else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */ return uPrime; } function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: Point, error: number, result: Point[]) { - var bezCurve = new Array(4); // Control points of fitted Bezier curve - var splitPoint2D: number; // Point2D to split point set at - var maxIterations = 4; // Max times to try iterating + const bezCurve = new Array(4); // Control points of fitted Bezier curve + const maxIterations = 4; // Max times to try iterating - var iterationError = error * error; // Error below which you try iterating - var nPts = last - first + 1; // Number of points in subset + const iterationError = error * error; // Error below which you try iterating + const nPts = last - first + 1; // Number of points in subset /* Use heuristic if region only has two points in it */ - if (nPts == 2) { - var dist = Math.sqrt((d[first].X - d[last].X) * (d[first].X - d[last].X) + (d[first].Y - d[last].Y) * (d[first].Y - d[last].Y)) / 3; + if (nPts === 2) { + const dist = Math.sqrt((d[first].X - d[last].X) * (d[first].X - d[last].X) + (d[first].Y - d[last].Y) * (d[first].Y - d[last].Y)) / 3; bezCurve[0] = d[first]; bezCurve[3] = d[last]; @@ -499,7 +482,7 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: GenerateBezier(d, first, last, u, tHat1, tHat2, bezCurve); /* Find max deviation of points to fitted curve */ - var { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u); // Maximum fitting error + const { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u); // Maximum fitting error if (maxError < Math.abs(error)) { result.push(bezCurve[1]); result.push(bezCurve[2]); @@ -511,9 +494,9 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: /* and iteration */ if (maxError < iterationError) { for (var i = 0; i < maxIterations; i++) { - var uPrime = ReparameterizeBezier(d, first, last, u, bezCurve); // Improved parameter values + const uPrime = ReparameterizeBezier(d, first, last, u, bezCurve); // Improved parameter values GenerateBezier(d, first, last, uPrime, tHat1, tHat2, bezCurve); - var { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, uPrime); + const { maxError } = ComputeMaxError(d, first, last, bezCurve, uPrime); if (maxError < error) { result.push(bezCurve[1]); result.push(bezCurve[2]); @@ -525,16 +508,15 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: } /* Fitting failed -- split at max error point and fit recursively */ - var tHatCenter = splitPoint2D >= last - 1 ? ComputeRightTangent(d, splitPoint2D) : ComputeCenterTangent(d, splitPoint2D); + const tHatCenter = splitPoint2D >= last - 1 ? ComputeRightTangent(d, splitPoint2D) : ComputeCenterTangent(d, splitPoint2D); FitCubic(d, first, splitPoint2D, tHat1, tHatCenter, error, result); - var negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y); + const negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y); FitCubic(d, splitPoint2D, last, negThatCenter, tHat2, error, result); } export function FitCurve(d: Point[], error: number) { - var tHat1 = ComputeLeftTangent(d, 0); // Unit tangent vectors at endpoints - var tHat2 = ComputeRightTangent(d, d.length - 1); - var result: Point[] = []; - result.push(d[0]); + const tHat1 = ComputeLeftTangent(d, 0); // Unit tangent vectors at endpoints + const tHat2 = ComputeRightTangent(d, d.length - 1); + const result = [d[0]]; FitCubic(d, 0, d.length - 1, tHat1, tHat2, error, result); return result; } @@ -543,14 +525,14 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) { tHat2 = tHat2 ?? Normalize(ComputeRightTangent(d, d.length - 1)); tHat2 = new Point(-tHat2.X, -tHat2.Y); var u = ChordLengthParameterize(d, 0, d.length - 1); - var bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; + const bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls); /* Find max deviation of points to fitted curve */ var finalCtrls = bezCurveCtrls.slice(); var { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u); for (var i = 0; i < 10; i++) { - var uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls); // Improved parameter values + const uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls); // Improved parameter values GenerateBezier(d, 0, d.length - 1, uPrime, tHat1, tHat2, bezCurveCtrls); - var { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime); + const { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime); if (maxError < error) { error = maxError; finalCtrls = bezCurveCtrls.slice(); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 1c0b1b995..7d3959eba 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -203,9 +203,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P const centerPoint = { X: (left + right) / 2, Y: (top + bottom) / 2 }; const previousPoint = { X: e.clientX, Y: e.clientY }; const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; - const angle = InkStrokeProperties.Instance?.angleChange(previousPoint, movedPoint, centerPoint); + const angle = InkStrokeProperties.Instance.angleChange(previousPoint, movedPoint, centerPoint); const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); - angle && InkStrokeProperties.Instance?.rotateInk(selectedInk, -angle, pt); + angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, pt); return false; }, () => { @@ -226,7 +226,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P this._inkDragDocs = DragManager.docsBeingDragged .filter(doc => doc.type === DocumentType.INK) .map(doc => { - if (InkStrokeProperties.Instance?._lock) { + if (InkStrokeProperties.Instance._lock) { Doc.SetNativeHeight(doc, NumCast(doc._height)); Doc.SetNativeWidth(doc, NumCast(doc._width)); } @@ -249,7 +249,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P const first = SelectionManager.Views()[0]; let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; var fixedAspect = Doc.NativeAspect(first.layoutDoc); - InkStrokeProperties.Instance?._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) + InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc)); const resizeHdl = this._resizeHdlId.split(" ")[0]; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 364bf05e2..d5e0ed962 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -112,7 +112,7 @@ export class KeyManager { case "escape": DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; - InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlButton = false); + InkStrokeProperties.Instance._controlButton = false; CurrentUserUtils.SelectedTool = InkTool.None; var doDeselect = true; if (SnappingManager.GetIsDragging()) { diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index a91e74c44..76ce73b0d 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -44,23 +44,23 @@ export class InkControlPtHandles extends React.Component { @action onControlDown = (e: React.PointerEvent, controlIndex: number): void => { const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen; - if (InkStrokeProperties.Instance && ptFromScreen) { + if (ptFromScreen) { const order = controlIndex % 4; const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length; const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length; const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number")); - const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex; + const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex; setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] }); const inkMoveStart = ptFromScreen({ X: 0, Y: 0 }); - InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); + InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); return false; }), action(() => { if (this.controlUndo) { - InkStrokeProperties.Instance?.snapControl(this.props.inkView, controlIndex); + InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex); } this.controlUndo?.end(); this.controlUndo = undefined; @@ -75,11 +75,11 @@ export class InkControlPtHandles extends React.Component { } else { if (brokenIndices?.includes(equivIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); - InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB); + InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB); } if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); - InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB); + InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB); } } this.controlUndo?.end(); @@ -102,7 +102,7 @@ export class InkControlPtHandles extends React.Component { @action onDelete = (e: KeyboardEvent) => { if (["-", "Backspace", "Delete"].includes(e.key)) { - InkStrokeProperties.Instance?.deletePoints(this.props.inkView, e.shiftKey); + InkStrokeProperties.Instance.deletePoints(this.props.inkView, e.shiftKey); e.stopPropagation(); } } @@ -111,11 +111,7 @@ export class InkControlPtHandles extends React.Component { * Changes the current selected control point. */ @action - changeCurrPoint = (i: number) => { - if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance._currentPoint = i; - } - } + changeCurrPoint = (i: number) => InkStrokeProperties.Instance._currentPoint = i render() { // Accessing the current ink's data and extracting all control points. @@ -202,9 +198,9 @@ export class InkEndPtHandles extends React.Component { const scaling = v2len / v1len; const v1n = { x: v1.x / v1len, y: v1.y / v1len }; const v2n = { x: v2.x / v2len, y: v2.y / v2len }; - const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y) - InkStrokeProperties.Instance?.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n); - InkStrokeProperties.Instance?.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y }); + const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y); + InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n); + InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y }); return false; }, action(() => { this.controlUndo?.end(); @@ -214,7 +210,7 @@ export class InkEndPtHandles extends React.Component { } render() { - const hdl = (pt: PointData, dragFunc: (e: React.PointerEvent) => void) => void) => { strokeWidth={0} onPointerLeave={action(() => this._overStart = false)} onPointerEnter={action(() => this._overStart = true)} - onPointerDown={e => dragFunc(e)} + onPointerDown={dragFunc} pointerEvents="all" />; return ( - {hdl(this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))} - {hdl(this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))} + {hdl("start", this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))} + {hdl("end", this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))} ); } diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss index 2127826b4..664f2448b 100644 --- a/src/client/views/InkStroke.scss +++ b/src/client/views/InkStroke.scss @@ -14,6 +14,9 @@ } .inkStroke-wrapper { + display: flex; + align-items: center; + height: 100%; .inkStroke { mix-blend-mode: multiply; stroke-linejoin: round; @@ -22,7 +25,7 @@ transform-origin: top left; width: 100%; height: 100%; - + pointer-events: none; svg:not(:root) { overflow: visible !important; } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 4808bbc77..7ab631b03 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -15,14 +15,15 @@ import { InkingStroke } from "./InkingStroke"; import { DocumentView } from "./nodes/DocumentView"; 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)); } @@ -150,15 +151,15 @@ export class InkStrokeProperties { } 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)); - var samples: Point[] = []; + const samples: Point[] = []; var startDir = { x: 0, y: 0 }; var endDir = { x: 0, y: 0 }; for (var i = 0; i < splicedPoints.length / 4; i++) { - var bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); + 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) { - var pt = bez.compute(t); + const pt = bez.compute(t); samples.push(new Point(pt.x, pt.y)); } } @@ -209,12 +210,12 @@ export class InkStrokeProperties { const ptFromScreen = view.ComponentView?.ptFromScreen; const ptToScreen = view.ComponentView?.ptToScreen; return !ptToScreen || !ptFromScreen ? ink : - ink.map(i => ptToScreen(i)).map(i => { + 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 + pvec.Y * (scrVec.x); const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x }; - const newpt = ptFromScreen!(newscrpt); + const newpt = ptFromScreen(newscrpt); return newpt; }); }); diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx index f88a20448..ab73e58a4 100644 --- a/src/client/views/InkTangentHandles.tsx +++ b/src/client/views/InkTangentHandles.tsx @@ -29,24 +29,23 @@ export class InkTangentHandles extends React.Component { * @param handleNum The index of the currently selected handle point. */ onHandleDown = (e: React.PointerEvent, handleIndex: number): void => { - if (InkStrokeProperties.Instance) { - var controlUndo: UndoManager.Batch | undefined; - const screenScale = this.props.ScreenToLocalTransform().Scale; - const order = handleIndex % 4; - const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; - const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length; - const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length; - setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { + var controlUndo: UndoManager.Batch | undefined; + const screenScale = this.props.ScreenToLocalTransform().Scale; + const order = handleIndex % 4; + const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; + const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length; + const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length; + setupMoveUpEvents(this, e, + (e: PointerEvent, down: number[], delta: number[]) => { if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent"); if (e.altKey) this.onBreakTangent(controlIndex); - InkStrokeProperties.Instance?.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); + InkStrokeProperties.Instance.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); return false; }, () => { controlUndo?.end(); UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); }, emptyFunction - ); - } + ); } /** @@ -66,9 +65,6 @@ export class InkTangentHandles extends React.Component { } render() { - const formatInstance = InkStrokeProperties.Instance; - if (!formatInstance) return (null); - // Accessing the current ink's data and extracting all handle points and handle lines. const data = this.props.screenCtrlPoints; const tangentHandles: HandlePoint[] = []; @@ -107,7 +103,7 @@ export class InkTangentHandles extends React.Component { onPointerDown={e => this.onHandleDown(e, pts.I)} pointerEvents="all" cursor="default" - display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} /> + display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} /> )} {tangentLines.map((pts, i) => { const tangentLine = (x1: number, y1: number, x2: number, y2: number) => @@ -119,7 +115,7 @@ export class InkTangentHandles extends React.Component { stroke={Colors.MEDIUM_BLUE} strokeDasharray={"1 1"} strokeWidth={1} - display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />; + display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />; return {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)} {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 8ff080f81..0b3619b22 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,3 +1,25 @@ +/* + InkingStroke - a document that represents an individual vector stroke drawn as a Bezier curve (open or closed) and optionally filled. + + The primary data is: + data - an InkField which is an array of PointData (X,Y values). The data is laid out as a sequence of simple bezier segments: + point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ... (Note that segment endpoints are duplicated ie Point2 = Point 3) + brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal + text - a text field that will be centered within a closed ink stroke + isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by + the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask. + + The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled. + Thus the mapping can roughly be described by: + the Top/Left of the ink data (minus 1/2 the ink width) maps to the Top/Left of the DocumentView + the Width/Height of the ink data (minus the ink width) is scaled to the PanelWidth/PanelHeight of the documentView + NOTE: use ptToScreen() and ptFromScreen() to transform between ink and screen space + + InkStrokes have a specialized 'componentUI' method that is called by MainView to render all of the interactive editing controls in + screen space (to avoid scaling artifacts) + + Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class +*/ import React = require("react"); import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; @@ -30,35 +52,32 @@ const InkDocument = makeInterface(documentSchema); @observer export class InkingStroke extends ViewBoxBaseComponent(InkDocument) { + static readonly MaskDim = 50000; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big) public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } - static readonly MaskDim = 50000; public static IsClosed(inkData: InkData) { return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y; } - @observable private _properties?: InkStrokeProperties; - _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated - _selDisposer: IReactionDisposer | undefined; + private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated + private _selDisposer?: IReactionDisposer; - @observable _nearestT: number | undefined; - @observable _nearestSeg: number | undefined; - @observable _nearestScrPt: { X: number, Y: number } | undefined; - @observable _inkSamplePts: { X: number, Y: number }[] | undefined; - - constructor(props: FieldViewProps & InkDocument) { - super(props); - - this._properties = InkStrokeProperties.Instance; - } + @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight) + @observable _nearestT?: number; // nearest t value within the nearest Bezier segment " + @observable _nearestScrPt?: { X: number, Y: number }; // nearst screen point on the ink stroke "" componentDidMount() { this.props.setContentView?.(this); this._selDisposer = reaction(() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles - selected => !selected && this.toggleControlButton()); + selected => !selected && (InkStrokeProperties.Instance._controlButton = false)); } componentWillUnmount() { this._selDisposer?.(); } + /** + * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space); + * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since + * the center of the ink stroke changes as the stroke is rotated. + */ getCenter = (xf: Transform) => { const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); const angle = -NumCast(this.layoutDoc.rotation); @@ -80,6 +99,12 @@ export class InkingStroke extends ViewBoxBaseComponent { inkDoc.isInkMask = !inkDoc.isInkMask; inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined; @@ -88,70 +113,47 @@ export class InkingStroke extends ViewBoxBaseComponent { - const ptFromScreen = this.ptFromScreen; this._handledClick = false; - if (InkStrokeProperties.Instance && ptFromScreen) { - const inkView = this.props.docViewPath().lastElement(); - const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); - const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint( - (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, - (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); - const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); - const controlIndex = nearestSeg; - const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex; - var controlUndo: UndoManager.Batch | undefined; - const isEditing = this._properties?._controlButton && this.props.isSelected(); - setupMoveUpEvents(this, e, - !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => { - if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); - const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] }); - const inkMoveStart = ptFromScreen({ X: 0, Y: 0 }); - InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); - InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3); - return false; - }), - !isEditing ? returnFalse : action(() => { - if (controlUndo) { - InkStrokeProperties.Instance?.snapControl(inkView, controlIndex); - InkStrokeProperties.Instance?.snapControl(inkView, controlIndex + 3); - } - controlUndo?.end(); - controlUndo = undefined; - UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); - }), - action((e: PointerEvent, doubleTap: boolean | undefined) => { - doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick; - if (doubleTap && this._properties) { - 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 if (isEditing) { - this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); + const inkView = this.props.docViewPath().lastElement(); + const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); + const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint( + (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, + (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); + const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); + const controlIndex = nearestSeg; + const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex; + var controlUndo: UndoManager.Batch | undefined; + const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected(); + setupMoveUpEvents(this, e, + !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => { + if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); + const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] }); + const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 }); + InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); + InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3); + return false; + }), + !isEditing ? returnFalse : action(() => { + controlUndo?.end(); + controlUndo = undefined; + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }), + action((e: PointerEvent, doubleTap: boolean | undefined) => { + doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick; + if (doubleTap) { + InkStrokeProperties.Instance._controlButton = true; + 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 + if (isEditing) { + this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); } - }), isEditing, isEditing, action(() => wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1))); - } - } - - /** - * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected. - */ - @action - toggleControlButton = () => { - if (!this.props.isSelected() && this._properties) { - this._properties._controlButton = false; - } - } - - @action - checkHighlighter = () => { - if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) { - // this._previousColor = ActiveInkColor(); - SetActiveInkColor("rgba(245, 230, 95, 0.75)"); - } + } + }), isEditing, isEditing, action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1))); } ptFromScreen = (scrPt: { X: number, Y: number }) => { @@ -173,13 +175,23 @@ export class InkingStroke extends ViewBoxBaseComponent { const { inkData } = this.inkScaledData(); - const inkPt = this.ptFromScreen(scrPt); - const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, inkPt, excludeSegs ?? []); + const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, this.ptFromScreen(scrPt), excludeSegs ?? []); return { nearestPt, distance: distance * this.props.ScreenToLocalTransform().inverse().Scale }; } + /** + * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale + * factor for converting between ink and screen space. + */ inkScaledData = () => { const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1); @@ -229,7 +241,7 @@ export class InkingStroke extends ViewBoxBaseComponent + return
this._nearestScrPt = undefined)} @@ -305,7 +313,7 @@ export class InkingStroke extends ViewBoxBaseComponent InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" }); - cm?.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" }); + cm?.addItem({ description: "Edit Points", event: action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton), icon: "paint-brush" }); }} > {clickableLine(this.onPointerDown)} diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 546b0e360..a9fea4a78 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -116,7 +116,6 @@ export class MainView extends React.Component { }, 0); setTimeout(() => ele.outerHTML = '', 1000); } - new InkStrokeProperties(); this._sidebarContent.proto = undefined; if (!MainView.Live) { DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition", diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 1083e0075..18d5f1642 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -535,16 +535,17 @@ export class PropertiesView extends React.Component { @computed get controlPointsButton() { - const formatInstance = InkStrokeProperties.Instance; - return !formatInstance ? (null) :
+ return
{"Edit points"}
}> -
formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}> +
InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)} >
- {formatInstance._lock ? "Unlock ratio" : "Lock ratio"}
}> -
formatInstance._lock = !formatInstance._lock)} > - + {InkStrokeProperties.Instance._lock ? "Unlock ratio" : "Lock ratio"}
}> +
InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock)} > +
{"Rotate 90˚"}
}> @@ -603,7 +604,7 @@ export class PropertiesView extends React.Component { const oldX = NumCast(this.selectedDoc?.x); const oldY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10)); - InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); + InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); const doc = this.selectedDoc; if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { const ink = Cast(doc.data, InkField)?.inkData; @@ -625,7 +626,7 @@ export class PropertiesView extends React.Component { const oX = NumCast(this.selectedDoc?.x); const oY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10)); - InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); + InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); const docu = this.selectedDoc; if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { const ink = Cast(docu.data, InkField)?.inkData; @@ -663,12 +664,12 @@ export class PropertiesView extends React.Component { set shapeWid(value) { const oldWidth = NumCast(this.selectedDoc?._width); this.selectedDoc && (this.selectedDoc._width = Number(value)); - InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); + InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); } set shapeHgt(value) { const oldHeight = NumCast(this.selectedDoc?._height); this.selectedDoc && (this.selectedDoc._height = Number(value)); - InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); + InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); } @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); } diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 8d8630907..f09d532ad 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -93,7 +93,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt { + selectAll(this._editorView.state, (tr) => { this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" }))); }); } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index fe297782c..09cfb2077 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -251,7 +251,7 @@ export class SearchBox extends ViewBoxBaseComponent { this._pageRanks.set(doc, 1.0 / this._results.size); - if (Doc.GetProto(doc)[DirectLinksSym].size == 0) { + if (Doc.GetProto(doc)[DirectLinksSym].size === 0) { this._linkedDocsOut.set(doc, new Set(this._results.keys())); this._results.forEach((_, linkedDoc) => { @@ -259,20 +259,20 @@ export class SearchBox extends ViewBoxBaseComponent = new Set(); + const linkedDocSet = new Set(); Doc.GetProto(doc)[DirectLinksSym].forEach((link) => { - let d1 = link?.anchor1 as Doc; - let d2 = link?.anchor2 as Doc; - if (doc == d1 && this._results.has(d2)) { + const d1 = link?.anchor1 as Doc; + const d2 = link?.anchor2 as Doc; + if (doc === d1 && this._results.has(d2)) { linkedDocSet.add(d2); this._linkedDocsIn.get(d2)?.add(doc); } - else if (doc == d2 && this._results.has(d1)) { + else if (doc === d2 && this._results.has(d1)) { linkedDocSet.add(d1); this._linkedDocsIn.get(d1)?.add(doc); } - }) + }); this._linkedDocsOut.set(doc, linkedDocSet); } @@ -291,7 +291,7 @@ export class SearchBox extends ViewBoxBaseComponent = new Map(); + const nextPageRanks = new Map(); this._results.forEach((_, doc) => { let nextPageRank = pageRankFromAll; @@ -397,7 +397,7 @@ export class SearchBox extends ViewBoxBaseComponent (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)) // sorted by page rank + const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)); // sorted by page rank const resultsJSX = Array(); -- cgit v1.2.3-70-g09d2 From 1cbbb1b6ebdb2bbf0f05ee1dcbe9922236495f58 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 1 Dec 2021 00:18:02 -0500 Subject: cleanup DocDecorations ink rotation --- src/client/views/DocumentDecorations.tsx | 30 +++++++++++------------------- src/client/views/InkStrokeProperties.ts | 14 +++++++------- 2 files changed, 18 insertions(+), 26 deletions(-) (limited to 'src/client/views/InkStrokeProperties.ts') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 7d3959eba..d85709f31 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -27,6 +27,8 @@ import { LightboxView } from './LightboxView'; import { DocumentView } from "./nodes/DocumentView"; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import React = require("react"); +import { InkingStroke } from './InkingStroke'; +import e = require('express'); @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> { @@ -37,12 +39,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P private _linkBoxHeight = 20 + 3; // link button height + margin private _titleHeight = 20; private _resizeUndo?: UndoManager.Batch; - private _rotateUndo?: UndoManager.Batch; private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border private _snapX = 0; _snapY = 0; // last snapped location of resize border - private _prevY = 0; private _dragHeights = new Map(); - private _inkCenterPts: { doc: Doc, X: number, Y: number }[] = []; private _inkDragDocs: { doc: Doc, x: number, y: number, width: number, height: number }[] = []; @observable private _accumulatedTitle = ""; @@ -194,30 +193,22 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P @action onRotateDown = (e: React.PointerEvent): void => { - this._rotateUndo = UndoManager.StartBatch("rotatedown"); - const pt = { x: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 }; + const rotateUndo = UndoManager.StartBatch("rotatedown"); + const centerPoint = { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 }; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { - const docView = SelectionManager.Views()[0]; - const { left, top, right, bottom } = docView.getBounds() || { left: 0, top: 0, right: 0, bottom: 0 }; - const centerPoint = { X: (left + right) / 2, Y: (top + bottom) / 2 }; const previousPoint = { X: e.clientX, Y: e.clientY }; const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; - const angle = InkStrokeProperties.Instance.angleChange(previousPoint, movedPoint, centerPoint); - const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); - angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, pt); + const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint); + const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); + angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); return false; }, () => { - this._rotateUndo?.end(); + rotateUndo?.end(); UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); }, emptyFunction); - this._prevY = e.clientY; - this._inkCenterPts = SelectionManager.Views() - .filter(dv => dv.rootDoc.type === DocumentType.INK) - .map(dv => ({ ink: Cast(dv.rootDoc.data, InkField)?.inkData ?? [{ X: 0, Y: 0 }], doc: dv.rootDoc })) - .map(({ ink, doc }) => ({ doc, X: Math.min(...ink.map(p => p.X)), Y: Math.min(...ink.map(p => p.Y)) })); } @action @@ -479,7 +470,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); - const useRotation = seldoc.rootDoc.type === DocumentType.INK; + const useRotation = seldoc.ComponentView instanceof InkingStroke; const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : ""; return (
@@ -518,7 +509,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")}
e.preventDefault()}>{useRotation && "⟲"}
+ onPointerDown={useRotation ? this.onRotateDown : this.onRadiusDown} + onContextMenu={e => e.preventDefault()}>{useRotation && "⟲"}
}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 7ab631b03..9634e6e83 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -183,10 +183,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 }; @@ -340,7 +340,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; @@ -364,7 +364,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; @@ -377,14 +377,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; } @@ -408,7 +408,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; -- cgit v1.2.3-70-g09d2 From 49e9103721ecfd6d5c900c754f2d88362f64a3ab Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 2 Dec 2021 00:22:05 -0500 Subject: added shift erase stroke to delete full strokes. added shift drag end of stroke to scale uniformly. added ctrl+p,ctrl+e to switch between eraser/pen. added delete to menu options for tree view items. cleaned up a lot of ink code. --- src/client/views/GlobalKeyHandler.ts | 4 + src/client/views/InkControlPtHandles.tsx | 18 +- src/client/views/InkStrokeProperties.ts | 13 +- src/client/views/collections/TreeView.tsx | 13 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 219 ++++++++++----------- src/fields/InkField.ts | 16 +- 6 files changed, 136 insertions(+), 147 deletions(-) (limited to 'src/client/views/InkStrokeProperties.ts') diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d5e0ed962..1a4080d81 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -230,6 +230,10 @@ export class KeyManager { } } break; + case "e": CurrentUserUtils.SelectedTool = InkTool.Eraser; + break; + case "p": CurrentUserUtils.SelectedTool = InkTool.Pen; + break; case "o": const target = SelectionManager.Docs().lastElement(); target && CollectionDockingView.OpenFullScreen(target); diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index 76ce73b0d..0996e75d4 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -191,16 +191,16 @@ export class InkEndPtHandles extends React.Component { setupMoveUpEvents(this, e, (e) => { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink"); // compute stretch factor by finding scaling along axis between start and end points - const v1 = { x: p1().X - p2().X, y: p1().Y - p2().Y }; - const v2 = { x: e.clientX - p2().X, y: e.clientY - p2().Y }; - const v1len = Math.sqrt(v1.x * v1.x + v1.y * v1.y); - const v2len = Math.sqrt(v2.x * v2.x + v2.y * v2.y); + const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y }; + const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y }; + const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y); + const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y); const scaling = v2len / v1len; - const v1n = { x: v1.x / v1len, y: v1.y / v1len }; - const v2n = { x: v2.x / v2len, y: v2.y / v2len }; - const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y); - InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n); - InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y }); + const v1n = { X: v1.X / v1len, Y: v1.Y / v1len }; + const v2n = { X: v2.X / v2len, Y: v2.Y / v2len }; + const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y); + InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, p2(), v1n, e.shiftKey); + InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, p2()); return false; }, action(() => { this.controlUndo?.end(); diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 9634e6e83..695bdcc5a 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -205,18 +205,17 @@ export class InkStrokeProperties { */ @undoBatch @action - stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: { x: number, y: number }, scrVec: { x: number, y: number }) => { + 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 + pvec.Y * (scrVec.x); - const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x }; - const newpt = ptFromScreen(newscrpt); - return newpt; + 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); }); }); } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index d8f984601..eedb353e3 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -254,9 +254,7 @@ export class TreeView extends React.Component { TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView }; return this.props.addDocument(folder); } - deleteFolder = () => { - return this.props.removeDoc?.(this.doc); - } + deleteItem = () => this.props.removeDoc?.(this.doc); preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; @@ -531,16 +529,16 @@ export class TreeView extends React.Component { } contextMenuItems = () => { const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "New Folder" }; - const deleteFolder = { script: ScriptField.MakeFunction(`scriptContext.deleteFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete Folder" }; - const folderOp = this.childDocs?.length ? makeFolder : deleteFolder; + const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete" }; + const folderOp = this.childDocs?.length ? [makeFolder] : []; const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: "copy", label: "Open Alias" }; const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: "eye", label: "Focus or Open" }; - return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [folderOp] : + return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? folderOp : Doc.IsSystem(this.doc) ? [] : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ? [openAlias, makeFolder] : this.doc.viewType === CollectionViewType.Docking ? [] : - [openAlias, focusDoc])]; + [deleteItem, openAlias, focusDoc])]; } childContextMenuItems = () => { const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []); @@ -784,6 +782,7 @@ export class TreeView extends React.Component { const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, "copy", undefined, false)); } + render() { TraceMobx(); const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ceee4051b..19c3bf745 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,11 +1,12 @@ -import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx"; +import { Bezier } from "bezier-js"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { DateField } from "../../../../fields/DateField"; import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; -import { InkData, InkField, InkTool, PointData, Intersection, Segment } from "../../../../fields/InkField"; +import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { ObjectField } from "../../../../fields/ObjectField"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -27,14 +28,15 @@ import { InteractionUtils } from "../../../util/InteractionUtils"; import { LinkManager } from "../../../util/LinkManager"; import { SearchUtil } from "../../../util/SearchUtil"; import { SelectionManager } from "../../../util/SelectionManager"; +import { ColorScheme } from "../../../util/SettingsManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { Transform } from "../../../util/Transform"; import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariables.scss"; import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; -import { DocumentDecorations } from "../../DocumentDecorations"; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkWidth, SetActiveFillColor, SetActiveInkColor } from "../../InkingStroke"; +import { GestureOverlay } from "../../GestureOverlay"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "../../InkingStroke"; import { LightboxView } from "../../LightboxView"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView"; @@ -50,10 +52,6 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { ColorScheme } from "../../../util/SettingsManager"; -import { Bezier } from "bezier-js"; -import { GestureOverlay } from "../../GestureOverlay"; -import { constants } from "perf_hooks"; export const panZoomSchema = createSchema({ _panX: "number", @@ -115,8 +113,6 @@ export class CollectionFreeFormView extends CollectionSubView(); @observable _marqueeRef = React.createRef(); @@ -439,28 +435,29 @@ export class CollectionFreeFormView extends CollectionSubView { - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || - ([InkTool.Pen, InkTool.Highlighter].includes(CurrentUserUtils.SelectedTool))) { - return; - } - this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); + this._downX = this._lastX = e.pageX; + this._downY = this._lastY = e.pageY; if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - // if not using a pen and in no ink mode - if (CurrentUserUtils.SelectedTool === InkTool.None) { - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; - } - // eraser plus anything else mode - else { - this._batch = UndoManager.StartBatch("collectionErase"); - this._prevPoint = { X: e.clientX, Y: e.clientY }; - e.stopPropagation(); - e.preventDefault(); - } + if (!e.nativeEvent.cancelBubble && + !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag + !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && + !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) + switch (CurrentUserUtils.SelectedTool) { + case InkTool.Highlighter: + case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views + case InkTool.Eraser: + document.addEventListener("pointermove", this.onEraserMove); + document.addEventListener("pointerup", this.onEraserUp); + this._batch = UndoManager.StartBatch("collectionErase"); + e.stopPropagation(); + e.preventDefault(); + break; + case InkTool.None: + this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + break; + } } } @@ -600,6 +597,16 @@ export class CollectionFreeFormView extends CollectionSubView { + if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { + document.removeEventListener("pointermove", this.onEraserMove); + document.removeEventListener("pointerup", this.onEraserUp); + this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc)); + this._deleteList = []; + this._batch?.end(); + } + } @action onPointerUp = (e: PointerEvent): void => { @@ -608,12 +615,6 @@ export class CollectionFreeFormView extends CollectionSubView ink.props.removeDocument?.(ink.rootDoc)); - this._prevPoint = this._currPoint = { X: -1, Y: -1 }; - this._deleteList = []; - this._batch?.end(); - } } } @@ -638,96 +639,82 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; - if (CurrentUserUtils.SelectedTool !== InkTool.None) { - this._currPoint = { X: e.clientX, Y: e.clientY }; - // Erasing ink strokes if intersections occur. - this.eraseInkStrokes(this.getEraserIntersections()); - } - if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - if (this.props.isContentActive(true)) e.stopPropagation(); - } else if (!e.cancelBubble) { - if (CurrentUserUtils.SelectedTool === InkTool.None) { - if (this.tryDragCluster(e, this._hitCluster)) { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } - else this.pan(e); - } - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); - } - } - /** - * Iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments, + * Erases strokes by intersecting them with an invisible "eraser stroke". + * By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments, * and deletes the original stroke. - * @param eraserIntersections The intersections made by the eraser. + * However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety. */ - eraseInkStrokes = (eraserIntersections: Intersection[]) => { - eraserIntersections.forEach(intersect => { - const ink = intersect.ink; - if (ink && !this._deleteList.includes(ink)) { - this._deleteList.push(ink); - SetActiveInkWidth(StrCast(ink.rootDoc.strokeWidth?.toString()) || "1"); - SetActiveInkColor(StrCast(ink.rootDoc.color?.toString()) || "black"); + @action + onEraserMove = (e: PointerEvent) => { + const currPoint = { X: e.clientX, Y: e.clientY }; + this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => { + if (!this._deleteList.includes(intersect.inkView)) { + this._deleteList.push(intersect.inkView); + SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || "1"); + SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || "black"); // create a new curve by appending all curves of the current segment together in order to render a single new stroke. - this.segmentInkStroke(ink, intersect.t ?? 0).forEach(segment => + !e.shiftKey && this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment => GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, segment.reduce((data, curve) => [...data, ...curve.points - .map(p => ink.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 }) + .map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 }) ], [] as PointData[]))); // Lower ink opacity to give the user a visual indicator of deletion. - ink.layoutDoc.opacity = 0.5; + intersect.inkView.layoutDoc.opacity = 0.5; } }); + this._lastX = currPoint.X; + this._lastY = currPoint.Y; + + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + } + + @action + onPointerMove = (e: PointerEvent): void => { + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; + if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { + if (this.props.isContentActive(true)) e.stopPropagation(); + } else if (!e.cancelBubble) { + if (this.tryDragCluster(e, this._hitCluster)) { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + else this.pan(e); + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + } } /** * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection. - * @returns A dictionary mapping the t-value intersection of the eraser with the corresponding ink DocumentView. + * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected */ - getEraserIntersections = (): Intersection[] => { - const intersections: Intersection[] = []; - this.childDocs - .filter(doc => doc.type === DocumentType.INK) - .forEach(doc => { - const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); - const inkStroke = inkView?.ComponentView as InkingStroke; - const { inkData } = inkStroke?.inkScaledData(); + getEraserIntersections = (lastPoint: { X: number, Y: number }, currPoint: { X: number, Y: number }) => { + const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) }; + const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) }; + return this.childDocs + .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)) + .filter(inkView => inkView?.ComponentView instanceof InkingStroke) + .map(inkView => ({ inkViewBounds: inkView!.getBounds(), inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! })) + .filter(({ inkViewBounds }) => inkViewBounds && // bounding box of eraser segment and ink stroke overlap + eraserMin.X <= inkViewBounds.right && eraserMin.Y <= inkViewBounds.bottom && + eraserMax.X >= inkViewBounds.left && eraserMax.Y >= inkViewBounds.top) + .reduce((intersections, { inkStroke, inkView }) => { + const { inkData } = inkStroke.inkScaledData(); + // Convert from screen space to ink space for the intersection. + const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); + const currPointInkSpace = inkStroke.ptFromScreen(currPoint); for (var i = 0; i < inkData.length - 3; i += 4) { - const array = inkData.slice(i, i + 4); - // Converting from screen space to ink space for the intersection. - const prevPointInkSpace = inkStroke?.ptFromScreen?.(this._prevPoint); - const currPointInkSpace = inkStroke?.ptFromScreen?.(this._currPoint); - if (prevPointInkSpace && currPointInkSpace) { - const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); - const intersects = curve.intersects({ - p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, - p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } - }); - if (inkView && intersects) { - for (const val of intersects) { - // Casting t-value from type: (string | number) to number for comparisons. - const t = +(Number(val) + Math.floor(i / 4)).toString(); // add start of curve segment to convert from local t value to t value along complete curve - var unique: boolean = true; - // Ensuring there are no duplicate intersections in the list returned. - for (const prevIntersect of intersections) { - if (prevIntersect.t === t) { - unique = false; - break; - } - } - if (unique) intersections.push({ t: +t.toString(), ink: inkView, curve: curve }); - } - } - } + const intersects = Array.from(new Set(InkField.Segment(inkData, i).intersects({ // compute all unique intersections + p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, + p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } + }) as (number | string)[])); // convert to more manageable union array type + // return tuples of the inkingStroke intersected, and the t value of the intersection + intersections.push(...intersects.map(t => ({ inkView, t: (+t) + Math.floor(i / 4) })));// convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve } - }); - return intersections; + return intersections; + }, [] as { t: number, inkView: DocumentView }[]); } /** @@ -746,23 +733,23 @@ export class CollectionFreeFormView extends CollectionSubView ({ x: p.X, y: p.Y }))); + const inkSegment = InkField.Segment(inkData, i); // Getting all t-value intersections of the current curve with all other curves. - const tVals = this.getInkIntersections(i, ink, curve).sort(); + const tVals = this.getInkIntersections(i, ink, inkSegment).sort(); if (tVals.length) { tVals.forEach((t, index) => { const docCurveTVal = t + Math.floor(i / 4); if (excludeT < startSegmentT || excludeT > docCurveTVal) { const localStartTVal = startSegmentT - Math.floor(i / 4); - segment.push(curve.split(localStartTVal < 0 ? 0 : localStartTVal, t)); + segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t)); segment.length && segments.push(segment); } // start a new segment from the intersection t value - segment = tVals.length - 1 === index ? [curve.split(t).right] : []; + segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : []; startSegmentT = docCurveTVal; }); } else { - segment.push(curve); + segment.push(inkSegment); } } if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) { @@ -786,7 +773,7 @@ export class CollectionFreeFormView extends CollectionSubView doc.type === DocumentType.INK) .forEach(doc => { const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke; - const { inkData: otherInkData } = otherInk.inkScaledData(); + const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] }; const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point)); const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt)); for (var j = 0; j < otherCtrlPts.length - 3; j += 4) { diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index f61313674..560cf3d63 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -3,7 +3,6 @@ import { Scripting } from "../client/util/Scripting"; import { Deserializable } from "../client/util/SerializationHelper"; import { Copy, ToScriptString, ToString } from "./FieldSymbols"; import { ObjectField } from "./ObjectField"; -import { DocumentView } from "../client/views/nodes/DocumentView"; import { Bezier } from "bezier-js"; // Helps keep track of the current ink tool in use. @@ -22,13 +21,6 @@ export interface PointData { Y: number; } -export interface Intersection { - t?: number; - ink?: DocumentView; - curve?: Bezier; - index?: number; -} - export type Segment = Array; // Defines an ink as an array of points. @@ -78,6 +70,14 @@ export class InkField extends ObjectField { this.inkData = data; } + /** + * Extacts a simple segment from a compound Bezier curve + * @param segIndex the start index of the simple bezier segment to extact (eg., 0, 4, 8, ...) + */ + public static Segment(inkData: InkData, segIndex: number) { + return new Bezier(inkData.slice(segIndex, segIndex + 4).map(pt => ({ x: pt.X, y: pt.Y }))); + } + [Copy]() { return new InkField(this.inkData); } -- cgit v1.2.3-70-g09d2 From c2cd77ca1d2a67539f0af2a68c1e7336b3bc232b Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 2 Dec 2021 13:46:17 -0500 Subject: added scale factor for arrows. added control point drag to reparameterize. fixed toggling tangent lines to not happen when dragging. --- src/Utils.ts | 12 ++++---- src/client/util/InteractionUtils.tsx | 15 ++++++---- src/client/util/bezierFit.ts | 7 +++-- src/client/views/GestureOverlay.tsx | 8 ++--- src/client/views/InkControlPtHandles.tsx | 3 +- src/client/views/InkStrokeProperties.ts | 50 +++++++++++++++++++++++++++++--- src/client/views/InkingStroke.tsx | 11 ++++--- src/client/views/PropertiesView.tsx | 15 ++++++++++ 8 files changed, 95 insertions(+), 26 deletions(-) (limited to 'src/client/views/InkStrokeProperties.ts') diff --git a/src/Utils.ts b/src/Utils.ts index ca1432de2..f2d9e7766 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -658,12 +658,6 @@ export function setupMoveUpEvents( (target as any)._lastTap = Date.now(); (target as any)._downX = (target as any)._lastX = e.clientX; (target as any)._downY = (target as any)._lastY = e.clientY; - if (!(target as any)._doubleTime && noDoubleTapTimeout) { - (target as any)._doubleTime = setTimeout(() => { - noDoubleTapTimeout?.(); - (target as any)._doubleTime = undefined; - }, doubleTapTimeout); - } const _moveEvent = (e: PointerEvent): void => { if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) { @@ -685,6 +679,12 @@ export function setupMoveUpEvents( const isClick = Math.abs(e.clientX - (target as any)._downX) < 4 && Math.abs(e.clientY - (target as any)._downY) < 4; upEvent(e, [e.clientX - (target as any)._downX, e.clientY - (target as any)._downY], isClick); if (isClick) { + if (!(target as any)._doubleTime && noDoubleTapTimeout) { + (target as any)._doubleTime = setTimeout(() => { + noDoubleTapTimeout?.(); + (target as any)._doubleTime = undefined; + }, doubleTapTimeout); + } if ((target as any)._doubleTime && (target as any)._doubleTap) { clearTimeout((target as any)._doubleTime); (target as any)._doubleTime = undefined; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 4eb0be320..61872417b 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -91,7 +91,7 @@ export namespace InteractionUtils { export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean, + markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean, downHdlr?: ((e: React.PointerEvent) => void)) { const pts = shape ? makePolygon(shape, points) : points; @@ -108,6 +108,9 @@ export namespace InteractionUtils { const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements; const makerStrokeWidth = strokeWidth / 2; + const arrowWidthFactor = 3 * (markerScale ? markerScale : 0.5);// used to be 1.5 + const arrowLengthFactor = 5 * (markerScale ? markerScale : 0.5); + const arrowNotchFactor = 2 * (markerScale ? markerScale : 0.5); return ( {/* setting the svg fill sets the arrowStart fill */} {nodefs ? (null) : {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : @@ -115,12 +118,14 @@ export namespace InteractionUtils { } {arrowStart !== "arrow" ? (null) : - - + + } {arrowEnd !== "arrow" ? (null) : - - + + } } diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 784bb2e18..8fc6de6f9 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -53,8 +53,11 @@ class SmartRect { } } -function Normalize(p: Point) { - const len = Math.sqrt(p.X * p.X + p.Y * p.Y); +export function Distance(p: Point) { + return Math.sqrt(p.X * p.X + p.Y * p.Y); +} +export function Normalize(p: Point) { + const len = Distance(p); return new Point(p.X / len, p.Y / len); } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index e2193c9ac..04abdbf37 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -3,7 +3,7 @@ import * as fitCurve from 'fit-curve'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; -import { InkData, InkTool, PointData } from "../../fields/InkField"; +import { InkData, InkTool } from "../../fields/InkField"; import { Cast, FieldValue, NumCast } from "../../fields/Types"; import MobileInkOverlay from "../../mobile/MobileInkOverlay"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; @@ -18,7 +18,7 @@ import { SelectionManager } from "../util/SelectionManager"; import { Transform } from "../util/Transform"; import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; import "./GestureOverlay.scss"; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveArrowScale, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke"; import { DocumentView } from "./nodes/DocumentView"; import { RadialMenu } from "./nodes/RadialMenu"; import HorizontalPalette from "./Palette"; @@ -850,14 +850,14 @@ export class GestureOverlay extends Touchable { const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 };//this.getBounds(l, true); return {InteractionUtils.CreatePolyline(l, b.left, b.top, strokeColor, width, width, "miter", "round", - ActiveInkBezierApprox(), "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), + ActiveInkBezierApprox(), "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveArrowScale(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)} ; }), this._points.length <= 1 ? (null) : {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, "miter", "round", "", - "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)} + "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveArrowScale(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)} ] ]; } diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index ef457cac4..24f796105 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -51,12 +51,13 @@ export class InkControlPtHandles extends React.Component { const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length; const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number")); const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex; + const origInk = this.props.inkCtrlPoints.slice(); setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] }); const inkMoveStart = ptFromScreen({ X: 0, Y: 0 }); - InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); + InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk); return false; }), action(() => { diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 695bdcc5a..cab4e1216 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,4 +1,5 @@ import { Bezier } from "bezier-js"; +import { Normalize, Distance } from "../util/bezierFit"; import { action, observable, reaction } from "mobx"; import { Doc, NumListCast, Opt } from "../../fields/Doc"; import { InkData, InkField, InkTool, PointData } from "../../fields/InkField"; @@ -225,14 +226,56 @@ export class InkStrokeProperties { */ @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 || @@ -246,7 +289,6 @@ export class InkStrokeProperties { } return pt; }); - return newpts; }) @@ -286,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; } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index f5dd66949..5c7fc94bd 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -31,7 +31,6 @@ import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; import { OmitKeys, returnFalse, setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; -import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; import { SnappingManager } from "../util/SnappingManager"; import { Transform } from "../util/Transform"; @@ -262,6 +261,7 @@ export class InkingStroke extends ViewBoxBaseComponent {InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth, StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier), - "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)} + "none", startMarker, endMarker, markerScale, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)} @@ -370,12 +371,14 @@ export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkP export function SetActiveFillColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeFillColor = value); } export function SetActiveArrowStart(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); } export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); } +export function SetActiveArrowScale(value: number) { ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); } export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); } export function ActiveInkPen(): Doc { return Doc.UserDoc(); } export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); } export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); } export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); } export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); } +export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); } export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 18d5f1642..8e2426006 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -756,6 +756,7 @@ export class PropertiesView extends React.Component { @computed get dashdStk() { return this.selectedDoc?.strokeDash || ""; } @computed get unStrokd() { return this.selectedDoc?.color ? true : false; } @computed get widthStk() { return this.getField("strokeWidth") || "1"; } + @computed get markScal() { return Number(this.getField("strokeMakerScale") || "1"); } @computed get markHead() { return this.getField("strokeStartMarker") || ""; } @computed get markTail() { return this.getField("strokeEndMarker") || ""; } set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } @@ -763,6 +764,7 @@ export class PropertiesView extends React.Component { value && (this._lastDash = value) && (this.unStrokd = false); this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined); } + set markScal(value) { this.selectedDoc && (this.selectedDoc.strokeMarkerScale = Number(value)); } set widthStk(value) { this.selectedDoc && (this.selectedDoc.strokeWidth = Number(value)); } set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } set markHead(value) { this.selectedDoc && (this.selectedDoc.strokeStartMarker = value); } @@ -770,6 +772,7 @@ export class PropertiesView extends React.Component { @computed get stkInput() { return this.regInput("stk", this.widthStk, (val: string) => this.widthStk = val); } + @computed get markScaleInput() { return this.regInput("scale", this.markScal.toString(), (val: string) => this.markScal = Number(val)); } regInput = (key: string, value: any, setter: (val: string) => {}) => { @@ -806,6 +809,18 @@ export class PropertiesView extends React.Component {
+
+
+
Arrow Scale:
+ {/*
{this.markScalInput}
*/} +
+ this.markScal = +e.target.value))} + onMouseDown={(e) => { this._widthUndo = UndoManager.StartBatch("scale undo"); }} + onMouseUp={(e) => { this._widthUndo?.end(); this._widthUndo = undefined; }} + /> +
Arrow Head: