From cc7c677399d1aafa4078741922ffcd0b81ad720f Mon Sep 17 00:00:00 2001
From: vkalev <victor_kalev@brown.edu>
Date: Wed, 24 Nov 2021 12:45:50 -0500
Subject: segmentation of ink strokes prior to deletion

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 192 ++++++++++++---------
 1 file changed, 114 insertions(+), 78 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 ce4acb7ad..4aac49469 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -5,7 +5,7 @@ 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 } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool, PointData, Intersection, Segment } from "../../../../fields/InkField";
 import { List } from "../../../../fields/List";
 import { ObjectField } from "../../../../fields/ObjectField";
 import { RichTextField } from "../../../../fields/RichTextField";
@@ -53,6 +53,7 @@ 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",
@@ -648,6 +649,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         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.
             const eraserIntersections: Intersection[] = this.getEraserIntersections();
             if (eraserIntersections) this.eraseInkStrokes(eraserIntersections);
         }
@@ -666,68 +668,28 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
     }
 
-    @action
+    /**
+     * 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.
+     */
     eraseInkStrokes = (eraserIntersections: Intersection[]) => {
         eraserIntersections.forEach(intersect => {
-            var t1, t2;
-            var distT1: number, distT2: number;
-            distT1 = distT2 = Number.MAX_SAFE_INTEGER;
-            const t = intersect.t;
             const ink = intersect.ink;
-            const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
-            for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
-                const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
-                const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
-                const inkIntersections: number[] = this.getInkIntersections(curve);
-                inkIntersections.forEach(currentT => {
-                    const diff = t - currentT;
-                    if (diff > 0 && diff < distT1) {
-                        distT1 = diff;
-                        t1 = currentT;
-                    } else if (diff < 0 && diff < distT2) {
-                        distT2 = -diff;
-                        t2 = currentT;
-                    }
-                });
-
-                // Normal case --> deletion of entire stroke
-                if (!t1 && !t2) {
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
-                } else if (t1 && !t2) {
-                    const splitCurve = curve.split(t1);
-                    const leftInkData: PointData[] = splitCurve.left.points.map(p => ({ X: p.x, Y: p.y }));
-                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData);
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
-                } else if (!t1 && t2) {
-                    const splitCurve = curve.split(t2);
-                    const rightInkData: PointData[] = splitCurve.right.points.map(p => ({ X: p.x, Y: p.y }));
-                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData);
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
-                } else if (t1 && t2) {
-                    const splitCurve1 = curve.split(t1);
-                    const leftInkData: PointData[] = splitCurve1.left.points.map(p => ({ X: p.x, Y: p.y }));
-                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData);
-                    const splitCurve2 = curve.split(t2);
-                    const rightInkData: PointData[] = splitCurve2.right.points.map(p => ({ X: p.x, Y: p.y }));
-                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData);
-
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
+            if (ink) {
+                if (!this._deleteList.includes(ink)) {
+                    const segments: Segment[] = this.segmentInkStroke(ink, intersect.index ?? 0);
+                    segments.forEach(segment => {
+                        const newInkData: PointData[] = [];
+                        // Appending all curves of the current segment together in order to render a single new stroke.
+                        segment.forEach(curve => {
+                            newInkData.push(...curve.points.map(p => ({ X: p.x, Y: p.y })));
+                        });
+                        GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, newInkData);
+                    });
+                    this._deleteList.push(ink);
+                    // Lowering ink opacity to give the user a visual indicator of deletion.
+                    ink.Document.opacity = 0.5;
                 }
             }
         });
@@ -756,9 +718,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                             p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
                         });
                         if (inkView && intersects) {
-                            intersects.forEach(t => {
-                                intersections.push({ t: +t.toString(), ink: inkView });
-                            });
+                            for (const val of intersects) {
+                                // Casting t-value from type: (string | number) to number for comparisons.
+                                const t = +val.toString();
+                                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, index: i });
+                            }
                         }
                     }
                 }
@@ -766,36 +738,100 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         return intersections;
     }
 
