From 244e06ec9873888dcef3cd08322880d73848fe69 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 28 Sep 2021 18:10:26 -0400
Subject: renamed some ink files

---
 src/client/views/InkControlPtHandles.tsx | 147 +++++++++++++++++++++++++++++++
 1 file changed, 147 insertions(+)
 create mode 100644 src/client/views/InkControlPtHandles.tsx

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

diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
new file mode 100644
index 000000000..eb0eebcdf
--- /dev/null
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -0,0 +1,147 @@
+import React = require("react");
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Doc } from "../../fields/Doc";
+import { ControlPoint, InkData, PointData } from "../../fields/InkField";
+import { listSpec } from "../../fields/Schema";
+import { Cast } from "../../fields/Types";
+import { setupMoveUpEvents } from "../../Utils";
+import { Transform } from "../util/Transform";
+import { UndoManager } from "../util/UndoManager";
+import { Colors } from "./global/globalEnums";
+import { InkStrokeProperties } from "./InkStrokeProperties";
+
+export interface InkControlProps {
+    inkDoc: Doc;
+    inkCtrlPoints: InkData;
+    screenCtrlPoints: InkData;
+    screenSpaceLineWidth: number;
+    ScreenToLocalTransform: () => Transform;
+    nearestScreenPt: () => PointData | undefined;
+}
+
+@observer
+export class InkControlPtHandles extends React.Component<InkControlProps> {
+
+    @observable private _overControl = -1;
+    @observable private _overAddPoint = -1;
+    /**
+     * Handles the movement of a selected control point when the user clicks and drags.
+     * @param controlIndex The index of the currently selected control point.
+     */
+    @action
+    onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
+        if (InkStrokeProperties.Instance) {
+            InkStrokeProperties.Instance.moveControl(0, 0, 1);
+            const controlUndo = UndoManager.StartBatch("DocDecs set radius");
+            const screenScale = this.props.ScreenToLocalTransform().Scale;
+            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"));
+            setupMoveUpEvents(this, e,
+                (e: PointerEvent, down: number[], delta: number[]) => {
+                    InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
+                    return false;
+                },
+                () => controlUndo?.end(),
+                action((e: PointerEvent, doubleTap: boolean | undefined) => {
+                    const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
+                    if (doubleTap && brokenIndices?.includes(equivIndex)) {
+                        InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB);
+                    }
+                    if (doubleTap && brokenIndices?.includes(controlIndex)) {
+                        InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+                    }
+                }));
+        }
+    }
+    /**
+     * Updates whether a user has hovered over a particular control point or point that could be added
+     * on click.
+     */
+    @action onEnterControl = (i: number) => { this._overControl = i; };
+    @action onLeaveControl = () => { this._overControl = -1; };
+    @action onEnterAddPoint = (i: number) => { this._overAddPoint = i; };
+    @action onLeaveAddPoint = () => { this._overAddPoint = -1; };
+
+    /**
+     * Deletes the currently selected point.
+     */
+    @action
+    onDelete = (e: KeyboardEvent) => {
+        if (["-", "Backspace", "Delete"].includes(e.key)) {
+            if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation();
+        }
+    }
+
+    /**
+     * Changes the current selected control point.
+     */
+    @action
+    changeCurrPoint = (i: number) => {
+        if (InkStrokeProperties.Instance) {
+            InkStrokeProperties.Instance._currentPoint = i;
+            document.addEventListener("keydown", this.onDelete, true);
+        }
+    }
+
+    render() {
+        const formatInstance = InkStrokeProperties.Instance;
+        if (!formatInstance) return (null);
+
+        // Accessing the current ink's data and extracting all control points.
+        const scrData = this.props.screenCtrlPoints;
+        const sreenCtrlPoints: ControlPoint[] = [];
+        for (let i = 0; i <= scrData.length - 4; i += 4) {
+            sreenCtrlPoints.push({ ...scrData[i], I: i });
+            sreenCtrlPoints.push({ ...scrData[i + 3], I: i + 3 });
+        }
+
+        const inkData = this.props.inkCtrlPoints;
+        const inkCtrlPts: ControlPoint[] = [];
+        for (let i = 0; i <= inkData.length - 4; i += 4) {
+            inkCtrlPts.push({ ...inkData[i], I: i });
+            inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 });
+        }
+
+        const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
+        const rectHdlSize = (i: number) => this._overControl === i ? screenSpaceLineWidth * 6 : screenSpaceLineWidth * 4;
+
+        const nearestScreenPt = this.props.nearestScreenPt();
+        return (<svg>
+            {!nearestScreenPt ? (null) :
+                <circle key={"npt"}
+                    cx={nearestScreenPt.X}
+                    cy={nearestScreenPt.Y}
+                    r={screenSpaceLineWidth * 4}
+                    fill={"#00007777"}
+                    stroke={"#00007777"}
+                    strokeWidth={0}
+                    pointerEvents="none"
+                    cursor="all-scroll"
+                />
+            }
+            {sreenCtrlPoints.map((control, i) =>
+                <rect key={i}
+                    x={control.X - rectHdlSize(i) / 2}
+                    y={control.Y - rectHdlSize(i) / 2}
+                    height={rectHdlSize(i)}
+                    width={rectHdlSize(i)}
+                    strokeWidth={screenSpaceLineWidth / 2}
+                    stroke={Colors.MEDIUM_BLUE}
+                    fill={formatInstance?._currentPoint === control.I ? Colors.MEDIUM_BLUE : Colors.WHITE}
+                    onPointerDown={e => {
+                        this.changeCurrPoint(control.I);
+                        this.onControlDown(e, control.I);
+                    }}
+                    onMouseEnter={e => this.onEnterControl(i)}
+                    onMouseLeave={this.onLeaveControl}
+                    pointerEvents="all"
+                    cursor="default"
+                />
+            )}
+        </svg>
+        );
+    }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From aed57a2d6435007676409aeba562fc11d0c4a44d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 29 Sep 2021 01:38:15 -0400
Subject: a number of undo/redo fixes for ink (snapping to tangent, add points,
 dragging tangents).  also tried to make storage of undo events more efficient
 when dragging ink controls (avoid saving hundreds of copies of the InkField)

