From f8053c9d1f7d849cc0c3e8c0b8bf4a466ffd85b9 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 14 May 2024 13:01:51 -0400
Subject: some bug fixes

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 152 ++++++++++++++-------
 1 file changed, 102 insertions(+), 50 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 622189b80..14d20eb4a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -95,8 +95,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     private _panZoomTransitionTimer: any;
     private _lastX: number = 0;
     private _lastY: number = 0;
-    @observable _eraserX: number = 0;
-    @observable _eraserY: number = 0;
     private _downX: number = 0;
     private _downY: number = 0;
     private _downTime = 0;
@@ -107,6 +105,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     private _batch: UndoManager.Batch | undefined = undefined;
     private _brushtimer: any;
     private _brushtimer1: any;
+    private prevZoom: number = 0;
 
     public get isAnnotationOverlay() {
         return this._props.isAnnotationOverlay;
@@ -134,6 +133,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _marqueeViewRef = React.createRef<MarqueeView>();
     @observable _brushedView: { width: number; height: number; panX: number; panY: number } | undefined = undefined; // highlighted region of freeform canvas used by presentations to indicate a region
     @observable GroupChildDrag: boolean = false; // child document view being dragged.  needed to update drop areas of groups when a group item is dragged.
+    @observable _eraserX: number = 0;
+    @observable _eraserY: number = 0;
+    @observable _showEraserCircle: boolean = false; // to determine whether the radius eraser should show
 
     @computed get contentViews() {
         const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele);
@@ -747,7 +749,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         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));
-        if (this._eraserLock) return false; // bcz: should be fixed by putting it on a queue to be processed after the last eraser movement is processed.
+        // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future
         this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
             if (!this._deleteList.includes(intersect.inkView)) {
                 this._deleteList.push(intersect.inkView);
@@ -755,7 +757,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
                 // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
                 if (Doc.ActiveTool !== InkTool.StrokeEraser) {
-                    this._eraserLock++;
+                    // this._eraserLock++;
                     const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
                     const newStrokes = segments?.map(segment => {
                         const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
@@ -784,23 +786,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return false;
     };
 
+    /**
+     * Erases strokes by intersecting them with a circle of variable radius. Essentially creates an InkField for the
+     * eraser circle, then determines its intersections with other ink strokes. Each stroke's DocumentView and its
+     * intersection t-values are put into a map, which gets looped through to take out the erased parts.
+     * @param e
+     * @param down
+     * @param delta
+     * @returns
+     */
     @action
     onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
-        // this._eraserX = e.clientX;
-        // this._eraserY = e.clientY;
+        // 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));
-        if (this._eraserLock) return false; // bcz: should be fixed by putting it on a queue to be processed after the last eraser movement is processed.
-        const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
+        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];
+
         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');
-                this._eraserLock++;
-                // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
-                const segments = this.radiusErase(stroke, intersects.sort());
+                const segments = this.radiusErase(stroke, intersects.sort(), eraserStroke);
                 segments?.forEach(segment =>
                     this.forceStrokeGesture(
                         e,
@@ -808,12 +821,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
                     )
                 );
-                setTimeout(() => this._eraserLock--);
             }
-            // Lower ink opacity to give the user a visual indicator of deletion.
             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;
     };
 
@@ -834,8 +851,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return false;
     };
 
-    createEraserOutline = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => {
-        const radius = ActiveEraserWidth() + 3; // add 3 to avoid eraser being too thin
+    /**
+     * Creates the eraser outline for a radius eraser. The outline be
+     * @param startInkCoordsIn
+     * @param endInkCoordsIn
+     * @param inkStrokeWidth
+     * @returns
+     */
+    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;
         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);
@@ -959,8 +984,13 @@ 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 
+     */
     getRadiusEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => {
-        const eraserRadius = ActiveEraserWidth() + 3;
+        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 };
         const strokeToTVals = new Map<DocumentView, number[]>();
@@ -976,12 +1006,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     eraserMax.X >= inkViewBounds.left &&
                     eraserMax.Y >= inkViewBounds.top
             );
-
+        const erasers: InkData[] = [];
         intersectingStrokes.forEach(({ inkStroke, inkView }) => {
-            const { inkData } = inkStroke.inkScaledData();
+            const { inkData, inkStrokeWidth } = inkStroke.inkScaledData();
             const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
             const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
-            const eraserInkData = this.createEraserOutline(prevPointInkSpace, currPointInkSpace).inkData;
+            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])) {
                 strokeToTVals.set(inkView, [0]);
@@ -994,6 +1026,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     strokeToTVals.set(inkView, [Math.floor(inkData.length / 4) + 1]);
                 }
             }
