From 84098c41aaa4970d43b88645489e64c1cac22934 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 15 May 2024 14:33:50 -0400
Subject: three eraser modes implemented

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 141 +++++++++++----------
 1 file changed, 73 insertions(+), 68 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 14d20eb4a..e2965c1ba 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -738,12 +738,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     _eraserLock = 0;
     _eraserPts: number[][] = []; // keep track of the last few eraserPts to make the eraser circle 'stretch'
+
     /**
      * 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.
      */
-
     @action
     onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
         const currPoint = { X: e.clientX, Y: e.clientY };
@@ -776,7 +776,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         );
                     });
                     newStrokes && this.addDocument?.(newStrokes);
-                    setTimeout(() => this._eraserLock--);
+                    // setTimeout(() => this._eraserLock--);  
                 }
                 // Lower ink opacity to give the user a visual indicator of deletion.
                 intersect.inkView.layoutDoc.opacity = 0;
@@ -797,23 +797,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      */
     @action
     onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
-        // const currZoom = this.zoomScaling();
-        // console.log("curr zoom", currZoom);
-        // console.log("prev zoom", this.prevZoom);
         const currPoint = { X: e.clientX, Y: e.clientY };
         this._eraserPts.push([currPoint.X, currPoint.Y]);
         this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
-        const intersections: any[] = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
-        // if (intersections[0].size > 0 && currZoom === this.prevZoom) {
-        const strokeMap: Map<DocumentView, number[]> = intersections[0];
-        const eraserStroke = intersections[1];
+        const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
 
         strokeMap.forEach((intersects, stroke) => {
             if (!this._deleteList.includes(stroke)) {
                 this._deleteList.push(stroke);
                 SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
                 SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
-                const segments = this.radiusErase(stroke, intersects.sort(), eraserStroke);
+                const segments = this.radiusErase(stroke, intersects.sort());
                 segments?.forEach(segment =>
                     this.forceStrokeGesture(
                         e,
@@ -825,12 +819,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             stroke.layoutDoc.opacity = 0;
             stroke.layoutDoc.dontIntersect = true;
         });
-        // } else if (intersections[0].length > 0) {
-        //     this.prevZoom = currZoom;
-        //     console.log("skipping occurred");
-        // } else if (this.prevZoom = currZoom) {
-        //     this.prevZoom = currZoom;
-        // }
         return false;
     };
 
@@ -852,7 +840,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     /**
-     * Creates the eraser outline for a radius eraser. The outline be
+     * Creates the eraser outline for a radius eraser. The outline is used to intersect with ink strokes and determine
+     * what falls inside the eraser outline.
      * @param startInkCoordsIn
      * @param endInkCoordsIn
      * @param inkStrokeWidth
@@ -860,7 +849,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      */
     createEraserOutline = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }, inkStrokeWidth: number) => {
         // increase radius slightly based on the erased stroke's width, added to make eraser look more realistic
-        var radius = ActiveEraserWidth() + inkStrokeWidth * 0.2;
+        var radius = ActiveEraserWidth() + 5 + inkStrokeWidth * 0.1; // add 5 to prevent eraser from being too small
         const c = 0.551915024494; // circle tangent length to side ratio
         const movement = { x: endInkCoordsIn.X - startInkCoordsIn.X, y: endInkCoordsIn.Y - startInkCoordsIn.Y };
         const moveLen = Math.sqrt(movement.x ** 2 + movement.y ** 2);
@@ -909,7 +898,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     /**
-     * Ray-tracing algorithm to determine whether a point is inside the eraser outline
+     * Ray-tracing algorithm to determine whether a point is inside the eraser outline.
      * @param eraserOutline
      * @param point
      * @returns
@@ -917,8 +906,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     insideEraserOutline = (eraserOutline: InkData, point: { X: number; Y: number }) => {
         var isInside = false;
         if (!isNaN(eraserOutline[0].X) && !isNaN(eraserOutline[0].Y)) {
-            let minX = eraserOutline[0].X, maxX = eraserOutline[0].X; // prettier-ignore
-            let minY = eraserOutline[0].Y, maxY = eraserOutline[0].Y; // prettier-ignore
+            let minX = eraserOutline[0].X, maxX = eraserOutline[0].X; 
+            let minY = eraserOutline[0].Y, maxY = eraserOutline[0].Y;
             for (let i = 1; i < eraserOutline.length; i++) {
                 const currPoint: { X: number; Y: number } = eraserOutline[i];
                 minX = Math.min(currPoint.X, minX);
@@ -985,14 +974,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     /**
-     * Same as getEraserIntersections but specific to the radius eraser. Populates a Map of each intersected DocumentView
-     * to the t-values where the eraser intersected it, then returns this map.
-     * @returns 
+     * Same as getEraserIntersections but specific to the radius eraser. The key difference is that the radius eraser
+     * will often intersect multiple strokes, depending on what strokes are inside the eraser. Populates a Map of each 
+     * intersected DocumentView to the t-values where the eraser intersected it, then returns this map.
+     * @returns
      */
     getRadiusEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => {
-        const eraserRadius = ActiveEraserWidth() / this.zoomScaling();
-        const eraserMin = { X: Math.min(lastPoint.X, currPoint.X) - eraserRadius, Y: Math.min(lastPoint.Y, currPoint.Y) - eraserRadius };
-        const eraserMax = { X: Math.max(lastPoint.X, currPoint.X) + eraserRadius, Y: Math.max(lastPoint.Y, currPoint.Y) + eraserRadius };
+        // set distance of the eraser's bounding box based on the zoom
+        var boundingBoxDist = ActiveEraserWidth() + 5;
+        this.zoomScaling() < 1 ? boundingBoxDist = boundingBoxDist / (this.zoomScaling() * 1.5) : boundingBoxDist *= this.zoomScaling();
+
+        const eraserMin = { X: Math.min(lastPoint.X, currPoint.X) - boundingBoxDist, Y: Math.min(lastPoint.Y, currPoint.Y) - boundingBoxDist };
+        const eraserMax = { X: Math.max(lastPoint.X, currPoint.X) + boundingBoxDist, Y: Math.max(lastPoint.Y, currPoint.Y) + boundingBoxDist };
         const strokeToTVals = new Map<DocumentView, number[]>();
         const intersectingStrokes = this.childDocs
             .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.()))