---
 src/client/util/UndoManager.ts           | 19 +++++++++++++++++++
 src/client/views/DocumentDecorations.tsx |  5 ++++-
 src/client/views/InkControlPtHandles.tsx | 27 ++++++++++++++++-----------
 src/client/views/InkStrokeProperties.ts  | 19 +++++++++++--------
 src/client/views/InkTangentHandles.tsx   | 24 ++++++++++++------------
 src/client/views/InkingStroke.tsx        |  8 ++++----
 src/fields/util.ts                       | 13 ++++++-------
 7 files changed, 72 insertions(+), 43 deletions(-)

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

diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 05fb9f378..536d90371 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -70,6 +70,7 @@ export namespace UndoManager {
     export interface UndoEvent {
         undo: () => void;
         redo: () => void;
+        prop: string;
     }
     type UndoBatch = UndoEvent[];
 
@@ -104,6 +105,23 @@ export namespace UndoManager {
     export function GetOpenBatches(): Without<Batch, 'end'>[] {
         return openBatches;
     }
+    export function FilterBatches(fieldTypes: string[]) {
+        var fieldCounts: { [key: string]: number } = {};
+        const lastStack = UndoManager.undoStack.lastElement();
+        if (lastStack) {
+            lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1));
+            var fieldCount2: { [key: string]: number } = {};
+            runInAction(() =>
+                UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
+                    if (fieldTypes.includes(ev.prop)) {
+                        fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1;
+                        if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true;
+                        return false;
+                    }
+                    return true;
+                }));
+        }
+    }
     export function TraceOpenBatches() {
         console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join("\n\t")}\n`);
     }
@@ -140,6 +158,7 @@ export namespace UndoManager {
         batchCounter--;
         // console.log("End " + batchCounter);
         if (batchCounter === 0 && currentBatch?.length) {
+            // console.log("------ended----")
             if (!cancel) {
                 undoStack.push(currentBatch);
             }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index bd61c9f60..bd9c3509b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -202,7 +202,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
                 InkStrokeProperties.Instance?.rotateInk(2 * movement.X / angle * (Math.PI / 180));
                 return false;
             },
-            () => this._rotateUndo?.end(),
+            () => {
+                this._rotateUndo?.end();
+                UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+            },
             emptyFunction);
         this._prevY = e.clientY;
         this._inkCenterPts = SelectionManager.Views()
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index eb0eebcdf..898c3bf26 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -24,7 +24,6 @@ export interface InkControlProps {
 export class InkControlPtHandles extends React.Component<InkControlProps> {
 
     @observable private _overControl = -1;
-    @observable private _overAddPoint = -1;
     /**
      * Handles the movement of a selected control point when the user clicks and drags.
      * @param controlIndex The index of the currently selected control point.
@@ -32,8 +31,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
         if (InkStrokeProperties.Instance) {
-            InkStrokeProperties.Instance.moveControl(0, 0, 1);
-            const controlUndo = UndoManager.StartBatch("DocDecs set radius");
+            var controlUndo: UndoManager.Batch | undefined;
             const screenScale = this.props.ScreenToLocalTransform().Scale;
             const order = controlIndex % 4;
             const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
@@ -41,17 +39,26 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
             const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
             setupMoveUpEvents(this, e,
                 (e: PointerEvent, down: number[], delta: number[]) => {
+                    if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
                     InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
                     return false;
                 },
-                () => controlUndo?.end(),
+                () => {
+                    controlUndo?.end();
+                    UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+                },
                 action((e: PointerEvent, doubleTap: boolean | undefined) => {
                     const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
-                    if (doubleTap && brokenIndices?.includes(equivIndex)) {
-                        InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB);
-                    }
-                    if (doubleTap && brokenIndices?.includes(controlIndex)) {
-                        InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+                    if (doubleTap) {
+                        if (brokenIndices?.includes(equivIndex)) {
+                            if (!controlUndo) controlUndo = UndoManager.StartBatch("make smooth");
+                            InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB);
+                        }
+                        if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
+                            if (!controlUndo) controlUndo = UndoManager.StartBatch("make smooth");
+                            InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+                        }
+                        controlUndo?.end();
                     }
                 }));
         }
@@ -62,8 +69,6 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
      */
     @action onEnterControl = (i: number) => { this._overControl = i; };
     @action onLeaveControl = () => { this._overControl = -1; };
-    @action onEnterAddPoint = (i: number) => { this._overAddPoint = i; };
-    @action onLeaveAddPoint = () => { this._overAddPoint = -1; };
 
     /**
      * Deletes the currently selected point.
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 6a503cc91..3770eb7c1 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -214,14 +214,16 @@ export class InkStrokeProperties {
      */
     snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => {
         this.applyFunction((doc: Doc, ink: InkData) => {
-            const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
-            if (brokenIndices) {
-                doc.brokenInkIndices = new List<number>(brokenIndices.filter(brokenIndex => brokenIndex !== controlIndex));
+            const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"), []);
+            const ind = brokenIndices.findIndex(value => value === controlIndex);
+            if (ind !== -1) {
+                brokenIndices.splice(ind, 1);
                 const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
                 const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
                 const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
-                ink[handleIndexB] = this.rotatePoint(handleB, controlPoint, angleDifference);
-                return ink;
+                const inkCopy = ink.slice();
+                inkCopy[handleIndexB] = this.rotatePoint(handleB, controlPoint, angleDifference);
+                return inkCopy;
             }
         });
     }
@@ -277,13 +279,14 @@ export class InkStrokeProperties {
             const oppositeHandlePoint = ink[oppositeHandleIndex];
             const controlPoint = ink[controlIndex];
             const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale };
-            ink[handleIndex] = newHandlePoint;
+            const inkCopy = ink.slice();
+            inkCopy[handleIndex] = newHandlePoint;
             const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
             // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
             if ((!brokenIndices || !brokenIndices?.includes(controlIndex)) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
                 const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
-                ink[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
+                inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
             }
-            return ink;
+            return inkCopy;
         })
 }
\ No newline at end of file
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index dbe9ca027..759e48134 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -27,18 +27,21 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
      */
     onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
         if (InkStrokeProperties.Instance) {
-            InkStrokeProperties.Instance.moveControl(0, 0, 1);
-            const controlUndo = UndoManager.StartBatch("DocDecs set radius");
+            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?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
                 return false;
-            }, () => controlUndo?.end(), emptyFunction
+            }, () => {
+                controlUndo?.end();
+                UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+            }, emptyFunction
             );
         }
     }
@@ -50,15 +53,12 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
      */
     @action
     onBreakTangent = (controlIndex: number) => {
-        const doc = this.props.inkDoc;
-        if (doc) {
-            const closed = this.props.screenCtrlPoints.lastElement().X === this.props.screenCtrlPoints[0].X && this.props.screenCtrlPoints.lastElement().Y === this.props.screenCtrlPoints[0].Y;
-            const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")) || new List;
-            if (!brokenIndices?.includes(controlIndex) &&
-                ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) {
-                brokenIndices.push(controlIndex);
-                doc.brokenInkIndices = brokenIndices;
-            }
+        const closed = this.props.screenCtrlPoints.lastElement().X === this.props.screenCtrlPoints[0].X && this.props.screenCtrlPoints.lastElement().Y === this.props.screenCtrlPoints[0].Y;
+        var brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
+        if (!brokenIndices?.includes(controlIndex) &&
+            ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) {
+            if (brokenIndices) brokenIndices.push(controlIndex);
+            else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
         }
     }
 
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index b921014a3..867677005 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -77,8 +77,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                         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) this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this._nearestT, this._nearestSeg, this.inkScaledData().inkData);
+                    } else if (this._properties?._controlButton) {
+                        this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
                     }
                 }), this._properties?._controlButton, this._properties?._controlButton
             );
@@ -125,7 +125,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
     }
 
     @action
-    onPointerOver = (e: React.PointerEvent) => {
+    onPointerMove = (e: React.PointerEvent) => {
         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,
@@ -212,7 +212,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                     overflow: "visible",
                 }}
                 onPointerLeave={action(e => this._nearestScrPt = undefined)}
-                onPointerMove={this.props.isSelected() ? this.onPointerOver : undefined}
+                onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined}
                 onPointerDown={this.onPointerDown}
                 onClick={e => this._handledClick && e.stopPropagation()}
                 onContextMenu={() => {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 3590c2dea..99dfc04d9 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -98,13 +98,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
         } else {
             DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
         }
-        !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({
-            redo: () => receiver[prop] = value,
-            undo: () => {
-                // console.log("Undo: " + prop + " = " + curValue); // bcz: uncomment to log undo
-                receiver[prop] = curValue;
-            }
-        });
+        !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) &&
+            UndoManager.AddEvent({
+                redo: () => receiver[prop] = value,
+                undo: () => receiver[prop] = curValue,
+                prop: prop?.toString()
+            });
         return true;
     }
     return false;
-- 
cgit v1.2.3-70-g09d2


From e5905220a84a62fff36965a3bf74a55b793ae31b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 29 Sep 2021 11:26:58 -0400
Subject: fixed filling of curves.  added toggling of brokenindex with
 right-click.   changed look of ink handles to be lighter weight and to
 reflect brokenindex sttate

---
 src/client/util/InteractionUtils.tsx     |  2 +-
 src/client/views/InkControlPtHandles.tsx | 95 ++++++++++++++++++--------------
 src/client/views/InkTangentHandles.tsx   | 55 +++++++++---------
 src/client/views/InkingStroke.tsx        |  1 +
 4 files changed, 82 insertions(+), 71 deletions(-)

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

diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 633876683..d43ebd692 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -99,7 +99,7 @@ export namespace InteractionUtils {
 
         const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
         const strpts = bezier ?
-            pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : "M" + toScr(pt) + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") :
+            pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") :
             pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, "");
 
         const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined;
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 898c3bf26..f80aca268 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -10,6 +10,7 @@ import { Transform } from "../util/Transform";
 import { UndoManager } from "../util/UndoManager";
 import { Colors } from "./global/globalEnums";
 import { InkStrokeProperties } from "./InkStrokeProperties";
+import { List } from "../../fields/List";
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -24,6 +25,8 @@ export interface InkControlProps {
 export class InkControlPtHandles extends React.Component<InkControlProps> {
 
     @observable private _overControl = -1;
+
+    @observable controlUndo: UndoManager.Batch | undefined;
     /**
      * Handles the movement of a selected control point when the user clicks and drags.
      * @param controlIndex The index of the currently selected control point.
@@ -31,34 +34,40 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
         if (InkStrokeProperties.Instance) {
-            var controlUndo: UndoManager.Batch | undefined;
             const screenScale = this.props.ScreenToLocalTransform().Scale;
             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"));
             setupMoveUpEvents(this, e,
-                (e: PointerEvent, down: number[], delta: number[]) => {
-                    if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
+                action((e: PointerEvent, down: number[], delta: number[]) => {
+                    if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
                     InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
                     return false;
-                },
-                () => {
-                    controlUndo?.end();
+                }),
+                action(() => {
+                    this.controlUndo?.end();
+                    this.controlUndo = undefined;
                     UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
-                },
+                }),
                 action((e: PointerEvent, doubleTap: boolean | undefined) => {
                     const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
-                    if (doubleTap) {
-                        if (brokenIndices?.includes(equivIndex)) {
-                            if (!controlUndo) controlUndo = UndoManager.StartBatch("make smooth");
-                            InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB);
-                        }
-                        if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
-                            if (!controlUndo) controlUndo = UndoManager.StartBatch("make smooth");
-                            InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+                    if (doubleTap || e.button === 2) {
+                        if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) {
+                            if (brokenIndices) brokenIndices.push(controlIndex);
+                            else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
+                        } else {
+                            if (brokenIndices?.includes(equivIndex)) {
+                                if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
+                                InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB);
+                            }
+                            if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
+                                if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
+                                InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+                            }
                         }
-                        controlUndo?.end();
+                        this.controlUndo?.end();
+                        this.controlUndo = undefined;
                     }
                 }));
         }
@@ -92,9 +101,6 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     }
 
     render() {
-        const formatInstance = InkStrokeProperties.Instance;
-        if (!formatInstance) return (null);
-
         // Accessing the current ink's data and extracting all control points.
         const scrData = this.props.screenCtrlPoints;
         const sreenCtrlPoints: ControlPoint[] = [];
@@ -111,41 +117,46 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         }
 
         const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
-        const rectHdlSize = (i: number) => this._overControl === i ? screenSpaceLineWidth * 6 : screenSpaceLineWidth * 4;
-
         const nearestScreenPt = this.props.nearestScreenPt();
+        const TagType = (broken?: boolean) => broken ? "rect" : "circle";
+        const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => {
+            const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I);
+            const Tag = TagType(broken) as keyof JSX.IntrinsicElements;
+            return <circle key={control.I.toString() + scale}
+                x={control.X - screenSpaceLineWidth * 3 * scale}
+                y={control.Y - screenSpaceLineWidth * 3 * scale}
+                cx={control.X}
+                cy={control.Y}
+                r={screenSpaceLineWidth * 2 * scale}
+                opacity={this.controlUndo ? 0.15 : 1}
+                height={screenSpaceLineWidth * 6 * scale}
+                width={screenSpaceLineWidth * 6 * scale}
+                strokeWidth={screenSpaceLineWidth / 2}
+                stroke={Colors.MEDIUM_BLUE}
+                fill={broken ? Colors.MEDIUM_BLUE : color}
+                onPointerDown={(e: any) => {
+                    this.changeCurrPoint(control.I);
+                    this.onControlDown(e, control.I);
+                }}
+                onMouseEnter={() => this.onEnterControl(control.I)}
+                onMouseLeave={this.onLeaveControl}
+                pointerEvents="all"
+                cursor="default"
+            />;
+        }
         return (<svg>
             {!nearestScreenPt ? (null) :
                 <circle key={"npt"}
                     cx={nearestScreenPt.X}
                     cy={nearestScreenPt.Y}
-                    r={screenSpaceLineWidth * 4}
+                    r={screenSpaceLineWidth * 2}
                     fill={"#00007777"}
                     stroke={"#00007777"}
                     strokeWidth={0}
                     pointerEvents="none"
-                    cursor="all-scroll"
                 />
             }
-            {sreenCtrlPoints.map((control, i) =>
-                <rect key={i}
-                    x={control.X - rectHdlSize(i) / 2}
-                    y={control.Y - rectHdlSize(i) / 2}
-                    height={rectHdlSize(i)}
-                    width={rectHdlSize(i)}
-                    strokeWidth={screenSpaceLineWidth / 2}
-                    stroke={Colors.MEDIUM_BLUE}
-                    fill={formatInstance?._currentPoint === control.I ? Colors.MEDIUM_BLUE : Colors.WHITE}
-                    onPointerDown={e => {
-                        this.changeCurrPoint(control.I);
-                        this.onControlDown(e, control.I);
-                    }}
-                    onMouseEnter={e => this.onEnterControl(i)}
-                    onMouseLeave={this.onLeaveControl}
-                    pointerEvents="all"
-                    cursor="default"
-                />
-            )}
+            {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
         </svg>
         );
     }
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index 759e48134..4f1a406c2 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -68,62 +68,61 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
 
         // Accessing the current ink's data and extracting all handle points and handle lines.
         const data = this.props.screenCtrlPoints;
-        const handlePoints: HandlePoint[] = [];
-        const handleLines: HandleLine[] = [];
+        const tangentHandles: HandlePoint[] = [];
+        const tangentLines: HandleLine[] = [];
         const closed = data.lastElement().X === data[0].X && data.lastElement().Y === data[0].Y;
         if (data.length >= 4) {
             for (let i = 0; i <= data.length - 4; i += 4) {
-                handlePoints.push({ ...data[i + 1], I: i + 1, dot1: i, dot2: i === 0 ? (closed ? data.length - 1 : i) : i - 1 });
-                handlePoints.push({ ...data[i + 2], I: i + 2, dot1: i + 3, dot2: i === data.length ? (closed ? (i + 4) % data.length : i + 3) : i + 4 });
+                tangentHandles.push({ ...data[i + 1], I: i + 1, dot1: i, dot2: i === 0 ? (closed ? data.length - 1 : i) : i - 1 });
+                tangentHandles.push({ ...data[i + 2], I: i + 2, dot1: i + 3, dot2: i === data.length ? (closed ? (i + 4) % data.length : i + 3) : i + 4 });
             }
             // Adding first and last (single) handle lines.
             if (closed) {
-                handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: data.length - 1 });
+                tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: data.length - 1 });
             }
             else {
-                handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 });
-                handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 });
+                tangentLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 });
+                tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 });
             }
             for (let i = 2; i < data.length - 4; i += 4) {
-                handleLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 });
+                tangentLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 });
             }
         }
         const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
 
         return (
             <>
-                {handlePoints.map((pts, i) =>
+                {tangentHandles.map((pts, i) =>
                     <svg height="10" width="10" key={`hdl${i}`}>
                         <circle
                             cx={pts.X}
                             cy={pts.Y}
                             r={screenSpaceLineWidth * 2}
-                            strokeWidth={0}
-                            fill={Colors.MEDIUM_BLUE}
+                            fill={"transparent"}
+                            strokeWidth={1}
+                            stroke={Colors.MEDIUM_BLUE}
+                            strokeDasharray={"1 1"}
                             onPointerDown={e => this.onHandleDown(e, pts.I)}
                             pointerEvents="all"
                             cursor="default"
                             display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
                     </svg>)}
-                {handleLines.map((pts, i) =>
-                    <svg height="100" width="100" key={`line${i}`}>
-                        <line
-                            x1={pts.X1}
-                            y1={pts.Y1}
-                            x2={pts.X2}
-                            y2={pts.Y2}
-                            stroke={Colors.MEDIUM_BLUE}
-                            strokeWidth={screenSpaceLineWidth}
-                            display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
+                {tangentLines.map((pts, i) => {
+                    const tangentLine = (x1: number, y1: number, x2: number, y2: number) =>
                         <line
-                            x1={pts.X2}
-                            y1={pts.Y2}
-                            x2={pts.X3}
-                            y2={pts.Y3}
+                            x1={x1}
+                            y1={y1}
+                            x2={x2}
+                            y2={y2}
                             stroke={Colors.MEDIUM_BLUE}
-                            strokeWidth={screenSpaceLineWidth}
-                            display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
-                    </svg>)}
+                            strokeDasharray={"1 1"}
+                            strokeWidth={1}
+                            display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._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)}
+                    </svg>;
+                })}
             </>
         );
     }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 867677005..4cd7f7698 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -210,6 +210,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                     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)}
                 onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined}
-- 
cgit v1.2.3-70-g09d2


From 5f95911a504a47c867198fccc32a75bf22d26056 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 29 Sep 2021 15:15:21 -0400
Subject: added snapping to close curve or to self-snap a vertex to its curve. 
 fixed ink decorations from being clipped when zoomed.  fixed crash with
 zero-length tangent

---
 src/client/views/InkControlPtHandles.tsx | 18 ++++++----
 src/client/views/InkStrokeProperties.ts  | 61 ++++++++++++++++++++++++++++++--
 src/client/views/InkingStroke.tsx        | 26 ++++----------
 src/client/views/MainView.tsx            |  2 +-
 4 files changed, 78 insertions(+), 29 deletions(-)

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

diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index f80aca268..249a0fa64 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -42,10 +42,13 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
             setupMoveUpEvents(this, e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
                     if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
-                    InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
+                    InkStrokeProperties.Instance?.moveControl(delta[0] * screenScale, delta[1] * screenScale, controlIndex);
                     return false;
                 }),
                 action(() => {
+                    if (this.controlUndo) {
+                        InkStrokeProperties.Instance?.snapControl(this.props.inkDoc, controlIndex);
+                    }
                     this.controlUndo?.end();
                     this.controlUndo = undefined;
                     UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
@@ -117,20 +120,21 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         }
 
         const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
+        const closed = inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
         const nearestScreenPt = this.props.nearestScreenPt();
         const TagType = (broken?: boolean) => broken ? "rect" : "circle";
         const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => {
             const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I);
-            const Tag = TagType(broken) as keyof JSX.IntrinsicElements;
-            return <circle key={control.I.toString() + scale}
-                x={control.X - screenSpaceLineWidth * 3 * scale}
-                y={control.Y - screenSpaceLineWidth * 3 * scale}
+            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}
                 cx={control.X}
                 cy={control.Y}
                 r={screenSpaceLineWidth * 2 * scale}
                 opacity={this.controlUndo ? 0.15 : 1}
-                height={screenSpaceLineWidth * 6 * scale}
-                width={screenSpaceLineWidth * 6 * scale}
+                height={screenSpaceLineWidth * 4 * scale}
+                width={screenSpaceLineWidth * 4 * scale}
                 strokeWidth={screenSpaceLineWidth / 2}
                 stroke={Colors.MEDIUM_BLUE}
                 fill={broken ? Colors.MEDIUM_BLUE : color}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 3770eb7c1..ac5cdfee2 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -189,7 +189,7 @@ export class InkStrokeProperties {
             const order = controlIndex % 4;
             const closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y;
 
-            return ink.map((pt, i) => {
+            const newpts = ink.map((pt, i) => {
                 const leftHandlePoint = order === 0 && i === controlIndex + 1;
                 const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
                 if (controlIndex === i ||
@@ -201,12 +201,68 @@ export class InkStrokeProperties {
                     (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
                     (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
                     ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) {
-                    return ({ X: pt.X - deltaX / xScale, Y: pt.Y - deltaY / yScale });
+                    return ({ X: pt.X + deltaX / xScale, Y: pt.Y + deltaY / yScale });
                 }
                 return pt;
             });
+            return newpts;
         })
 
+
+    public static nearestPtToStroke(ctrlPoints: { X: number, Y: number }[], refPt: { X: number, Y: number }, excludeSegs?: number[]) {
+        var distance = Number.MAX_SAFE_INTEGER;
+        var nearestT = -1;
+        var nearestSeg = -1;
+        var nearestPt = { X: 0, Y: 0 };
+        for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
+            if (excludeSegs?.includes(i)) continue;
+            const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
+            const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refPt.X, y: refPt.Y });
+            if (point.t !== undefined) {
+                const dist = Math.sqrt((point.x - refPt.X) * (point.x - refPt.X) + (point.y - refPt.Y) * (point.y - refPt.Y));
+                if (dist < distance) {
+                    distance = dist;
+                    nearestT = point.t;
+                    nearestSeg = i;
+                    nearestPt = { X: point.x, Y: point.y };
+                }
+            }
+        }
+        return { distance, nearestT, nearestSeg, nearestPt };
+    }
+
+    /**
+     * Handles the movement/scaling of a control point.
+     */
+    snapControl = (inkDoc: Doc, controlIndex: number) => {
+        const ink = Cast(inkDoc.data, InkField)?.inkData;
+        if (ink) {
+            const closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y;
+
+            // figure out which segments we don't want to snap to - avoid the dragged control point's segment and the next and prev segments (when they exist -- ie not for endpoints of unclosed curve)
+            const thisseg = Math.floor(controlIndex / 4) * 4;
+            const which = controlIndex % 4;
+            const nextseg = which > 1 && (closed || controlIndex < ink.length - 1) ? (thisseg + 4) % ink.length : -1;
+            const prevseg = which < 2 && (closed || controlIndex > 0) ? (thisseg - 4 + ink.length) % ink.length : -1;
+            const refPt = ink[controlIndex];
+            const { nearestPt } = InkStrokeProperties.nearestPtToStroke(ink, refPt, [thisseg, prevseg, nextseg]);
+
+            // nearestPt is in inkDoc coordinates -- we need to compute the distance in screen coordinates.
+            // so we scale the X & Y distances by the internal ink scale factor and then transform the final distance by the ScreenToLocal.Scale of the inkDoc itself.
+            const oldXrange = (xs => ({ coord: NumCast(inkDoc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X));
+            const oldYrange = (ys => ({ coord: NumCast(inkDoc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
+            const ptsXscale = NumCast(inkDoc._width) / (oldXrange.max - oldXrange.min);
+            const ptsYscale = NumCast(inkDoc._height) / (oldYrange.max - oldYrange.min);
+            const near = Math.sqrt((nearestPt.X - refPt.X) * (nearestPt.X - refPt.X) * ptsXscale * ptsXscale +
+                (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale);
+
+            if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) {
+                return this.moveControl((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex)
+            }
+        }
+        return false;
+    }
+
     /**
      * Snaps a control point with broken tangency back to synced rotation.
      * @param handleIndexA The handle point that retains its current position.
@@ -247,6 +303,7 @@ export class InkStrokeProperties {
     angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => {
         const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y);
         const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y);
+        if (magnitudeA === 0 || magnitudeB === 0) return 0;
         // Normalizing the vectors.
         vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA };
         vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB };
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 4cd7f7698..8b1b3ea32 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -130,25 +130,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         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] }));
-        var nearest = Number.MAX_SAFE_INTEGER;
-        this._nearestT = -1;
-        this._nearestSeg = -1;
-        this._nearestScrPt = { X: 0, Y: 0 };
-        for (var i = 0; i < screenPts.length - 3; i += 4) {
-            const array = [screenPts[i], screenPts[i + 1], screenPts[i + 2], screenPts[i + 3]];
-            const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: e.clientX, y: e.clientY });
-            if (point.t) {
-                const dist = (point.x - e.clientX) * (point.x - e.clientX) + (point.y - e.clientY) * (point.y - e.clientY);
-                if (dist < nearest) {
-                    nearest = dist;
-                    this._nearestT = point.t;
-                    this._nearestSeg = i;
-                    this._nearestScrPt = { X: point.x, Y: point.y };
-                }
-            }
-        }
+        const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
+
+        this._nearestT = nearestT;
+        this._nearestSeg = nearestSeg;
+        this._nearestScrPt = nearestPt;
     }
 
+
     nearestScreenPt = () => this._nearestScrPt;
     componentUI = (boundsLeft: number, boundsTop: number) => {
         const inkDoc = this.props.Document;
@@ -159,11 +148,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         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 screenOrigin = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
         const screenHdlPts = screenPts;
 
         return <div className="inkstroke-UI" style={{
-            clip: `rect(${boundsTop - screenOrigin[1]}px, 10000px, 10000px, ${boundsLeft - screenOrigin[0]}px)`
+            clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)`
         }} >
             {InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
                 StrCast(inkDoc.strokeBezier), StrCast(inkDoc.fillColor, "none"),
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 3f7df705f..6d0d5eb39 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -618,7 +618,7 @@ export class MainView extends React.Component {
             <GroupManager />
             <GoogleAuthenticationManager />
             <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} />
-            <ComponentDecorations boundsLeft={0} boundsTop={this.topOfMainDocContent} />
+            <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} />
             {this.topbar}
             {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
             {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)}