-    getInkIntersections = (curve: Bezier): number[] => {
-        const intersections: number[] = [];
-        const selfIntersect = curve.selfintersects();
-        if (selfIntersect) {
-            selfIntersect.forEach(t => {
-                intersections.push(+t.toString());
-            });
+    /**
+     * Performs segmentation of the ink stroke - creates "segments" or subsections of the current ink stroke at points in which the
+     * ink stroke intersects any other ink stroke (including itself).
+     * @param ink The ink DocumentView intersected by the eraser.
+     * @param excludeCurve The index of the curve in the ink document that the eraser intersection occurred.
+     * @returns The ink stroke represented as a list of segments, excluding the segment in which the eraser intersection occurred.
+     */
+    @action
+    segmentInkStroke = (ink: DocumentView, excludeCurve: number): Segment[] => {
+        const segments: Segment[] = [];
+        var segment: Segment = [];
+        var excludeSegment: boolean = false;
+        const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
+        // Iterating through all of the curves of the intersected ink stroke.
+        for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
+            // Performing "deletion" of a segment by excluding the segment in which intersection with the eraser occurred.
+            if (i === excludeCurve) excludeSegment = true;
+            const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
+            const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
+            // Getting all t-value intersections of the current curve with all other curves.
+            const tVals: number[] = this.getInkIntersections(i, ink, curve);
+            if (tVals.length > 0) {
+                tVals.sort();
+                const curveCopy = new Bezier(curve.points);
+                // Splitting only by the first t-value.
+                const pair = curveCopy.split(tVals[0]);
+                segment.push(pair.left);
+                if (!excludeSegment) {
+                    segments.push(segment);
+                    segment = [pair.right];
+                    excludeSegment = false;
+                }
+                // Splitting by the number of t-values returned.
+                // for (var index = 0; index < tVals.length; index++) {
+                //     const curveCopy = new Bezier(curve.points);
+                //     if (index === 0) {
+                //         const pair = curveCopy.split(tVals[index]);
+                //         segment.push(pair.left);
+                //         segments.push(segment);
+                //         segment = [pair.right];
+                //     } else if (index === tVals.length - 1) {
+                //         segments.push(segment);
+                //         const pair = curveCopy.split(tVals[index]);
+                //         segment = [pair.right];
+                //     } else {
+                //         segments.push(segment);
+                //         const curve = curveCopy.split(index, index + 1);
+                //         segment = [curve];
+                //     }
+                // }
+            } else {
+                segment.push(curve);
+            }
         }
+        if (!excludeSegment) segments.push(segment);
+        return segments;
+    }
 
+    /**
+     * Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all
+     * ink strokes in the current collection.
+     * @param i The index of the current curve within the inkData of the intersected ink stroke.
+     * @param ink The intersected DocumentView of the ink stroke.
+     * @param curve The current curve of the intersected ink stroke.
+     * @returns A list of all t-values at which intersections occur at the current curve of the intersected ink stroke.
+     */
+    getInkIntersections = (i: number, ink: DocumentView, curve: Bezier): number[] => {
+        const tVals: number[] = [];
+        // Iterating through all ink strokes in the current freeform collection.
         this.childDocs
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
                 const currInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
                 const currCtrls = Cast(currInk?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
-                for (var i = 0; i < currCtrls.length - 3; i += 4) {
-                    const currArray = [currCtrls[i], currCtrls[i + 1], currCtrls[i + 2], currCtrls[i + 3]];
+                for (var j = 0; j < currCtrls.length - 3; j += 4) {
+                    const exclude = i === j || i === j - 4 || i === j + 4;
+                    const sameDoc = ink?.Document === currInk?.Document;
+                    // Ensuring that the curve intersected by the eraser is not checked for further ink intersections.
+                    if (sameDoc && exclude) continue;
+                    const currArray = [currCtrls[j], currCtrls[j + 1], currCtrls[j + 2], currCtrls[j + 3]];
                     const currCurve = new Bezier(currArray.map(p => ({ x: p.X, y: p.Y })));
                     const intersect = curve.intersects(currCurve);
-                    if (intersect) {
-                        intersect.forEach(t => {
-                            intersections.push(+this.parseTValue(t.toString()));
+                    if (intersect.length > 0) {
+                        intersect.forEach(val => {
+                            // Converting the Bezier.js Split type to a t-value number.
+                            const t = +val.toString().split("/")[0];
+                            if (!tVals.includes(t)) tVals.push(t);
                         });
                     }
                 }
             });
-        return intersections;
+        return tVals;
     }
 
-    parseTValue = (t: string) => t.split("/", 1)[0];
-
     handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
         if (!e.cancelBubble) {
             const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
-- 
cgit v1.2.3-70-g09d2

