From 39ca6f5ea22c99f3e7f530559d0b08c00aa19cb4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 27 Oct 2021 00:57:47 -0400
Subject: fixed warnings/errors.

---
 src/client/views/InkControlPtHandles.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/InkControlPtHandles.tsx')

diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 607137c2f..f24dab949 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -43,7 +43,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
      */
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
-        const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen
+        const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
         if (InkStrokeProperties.Instance && ptFromScreen) {
             const order = controlIndex % 4;
             const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
-- 
cgit v1.2.3-70-g09d2


From 061dab5285d3a334a258d8097a6e95b065b30de3 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
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/InkControlPtHandles.tsx')

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 (<svg fill={color}> {/* setting the svg fill sets the arrowStart fill */}
+        return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
             {nodefs ? (null) : <defs>
                 {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
                     <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
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<InkControlProps> {
             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<InkControlProps> {
             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 <Tag key={control.I.toString() + scale}
-                x={control.X - screenSpaceLineWidth * 2 * scale}
-                y={control.Y - screenSpaceLineWidth * 2 * scale}
+                x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
+                y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
                 cx={control.X}
                 cy={control.Y}
-                r={screenSpaceLineWidth * 2 * scale}
+                r={this.props.screenSpaceLineWidth * 2 * scale}
                 opacity={this.controlUndo ? 0.15 : 1}
-                height={screenSpaceLineWidth * 4 * scale}
-                width={screenSpaceLineWidth * 4 * scale}
-                strokeWidth={screenSpaceLineWidth / 2}
+                height={this.props.screenSpaceLineWidth * 4 * scale}
+                width={this.props.screenSpaceLineWidth * 4 * scale}
+                strokeWidth={this.props.screenSpaceLineWidth / 2}
                 stroke={Colors.MEDIUM_BLUE}
                 fill={broken ? Colors.MEDIUM_BLUE : color}
-                onPointerDown={(e: any) => 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<InkControlProps> {
                 <circle key={"npt"}
                     cx={nearestScreenPt.X}
                     cy={nearestScreenPt.Y}
-                    r={screenSpaceLineWidth * 2}
+                    r={this.props.screenSpaceLineWidth * 2}
                     fill={"#00007777"}
                     stroke={"#00007777"}
                     strokeWidth={0}
@@ -175,4 +174,62 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         </svg>
         );
     }
+}
+
+
+export interface InkEndProps {
+    inkDoc: Doc;
+    inkView: DocumentView;
+    screenSpaceLineWidth: number;
+    startPt: PointData;
+    endPt: PointData;
+}
+@observer
+export class InkEndPtHandles extends React.Component<InkEndProps> {
+    @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) => <circle key={"npt"}
+            cx={pt.X}
+            cy={pt.Y}
+            r={this.props.screenSpaceLineWidth * 2}
+            fill={this._overStart ? "#aaaaaa" : "#99999977"}
+            stroke={"#00007777"}
+            strokeWidth={0}
+            onPointerLeave={action(() => this._overStart = false)}
+            onPointerEnter={action(() => this._overStart = true)}
+            onPointerDown={e => dragFunc(e)}
+            pointerEvents="all"
+        />;
+        return (<svg>
+            {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))}
+        </svg>
+        );
+    }
 }
\ 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<FieldViewProps, InkDocume
     /**
      * Handles the movement of the entire ink object when the user clicks and drags.
      */
+    @action
     onPointerDown = (e: React.PointerEvent) => {
+        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<FieldViewProps, InkDocume
         this._nearestScrPt = nearestPt;
     }
 
-
     nearestScreenPt = () => this._nearestScrPt;
     componentUI = (boundsLeft: number, boundsTop: number) => {
         const inkDoc = this.props.Document;
@@ -200,11 +228,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
 
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
-        return SnappingManager.GetIsDragging() ? (null) : <div className="inkstroke-UI" style={{
-            clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)`
-        }} >
-            {!this._properties?._controlButton ? (null) :
-                <>
+        return SnappingManager.GetIsDragging() ? (null) :
+            !this._properties?._controlButton ?
+                (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
+                    <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
+                        <InkEndPtHandles
+                            inkView={this.props.docViewPath().lastElement()}
+                            inkDoc={inkDoc}
+                            startPt={this.ptToScreen(inkData[0])}
+                            endPt={this.ptToScreen(inkData.lastElement())}
+                            screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /></div>) :
+                <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                     {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<FieldViewProps, InkDocume
                         screenCtrlPoints={screenHdlPts}
                         screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
                         ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
-                </>}
-        </div>;
+                </div>;
     }
 
     render() {
@@ -246,11 +279,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") :
             ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
         // Invisible polygonal line that enables the ink to be selected by the user.
-        const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
-            inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+        const clickableLine = (downHdlr?: (e: React.PointerEvent) => 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 <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}>
@@ -266,7 +300,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                 }}
                 onPointerLeave={action(e => 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<FieldViewProps, InkDocume
                     cm?.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" });
                 }}
             >
-                {clickableLine}
+                {clickableLine(this.onPointerDown)}
                 {inkLine}
             </svg>
             {!closed ? (null) :
-- 
cgit v1.2.3-70-g09d2


From f313cfa5ae644eadb57d936bc81bd355e0c88e17 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
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/InkControlPtHandles.tsx')

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<number>(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<number>(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<Array<Point>>(4);
+    for (var i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(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<Point>(4), a2 = new Array<Point>(4);
+        splitCubic(a, 0.5, a1, a2);
+        var tmid = (t0 + t1) * 0.5;
+        deptha--;
+        if (depthb > 0) {
+            var b1 = new Array<Point>(4), b2 = new Array<Point>(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<Point>(4), b2 = new Array<Point>(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<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    var Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> 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<Point>(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<BezierRep::BezierLock> &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<BezierRep::BezierLock> &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<int>(tMove);
+auto tend   = static_cast<int>(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<BezierRep::BezierLock> &locked, bool rangeDrag, double errorTolerance, double influenceLDistance, double influenceRDistance, double ctrlPtLRotate, double ctrlPtRRotate, double ctrlPtScale, bool moveEnds)
+{
+auto tleftMove = rangeDrag ? (static_cast<int>(tMove) == tMove ? tMove-1 : static_cast<int>(tMove)) : tMove;
+auto trightMove = rangeDrag ? (static_cast<int>(tMove) == tMove ? tMove+1 : static_cast<int>(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 &center)
+{
+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<std::tuple<double, double>> BezierRep::Find_intersections(const BezierRep & a, const BezierRep & b, size_t t_a_off, size_t t_b_off)
+{
+auto ints = std::vector<std::tuple<double, double>>();
+if (a.p.size() == 0 || b.p.size() == 0)
+return ints;
+if (a.p.size() == 4 && b.p.size() == 4)
+{
+std::vector<std::tuple<double, double>> 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<std::tuple<double, double>> modParameters;
+for (size_t i = 0; i < parameters.size(); i++) {
+modParameters.push_back(std::tuple<double,double>(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<Point2D> 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<Point2D> 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<std::vector<Point2D> >      BezierRep::FitCurveSet(    const Point2D d[], size_t dSize, double error, bool & isLoop) {
+std::vector<std::vector<Point2D>> fitSet;
+fitSet.push_back(::FitCurve(d, dSize, error));
+return fitSet;
+}
+std::vector<Point2D>                    BezierRep::FitCurve(       const std::vector<Point2D> &d, double error)
+{
+return ::FitCurve(d.data(), d.size(), error);
+}
+std::vector<Point2D>                    BezierRep::FitOneCurve(const std::vector<Point2D> &d)
+{
+return::FitOneCurve(d.data(), d.size());
+}
+
+std::vector<double>                     BezierRep::Reparameterize( const Point2D d[], size_t first, size_t last, const std::vector<double> &u, const Point2D bezCurve[4])
+{
+std::vector<double> 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<double> &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<double>                     BezierRep::ChordLengthParameterize(const Point2D d[], size_t first, size_t last)
+{
+std::vector<double> 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<Point2D> 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<Point2D>                    BezierRep::Split(double tstart, double tend) const
+{
+    auto t_start_base = static_cast<size_t>(tstart);
+    auto t_end_base   = static_cast<size_t>(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<Point2D> 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<BezierLock> &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<Point2D> &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<size_t>(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<Point2D> 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<int>(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<int>(T));
+auto t = T - static_cast<int>(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<double>(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<Point2D> 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<std::vector<std::tuple<BezierRep,BezierRep>>>  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<Point2D> 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<std::vector<BezierRep>> flipSets;
+std::vector<BezierRep>              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<double>(b.MaxIndex())));
+
+}
+
+std::vector<std::vector<std::tuple<BezierRep,BezierRep>>> mirroredSides;
+for (auto &side: fragments) {
+std::vector<std::tuple<BezierRep,BezierRep>> mirrors;
+for (auto &f : side)
+mirrors.push_back(std::tuple<BezierRep,BezierRep>(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<InkControlProps> {
     @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<FieldViewProps, InkDocume
                     } else if (isEditing) {
                         this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
                     }
-                }), isEditing, isEditing, () => 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 <zzzman@gmail.com>
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/InkControlPtHandles.tsx')

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<number>(last - first + 1); //  New parameter values
+    const uPrime = new Array<number>(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<number>(last - first + 1);//  Parameterization
+    const u = new Array<number>(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<Array<Point>>(4);
+    const sz = 4;
+    const Vtemp = new Array<Array<Point>>(4);
     for (var i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(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<Point>(4), a2 = new Array<Point>(4);
+        const a1 = new Array<Point>(4), a2 = new Array<Point>(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<Point>(4), b2 = new Array<Point>(4);
+            const b1 = new Array<Point>(4), b2 = new Array<Point>(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<Point>(4), b2 = new Array<Point>(4);
+            const b1 = new Array<Point>(4), b2 = new Array<Point>(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<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
-    var Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    const nPts = last - first + 1; // Number of pts in sub-curve
+    const Ax = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    const Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> 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<Point>(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<Point>(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<InkControlProps> {
     @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<InkControlProps> {
                         } 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<InkControlProps> {
     @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<InkControlProps> {
      * 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<InkEndProps> {
             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<InkEndProps> {
     }
 
     render() {
-        const hdl = (pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={"npt"}
+        const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={key}
             cx={pt.X}
             cy={pt.Y}
             r={this.props.screenSpaceLineWidth * 2}
@@ -223,12 +219,12 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             strokeWidth={0}
             onPointerLeave={action(() => this._overStart = false)}
             onPointerEnter={action(() => this._overStart = true)}
-            onPointerDown={e => dragFunc(e)}
+            onPointerDown={dragFunc}
             pointerEvents="all"
         />;
         return (<svg>
-            {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))}
         </svg>
         );
     }
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<InkHandlesProps> {
      * @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<InkHandlesProps> {
     }
 
     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<InkHandlesProps> {
                             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"} />
                     </svg>)}
                 {tangentLines.map((pts, i) => {
                     const tangentLine = (x1: number, y1: number, x2: number, y2: number) =>
@@ -119,7 +115,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                             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 <svg height="100" width="100" key={`line${i}`}>
                         {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<FieldViewProps, InkDocument>(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<FieldViewProps, InkDocume
         CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
     }
 
+    /**
+     * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
+     *    When displayed as a mask, the stroke is rendered with mixBlendMode set to multiply so that the stroke will
+     *  appear to illuminate what it covers up.  At the same time, all pixels that are not under the stroke will be
+     *  dimmed by a semi-opaque overlay mask.
+    */
     public static toggleMask = action((inkDoc: Doc) => {
         inkDoc.isInkMask = !inkDoc.isInkMask;
         inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
@@ -88,70 +113,47 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
     });
     /**
-     * Handles the movement of the entire ink object when the user clicks and drags.
+     * Drags the a simple bezier segment of the stroke.
+     * Also adds a control point when double clicking on the stroke.
      */
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        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<FieldViewProps, InkDocume
         return { X: scrPt[0], Y: scrPt[1] };
     }
 
+    /**
+    * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs'
+    * @param scrPt - the point to snap to this stroke
+    * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments)
+    *
+    * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point
+    */
     snapPt = (scrPt: { X: number, Y: number }, excludeSegs?: number[]) => {
         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<FieldViewProps, InkDocume
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
         return SnappingManager.GetIsDragging() ? (null) :
-            !this._properties?._controlButton ?
+            !InkStrokeProperties.Instance._controlButton ?
                 (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
                     <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                         <InkEndPtHandles
@@ -287,15 +299,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             false, downHdlr);
         // Set of points rendered upon the ink that can be added if a user clicks on one.
 
-        return <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}>
+        return <div className="inkStroke-wrapper">
             <svg className="inkStroke"
                 style={{
-                    width: "100%",
-                    height: "100%",
-                    pointerEvents: "none",
                     transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
                     mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
-                    overflow: "visible",
                     cursor: this.props.isSelected() ? "default" : undefined
                 }}
                 onPointerLeave={action(e => this._nearestScrPt = undefined)}
@@ -305,7 +313,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                     const cm = ContextMenu.Instance;
                     !Doc.UserDoc().noviceMode && cm?.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
                     cm?.addItem({ description: "Toggle Mask", event: () => 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<PropertiesViewProps> {
 
     @computed
     get controlPointsButton() {
-        const formatInstance = InkStrokeProperties.Instance;
-        return !formatInstance ? (null) : <div className="inking-button">
+        return <div className="inking-button">
             <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}>
-                <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}>
+                <div className="inking-button-points"
+                    style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? "black" : "" }}
+                    onPointerDown={action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)} >
                     <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
                 </div>
             </Tooltip>
-            <Tooltip title={<div className="dash-tooltip">{formatInstance._lock ? "Unlock ratio" : "Lock ratio"}</div>}>
-                <div className="inking-button-lock" onPointerDown={action(() => formatInstance._lock = !formatInstance._lock)} >
-                    <FontAwesomeIcon icon={formatInstance._lock ? "lock" : "unlock"} color="white" size="lg" />
+            <Tooltip title={<div className="dash-tooltip">{InkStrokeProperties.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}>
+                <div className="inking-button-lock" onPointerDown={action(() => InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock)} >
+                    <FontAwesomeIcon icon={InkStrokeProperties.Instance._lock ? "lock" : "unlock"} color="white" size="lg" />
                 </div>
             </Tooltip>
             <Tooltip title={<div className="dash-tooltip">{"Rotate 90˚"}</div>}>
@@ -603,7 +604,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 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<PropertiesViewProps> {
                 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<PropertiesViewProps> {
     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<Doc>, props: Opt<DocumentViewProps
         case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null));
         case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
         case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize)));
-        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.["fontFamily"], StrCast(Doc.UserDoc().fontFamily)));
+        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily)));
         case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc &&
             StrCast(doc._showTitle,
                 props?.showTitle?.() ||
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9d0402075..72e84c141 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1146,7 +1146,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 if (startupText) {
                     dispatch(state.tr.insertText(startupText));
                 } else if (!FormattedTextBox.LiveTextUndo) {
-                    selectAll(this._editorView!.state, (tr) => {
+                    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<SearchBoxProps, SearchBoxDoc
         this._results.forEach((_, doc) => {
             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<SearchBoxProps, SearchBoxDoc
                 });
             }
             else {
-                let linkedDocSet: Set<Doc> = new Set();
+                const linkedDocSet = new Set<Doc>();
 
                 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<SearchBoxProps, SearchBoxDoc
         let converged = true;
         const pageRankFromAll = (1 - DAMPENING_FACTOR) / this._results.size;
 
-        let nextPageRanks: Map<Doc, number> = new Map<Doc, number>();
+        const nextPageRanks = new Map<Doc, number>();
 
         this._results.forEach((_, doc) => {
             let nextPageRank = pageRankFromAll;
@@ -397,7 +397,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
 
         const isLinkSearch: boolean = this.props.linkSearch;
 
-        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 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 49e9103721ecfd6d5c900c754f2d88362f64a3ab Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
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/InkControlPtHandles.tsx')

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<InkEndProps> {
         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<TreeViewProps> {
         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<TreeViewProps> {
     }
     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<TreeViewProps> {
         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<PanZoomDocument, P
     @observable _pullDirection: string = "";
     @observable _showAnimTimeline = false;
     @observable _clusterSets: (Doc[])[] = [];
-    @observable _prevPoint: PointData = { X: -1, Y: -1 };
-    @observable _currPoint: PointData = { X: -1, Y: -1 };
     @observable _deleteList: DocumentView[] = [];
     @observable _timelineRef = React.createRef<Timeline>();
     @observable _marqueeRef = React.createRef<HTMLDivElement>();
@@ -439,28 +435,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        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<PanZoomDocument, P
                 }
         }
     }
+    @action
+    onEraserUp = (e: PointerEvent): void => {
+        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<PanZoomDocument, P
             document.removeEventListener("pointerup", this.onPointerUp);
             this.removeMoveListeners();
             this.removeEndListeners();
-            if (CurrentUserUtils.SelectedTool !== InkTool.None) {
-                this._deleteList.forEach(ink => 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<PanZoomDocument, P
         this._lastY = e.clientY;
     }
 
-    @action
-    onPointerMove = (e: PointerEvent): void => {
-        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<PanZoomDocument, P
         // This iterates through all segments of the curve and splits them where they intersect another curve.
         // if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted)
         for (var i = 0; i < inkData.length - 3; i += 4) {
-            const curve = new Bezier(inkData.slice(i, i + 4).map(p => ({ 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<PanZoomDocument, P
             .filter(doc => 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<Bezier>;
 
 // 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 f0495cc1a050abfbaffe96b73bed24c8afe4b7b4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 2 Dec 2021 00:37:54 -0500
Subject: fixed filled strokes when erasing to render properly.  fixed stretch
 ink to work with multiple selections.

---
 src/client/views/InkControlPtHandles.tsx                             | 5 +++--
 src/client/views/InkingStroke.tsx                                    | 2 +-
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx  | 3 ++-
 3 files changed, 6 insertions(+), 4 deletions(-)

(limited to 'src/client/views/InkControlPtHandles.tsx')

diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 0996e75d4..ef457cac4 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -13,6 +13,7 @@ import { Colors } from "./global/globalEnums";
 import { InkingStroke } from "./InkingStroke";
 import { InkStrokeProperties } from "./InkStrokeProperties";
 import { DocumentView } from "./nodes/DocumentView";
+import { SelectionManager } from "../util/SelectionManager";
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -199,8 +200,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             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());
+            InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2(), v1n, e.shiftKey);
+            InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, p2());
             return false;
         }, action(() => {
             this.controlUndo?.end();
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index fc1727055..f5dd66949 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -314,7 +314,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
         // Invisible polygonal line that enables the ink to be selected by the user.
         const clickableLine = (downHdlr?: (e: React.PointerEvent) => void) => InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
-            inkStrokeWidth, inkStrokeWidth + (CurrentUserUtils.SelectedTool === InkTool.Eraser ? 0 : highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+            inkStrokeWidth, inkStrokeWidth + (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,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 19c3bf745..aeda71d01 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -441,7 +441,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             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))
+                !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
@@ -458,6 +458,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                         document.addEventListener("pointerup", this.onPointerUp);
                         break;
                 }
+            }
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From c2cd77ca1d2a67539f0af2a68c1e7336b3bc232b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
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/InkControlPtHandles.tsx')

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 (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
             {nodefs ? (null) : <defs>
                 {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
@@ -115,12 +118,14 @@ export namespace InteractionUtils {
                         <circle r={strokeWidth * 1.5} fill="context-stroke" />
                     </marker>}
                 {arrowStart !== "arrow" ? (null) :
-                    <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7">
-                        <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`${3 * makerStrokeWidth} ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth * 2} 0, ${3 * makerStrokeWidth} ${makerStrokeWidth * 1.5}, 0 0`} />
+                    <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
+                        <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3}
+                            points={`${arrowLengthFactor * makerStrokeWidth} ${-makerStrokeWidth * arrowWidthFactor}, ${makerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * makerStrokeWidth} ${makerStrokeWidth * arrowWidthFactor}, 0 0`} />
                     </marker>}
                 {arrowEnd !== "arrow" ? (null) :
-                    <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7">
-                        <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`0 ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth} 0, 0 ${makerStrokeWidth * 1.5}, ${3 * makerStrokeWidth} 0`} />
+                    <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
+                        <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3}
+                            points={`0 ${-makerStrokeWidth * arrowWidthFactor}, ${makerStrokeWidth * arrowNotchFactor} 0, 0 ${makerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * makerStrokeWidth} 0`} />
                     </marker>}
             </defs>}
 
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 <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
                     {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)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height}
                 style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
                 {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)}
             </svg>]
         ];
     }
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<InkControlProps> {
             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<FieldViewProps, InkDocume
 
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
+        const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
         return SnappingManager.GetIsDragging() ? (null) :
             !InkStrokeProperties.Instance._controlButton ?
                 (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
@@ -275,7 +275,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                 <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                     {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)}
                     <InkControlPtHandles
                         inkView={this.props.docViewPath().lastElement()}
                         inkDoc={inkDoc}
@@ -299,6 +299,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
 
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
+        const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
         const closed = InkingStroke.IsClosed(inkData);
         const fillColor = StrCast(this.layoutDoc.fillColor, "transparent");
         const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color);
@@ -307,7 +308,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth,
             StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
             StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
-            StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
+            markerScale, StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
         const highlightIndex = BoolCast(this.props.Document.isLinkButton) && Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
         const highlightColor = !highlightIndex ?
             StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") :
@@ -317,7 +318,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             inkStrokeWidth, inkStrokeWidth + (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,
+            markerScale, undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0,
             false, downHdlr);
 
         return <div className="inkStroke-wrapper">
@@ -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<PropertiesViewProps> {
     @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<PropertiesViewProps> {
         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<PropertiesViewProps> {
 
 
     @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<PropertiesViewProps> {
             </div>
 
             <div className="arrows">
+                <div className="arrows-head">
+                    <div className="width-top">
+                        <div className="width-title">Arrow Scale:</div>
+                        {/* <div className="width-input">{this.markScalInput}</div> */}
+                    </div>
+                    <input className="width-range" type="range"
+                        defaultValue={this.markScal} min={0} max={10}
+                        onChange={(action(e => this.markScal = +e.target.value))}
+                        onMouseDown={(e) => { this._widthUndo = UndoManager.StartBatch("scale undo"); }}
+                        onMouseUp={(e) => { this._widthUndo?.end(); this._widthUndo = undefined; }}
+                    />
+                </div>
                 <div className="arrows-head">
                     <div className="arrows-head-title" >Arrow Head: </div>
                     <input key="markHead" className="arrows-head-input" type="checkbox"
-- 
cgit v1.2.3-70-g09d2