-- 
cgit v1.2.3-70-g09d2


From 1d2f03125cbfd1c7a03f0000454de4d70e73d690 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 29 Sep 2021 22:51:21 -0400
Subject: made IsClosed a static function.  fixed warnings and errors.

---
 src/client/util/UndoManager.ts                            | 4 ++--
 src/client/views/InkControlPtHandles.tsx                  | 5 +++--
 src/client/views/InkStrokeProperties.ts                   | 9 +++++----
 src/client/views/InkTangentHandles.tsx                    | 7 ++++---
 src/client/views/InkingStroke.tsx                         | 6 ++++--
 src/client/views/nodes/DocumentView.tsx                   | 2 +-
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 +-
 src/fields/util.ts                                        | 9 ++++++---
 8 files changed, 26 insertions(+), 18 deletions(-)

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

diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 536d90371..44e44d335 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -106,11 +106,11 @@ export namespace UndoManager {
         return openBatches;
     }
     export function FilterBatches(fieldTypes: string[]) {
-        var fieldCounts: { [key: string]: number } = {};
+        const fieldCounts: { [key: string]: number } = {};
         const lastStack = UndoManager.undoStack.lastElement();
         if (lastStack) {
             lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1));
-            var fieldCount2: { [key: string]: number } = {};
+            const fieldCount2: { [key: string]: number } = {};
             runInAction(() =>
                 UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
                     if (fieldTypes.includes(ev.prop)) {
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 249a0fa64..8162f3fdc 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -11,6 +11,7 @@ import { UndoManager } from "../util/UndoManager";
 import { Colors } from "./global/globalEnums";
 import { InkStrokeProperties } from "./InkStrokeProperties";
 import { List } from "../../fields/List";
+import { InkingStroke } from "./InkingStroke";
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -120,7 +121,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         }
 
         const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
-        const closed = inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
+        const closed = InkingStroke.IsClosed(inkData);
         const nearestScreenPt = this.props.nearestScreenPt();
         const TagType = (broken?: boolean) => broken ? "rect" : "circle";
         const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => {
@@ -147,7 +148,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
                 pointerEvents="all"
                 cursor="default"
             />;
-        }
+        };
         return (<svg>
             {!nearestScreenPt ? (null) :
                 <circle key={"npt"}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index d5033f44b..72912ff20 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -10,6 +10,7 @@ import { DocumentType } from "../documents/DocumentTypes";
 import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { SelectionManager } from "../util/SelectionManager";
 import { undoBatch } from "../util/UndoManager";
+import { InkingStroke } from "./InkingStroke";
 
 export class InkStrokeProperties {
     static Instance: InkStrokeProperties | undefined;
@@ -187,7 +188,7 @@ export class InkStrokeProperties {
     moveControl = (deltaX: number, deltaY: number, controlIndex: number) =>
         this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
             const order = controlIndex % 4;
-            const closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y;
+            const closed = InkingStroke.IsClosed(ink);
 
             const newpts = ink.map((pt, i) => {
                 const leftHandlePoint = order === 0 && i === controlIndex + 1;
@@ -237,7 +238,7 @@ export class InkStrokeProperties {
     snapControl = (inkDoc: Doc, controlIndex: number) => {
         const ink = Cast(inkDoc.data, InkField)?.inkData;
         if (ink) {
-            const closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y;
+            const closed = InkingStroke.IsClosed(ink);
 
             // figure out which segments we don't want to snap to - avoid the dragged control point's segment and the next and prev segments (when they exist -- ie not for endpoints of unclosed curve)
             const thisseg = Math.floor(controlIndex / 4) * 4;
@@ -257,7 +258,7 @@ export class InkStrokeProperties {
                 (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale);
 
             if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) {
-                return this.moveControl((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex)
+                return this.moveControl((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex);
             }
         }
         return false;
@@ -331,7 +332,7 @@ export class InkStrokeProperties {
     @action
     moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
         this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
-            const closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y;
+            const closed = InkingStroke.IsClosed(ink);
             const oldHandlePoint = ink[handleIndex];
             const oppositeHandlePoint = ink[oppositeHandleIndex];
             const controlPoint = ink[controlIndex];
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index 4f1a406c2..8f7bef936 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -11,6 +11,7 @@ import { Transform } from "../util/Transform";
 import { UndoManager } from "../util/UndoManager";
 import { Colors } from "./global/globalEnums";
 import { InkStrokeProperties } from "./InkStrokeProperties";
+import { InkingStroke } from "./InkingStroke";
 
 export interface InkHandlesProps {
     inkDoc: Doc;
@@ -53,8 +54,8 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
      */
     @action
     onBreakTangent = (controlIndex: number) => {
-        const closed = this.props.screenCtrlPoints.lastElement().X === this.props.screenCtrlPoints[0].X && this.props.screenCtrlPoints.lastElement().Y === this.props.screenCtrlPoints[0].Y;
-        var brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
+        const closed = InkingStroke.IsClosed(this.props.screenCtrlPoints);
+        const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
         if (!brokenIndices?.includes(controlIndex) &&
             ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) {
             if (brokenIndices) brokenIndices.push(controlIndex);
@@ -70,7 +71,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
         const data = this.props.screenCtrlPoints;
         const tangentHandles: HandlePoint[] = [];
         const tangentLines: HandleLine[] = [];
-        const closed = data.lastElement().X === data[0].X && data.lastElement().Y === data[0].Y;
+        const closed = InkingStroke.IsClosed(data);
         if (data.length >= 4) {
             for (let i = 0; i <= data.length - 4; i += 4) {
                 tangentHandles.push({ ...data[i + 1], I: i + 1, dot1: i, dot2: i === 0 ? (closed ? data.length - 1 : i) : i - 1 });
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 552318e23..42a09fa52 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -29,6 +29,9 @@ const InkDocument = makeInterface(documentSchema);
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
     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;
@@ -144,7 +147,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const inkDoc = this.props.Document;
         const screenSpaceCenterlineStrokeWidth = 3;  // the width of the blue line widget that shows the centerline of the ink stroke
         const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
-        const closed = inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
 
         const screenInkWidth = this.props.ScreenToLocalTransform().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth);
         const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
@@ -184,7 +186,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
 
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
-        const closed = inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
+        const closed = InkingStroke.IsClosed(inkData);
         const fillColor = StrCast(this.layoutDoc.fillColor, "transparent");
         const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color);
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1187521b3..1ccf38de2 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -92,7 +92,7 @@ export interface DocComponentView {
     playFrom?: (time: number, endTime?: number) => void;
     Pause?: () => void;
     setFocus?: () => void;
-    componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element;
+    componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
     fieldKey?: string;
     annotationKey?: string;
     getTitle?: () => string;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 7afa54d5f..aa53f751d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -960,7 +960,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     GoogleApiClientUtils.Docs.write({ reference, content, mode });
                 }
             };
-            UndoManager.AddEvent({ undo, redo });
+            UndoManager.AddEvent({ undo, redo, prop: "" });
             redo();
         });
     }
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 99dfc04d9..c708affe3 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -405,7 +405,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                 ind !== -1 && receiver[prop].splice(ind, 1);
                             });
                             lastValue = ObjectField.MakeCopy(receiver[prop]);
-                        })
+                        }),
+                        prop: ""
                     } :
                     diff?.op === "$remFromSet" ?
                         {
@@ -423,7 +424,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                     ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item);
                                 });
                                 lastValue = ObjectField.MakeCopy(receiver[prop]);
-                            }
+                            },
+                            prop: ""
                         }
                         : {
                             redo: () => {
@@ -434,7 +436,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                 // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo 
                                 receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>);
                                 lastValue = ObjectField.MakeCopy(receiver[prop]);
-                            }
+                            },
+                            prop: ""
                         });
         }
         target[Update](op);
