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.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 219 ++++++++++-----------
 1 file changed, 103 insertions(+), 116 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

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) {
-- 
cgit v1.2.3-70-g09d2