+
             for (var i = 0; i < inkData.length - 3; i += 4) {
                 // iterate over each segment of bezier curve
                 for (var j = 0; j < eraserInkData.length - 3; j += 4) {
@@ -1006,7 +1039,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             // here, add to the map
                             const inkList = strokeToTVals.get(inkView);
                             if (inkList !== undefined) {
-                                const tValOffset = ActiveEraserWidth() / 1030; // to prevent tVals from being added when too close, but scaled by eraser width
+                                const tValOffset = ActiveEraserWidth() / 1000; // 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));
@@ -1019,33 +1052,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }
             }
         });
-        return strokeToTVals;
+        return [strokeToTVals, erasers];
     };
 
     /**
-     * Splits the passed in ink stroke at the intersection t values. Generally 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.
+     * 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[]): Segment[] => {
+    radiusErase = (ink: DocumentView, tVals: number[], erasers: InkData[]): 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 = [];
         if (tVals.length % 2 !== 0) {
-            // should always have even tVals
+            // 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;
+            return segments; // want to return the full original stroke
         }
 
-        var continueErasing = false;
-        var firstSegment: Segment = [];
+        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 
+
         // early return if nothing to split on
         if (tVals.length === 0 || (tVals.length === 1 && tVals[0] === 0)) {
             for (var i = 0; i < inkData.length - 3; i += 4) {
@@ -1055,26 +1098,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             return segments;
         }
 
+        // loop through all segments of an ink stroke, string together the pieces, excluding the erased parts,
+        // and push each piece we want to keep to the return list
         for (var i = 0; i < inkData.length - 3; i += 4) {
             const currCurveT = Math.floor(i / 4);
             const inkBezier: Bezier = InkField.Segment(inkData, i);
+            // filter to this segment's t-values
             const segmentTs = tVals.filter(t => t >= currCurveT && t < currCurveT + 1);
 
             if (segmentTs.length > 0) {
                 for (var j = 0; j < segmentTs.length; j++) {
-                    // if the first end of the segment is within the eraser
-                    if (segmentTs[j] === 0) {
+                    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) {
+                    } 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 {
+                        } else { // we've reached the end of the part to take out...
                             continueErasing = false;
                             if (currSegment.length > 0) {
-                                segments.push(currSegment);
+                                segments.push(currSegment); // ...so we add it to the list and reset currSegment
                                 if (firstSegment.length === 0) {
                                     firstSegment = currSegment;
                                 }
@@ -1091,15 +1136,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }
             }
         }
+
         if (currSegment.length > 0) {
+            // add the first segment onto the last to avoid fragmentation for closed curves
             if (InkingStroke.IsClosed(inkData)) {
                 currSegment = currSegment.concat(firstSegment);
             }
             segments.push(currSegment);
         }
-        if (segments.length === 0) {
-            console.log("segments is 0");
-        }
         return segments;
     };
 
@@ -1219,6 +1263,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
 
+        // add the first segment onto the second one for closed curves, so they don't get fragmented into two pieces
         if (isClosedCurve && segment1.length > 0 && segment2.length > 0) {
             segment2 = segment2.concat(segment1);
             segment1 = [];
@@ -1439,7 +1484,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const maxPanY =
                 minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by layout_fitWidth
                 scale * NumCast(this.dataDoc._panY_max, nativeHeight) +
-                (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning
+                (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrollaing is handled via a scrollbar, not panning
             let newPanY = Math.max(minPanY, Math.min(maxPanY, panY));
             if (false && NumCast(this.layoutDoc.layout_scrollTop) && NumCast(this.layoutDoc._freeform_scale, minScale) !== minScale) {
                 const relTop = NumCast(this.layoutDoc.layout_scrollTop) / maxScrollTop;
@@ -2000,6 +2045,11 @@ 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));
     };
 
@@ -2286,19 +2336,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
                     height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
                 }}>
-                {/* <div
-                    onPointerMove={this.onCursorMove}
-                    style={{
-                        position: 'fixed',
-                        left: this._eraserX,
-                        top: this._eraserY,
-                        width: '30px',
-                        height: '30px',
-                        borderRadius: '50%',
-                        // backgroundColor: 'transparent',
-                        border: '1px solid black',
-                    }}
-                /> */}
+                {Doc.ActiveTool === InkTool.RadiusEraser && (
+                    <div
+                        onPointerMove={this.onCursorMove}
+                        style={{
+                            position: 'fixed',
+                            left: this._eraserX - 60,
+                            top: this._eraserY - 100,
+                            width: ActiveEraserWidth() * 2,
+                            height: ActiveEraserWidth() * 2,
+                            borderRadius: '50%',
+                            border: '1px solid gray',
+                            transform: 'translate(-50%, -50%)',
+                        }}
+                    />
+                )}
                 {this.paintFunc ? (
                     <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
                 ) : this._lightboxDoc ? (
-- 
cgit v1.2.3-70-g09d2