-- 
cgit v1.2.3-70-g09d2


From 4e4a1ec7bb6c479e8fd1b0a7bfe73e2447bafc74 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 30 Sep 2021 13:11:26 -0400
Subject: fixed creating/drawing straight horizontal/vertical lines.  fixed
 showing proper context menu for ink.

---
 src/client/views/DocumentButtonBar.tsx   |  3 ++-
 src/client/views/InkControlPtHandles.tsx |  2 +-
 src/client/views/InkStrokeProperties.ts  | 12 ++++++------
 src/client/views/InkTangentHandles.tsx   |  2 +-
 src/client/views/InkingStroke.tsx        |  4 ++--
 5 files changed, 12 insertions(+), 11 deletions(-)

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

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 8edd7e5bd..aa9318310 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -28,6 +28,7 @@ import { PresBox } from './nodes/trails/PresBox';
 import { undoBatch } from '../util/UndoManager';
 import { CollectionViewType } from './collections/CollectionView';
 import { Colors } from './global/globalEnums';
+import { DashFieldView } from './nodes/formattedText/DashFieldView';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -338,7 +339,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         while (child.children.length) {
             const next = Array.from(child.children).find(c => c.className?.toString().includes("SVGAnimatedString") || typeof (c.className) === "string");
             if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break;
-            if (next?.className?.toString().includes("dashFieldView")) break;
+            if (next?.className?.toString().includes(DashFieldView.name)) break;
             if (next) child = next;
             else break;
         }
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 8162f3fdc..8eb74381a 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -43,7 +43,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
             setupMoveUpEvents(this, e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
                     if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
-                    InkStrokeProperties.Instance?.moveControl(delta[0] * screenScale, delta[1] * screenScale, controlIndex);
+                    InkStrokeProperties.Instance?.moveControlPtHandle(delta[0] * screenScale, delta[1] * screenScale, controlIndex);
                     return false;
                 }),
                 action(() => {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 72912ff20..00201fa56 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -45,8 +45,8 @@ export class InkStrokeProperties {
                     if (ink) {
                         const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X));
                         const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
-                        const ptsXscale = NumCast(doc._width) / (oldXrange.max - oldXrange.min);
-                        const ptsYscale = NumCast(doc._height) / (oldYrange.max - oldYrange.min);
+                        const ptsXscale = NumCast(doc._width) / ((oldXrange.max - oldXrange.min) || 1);
+                        const ptsYscale = NumCast(doc._height) / ((oldYrange.max - oldYrange.min) || 1);
                         const newPoints = func(doc, ink, ptsXscale, ptsYscale);
                         if (newPoints) {
                             const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X));
@@ -185,7 +185,7 @@ export class InkStrokeProperties {
      */
     @undoBatch
     @action
-    moveControl = (deltaX: number, deltaY: number, controlIndex: number) =>
+    moveControlPtHandle = (deltaX: number, deltaY: number, controlIndex: number) =>
         this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
             const order = controlIndex % 4;
             const closed = InkingStroke.IsClosed(ink);
@@ -258,7 +258,7 @@ export class InkStrokeProperties {
                 (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale);
 
             if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) {
-                return this.moveControl((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex);
+                return this.moveControlPtHandle((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex);
             }
         }
         return false;
@@ -278,7 +278,7 @@ export class InkStrokeProperties {
                 const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
                 const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
                 const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
-                const inkCopy = ink.slice();
+                const inkCopy = ink.slice(); // have to make a new copy of the array to keep from corrupting undo/redo. without slicing, the same array will be stored in each undo step meaning earlier undo steps will be inadvertently updated to store the latest value.
                 inkCopy[handleIndexB] = this.rotatePoint(handleB, controlPoint, angleDifference);
                 return inkCopy;
             }
@@ -330,7 +330,7 @@ export class InkStrokeProperties {
      */
     @undoBatch
     @action
-    moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
+    moveTangentHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
         this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
             const closed = InkingStroke.IsClosed(ink);
             const oldHandlePoint = ink[handleIndex];
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index 9e37e005b..df5bebf31 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -37,7 +37,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
             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?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
+                InkStrokeProperties.Instance?.moveTangentHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
                 return false;
             }, () => {
                 controlUndo?.end();
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 42a09fa52..5438350d8 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -123,8 +123,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             inkLeft,
             inkWidth,
             inkHeight,
-            inkScaleX: inkHeight === inkStrokeWidth ? 1 : (this.props.PanelWidth() - inkStrokeWidth) / (inkWidth - inkStrokeWidth),
-            inkScaleY: inkWidth === inkStrokeWidth ? 1 : (this.props.PanelHeight() - inkStrokeWidth) / (inkHeight - inkStrokeWidth)
+            inkScaleX: ((this.props.PanelWidth() - inkStrokeWidth) / ((inkWidth - inkStrokeWidth) || 1) || 1),
+            inkScaleY: ((this.props.PanelHeight() - inkStrokeWidth) / ((inkHeight - inkStrokeWidth) || 1) || 1)
         };
     }
 
-- 
cgit v1.2.3-70-g09d2


From c0423cc9c04e87bb4bff79d2d1bcdd0612efb1a2 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 30 Sep 2021 14:59:06 -0400
Subject: dont select ink control point on drag -- only with an explicit click.
  toggle ink control point selection with clicks.

---
 src/client/views/InkControlPtHandles.tsx | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 8eb74381a..94c238b2b 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -40,6 +40,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
             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;
             setupMoveUpEvents(this, e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
                     if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
@@ -73,7 +74,8 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
                         this.controlUndo?.end();
                         this.controlUndo = undefined;
                     }
-                }));
+                    this.changeCurrPoint(controlIndex);
+                }), undefined, undefined, () => wasSelected && this.changeCurrPoint(-1));
         }
     }
     /**
@@ -139,10 +141,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
                 strokeWidth={screenSpaceLineWidth / 2}
                 stroke={Colors.MEDIUM_BLUE}
                 fill={broken ? Colors.MEDIUM_BLUE : color}
-                onPointerDown={(e: any) => {
-                    this.changeCurrPoint(control.I);
-                    this.onControlDown(e, control.I);
-                }}
+                onPointerDown={(e: any) => this.onControlDown(e, control.I)}
                 onMouseEnter={() => this.onEnterControl(control.I)}
                 onMouseLeave={this.onLeaveControl}
                 pointerEvents="all"
-- 
cgit v1.2.3-70-g09d2


From cc5294616936c5e8a3a09f51fba4e6e89ce2c26c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 1 Oct 2021 11:30:45 -0400
Subject: fixed keyboard delete of ink control points to never delete the
 stroke.

---
 src/client/views/InkControlPtHandles.tsx | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 94c238b2b..0644488b3 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -28,6 +28,13 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @observable private _overControl = -1;
 
     @observable controlUndo: UndoManager.Batch | undefined;
+
+    componentDidMount() {
+        document.addEventListener("keydown", this.onDelete, true);
+    }
+    componentWillUnmount() {
+        document.removeEventListener("keydown", this.onDelete, true);
+    }
     /**
      * Handles the movement of a selected control point when the user clicks and drags.
      * @param controlIndex The index of the currently selected control point.
@@ -91,7 +98,8 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @action
     onDelete = (e: KeyboardEvent) => {
         if (["-", "Backspace", "Delete"].includes(e.key)) {
-            if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation();
+            InkStrokeProperties.Instance?.deletePoints();
+            e.stopPropagation();
         }
     }
 
@@ -102,7 +110,6 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     changeCurrPoint = (i: number) => {
         if (InkStrokeProperties.Instance) {
             InkStrokeProperties.Instance._currentPoint = i;
-            document.addEventListener("keydown", this.onDelete, true);
         }
     }
 
-- 
cgit v1.2.3-70-g09d2