@@ -1006,13 +999,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     eraserMax.X >= inkViewBounds.left &&
                     eraserMax.Y >= inkViewBounds.top
             );
-        const erasers: InkData[] = [];
+
         intersectingStrokes.forEach(({ inkStroke, inkView }) => {
             const { inkData, inkStrokeWidth } = inkStroke.inkScaledData();
             const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
             const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
             const eraserInkData = this.createEraserOutline(prevPointInkSpace, currPointInkSpace, inkStrokeWidth).inkData;
-            // erasers.push(eraserInkData);
 
             // add the ends of the stroke in as "intersections"
             if (this.insideEraserOutline(eraserInkData, inkData[0])) {
@@ -1039,7 +1031,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             // here, add to the map
                             const inkList = strokeToTVals.get(inkView);
                             if (inkList !== undefined) {
-                                const tValOffset = ActiveEraserWidth() / 1000; // to prevent tVals from being added when too close, but scaled by eraser width
+                                const tValOffset = ActiveEraserWidth() / 1050; // to prevent tVals from being added when too close, but scaled by eraser width
                                 const inList = inkList.some(val => Math.abs(val - (t + Math.floor(i / 4))) <= tValOffset);
                                 if (!inList) {
                                     inkList.push(t + Math.floor(i / 4));
@@ -1052,42 +1044,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }
             }
         });
-        return [strokeToTVals, erasers];
+        return strokeToTVals;
     };
 
     /**
-     * Splits the passed in ink stroke at the intersection t values, taking out the erased parts. 
+     * Splits the passed in ink stroke at the intersection t values, taking out the erased parts.
      * Operates in pairs of t values, where the first t value is the start of the erased portion and the following t value is the end.
      * @param ink the ink stroke DocumentView to split
      * @param tVals all the t values to split the ink stroke at
      * @returns a list of the new segments with the erased part removed
      */
     @action
-    radiusErase = (ink: DocumentView, tVals: number[], erasers: InkData[]): Segment[] => {
+    radiusErase = (ink: DocumentView, tVals: number[]): Segment[] => {
         const segments: Segment[] = [];
-        var eraser: Segment = [];
-        // for (var i = 0; i < erasers.length; i ++) {
-        //     for (var j = 0; j < erasers[i].length - 3; j +=4) {
-        //         eraser.push(InkField.Segment(erasers[i], j));
-        //     }
-        //     segments.push(eraser);
-        //     eraser = [];
-        // }
-
         const inkStroke = ink?.ComponentView as InkingStroke;
         const { inkData } = inkStroke.inkScaledData();
         var currSegment: Segment = [];
+
+        // any radius erase stroke will always result in even tVals, since the ends are included
         if (tVals.length % 2 !== 0) {
-            // any radius erase stroke will always result in even tVals, since the ends are included
             for (var i = 0; i < inkData.length - 3; i += 4) {
                 currSegment.push(InkField.Segment(inkData, i));
             }
             segments.push(currSegment);
-            return segments; // want to return the full original stroke
+            return segments; // return the full original stroke
         }
 
         var continueErasing = false; // used to erase segments if they are completely enclosed in the eraser
-        var firstSegment: Segment = []; // used to keep track of the first segment for closed curves 
+        var firstSegment: Segment = []; // used to keep track of the first segment for closed curves
 
         // early return if nothing to split on
         if (tVals.length === 0 || (tVals.length === 1 && tVals[0] === 0)) {
@@ -1108,15 +1092,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
             if (segmentTs.length > 0) {
                 for (var j = 0; j < segmentTs.length; j++) {
-                    if (segmentTs[j] === 0) { // if the first end of the segment is within the eraser
+                    if (segmentTs[j] === 0) {
+                        // if the first end of the segment is within the eraser
                         continueErasing = true;
-                    } else if (segmentTs[j] === Math.floor(inkData.length / 4) + 1) { // the last end
+                    } else if (segmentTs[j] === Math.floor(inkData.length / 4) + 1) {
+                        // the last end
                         break;
                     } else {
                         if (!continueErasing) {
                             currSegment.push(inkBezier.split(0, segmentTs[j] - currCurveT));
                             continueErasing = true;
-                        } else { // we've reached the end of the part to take out...
+                        } else {
+                            // we've reached the end of the part to take out...
                             continueErasing = false;
                             if (currSegment.length > 0) {
                                 segments.push(currSegment); // ...so we add it to the list and reset currSegment
@@ -1168,7 +1155,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         for (var i = 0; i < inkData.length - 3; i += 4) {
             const inkSegment: Bezier = InkField.Segment(inkData, i);
             var currIntersects = this.getInkIntersections(i, ink, inkSegment).sort();
-            // get current segments intersections (if any) and add the curve index
+            // get current segment's intersections (if any) and add the curve index
             currIntersects = currIntersects.filter(tVal => tVal > 0 && tVal < 1).map(tVal => tVal + Math.floor(i / 4));
             if (currIntersects.length) {
                 intersections = [...intersections, ...currIntersects];
@@ -1181,6 +1168,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         var isClosedCurve = false;
         if (InkingStroke.IsClosed(inkData)) {
             isClosedCurve = true;
+            if (intersections.length === 1) { // delete whole stroke if a closed curve has 1 intersection
+                return segments;
+            }
         }
 
         if (intersections.length) {
@@ -1190,19 +1180,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             // find the segments that need to be split
             var splitSegment1 = -1; // stays -1 if left end is deleted
             var splitSegment2 = -1; // stays -1 if right end is deleted
-            if (closestTs[0] !== -1 && closestTs[1] !== -1) {
-                // if not on the ends
+            if (closestTs[0] !== -1 && closestTs[1] !== -1) { // if not on the ends
                 splitSegment1 = segmentIndexes[closestTs[0]];
                 splitSegment2 = segmentIndexes[closestTs[1]];
-            } else if (closestTs[0] === -1) {
-                // for a curve before an intersection
+            } else if (closestTs[0] === -1) { // for a curve before an intersection
                 splitSegment2 = segmentIndexes[closestTs[1]];
-            } else {
-                // for a curve after an intersection
+            } else { // for a curve after an intersection
                 splitSegment1 = segmentIndexes[closestTs[0]];
             }
-
-            // so here splitSegment1 and splitSegment2 will be the index(es) of the segment(s) to split
+            // so by this point splitSegment1 and splitSegment2 will be the index(es) of the segment(s) to split
 
             var hasSplit = false;
             var continueErasing = false;
@@ -1236,7 +1222,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 } else if (splitSegment1 === -1) {
                     // case where first end is erased
                     if (currCurveT === splitSegment2) {
-                        segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
+                        if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) {
+                            segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, intersections.lastElement() - currCurveT));
+                            continueErasing = true;
+                        } else {
+                            segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
+                        }
                         hasSplit = true;
                     } else {
                         if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) {
@@ -1249,13 +1240,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 } else {
                     // case where last end is erased
                     if (currCurveT === segmentIndexes[0] && isClosedCurve) {
-                        segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
+                        if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) {
+                            segment1.push(inkSegment.split(intersections[0] - currCurveT, intersections.lastElement() - currCurveT));
+                            continueErasing = true;
+                        } else {
+                            segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
+                        }
                         hasSplit = true;
                     } else if (currCurveT === splitSegment1) {
                         segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
                         hasSplit = true;
+                        continueErasing = true;
                     } else {
-                        if ((isClosedCurve && hasSplit) || (!isClosedCurve && !hasSplit)) {
+                        if ((isClosedCurve && hasSplit && !continueErasing) || (!isClosedCurve && !hasSplit)) {
                             segment1.push(inkSegment);
                         }
                     }
@@ -1311,6 +1308,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     };
 
+
     // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection
     // call in a test for linearity
     bintersects = (curve: Bezier, otherCurve: Bezier) => {
@@ -2045,14 +2043,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     onCursorMove = (e: React.PointerEvent) => {
         this._eraserX = e.clientX;
         this._eraserY = e.clientY;
-        if (this._eraserX < 0 || this._eraserY < 0) {
-            this._showEraserCircle = false;
-        } else {
-            this._showEraserCircle = true;
-        }
         //  super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     };
 
+    @action
+    onMouseLeave = () => {
+        this._showEraserCircle = false;
+    };
+
+    @action
+    onMouseEnter = () => {
+        this._showEraserCircle = true;
+    };
+
     @undoBatch
     promoteCollection = () => {
         const childDocs = this.childDocs.slice();
@@ -2321,6 +2324,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     this._oldWheel = r;
                     // prevent wheel events from passivly propagating up through containers
                     r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                    r?.addEventListener('mouseleave', this.onMouseLeave);
+                    r?.addEventListener('mouseenter', this.onMouseEnter);
                 }}
                 onWheel={this.onPointerWheel}
                 onClick={this.onClick}
@@ -2336,15 +2341,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
                     height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
                 }}>
-                {Doc.ActiveTool === InkTool.RadiusEraser && (
+                {(Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle) && (
                     <div
                         onPointerMove={this.onCursorMove}
                         style={{
                             position: 'fixed',
                             left: this._eraserX - 60,
                             top: this._eraserY - 100,
-                            width: ActiveEraserWidth() * 2,
-                            height: ActiveEraserWidth() * 2,
+                            width: (ActiveEraserWidth() + 5) * 2,
+                            height: (ActiveEraserWidth() + 5) * 2,
                             borderRadius: '50%',
                             border: '1px solid gray',
                             transform: 'translate(-50%, -50%)',
-- 
cgit v1.2.3-70-g09d2

