From 33cb02498a330ede6a90e67bdaf0ff56aed21819 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 12 Mar 2024 16:26:00 -0400
Subject: i have an error

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 181 +++++++++++++++++++--
 1 file changed, 171 insertions(+), 10 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 e4c71a086..70b21309e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,3 +1,4 @@
+import { findSegment } from '@turf/turf';
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
@@ -739,7 +740,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
                 if (!e.shiftKey) {
                     this._eraserLock++;
-                    const segments = this.segmentInkStroke(intersect.inkView, intersect.t);
+                    const segments = this.segmentInkStroke(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
+                    // we delete the original ink stroke, then create new strokes for each of the remaining segments
+                    // we do this through the hack of calling the stroke Gesture code as if the strokes had just be drawn by hand
+                    // [[(0,0), (3,7), (9,4), (4,5)],[(4,5), (13,17), (19,14), (14,15)],       [(23,19),(...}]]
+                    // [[(0,0), (3,7), (9,4), (4,5), (13,17), (19,14), (14,15)],       [(23,19),(...}]]
+                    // segments.map(segment => segment.map(pt => pt))
+                    // segments[0][3].x === segments[1][0].x
+
+                    //   3 == "3"   true
+                    //   3 === "3" falsen   !==
+
+                    // for (var i = 0; i < segments.length; i ++) {
+                    //     if (segments[i][0].points.lastElement().x === segments[i+1][0].points.lastElement().x && segments[i][0].points.lastElement().y === segments[i+1][0].points.lastElement().y) {
+                    //         segments[i][0].points.pop();
+                    //         segments[i][0].points = segments[i][0].points.concat(segments[i+1][0].points);
+                    //         segments.splice(i+1, 1);
+                    //     }
+                    //     i++;
+                    // }
                     segments.forEach(segment =>
                         this.forceStrokeGesture(
                             e,
@@ -750,7 +769,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     setTimeout(() => this._eraserLock--);
                 }
                 // Lower ink opacity to give the user a visual indicator of deletion.
-                intersect.inkView.layoutDoc.opacity = 0.5;
+                intersect.inkView.layoutDoc.opacity = 0.0;
                 intersect.inkView.layoutDoc.dontIntersect = true;
             }
         });
@@ -796,12 +815,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             )
             .reduce(
                 (intersections, { inkStroke, inkView }) => {
-                    const { inkData } = inkStroke.inkScaledData();
+                    const { inkData } = inkStroke.inkScaledData(); // get bezier curve as set of control points
                     // 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) {
+                        // iterate over each segment of bezier curve
                         const rawIntersects = InkField.Segment(inkData, i).intersects({
+                            // segment's are indexed by 0, 4, 8,
                             // compute all unique intersections
                             p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
                             p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y },
@@ -825,9 +846,106 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      */
     @action
     segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => {
+        // const segments: Segment[] = [];
+        // var segment: Segment = [];
+        // var segment2: Segment = [];
+        // var startSegmentT = 0;
+        // const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
+        // 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)
+
+        // var hasSplit = false;
+        // var continueErasing = false;
+        // for (var i = 0; i < inkData.length - 3; i += 4) {
+        //     const inkSegment: Bezier = InkField.Segment(inkData, i);
+        //     const tVals: number[] = this.getInkIntersections(i, ink, inkSegment).sort();
+        //     console.log("TVALS", tVals);
+        //     if (tVals.length) {
+        //         if ((!hasSplit) && ((excludeT <= tVals[0]) ||
+        //             (excludeT > tVals[0] && excludeT < tVals.lastElement()) || continueErasing)) {
+
+        //             const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
+        //             if (closestTs[0] != -1 && closestTs[1] != -1) {
+        //                 segment1.push(inkSegment.split(0, closestTs[0]));
+        //                 segment2.push(inkSegment.split(closestTs[1]).right);
+        //             } else if (closestTs[0] == -1) {
+        //                 segment2.push(inkSegment.split(closestTs[1]).right);
+        //                 continueErasing = !continueErasing;
+        //             } else {
+        //                 segment1.push(inkSegment.split(0, closestTs[0]));
+        //                 continueErasing = !continueErasing;
+        //             }
+        //             if (!continueErasing) {
+        //                 hasSplit = true;
+        //             }
+        //             // segment1 = [];
+        //             // segment2 = [];
+        //         }
+        //         else {
+        //             if (hasSplit) {
+        //                 segment2.push(inkSegment);
+        //             } else {
+        //                 segment1.push(inkSegment);
+        //             }
+        //         }
+        //     } else if (hasSplit) {
+        //         segment2.push(inkSegment);
+        //     } else {
+        //         continueErasing = true;
+        //     }
+        // }
+
+        // if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
+        //     segments.push(segment1);
+        // }
+        // if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
+        //     segments.push(segment2);
+        // }
+        // if (excludeT < startSegmentT || excludeT > inkData.length / 4) {
+        //     segment1.length && segments.push(segment1);
+        // }
+        //     const tVals  = []
+        // for (var i = 0; i < inkData.length - 3; i += 4) {
+        //     const inkSegment = InkField.Segment(inkData, i);
+        //     // Getting all t-value intersections of the current curve with all other curves.
+        //     tVals.push(this.getInkIntersections(i, ink, inkSegment));
+        //     tVals.sort();
+
+        // if (tVals.length) {
+        //     const docCurveTVal = tVals.lastElement() + Math.floor(i / 4);
+        //     if (excludeT > docCurveTVal) {
+        //         continue;
+        //     } else {
+        //         const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
+        //         segment.push(inkSegment.split(0, closestTs[0]));
+        //         if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) {
+        //             segments.push(segment);
+        //         }
+        //     }
+
+        //         const closestT = this.getClosestT(tVals, excludeT, 0, tVals.length - 1);
+        //         if (closestT != undefined) {
+        //             var startSegmentT = closestT[0]
+        //             const localStartTVal = startSegmentT - Math.floor(i / 4);
+        //             segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, startSegmentT))
+        //             if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) {
+        //                 segments.push(segment);
+        //             }
+        //             const split = inkSegment.split(closestT[1]).right;
+        //             if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) {
+        //                 segments.push([split]);
+        //             }
+        //         }
+        //     } else {
+        //         segment.push(inkSegment);
+        //         segments.push(segment);
+        //     }
+        // }
         const segments: Segment[] = [];
-        var segment: Segment = [];
+        var segment1: Segment = [];
+        var segment2: Segment = [];
         var startSegmentT = 0;
+        var hasSplit = false;
         const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
         // 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)
@@ -840,23 +958,66 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     const docCurveTVal = t + Math.floor(i / 4);
                     if (excludeT < startSegmentT || excludeT > docCurveTVal) {
                         const localStartTVal = startSegmentT - Math.floor(i / 4);
-                        t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
-                        segment.length && segments.push(segment);
+                        if (!hasSplit) {
+                            t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment1.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+                        } else {
+                            t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment2.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+                        }
+                        // if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) segments.push(segment);
                     }
                     // start a new segment from the intersection t value
-                    segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : [];
+                    if (tVals.length - 1 === index) {
+                        const split = inkSegment.split(t).right;
+                        if (!hasSplit) {
+                            if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment1.push(split);
+                        } else {
+                            if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment2.push(split);
+                        }
+                    }
                     startSegmentT = docCurveTVal;
                 });
             } else {
-                segment.push(inkSegment);
+                if (!hasSplit) {
+                    segment1.push(inkSegment);
+                } else {
+                    segment2.push(inkSegment);
+                }
             }
         }
-        if (excludeT < startSegmentT || excludeT > inkData.length / 4) {
-            segment.length && segments.push(segment);
+        if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
+            segments.push(segment1);
+        }
+        if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
+            segments.push(segment2);
         }
         return segments;
     };
 
+    getClosestTs = (tVals: number[], excludeT: number, startIndex: number, endIndex: number): number[] => {
+        if (tVals[startIndex] >= excludeT) {
+            return [-1, tVals[startIndex]];
+        }
+        else if (tVals[endIndex] < excludeT) {
+            return [tVals[endIndex], -1];
+        }
+        else {
+            const mid = Math.floor(startIndex + endIndex / 2);
+            if (excludeT >= tVals[mid]) {
+                if (mid + 1 <= endIndex && tVals[mid + 1] > excludeT) {
+                    return [tVals[mid], tVals[mid + 1]];
+                } else {
+                    return this.getClosestTs(tVals, excludeT, mid + 1, endIndex);
+                }
+            } else {
+                if (mid - 1 <= startIndex && tVals[mid - 1] < excludeT) {
+                    return [tVals[mid - 1], tVals[mid]];
+                } else {
+                    return this.getClosestTs(tVals, excludeT, startIndex, mid - 1);
+                }
+            }
+        }
+    };
+
     // 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) => {
-- 
cgit v1.2.3-70-g09d2


From f4158b1a3249e500a1031d9edb91f6769777c5d2 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 19 Mar 2024 16:27:29 -0400
Subject: functional erase code complete

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 357 ++++++++++++++-------
 1 file changed, 239 insertions(+), 118 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 70b21309e..76e1646e2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -846,171 +846,292 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      */
     @action
     segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => {
-        // const segments: Segment[] = [];
-        // var segment: Segment = [];
-        // var segment2: Segment = [];
-        // var startSegmentT = 0;
-        // const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
+
+        const segments: Segment[] = [];
+        var segment1: Segment = [];
+        var segment2: Segment = [];
+        const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
         // 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)
 
-        // var hasSplit = false;
-        // var continueErasing = false;
+        // find all t values of the curve's intersections
+        var intersections: number[] = [];
+        var segmentIndexes: number[] = [];
+        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
+            currIntersects = currIntersects.map(tVal => tVal + Math.floor(i/4));
+            if (currIntersects.length) {
+                intersections = [...intersections, ...currIntersects];
+                // segmentIndexes is to keep track of the segment index that corresponds to an intersection
+                for (var j = 0; j < currIntersects.length; j++) {
+                    segmentIndexes.push(Math.floor(i / 4));
+                }
+            }
+        }
+
+        if (intersections.length) {
+            // this is the indexes of the closest Ts
+            const closestTs = this.getClosestTs(intersections, excludeT, 0, intersections.length - 1);
+
+            // 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
+                splitSegment1 = segmentIndexes[closestTs[0]];
+                splitSegment2 = segmentIndexes[closestTs[1]];
+            } else if (closestTs[0] === -1) { // for a curve left of intersection
+                splitSegment2 = segmentIndexes[closestTs[1]];
+            } else { // for a curve right of intersection
+                splitSegment1 = segmentIndexes[closestTs[0]];
+            }
+
+            // so here splitSegment1 and splitSegment2 will be the index(es) of the segment(s) to split
+
+            var hasSplit = false;
+            var continueErasing = false;
+            // loop through segments again and split if they match the split segments
+            for (var i = 0; i < inkData.length - 3; i += 4) {
+                const currCurveT = Math.floor(i / 4);
+                const inkSegment: Bezier = InkField.Segment(inkData, i);
+
+                // case where the current curve is the first to split
+                if (splitSegment1 !== -1 && splitSegment2 !== -1) {
+                    if (splitSegment1 === splitSegment2 && splitSegment1 === currCurveT) {
+                        // if it's the same segment
+                        segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
+                        segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
+                        hasSplit = true;
+                    } else if (splitSegment1 === currCurveT) {
+                        segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
+                        continueErasing = true;
+                    } else if (splitSegment2 === currCurveT) {
+                        segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
+                        continueErasing = false;
+                        hasSplit = true;
+                    } else {
+                        if (!continueErasing && !hasSplit) {
+                            // segment doesn't get pushed if continueErasing is true
+                            segment1.push(inkSegment);
+                        } else if (!continueErasing && hasSplit) {
+                            segment2.push(inkSegment);
+                        }
+                    }
+                } else if (splitSegment1 === -1) { // case where left end is erased
+                    if (currCurveT === splitSegment2) {
+                        segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
+                        hasSplit = true;
+                    } else {
+                        if (hasSplit) {
+                            segment2.push(inkSegment);
+                        }
+                    }
+                } else { // case where right end is erased
+                    if (currCurveT === splitSegment1) {
+                        segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
+                        hasSplit = true;
+                    } else {
+                        if (!hasSplit) {
+                            segment1.push(inkSegment);
+                        }
+                    }
+                }
+            }
+        }
+
+        // push 1 or both segments if they are not empty
+        if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
+            segments.push(segment1);
+        }
+        if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
+            segments.push(segment2);
+        }
+
+        return segments;
+        
+        // var deletedSegments = [];
         // for (var i = 0; i < inkData.length - 3; i += 4) {
+        //     const segmentStartT = Math.floor(i/4);
         //     const inkSegment: Bezier = InkField.Segment(inkData, i);
         //     const tVals: number[] = this.getInkIntersections(i, ink, inkSegment).sort();
-        //     console.log("TVALS", tVals);
-        //     if (tVals.length) {
-        //         if ((!hasSplit) && ((excludeT <= tVals[0]) ||
-        //             (excludeT > tVals[0] && excludeT < tVals.lastElement()) || continueErasing)) {
 
+        //     if (excludeT > segmentStartT && excludeT < segmentStartT + 1) {
+        //         if (tVals.length) {
         //             const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
         //             if (closestTs[0] != -1 && closestTs[1] != -1) {
         //                 segment1.push(inkSegment.split(0, closestTs[0]));
         //                 segment2.push(inkSegment.split(closestTs[1]).right);
+        //                 deletedSegments.push(inkSegment.split(closestTs[0], closestTs[1]));
         //             } else if (closestTs[0] == -1) {
         //                 segment2.push(inkSegment.split(closestTs[1]).right);
-        //                 continueErasing = !continueErasing;
+        //                 deletedSegments.push(inkSegment.split(closestTs[1]).left);
+        //                 if (excludeT > tVals[0]) {
+        //                     continueErasing = !continueErasing;
+        //                 }
         //             } else {
-        //                 segment1.push(inkSegment.split(0, closestTs[0]));
+        //                 continueErasing ? segment2.push(inkSegment.split(closestTs[0]).right) : segment1.push(inkSegment.split(0, closestTs[0]));
+        //                 continueErasing ? deletedSegments.push(inkSegment.split(closestTs[0]).left) : deletedSegments.push(inkSegment.split(closestTs[0]).right);
         //                 continueErasing = !continueErasing;
         //             }
         //             if (!continueErasing) {
         //                 hasSplit = true;
         //             }
-        //             // segment1 = [];
-        //             // segment2 = [];
+        //         } else if (hasSplit) {
+        //             segment2.push(inkSegment);
+        //         } else {
+        //             continueErasing = true;
         //         }
-        //         else {
-        //             if (hasSplit) {
-        //                 segment2.push(inkSegment);
-        //             } else {
-        //                 segment1.push(inkSegment);
-        //             }
-        //         }
-        //     } else if (hasSplit) {
-        //         segment2.push(inkSegment);
         //     } else {
-        //         continueErasing = true;
+        //         hasSplit ? segment1.push(inkSegment) : segment2.push(inkSegment);
         //     }
-        // }
+        
+            // if (tVals.length) {
+            //     if (!hasSplit) {
+            //         if (excludeT <= tVals[0] || (excludeT > tVals[0] && excludeT < tVals.lastElement()) || excludeT > tVals.lastElement() || continueErasing) {
+            //             const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
+            //             if (closestTs[0] != -1 && closestTs[1] != -1) {
+            //                 segment1.push(inkSegment.split(0, closestTs[0]));
+            //                 segment2.push(inkSegment.split(closestTs[1]).right);
+            //                 deletedSegments.push(inkSegment.split(closestTs[0], closestTs[1]));
+            //             } else if (closestTs[0] == -1) {
+            //                 segment2.push(inkSegment.split(closestTs[1]).right);
+            //                 deletedSegments.push(inkSegment.split(closestTs[1]).left);
+            //                 if (excludeT > tVals[0]) {
+            //                     continueErasing = !continueErasing;
+            //                 }
+            //             } else {
+            //                 continueErasing ? segment2.push(inkSegment.split(closestTs[0]).right) : segment1.push(inkSegment.split(0, closestTs[0]));
+            //                 continueErasing ? deletedSegments.push(inkSegment.split(closestTs[0]).left) : deletedSegments.push(inkSegment.split(closestTs[0]).right);
+            //                 continueErasing = !continueErasing;
+            //             }
+            //             if (!continueErasing) {
+            //                 hasSplit = true;
+            //             }
+            //         } else {
+            //             if (hasSplit) {
+            //                 segment2.push(inkSegment);
+            //             } else {
+            //                 segment1.push(inkSegment);
+            //             }
+            //         }
+            //     } else if (excludeT > tVals[0] && excludeT < tVals.lastElement()) {
+            //         // here we know hasSplit is true (but it shouldnt be!)
+            //         segment1 = [...deletedSegments];
+            //         deletedSegments = [];
+            //         hasSplit = false;
+            //         const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
+            //         if (closestTs[0] != -1 && closestTs[1] != -1) {
+            //             segment1.push(inkSegment.split(0, closestTs[0]));
+            //             segment2.push(inkSegment.split(closestTs[1]).right);
+            //         } else if (closestTs[0] == -1) {
+            //             segment2.push(inkSegment.split(closestTs[1]).right);
+            //             if (excludeT > tVals[0]) {
+            //                 continueErasing = !continueErasing;
+            //             }
+            //         }
+            //     } else {
+            //         segment2.push(inkSegment);
+            //     }
+            // } else if (hasSplit) {
+            //     segment2.push(inkSegment);
+            // } else {
+            //     continueErasing = true;
+            // }
 
-        // if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
-        //     segments.push(segment1);
-        // }
-        // if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
-        //     segments.push(segment2);
-        // }
-        // if (excludeT < startSegmentT || excludeT > inkData.length / 4) {
-        //     segment1.length && segments.push(segment1);
+
+
+
+            // if (hasSplit && i + 4 >= lastIteration && ((excludeT > tVals[0] && excludeT < tVals.lastElement()) || (excludeT > tVals.lastElement()))) {
+            //     // here we know hasSplit is true (but it shouldnt be!)
+            //     segment1 = [...deletedSegments];
+            //     deletedSegments = [];
+            //     hasSplit = false;
+            //     const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
+            //     if (closestTs[0] != -1 && closestTs[1] != -1) {
+            //         segment1.push(inkSegment.split(0, closestTs[0]));
+            //         segment2.push(inkSegment.split(closestTs[1]).right);
+            //     } else if (closestTs[0] == -1) {
+            //         segment2.push(inkSegment.split(closestTs[1]).right);
+            //         if (excludeT > tVals[0]) {
+            //             continueErasing = !continueErasing;
+            //         }
+            //     }
+            // }
         // }
-        //     const tVals  = []
+
+
+        // const segments: Segment[] = [];
+        // var segment1: Segment = [];
+        // var segment2: Segment = [];
+        // var startSegmentT = 0;
+        // var hasSplit = false;
+        // const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
+        // // 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 inkSegment = InkField.Segment(inkData, i);
         //     // Getting all t-value intersections of the current curve with all other curves.
-        //     tVals.push(this.getInkIntersections(i, ink, inkSegment));
-        //     tVals.sort();
-
-        // if (tVals.length) {
-        //     const docCurveTVal = tVals.lastElement() + Math.floor(i / 4);
-        //     if (excludeT > docCurveTVal) {
-        //         continue;
-        //     } else {
-        //         const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
-        //         segment.push(inkSegment.split(0, closestTs[0]));
-        //         if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) {
-        //             segments.push(segment);
-        //         }
-        //     }
-
-        //         const closestT = this.getClosestT(tVals, excludeT, 0, tVals.length - 1);
-        //         if (closestT != undefined) {
-        //             var startSegmentT = closestT[0]
-        //             const localStartTVal = startSegmentT - Math.floor(i / 4);
-        //             segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, startSegmentT))
-        //             if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) {
-        //                 segments.push(segment);
+        //     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);
+        //                 if (!hasSplit) {
+        //                     t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment1.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+        //                 } else {
+        //                     t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment2.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+        //                 }
+        //                 // if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) segments.push(segment);
         //             }
-        //             const split = inkSegment.split(closestT[1]).right;
-        //             if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) {
-        //                 segments.push([split]);
+        //             // start a new segment from the intersection t value
+        //             if (tVals.length - 1 === index) {
+        //                 const split = inkSegment.split(t).right;
+        //                 if (!hasSplit) {
+        //                     if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment1.push(split);
+        //                 } else {
+        //                     if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment2.push(split);
+        //                 }
         //             }
-        //         }
+        //             startSegmentT = docCurveTVal;
+        //         });
         //     } else {
-        //         segment.push(inkSegment);
-        //         segments.push(segment);
+        //         if (!hasSplit) {
+        //             segment1.push(inkSegment);
+        //         } else {
+        //             segment2.push(inkSegment);
+        //         }
         //     }
         // }
-        const segments: Segment[] = [];
-        var segment1: Segment = [];
-        var segment2: Segment = [];
-        var startSegmentT = 0;
-        var hasSplit = false;
-        const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
-        // 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 inkSegment = InkField.Segment(inkData, i);
-            // Getting all t-value intersections of the current curve with all other curves.
-            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);
-                        if (!hasSplit) {
-                            t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment1.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
-                        } else {
-                            t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment2.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
-                        }
-                        // if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) segments.push(segment);
-                    }
-                    // start a new segment from the intersection t value
-                    if (tVals.length - 1 === index) {
-                        const split = inkSegment.split(t).right;
-                        if (!hasSplit) {
-                            if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment1.push(split);
-                        } else {
-                            if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment2.push(split);
-                        }
-                    }
-                    startSegmentT = docCurveTVal;
-                });
-            } else {
-                if (!hasSplit) {
-                    segment1.push(inkSegment);
-                } else {
-                    segment2.push(inkSegment);
-                }
-            }
-        }
-        if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
-            segments.push(segment1);
-        }
-        if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
-            segments.push(segment2);
-        }
+        // if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
+        //     segments.push(segment1);
+        // }
+        // if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
+        //     segments.push(segment2);
+        // }
         return segments;
     };
 
     getClosestTs = (tVals: number[], excludeT: number, startIndex: number, endIndex: number): number[] => {
         if (tVals[startIndex] >= excludeT) {
-            return [-1, tVals[startIndex]];
+            return [-1, startIndex];
         }
         else if (tVals[endIndex] < excludeT) {
-            return [tVals[endIndex], -1];
+            return [endIndex, -1];
         }
         else {
-            const mid = Math.floor(startIndex + endIndex / 2);
+            const mid = Math.floor((startIndex + endIndex) / 2);
             if (excludeT >= tVals[mid]) {
                 if (mid + 1 <= endIndex && tVals[mid + 1] > excludeT) {
-                    return [tVals[mid], tVals[mid + 1]];
+                    return [mid, mid + 1];
                 } else {
                     return this.getClosestTs(tVals, excludeT, mid + 1, endIndex);
                 }
             } else {
-                if (mid - 1 <= startIndex && tVals[mid - 1] < excludeT) {
-                    return [tVals[mid - 1], tVals[mid]];
+                if (mid - 1 >= startIndex && tVals[mid - 1] < excludeT) {
+                    return [mid - 1, mid];
                 } else {
                     return this.getClosestTs(tVals, excludeT, startIndex, mid - 1);
                 }
-- 
cgit v1.2.3-70-g09d2


From 627e436b16745df6982dec3954f2b385f180bad8 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 20 Mar 2024 23:27:21 -0400
Subject: tried to start closed curve management

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 53 +++++++++++++++-------
 1 file changed, 36 insertions(+), 17 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 76e1646e2..b8d74dff2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -838,8 +838,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     /**
-     * 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).
+     * Erases ink strokes by segments. Locates intersections of the current ink stroke with all other ink strokes (including itself),
+     * then erases the segment that was intersected by the eraser. This is done by creating either 1 or two resulting segments
+     * (this depends on whether the eraser his the middle or end of a stroke), and returning the segments to "redraw."
      * @param ink The ink DocumentView intersected by the eraser.
      * @param excludeT 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.
@@ -851,29 +852,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         var segment1: Segment = [];
         var segment2: Segment = [];
         const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
-        // 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)
-
-        // find all t values of the curve's intersections
-        var intersections: number[] = [];
-        var segmentIndexes: number[] = [];
+        var intersections: number[] = []; // list of the ink stroke's intersections
+        var segmentIndexes: number[] = []; // list of indexes of the curve's segment where each intersection occured
+        
+        // loops through each segment and adds intersections to the list
         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
             currIntersects = currIntersects.map(tVal => tVal + Math.floor(i/4));
             if (currIntersects.length) {
-                intersections = [...intersections, ...currIntersects];
-                // segmentIndexes is to keep track of the segment index that corresponds to an intersection
+                intersections = [...intersections, ...currIntersects]
                 for (var j = 0; j < currIntersects.length; j++) {
                     segmentIndexes.push(Math.floor(i / 4));
                 }
             }
         }
 
+        // for closed curve bookkeeping
+        var isClosedCurve = false;
+        if (intersections[0] === 0) {
+            isClosedCurve = true;
+            intersections = intersections.slice(1, intersections.length ); // take the 0 intersection out
+            segmentIndexes = segmentIndexes.slice(1, segmentIndexes.length); // same for indexes
+        }
+
         if (intersections.length) {
-            // this is the indexes of the closest Ts
-            const closestTs = this.getClosestTs(intersections, excludeT, 0, intersections.length - 1);
+            // this is the indexes of the closest intersection(s)
+            var closestTs = this.getClosestTs(intersections, excludeT, 0, intersections.length - 1);
 
             // find the segments that need to be split
             var splitSegment1 = -1; // stays -1 if left end is deleted
@@ -882,9 +888,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // if not on the ends
                 splitSegment1 = segmentIndexes[closestTs[0]];
                 splitSegment2 = segmentIndexes[closestTs[1]];
-            } else if (closestTs[0] === -1) { // for a curve left of intersection
+            } else if (closestTs[0] === -1) { // for a curve before an intersection
                 splitSegment2 = segmentIndexes[closestTs[1]];
-            } else { // for a curve right of intersection
+            } else { // for a curve after an intersection
                 splitSegment1 = segmentIndexes[closestTs[0]];
             }
 
@@ -892,7 +898,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
             var hasSplit = false;
             var continueErasing = false;
-            // loop through segments again and split if they match the split segments
+            // loop through segments again and split them if they match the split segments
             for (var i = 0; i < inkData.length - 3; i += 4) {
                 const currCurveT = Math.floor(i / 4);
                 const inkSegment: Bezier = InkField.Segment(inkData, i);
@@ -924,7 +930,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
                         hasSplit = true;
                     } else {
-                        if (hasSplit) {
+                        if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) {
+                            segment2.push(inkSegment.split(0, intersections.lastElement() - currCurveT));
+                            continueErasing = true;
+                        } else if (hasSplit && !continueErasing) {
                             segment2.push(inkSegment);
                         }
                     }
@@ -932,8 +941,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (currCurveT === splitSegment1) {
                         segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
                         hasSplit = true;
+                    } else if (isClosedCurve && currCurveT === segmentIndexes[1]) {
+                        segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
                     } else {
-                        if (!hasSplit) {
+                        if (!hasSplit && !isClosedCurve) {
                             segment1.push(inkSegment);
                         }
                     }
@@ -1114,6 +1125,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return segments;
     };
 
+    /**
+     * Standard logarithmic search function to search a sorted list of tVals for the ones closest to excludeT.
+     * @param tVals list of tvalues (usage is for intersection t values) to search within
+     * @param excludeT the t value of where the eraser intersected the curve
+     * @param startIndex the start index to search from
+     * @param endIndex the end index to search to
+     * @returns 2-item array of the closest tVals indexes
+     */
     getClosestTs = (tVals: number[], excludeT: number, startIndex: number, endIndex: number): number[] => {
         if (tVals[startIndex] >= excludeT) {
             return [-1, startIndex];
-- 
cgit v1.2.3-70-g09d2


From 3c6707abda0054335a4924a453b945bb87723a59 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 31 Mar 2024 14:13:35 -0400
Subject: some cleanup

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 111 ---------------------
 1 file changed, 111 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 b8d74dff2..18b75d8ff 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -961,116 +961,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
 
         return segments;
-        
-        // var deletedSegments = [];
-        // for (var i = 0; i < inkData.length - 3; i += 4) {
-        //     const segmentStartT = Math.floor(i/4);
-        //     const inkSegment: Bezier = InkField.Segment(inkData, i);
-        //     const tVals: number[] = this.getInkIntersections(i, ink, inkSegment).sort();
-
-        //     if (excludeT > segmentStartT && excludeT < segmentStartT + 1) {
-        //         if (tVals.length) {
-        //             const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
-        //             if (closestTs[0] != -1 && closestTs[1] != -1) {
-        //                 segment1.push(inkSegment.split(0, closestTs[0]));
-        //                 segment2.push(inkSegment.split(closestTs[1]).right);
-        //                 deletedSegments.push(inkSegment.split(closestTs[0], closestTs[1]));
-        //             } else if (closestTs[0] == -1) {
-        //                 segment2.push(inkSegment.split(closestTs[1]).right);
-        //                 deletedSegments.push(inkSegment.split(closestTs[1]).left);
-        //                 if (excludeT > tVals[0]) {
-        //                     continueErasing = !continueErasing;
-        //                 }
-        //             } else {
-        //                 continueErasing ? segment2.push(inkSegment.split(closestTs[0]).right) : segment1.push(inkSegment.split(0, closestTs[0]));
-        //                 continueErasing ? deletedSegments.push(inkSegment.split(closestTs[0]).left) : deletedSegments.push(inkSegment.split(closestTs[0]).right);
-        //                 continueErasing = !continueErasing;
-        //             }
-        //             if (!continueErasing) {
-        //                 hasSplit = true;
-        //             }
-        //         } else if (hasSplit) {
-        //             segment2.push(inkSegment);
-        //         } else {
-        //             continueErasing = true;
-        //         }
-        //     } else {
-        //         hasSplit ? segment1.push(inkSegment) : segment2.push(inkSegment);
-        //     }
-        
-            // if (tVals.length) {
-            //     if (!hasSplit) {
-            //         if (excludeT <= tVals[0] || (excludeT > tVals[0] && excludeT < tVals.lastElement()) || excludeT > tVals.lastElement() || continueErasing) {
-            //             const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
-            //             if (closestTs[0] != -1 && closestTs[1] != -1) {
-            //                 segment1.push(inkSegment.split(0, closestTs[0]));
-            //                 segment2.push(inkSegment.split(closestTs[1]).right);
-            //                 deletedSegments.push(inkSegment.split(closestTs[0], closestTs[1]));
-            //             } else if (closestTs[0] == -1) {
-            //                 segment2.push(inkSegment.split(closestTs[1]).right);
-            //                 deletedSegments.push(inkSegment.split(closestTs[1]).left);
-            //                 if (excludeT > tVals[0]) {
-            //                     continueErasing = !continueErasing;
-            //                 }
-            //             } else {
-            //                 continueErasing ? segment2.push(inkSegment.split(closestTs[0]).right) : segment1.push(inkSegment.split(0, closestTs[0]));
-            //                 continueErasing ? deletedSegments.push(inkSegment.split(closestTs[0]).left) : deletedSegments.push(inkSegment.split(closestTs[0]).right);
-            //                 continueErasing = !continueErasing;
-            //             }
-            //             if (!continueErasing) {
-            //                 hasSplit = true;
-            //             }
-            //         } else {
-            //             if (hasSplit) {
-            //                 segment2.push(inkSegment);
-            //             } else {
-            //                 segment1.push(inkSegment);
-            //             }
-            //         }
-            //     } else if (excludeT > tVals[0] && excludeT < tVals.lastElement()) {
-            //         // here we know hasSplit is true (but it shouldnt be!)
-            //         segment1 = [...deletedSegments];
-            //         deletedSegments = [];
-            //         hasSplit = false;
-            //         const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
-            //         if (closestTs[0] != -1 && closestTs[1] != -1) {
-            //             segment1.push(inkSegment.split(0, closestTs[0]));
-            //             segment2.push(inkSegment.split(closestTs[1]).right);
-            //         } else if (closestTs[0] == -1) {
-            //             segment2.push(inkSegment.split(closestTs[1]).right);
-            //             if (excludeT > tVals[0]) {
-            //                 continueErasing = !continueErasing;
-            //             }
-            //         }
-            //     } else {
-            //         segment2.push(inkSegment);
-            //     }
-            // } else if (hasSplit) {
-            //     segment2.push(inkSegment);
-            // } else {
-            //     continueErasing = true;
-            // }
-
-
-
-
-            // if (hasSplit && i + 4 >= lastIteration && ((excludeT > tVals[0] && excludeT < tVals.lastElement()) || (excludeT > tVals.lastElement()))) {
-            //     // here we know hasSplit is true (but it shouldnt be!)
-            //     segment1 = [...deletedSegments];
-            //     deletedSegments = [];
-            //     hasSplit = false;
-            //     const closestTs = this.getClosestTs(tVals, excludeT, 0, tVals.length - 1);
-            //     if (closestTs[0] != -1 && closestTs[1] != -1) {
-            //         segment1.push(inkSegment.split(0, closestTs[0]));
-            //         segment2.push(inkSegment.split(closestTs[1]).right);
-            //     } else if (closestTs[0] == -1) {
-            //         segment2.push(inkSegment.split(closestTs[1]).right);
-            //         if (excludeT > tVals[0]) {
-            //             continueErasing = !continueErasing;
-            //         }
-            //     }
-            // }
-        // }
 
 
         // const segments: Segment[] = [];
@@ -1122,7 +1012,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
         //     segments.push(segment2);
         // }
-        return segments;
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From ba58b46b55527c23cd23b34c2ca13be945c4bf4e Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 7 Apr 2024 13:35:40 -0400
Subject: radius erase changes

---
 src/client/util/CurrentUserUtils.ts                |  10 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 165 ++++++++++-----------
 src/fields/InkField.ts                             |   3 +
 3 files changed, 94 insertions(+), 84 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 714e33d25..96edce177 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -650,7 +650,15 @@ export class CurrentUserUtils {
         return [
             { title: "Pen",    toolTip: "Pen (Ctrl+P)",                     btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                            scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",             btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
-            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.ToggleButton, icon: "eraser",     toolType: "eraser",                         scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
+            // { title: "Eraser", toolTip: "Eraser (Ctrl+E)", width: 50,       btnType: ButtonType.DropdownList, icon: "eraser",     toolType: "eraser", ignoreClick: true,      scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" },
+            //     btnList: new List<string>(["Stroke Erase", "Segment Erase", "Radius Erase"]) },
+            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.MultiToggleButton,                toolType:"eraser",                          scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'},
+              subMenu: [
+                    { title: "Stroke",   toolTip: "Stroke Erase",           btnType: ButtonType.ToggleButton, icon: "eraser",     toolType:"strokeeraser", ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+                    { title: "Segment",  toolTip: "Segment Erase",          btnType: ButtonType.ToggleButton, icon: "minus",      toolType:"segmenteraser",ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+                    { title: "Radius",   toolTip: "Radius Erase",           btnType: ButtonType.ToggleButton, icon: "circle",     toolType:"radiuseraser", ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+                    // { title: "Width",  toolTip: "Eraser Width",                     btnType: ButtonType.NumberSliderButton,               toolType:"strokeWidth",  ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
+            ]},
             { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType:GestureUtils.Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType:GestureUtils.Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType:GestureUtils.Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 18b75d8ff..da4c53d66 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -610,10 +610,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
                 // prettier-ignore
                 switch (Doc.ActiveTool) {
-                    case InkTool.Highlighter: break;
-                    case InkTool.Write: break;
-                    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:
+                    case InkTool.Highlighter:
+                        break;
+                    case InkTool.Write:
+                        break;
+                    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.StrokeEraser:
+                        this._batch = UndoManager.StartBatch('collectionErase');
+                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
+                        break;
+                    case InkTool.SegmentEraser:
+                        this._batch = UndoManager.StartBatch('collectionErase');
+                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
+                        break;
+                    case InkTool.RadiusEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
@@ -738,28 +749,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
                 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 (!e.shiftKey) {
+                if (Doc.ActiveTool !== InkTool.StrokeEraser) {
                     this._eraserLock++;
-                    const segments = this.segmentInkStroke(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
-                    // we delete the original ink stroke, then create new strokes for each of the remaining segments
-                    // we do this through the hack of calling the stroke Gesture code as if the strokes had just be drawn by hand
-                    // [[(0,0), (3,7), (9,4), (4,5)],[(4,5), (13,17), (19,14), (14,15)],       [(23,19),(...}]]
-                    // [[(0,0), (3,7), (9,4), (4,5), (13,17), (19,14), (14,15)],       [(23,19),(...}]]
-                    // segments.map(segment => segment.map(pt => pt))
-                    // segments[0][3].x === segments[1][0].x
-
-                    //   3 == "3"   true
-                    //   3 === "3" falsen   !==
-
-                    // for (var i = 0; i < segments.length; i ++) {
-                    //     if (segments[i][0].points.lastElement().x === segments[i+1][0].points.lastElement().x && segments[i][0].points.lastElement().y === segments[i+1][0].points.lastElement().y) {
-                    //         segments[i][0].points.pop();
-                    //         segments[i][0].points = segments[i][0].points.concat(segments[i+1][0].points);
-                    //         segments.splice(i+1, 1);
-                    //     }
-                    //     i++;
+                    var segments;
+                    if (Doc.ActiveTool === InkTool.SegmentEraser) {
+                        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
+                    } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
+                        console.log("RADIUS");
+                        segments = this.radiusErase(intersect.inkView, intersect.t);
+                    }
+                        // } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
+                    //     segments = undefined;
                     // }
-                    segments.forEach(segment =>
+                    segments?.forEach(segment =>
                         this.forceStrokeGesture(
                             e,
                             GestureUtils.Gestures.Stroke,
@@ -837,6 +839,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             );
     };
 
+    @action
+    radiusErase = (ink: DocumentView, eraseT: number): Segment[] => {
+        const segments: Segment[] = [];
+        var segment1: Segment = [];
+        var segment2: Segment = [];
+        const eraseWidth = ActiveInkWidth() / 100;
+        const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
+        for (var i = 0; i < inkData.length - 3; i += 4) {
+            const currCurveT = Math.floor(i/4);
+            const inkSegment: Bezier = InkField.Segment(inkData, i);
+            if (eraseT >= currCurveT && eraseT < currCurveT+1) {
+                if (eraseT - eraseWidth > currCurveT && eraseT + eraseWidth < currCurveT + 1) {
+                    segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
+                    segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
+                } else if (eraseT - eraseWidth < currCurveT) {
+                    segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
+                } else if (eraseT + eraseWidth > currCurveT + 1) {
+                    segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
+                }
+            } else if (eraseT > currCurveT + 1) {
+                segment1.push(inkSegment);
+            } else {
+                segment2.push(inkSegment);
+            }
+        }
+
+        // push 1 or both segments if they are not empty
+        if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
+            segments.push(segment1);
+        }
+        if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
+            segments.push(segment2);
+        }
+        return segments;
+    }
+
     /**
      * Erases ink strokes by segments. Locates intersections of the current ink stroke with all other ink strokes (including itself),
      * then erases the segment that was intersected by the eraser. This is done by creating either 1 or two resulting segments
@@ -846,7 +884,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * @returns The ink stroke represented as a list of segments, excluding the segment in which the eraser intersection occurred.
      */
     @action
-    segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => {
+    segmentErase = (ink: DocumentView, excludeT: number): Segment[] => {
 
         const segments: Segment[] = [];
         var segment1: Segment = [];
@@ -873,8 +911,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         var isClosedCurve = false;
         if (intersections[0] === 0) {
             isClosedCurve = true;
-            intersections = intersections.slice(1, intersections.length ); // take the 0 intersection out
-            segmentIndexes = segmentIndexes.slice(1, segmentIndexes.length); // same for indexes
+
+            // now reduce intersections list to the actual intersections because closed curves have additional ones with itself
+            const indices = intersections
+                .map((value, index) => value > 0.0001 && value < Math.floor(inkData.length / 4) ? index : -1)
+                .filter(index => index !== -1);
+
+            // Filter intersections and segmentIndexes based on validIndices
+            intersections = indices.map(index => intersections[index]);
+            segmentIndexes = indices.map(index => segmentIndexes[index]);
+            // intersections = intersections.slice(1, intersections.length ); // take the 0 intersection out
+            // segmentIndexes = segmentIndexes.slice(1, segmentIndexes.length); // same for indexes
         }
 
         if (intersections.length) {
@@ -938,13 +985,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         }
                     }
                 } else { // case where right end is erased
-                    if (currCurveT === splitSegment1) {
-                        segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
-                        hasSplit = true;
-                    } else if (isClosedCurve && currCurveT === segmentIndexes[1]) {
+                    if (currCurveT === segmentIndexes[0] && isClosedCurve) {
                         segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
+                        hasSplit = true;
+                    } else if (currCurveT === splitSegment1) {
+                        segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
+                        hasSplit = false; // although this is not technically true, we set to false so the segments after don't get added
+                    // } else if (isClosedCurve && currCurveT === segmentIndexes[1]) {
+                    //     segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
                     } else {
-                        if (!hasSplit && !isClosedCurve) {
+                        if ((isClosedCurve && hasSplit) || (!isClosedCurve && !hasSplit)) {
                             segment1.push(inkSegment);
                         }
                     }
@@ -961,57 +1011,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
 
         return segments;
-
-
-        // const segments: Segment[] = [];
-        // var segment1: Segment = [];
-        // var segment2: Segment = [];
-        // var startSegmentT = 0;
-        // var hasSplit = false;
-        // const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
-        // // 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 inkSegment = InkField.Segment(inkData, i);
-        //     // Getting all t-value intersections of the current curve with all other curves.
-        //     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);
-        //                 if (!hasSplit) {
-        //                     t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment1.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
-        //                 } else {
-        //                     t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment2.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
-        //                 }
-        //                 // if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) segments.push(segment);
-        //             }
-        //             // start a new segment from the intersection t value
-        //             if (tVals.length - 1 === index) {
-        //                 const split = inkSegment.split(t).right;
-        //                 if (!hasSplit) {
-        //                     if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment1.push(split);
-        //                 } else {
-        //                     if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment2.push(split);
-        //                 }
-        //             }
-        //             startSegmentT = docCurveTVal;
-        //         });
-        //     } else {
-        //         if (!hasSplit) {
-        //             segment1.push(inkSegment);
-        //         } else {
-        //             segment2.push(inkSegment);
-        //         }
-        //     }
-        // }
-        // if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
-        //     segments.push(segment1);
-        // }
-        // if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
-        //     segments.push(segment2);
-        // }
     };
 
     /**
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index b3e01229a..9c8c5df2c 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -11,6 +11,9 @@ export enum InkTool {
     Pen = 'pen',
     Highlighter = 'highlighter',
     Eraser = 'eraser',
+    StrokeEraser = 'strokeeraser',
+    SegmentEraser = 'segmenteraser',
+    RadiusEraser = 'radiuseraser',
     Stamp = 'stamp',
     Write = 'write',
     PresentationPin = 'presentationpin',
-- 
cgit v1.2.3-70-g09d2


From 172d791a95126cbdb2ebf59fd8c28a63698002d1 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Mon, 8 Apr 2024 23:20:18 -0400
Subject: radius erase re-implement started

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/views/InkingStroke.tsx                  | 22 +++++++++++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 38 ++++++++++++++++++----
 3 files changed, 54 insertions(+), 8 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2d2f5fe4a..78a28ed62 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1104,7 +1104,7 @@ export namespace Docs {
             I.color = color;
             I.fillColor = fillColor;
             I.stroke = new InkField(points);
-            I.stroke_width = strokeWidth;
+            I.stroke_xwidth = strokeWidth;
             I.stroke_bezier = stroke_bezier;
             I.stroke_startMarker = arrowStart;
             I.stroke_endMarker = arrowEnd;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 92644d3c5..a91fc5995 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -330,6 +330,28 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() impleme
         );
     };
 
+    splitByEraser = (inkCoords: {X: number; Y: number}, radius: number) => {
+        // const ptsXscale = (NumCast(radius) - NumCast(radius)) / (oldXrange.max - oldXrange.min || 1) || 1;
+        // const ptsYscale = (NumCast(doc._height) - NumCast(doc.stroke_width)) / (oldYrange.max - oldYrange.min || 1) || 1;
+        // const newPoints = func(this.DocumentView?.(), ink, ptsXscale, ptsYscale, NumCast(radius));
+        const points: { X: number; Y: number }[] = [
+            { X: inkCoords.X + radius, Y: inkCoords.Y },
+            { X: inkCoords.X + radius, Y: inkCoords.Y - radius },
+            { X: inkCoords.X + radius, Y: inkCoords.Y - radius },
+            { X: inkCoords.X, Y: inkCoords.Y - radius },
+            { X: inkCoords.X - radius, Y: inkCoords.Y - radius },
+            { X: inkCoords.X - radius, Y: inkCoords.Y - radius },
+            { X: inkCoords.X - radius, Y: inkCoords.Y },
+            { X: inkCoords.X - radius, Y: inkCoords.Y + radius },
+            { X: inkCoords.X - radius, Y: inkCoords.Y + radius },
+            { X: inkCoords.X, Y: inkCoords.Y + radius },
+            { X: inkCoords.X + radius, Y: inkCoords.Y + radius },
+            { X: inkCoords.X + radius, Y: inkCoords.Y + radius }
+        ];
+        const eraserCircle = new InkField(points);
+        return points;
+    }
+
     _subContentView: ViewBoxInterface | undefined;
     setSubContentView = (doc: ViewBoxInterface) => (this._subContentView = doc);
     @computed get fillColor() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index da4c53d66..26afa5297 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,4 +1,4 @@
-import { findSegment } from '@turf/turf';
+import { findSegment, intersect } from '@turf/turf';
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
@@ -755,8 +755,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (Doc.ActiveTool === InkTool.SegmentEraser) {
                         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
                     } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
-                        console.log("RADIUS");
-                        segments = this.radiusErase(intersect.inkView, intersect.t);
+                        const coords = this.props.ScreenToLocalTransform().inverse().transformPoint(e.clientX, e.clientY);
+                        const inkCoords = intersect.inkView.ComponentView?.ptFromScreen?.({X: coords[0], Y: coords[1]}); // coordinates in ink space
+                        if (inkCoords !== undefined) {
+                            segments = this.radiusErase(intersect.inkView, intersect.t, inkCoords);
+                        }
                     }
                         // } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
                     //     segments = undefined;
@@ -840,16 +843,37 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    radiusErase = (ink: DocumentView, eraseT: number): Segment[] => {
+    radiusErase = (ink: DocumentView, eraseT: number, inkCoords: {X: number, Y: number}): Segment[] => {
         const segments: Segment[] = [];
         var segment1: Segment = [];
         var segment2: Segment = [];
-        const eraseWidth = ActiveInkWidth() / 100;
-        const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
+        const eraseWidth = ActiveInkWidth();
+        const inkStroke = ink?.ComponentView as InkingStroke;
+        const { inkData } = (inkStroke).inkScaledData();
+
+        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseWidth) as InkData;
+
+        const tVals: number[] = []; // should be the tvals of the intersections
+
+        for (var i = 0; i < eraserInkData.length - 3; i += 4) {
+            const currCurveT = Math.floor(i/4);
+            const eraserBezier = InkField.Segment(eraserInkData, i);
+            for (var j = 0; j < inkData.length; j +=4) {
+            const inkSegment: Bezier = InkField.Segment(inkData, i);
+                this.bintersects(inkSegment, eraserBezier).forEach((val: string | number, i: number) => {
+                    // Converting the Bezier.js Split type to a t-value number.
+                    const t = +val.toString().split('/')[0];
+                    if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
+                });
+            }
+        }
+
         for (var i = 0; i < inkData.length - 3; i += 4) {
             const currCurveT = Math.floor(i/4);
             const inkSegment: Bezier = InkField.Segment(inkData, i);
-            if (eraseT >= currCurveT && eraseT < currCurveT+1) {
+            if (tVals[0] >= currCurveT && tVals[0] < currCurveT+1) {
+                tVals.shift()
+                i -= 4;
                 if (eraseT - eraseWidth > currCurveT && eraseT + eraseWidth < currCurveT + 1) {
                     segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
                     segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
-- 
cgit v1.2.3-70-g09d2


From 3bba8465a3d823596e7f8cfab24b9afa9c05cf2c Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 10 Apr 2024 22:04:55 -0400
Subject: eraser circled created

---
 src/client/views/InkingStroke.tsx                  | 31 ++++++-----
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 65 +++++++++++-----------
 2 files changed, 51 insertions(+), 45 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a91fc5995..764062682 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -334,22 +334,27 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() impleme
         // const ptsXscale = (NumCast(radius) - NumCast(radius)) / (oldXrange.max - oldXrange.min || 1) || 1;
         // const ptsYscale = (NumCast(doc._height) - NumCast(doc.stroke_width)) / (oldYrange.max - oldYrange.min || 1) || 1;
         // const newPoints = func(this.DocumentView?.(), ink, ptsXscale, ptsYscale, NumCast(radius));
+        const controlPointDistance = 0.552284749831 * radius; // tan(pi / 8) * radius
         const points: { X: number; Y: number }[] = [
-            { X: inkCoords.X + radius, Y: inkCoords.Y },
-            { X: inkCoords.X + radius, Y: inkCoords.Y - radius },
-            { X: inkCoords.X + radius, Y: inkCoords.Y - radius },
-            { X: inkCoords.X, Y: inkCoords.Y - radius },
-            { X: inkCoords.X - radius, Y: inkCoords.Y - radius },
-            { X: inkCoords.X - radius, Y: inkCoords.Y - radius },
-            { X: inkCoords.X - radius, Y: inkCoords.Y },
-            { X: inkCoords.X - radius, Y: inkCoords.Y + radius },
-            { X: inkCoords.X - radius, Y: inkCoords.Y + radius },
-            { X: inkCoords.X, Y: inkCoords.Y + radius },
-            { X: inkCoords.X + radius, Y: inkCoords.Y + radius },
-            { X: inkCoords.X + radius, Y: inkCoords.Y + radius }
+            { X: inkCoords.X + radius, Y: inkCoords.Y }, // right point
+            { X: inkCoords.X + radius, Y: inkCoords.Y - controlPointDistance }, // right's top ctrl point
+            { X: inkCoords.X + controlPointDistance, Y: inkCoords.Y - radius }, // top's right ctrl point
+            { X: inkCoords.X, Y: inkCoords.Y - radius }, // top
+            { X: inkCoords.X, Y: inkCoords.Y - radius }, // top again
+            { X: inkCoords.X - controlPointDistance, Y: inkCoords.Y - radius }, // top's left ctrl point
+            { X: inkCoords.X - radius, Y: inkCoords.Y - controlPointDistance }, // left's top ctrl point
+            { X: inkCoords.X - radius, Y: inkCoords.Y }, // left
+            { X: inkCoords.X - radius, Y: inkCoords.Y }, // left again
+            { X: inkCoords.X - radius, Y: inkCoords.Y + controlPointDistance }, // left's bottom ctrl point
+            { X: inkCoords.X - controlPointDistance, Y: inkCoords.Y + radius }, // bottom's left ctrl point
+            { X: inkCoords.X, Y: inkCoords.Y + radius }, // bottom
+            { X: inkCoords.X, Y: inkCoords.Y + radius }, // bottom again
+            { X: inkCoords.X + controlPointDistance, Y: inkCoords.Y + radius }, // bottom's right ctrl point
+            { X: inkCoords.X + radius, Y: inkCoords.Y + controlPointDistance }, // right's bottom ctrl point
+            { X: inkCoords.X + radius, Y: inkCoords.Y }, // right again
         ];
         const eraserCircle = new InkField(points);
-        return points;
+        return eraserCircle;
     }
 
     _subContentView: ViewBoxInterface | undefined;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 26afa5297..a72b499b2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -625,8 +625,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
                     case InkTool.RadiusEraser:
-                        this._batch = UndoManager.StartBatch('collectionErase');
-                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
+                        // this._batch = UndoManager.StartBatch('collectionErase');
+                        // setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
@@ -851,43 +851,44 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const inkStroke = ink?.ComponentView as InkingStroke;
         const { inkData } = (inkStroke).inkScaledData();
 
-        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseWidth) as InkData;
+        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseWidth);
 
         const tVals: number[] = []; // should be the tvals of the intersections
 
         for (var i = 0; i < eraserInkData.length - 3; i += 4) {
-            const currCurveT = Math.floor(i/4);
-            const eraserBezier = InkField.Segment(eraserInkData, i);
-            for (var j = 0; j < inkData.length; j +=4) {
-            const inkSegment: Bezier = InkField.Segment(inkData, i);
-                this.bintersects(inkSegment, eraserBezier).forEach((val: string | number, i: number) => {
-                    // Converting the Bezier.js Split type to a t-value number.
-                    const t = +val.toString().split('/')[0];
-                    if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
-                });
-            }
-        }
-
-        for (var i = 0; i < inkData.length - 3; i += 4) {
-            const currCurveT = Math.floor(i/4);
+            const eraserBezier: Bezier = InkField.Segment(eraserInkData, i);
+            segment1.push(eraserBezier);
+            for (var j = 0; j < inkData.length; j += 4) {
             const inkSegment: Bezier = InkField.Segment(inkData, i);
-            if (tVals[0] >= currCurveT && tVals[0] < currCurveT+1) {
-                tVals.shift()
-                i -= 4;
-                if (eraseT - eraseWidth > currCurveT && eraseT + eraseWidth < currCurveT + 1) {
-                    segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
-                    segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
-                } else if (eraseT - eraseWidth < currCurveT) {
-                    segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
-                } else if (eraseT + eraseWidth > currCurveT + 1) {
-                    segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
-                }
-            } else if (eraseT > currCurveT + 1) {
-                segment1.push(inkSegment);
-            } else {
-                segment2.push(inkSegment);
+            // this.bintersects(inkSegment, eraserBezier).forEach((val: string | number, i: number) => {
+            //     // Converting the Bezier.js Split type to a t-value number.
+            //     const t = +val.toString().split('/')[0];
+            //     if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
+            // });
             }
         }
+        // segment1.push(eraserBezier);
+    
+        // for (var i = 0; i < inkData.length - 3; i += 4) {
+        //     const currCurveT = Math.floor(i/4);
+        //     const inkSegment: Bezier = InkField.Segment(inkData, i);
+        //     if (tVals[0] >= currCurveT && tVals[0] < currCurveT+1) {
+        //         tVals.shift()
+        //         i -= 4;
+        //         if (eraseT - eraseWidth > currCurveT && eraseT + eraseWidth < currCurveT + 1) {
+        //             segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
+        //             segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
+        //         } else if (eraseT - eraseWidth < currCurveT) {
+        //             segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
+        //         } else if (eraseT + eraseWidth > currCurveT + 1) {
+        //             segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
+        //         }
+        //     } else if (eraseT > currCurveT + 1) {
+        //         segment1.push(inkSegment);
+        //     } else {
+        //         segment2.push(inkSegment);
+        //     }
+        // }
 
         // push 1 or both segments if they are not empty
         if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
-- 
cgit v1.2.3-70-g09d2


From d938cd08650279f5c7894793d5fd78ec4068694c Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 14 Apr 2024 14:25:29 -0400
Subject: working on radius eraser bug

---
 src/client/documents/Documents.ts                                 | 2 +-
 src/client/views/InkingStroke.tsx                                 | 2 +-
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx     | 8 ++++----
 3 files changed, 6 insertions(+), 6 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 78a28ed62..2d2f5fe4a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1104,7 +1104,7 @@ export namespace Docs {
             I.color = color;
             I.fillColor = fillColor;
             I.stroke = new InkField(points);
-            I.stroke_xwidth = strokeWidth;
+            I.stroke_width = strokeWidth;
             I.stroke_bezier = stroke_bezier;
             I.stroke_startMarker = arrowStart;
             I.stroke_endMarker = arrowEnd;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 764062682..7e0b2ba92 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -334,7 +334,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() impleme
         // const ptsXscale = (NumCast(radius) - NumCast(radius)) / (oldXrange.max - oldXrange.min || 1) || 1;
         // const ptsYscale = (NumCast(doc._height) - NumCast(doc.stroke_width)) / (oldYrange.max - oldYrange.min || 1) || 1;
         // const newPoints = func(this.DocumentView?.(), ink, ptsXscale, ptsYscale, NumCast(radius));
-        const controlPointDistance = 0.552284749831 * radius; // tan(pi / 8) * radius
+        const controlPointDistance = 0.552284749831 * radius; // (4/3) * tan(pi / 8) * radius
         const points: { X: number; Y: number }[] = [
             { X: inkCoords.X + radius, Y: inkCoords.Y }, // right point
             { X: inkCoords.X + radius, Y: inkCoords.Y - controlPointDistance }, // right's top ctrl point
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a72b499b2..ef4ab55db 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -625,8 +625,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
                     case InkTool.RadiusEraser:
-                        // this._batch = UndoManager.StartBatch('collectionErase');
-                        // setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
+                        this._batch = UndoManager.StartBatch('collectionErase');
+                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
@@ -847,11 +847,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const segments: Segment[] = [];
         var segment1: Segment = [];
         var segment2: Segment = [];
-        const eraseWidth = ActiveInkWidth();
+        const eraseRadius = ActiveInkWidth() / 2;
         const inkStroke = ink?.ComponentView as InkingStroke;
         const { inkData } = (inkStroke).inkScaledData();
 
-        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseWidth);
+        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseRadius);
 
         const tVals: number[] = []; // should be the tvals of the intersections
 
-- 
cgit v1.2.3-70-g09d2


From 1b4665378cd93aa49388d92831ce5785b4423bbf Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 Apr 2024 16:19:55 -0400
Subject: fixed coordinate system transform for ink

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 60 +++++++++++-----------
 1 file changed, 29 insertions(+), 31 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 2bf4f3c1e..57868825e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -755,13 +755,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (Doc.ActiveTool === InkTool.SegmentEraser) {
                         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
                     } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
-                        const coords = this.props.ScreenToLocalTransform().inverse().transformPoint(e.clientX, e.clientY);
-                        const inkCoords = intersect.inkView.ComponentView?.ptFromScreen?.({X: coords[0], Y: coords[1]}); // coordinates in ink space
+                        const inkCoords = intersect.inkView.ComponentView?.ptFromScreen?.(currPoint); // coordinates in ink space
                         if (inkCoords !== undefined) {
                             segments = this.radiusErase(intersect.inkView, intersect.t, inkCoords);
                         }
                     }
-                        // } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
+                    // } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
                     //     segments = undefined;
                     // }
                     segments?.forEach(segment =>
@@ -842,15 +841,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    radiusErase = (ink: DocumentView, eraseT: number, inkCoords: {X: number, Y: number}): Segment[] => {
+    radiusErase = (ink: DocumentView, eraseT: number, inkCoords: { X: number; Y: number }): Segment[] => {
         const segments: Segment[] = [];
         var segment1: Segment = [];
         var segment2: Segment = [];
         const eraseRadius = ActiveInkWidth() / 2;
         const inkStroke = ink?.ComponentView as InkingStroke;
-        const { inkData } = (inkStroke).inkScaledData();
+        const { inkData } = inkStroke.inkScaledData();
 
-        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseRadius);
+        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseRadius).inkData;
 
         const tVals: number[] = []; // should be the tvals of the intersections
 
@@ -858,16 +857,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const eraserBezier: Bezier = InkField.Segment(eraserInkData, i);
             segment1.push(eraserBezier);
             for (var j = 0; j < inkData.length; j += 4) {
-            const inkSegment: Bezier = InkField.Segment(inkData, i);
-            // this.bintersects(inkSegment, eraserBezier).forEach((val: string | number, i: number) => {
-            //     // Converting the Bezier.js Split type to a t-value number.
-            //     const t = +val.toString().split('/')[0];
-            //     if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
-            // });
+                const inkSegment: Bezier = InkField.Segment(inkData, i);
+                // this.bintersects(inkSegment, eraserBezier).forEach((val: string | number, i: number) => {
+                //     // Converting the Bezier.js Split type to a t-value number.
+                //     const t = +val.toString().split('/')[0];
+                //     if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
+                // });
             }
         }
         // segment1.push(eraserBezier);
-    
+
         // for (var i = 0; i < inkData.length - 3; i += 4) {
         //     const currCurveT = Math.floor(i/4);
         //     const inkSegment: Bezier = InkField.Segment(inkData, i);
@@ -897,7 +896,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             segments.push(segment2);
         }
         return segments;
-    }
+    };
 
     /**
      * Erases ink strokes by segments. Locates intersections of the current ink stroke with all other ink strokes (including itself),
@@ -909,22 +908,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      */
     @action
     segmentErase = (ink: DocumentView, excludeT: number): Segment[] => {
-
         const segments: Segment[] = [];
         var segment1: Segment = [];
         var segment2: Segment = [];
         const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
         var intersections: number[] = []; // list of the ink stroke's intersections
         var segmentIndexes: number[] = []; // list of indexes of the curve's segment where each intersection occured
-        
+
         // loops through each segment and adds intersections to the list
         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
-            currIntersects = currIntersects.map(tVal => tVal + Math.floor(i/4));
+            currIntersects = currIntersects.map(tVal => tVal + Math.floor(i / 4));
             if (currIntersects.length) {
-                intersections = [...intersections, ...currIntersects]
+                intersections = [...intersections, ...currIntersects];
                 for (var j = 0; j < currIntersects.length; j++) {
                     segmentIndexes.push(Math.floor(i / 4));
                 }
@@ -937,9 +935,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             isClosedCurve = true;
 
             // now reduce intersections list to the actual intersections because closed curves have additional ones with itself
-            const indices = intersections
-                .map((value, index) => value > 0.0001 && value < Math.floor(inkData.length / 4) ? index : -1)
-                .filter(index => index !== -1);
+            const indices = intersections.map((value, index) => (value > 0.0001 && value < Math.floor(inkData.length / 4) ? index : -1)).filter(index => index !== -1);
 
             // Filter intersections and segmentIndexes based on validIndices
             intersections = indices.map(index => intersections[index]);
@@ -959,9 +955,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // 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]];
             }
 
@@ -996,7 +994,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             segment2.push(inkSegment);
                         }
                     }
-                } else if (splitSegment1 === -1) { // case where left end is erased
+                } else if (splitSegment1 === -1) {
+                    // case where left end is erased
                     if (currCurveT === splitSegment2) {
                         segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
                         hasSplit = true;
@@ -1008,15 +1007,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             segment2.push(inkSegment);
                         }
                     }
-                } else { // case where right end is erased
+                } else {
+                    // case where right end is erased
                     if (currCurveT === segmentIndexes[0] && isClosedCurve) {
                         segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
                         hasSplit = true;
                     } else if (currCurveT === splitSegment1) {
                         segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
                         hasSplit = false; // although this is not technically true, we set to false so the segments after don't get added
-                    // } else if (isClosedCurve && currCurveT === segmentIndexes[1]) {
-                    //     segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
+                        // } else if (isClosedCurve && currCurveT === segmentIndexes[1]) {
+                        //     segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
                     } else {
                         if ((isClosedCurve && hasSplit) || (!isClosedCurve && !hasSplit)) {
                             segment1.push(inkSegment);
@@ -1048,11 +1048,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getClosestTs = (tVals: number[], excludeT: number, startIndex: number, endIndex: number): number[] => {
         if (tVals[startIndex] >= excludeT) {
             return [-1, startIndex];
-        }
-        else if (tVals[endIndex] < excludeT) {
+        } else if (tVals[endIndex] < excludeT) {
             return [endIndex, -1];
-        }
-        else {
+        } else {
             const mid = Math.floor((startIndex + endIndex) / 2);
             if (excludeT >= tVals[mid]) {
                 if (mid + 1 <= endIndex && tVals[mid + 1] > excludeT) {
-- 
cgit v1.2.3-70-g09d2


From 76c815c248093320510de9af4c959921800e8d34 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 Apr 2024 16:22:49 -0400
Subject: from last

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(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 57868825e..4654de427 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -849,7 +849,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const inkStroke = ink?.ComponentView as InkingStroke;
         const { inkData } = inkStroke.inkScaledData();
 
-        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseRadius).inkData;
+        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseRadius);
 
         const tVals: number[] = []; // should be the tvals of the intersections
 
-- 
cgit v1.2.3-70-g09d2


From 29de3f24c647e1a9cc78dbef2b757114d1c2d649 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 Apr 2024 16:28:49 -0400
Subject: from last

---
 .../collectionFreeForm/CollectionFreeFormView.tsx      | 18 ++++++++----------
 1 file changed, 8 insertions(+), 10 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 4654de427..23fa227c2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -751,15 +751,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // 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++;
-                    var segments;
-                    if (Doc.ActiveTool === InkTool.SegmentEraser) {
-                        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
-                    } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
-                        const inkCoords = intersect.inkView.ComponentView?.ptFromScreen?.(currPoint); // coordinates in ink space
-                        if (inkCoords !== undefined) {
-                            segments = this.radiusErase(intersect.inkView, intersect.t, inkCoords);
-                        }
-                    }
+                    const segments =
+                        Doc.ActiveTool === InkTool.SegmentEraser
+                            ? 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
+                            : this.radiusErase(intersect.inkView, intersect.t, currPoint);
                     // } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
                     //     segments = undefined;
                     // }
@@ -841,8 +836,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    radiusErase = (ink: DocumentView, eraseT: number, inkCoords: { X: number; Y: number }): Segment[] => {
+    radiusErase = (ink: DocumentView, eraseT: number, screenEraserPt: { X: number; Y: number }): Segment[] => {
         const segments: Segment[] = [];
+        const inkCoords = ink.ComponentView?.ptFromScreen?.(screenEraserPt); // coordinates in ink space
+        if (!inkCoords) return [];
+
         var segment1: Segment = [];
         var segment2: Segment = [];
         const eraseRadius = ActiveInkWidth() / 2;
-- 
cgit v1.2.3-70-g09d2


From d8e4ff91b55736608a02d1ac68cb5c165841d6bb Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 Apr 2024 16:30:32 -0400
Subject: clean up imports

---
 .../collectionFreeForm/CollectionFreeFormView.tsx       | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 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 23fa227c2..4e8c2badf 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,10 +1,10 @@
-import { findSegment, intersect } from '@turf/turf';
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
+import { DashColor, OmitKeys, Utils, aggregateBounds, emptyFunction, intersectRect, lightOrDark, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
 import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
 import { DocData, Height, Width } from '../../../../fields/DocSymbols';
@@ -18,10 +18,9 @@ import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, numberValue, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
-import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { DocUtils, Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { ReplayMovements } from '../../../util/ReplayMovements';
@@ -31,26 +30,26 @@ import { SelectionManager } from '../../../util/SelectionManager';
 import { freeformScrollMode } from '../../../util/SettingsManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
-import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
-import { Timeline } from '../../animationtimeline/Timeline';
+import { UndoManager, undoBatch, undoable } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { GestureOverlay } from '../../GestureOverlay';
 import { CtrlKey } from '../../GlobalKeyHandler';
 import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
+import { StyleProp } from '../../StyleProvider';
+import { Timeline } from '../../animationtimeline/Timeline';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
 import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
+import { CreateImage } from '../../nodes/WebBoxRenderer';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { PinProps, PresBox } from '../../nodes/trails/PresBox';
-import { CreateImage } from '../../nodes/WebBoxRenderer';
-import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid';
 import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI';
-import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { PoolData, ViewDefBounds, ViewDefResult, computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
-- 
cgit v1.2.3-70-g09d2


From a374ab9992962ebb6c4af43b82348c706330b58e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 Apr 2024 16:37:27 -0400
Subject: from last

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 6 ------
 1 file changed, 6 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 9a0f34074..60f7662ff 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -617,13 +617,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     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.StrokeEraser:
-                        this._batch = UndoManager.StartBatch('collectionErase');
-                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
-                        break;
                     case InkTool.SegmentEraser:
-                        this._batch = UndoManager.StartBatch('collectionErase');
-                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
-                        break;
                     case InkTool.RadiusEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
-- 
cgit v1.2.3-70-g09d2


From 0c33bc8033c9877abbe6e4074a687559bc4948d0 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 23 Apr 2024 15:16:06 -0400
Subject: erase multiple segments bug

---
 package-lock.json                                  |   3 +-
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/CurrentUserUtils.ts                |   7 +-
 src/client/views/InkingStroke.tsx                  | 110 +++++++++++---
 src/client/views/MainView.tsx                      |   3 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 167 ++++++++++++++-------
 src/client/views/global/globalScripts.ts           |  19 ++-
 7 files changed, 228 insertions(+), 85 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index 9be0d6cb9..eda529cad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32074,7 +32074,8 @@
         "node_modules/text-table": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+            "dev": true
         },
         "node_modules/textarea-caret": {
             "version": "3.1.0",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b160379df..61a58caaa 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -26,7 +26,7 @@ import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { UndoManager, undoable } from '../util/UndoManager';
 import { ContextMenu } from '../views/ContextMenu';
 import { ContextMenuProps } from '../views/ContextMenuItem';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke } from '../views/InkingStroke';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveEraserWidth, ActiveIsInkMask, InkingStroke } from '../views/InkingStroke';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionView } from '../views/collections/CollectionView';
 import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView';
@@ -1123,6 +1123,7 @@ export namespace Docs {
             points: PointData[],
             options: DocumentOptions = {},
             strokeWidth = ActiveInkWidth(),
+            eraserWidth = ActiveEraserWidth(),
             color = ActiveInkColor(),
             stroke_bezier = ActiveInkBezierApprox(),
             fillColor = ActiveFillColor(),
@@ -1138,6 +1139,7 @@ export namespace Docs {
             I.fillColor = fillColor;
             I.stroke = new InkField(points);
             I.stroke_width = strokeWidth;
+            I.eraser_width = eraserWidth;
             I.stroke_bezier = stroke_bezier;
             I.stroke_startMarker = arrowStart;
             I.stroke_endMarker = arrowEnd;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 031b79b25..59fc30635 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -34,6 +34,7 @@ import { SnappingManager } from "./SnappingManager";
 import { UndoManager } from "./UndoManager";
 import { LabelBox } from "../views/nodes/LabelBox";
 import { ImageBox } from "../views/nodes/ImageBox";
+import { PiEraser } from "react-icons/pi";
 
 interface Button {
     // DocumentOptions fields a button can set
@@ -756,9 +757,8 @@ pie title Minerals in my tap water
             { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.MultiToggleButton,                toolType:"eraser",                          scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'},
               subMenu: [
                     { title: "Stroke",   toolTip: "Stroke Erase",           btnType: ButtonType.ToggleButton, icon: "eraser",     toolType:"strokeeraser", ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-                    { title: "Segment",  toolTip: "Segment Erase",          btnType: ButtonType.ToggleButton, icon: "minus",      toolType:"segmenteraser",ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-                    { title: "Radius",   toolTip: "Radius Erase",           btnType: ButtonType.ToggleButton, icon: "circle",     toolType:"radiuseraser", ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-                    // { title: "Width",  toolTip: "Eraser Width",                     btnType: ButtonType.NumberSliderButton,               toolType:"strokeWidth",  ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
+                    { title: "Segment",  toolTip: "Segment Erase",          btnType: ButtonType.ToggleButton, icon: "xmarks-lines",      toolType:"segmenteraser",ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+                    { title: "Radius",   toolTip: "Radius Erase",           btnType: ButtonType.ToggleButton, icon: "circle-xmark",     toolType:"radiuseraser", ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
             ]},
             { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType:GestureUtils.Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType:GestureUtils.Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
@@ -766,6 +766,7 @@ pie title Minerals in my tap water
             { title: "Mask",   toolTip: "Mask",                             btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
             { title: "Labels", toolTip: "Lab els",                          btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
             { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberSliderButton,               toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
+            { title: "Eraser Width",  toolTip: "Eraser Width",              btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
             { title: "Ink",    toolTip: "Stroke color",                     btnType: ButtonType.ColorButton,  icon: "pen",        toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
         ];
     }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 1c24a4903..1ae96edfd 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -45,6 +45,7 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { PinProps, PresBox } from './nodes/trails';
 import { StyleProp } from './StyleProvider';
 const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
+
 @observer
 export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
     static readonly MaskDim = INK_MASK_SIZE; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
@@ -330,31 +331,86 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     };
 
-    splitByEraser = (inkCoords: {X: number; Y: number}, radius: number) => {
-        // const ptsXscale = (NumCast(radius) - NumCast(radius)) / (oldXrange.max - oldXrange.min || 1) || 1;
-        // const ptsYscale = (NumCast(doc._height) - NumCast(doc.stroke_width)) / (oldYrange.max - oldYrange.min || 1) || 1;
-        // const newPoints = func(this.DocumentView?.(), ink, ptsXscale, ptsYscale, NumCast(radius));
-        const controlPointDistance = 0.552284749831 * radius; // (4/3) * tan(pi / 8) * radius
-        const points: { X: number; Y: number }[] = [
-            { X: inkCoords.X + radius, Y: inkCoords.Y }, // right point
-            { X: inkCoords.X + radius, Y: inkCoords.Y - controlPointDistance }, // right's top ctrl point
-            { X: inkCoords.X + controlPointDistance, Y: inkCoords.Y - radius }, // top's right ctrl point
-            { X: inkCoords.X, Y: inkCoords.Y - radius }, // top
-            { X: inkCoords.X, Y: inkCoords.Y - radius }, // top again
-            { X: inkCoords.X - controlPointDistance, Y: inkCoords.Y - radius }, // top's left ctrl point
-            { X: inkCoords.X - radius, Y: inkCoords.Y - controlPointDistance }, // left's top ctrl point
-            { X: inkCoords.X - radius, Y: inkCoords.Y }, // left
-            { X: inkCoords.X - radius, Y: inkCoords.Y }, // left again
-            { X: inkCoords.X - radius, Y: inkCoords.Y + controlPointDistance }, // left's bottom ctrl point
-            { X: inkCoords.X - controlPointDistance, Y: inkCoords.Y + radius }, // bottom's left ctrl point
-            { X: inkCoords.X, Y: inkCoords.Y + radius }, // bottom
-            { X: inkCoords.X, Y: inkCoords.Y + radius }, // bottom again
-            { X: inkCoords.X + controlPointDistance, Y: inkCoords.Y + radius }, // bottom's right ctrl point
-            { X: inkCoords.X + radius, Y: inkCoords.Y + controlPointDistance }, // right's bottom ctrl point
-            { X: inkCoords.X + radius, Y: inkCoords.Y }, // right again
-        ];
+    coordWithSlope = (distance: number, slope: number) =>  {
+        return Math.sqrt((distance * distance) / (1 + slope * slope))
+    }
+    
+
+    splitByEraser = (startInkCoords: {X: number; Y: number}, inkCoords: {X: number; Y: number}) => {
+
+        const radius = ActiveEraserWidth() / 2 + 3; // reduce values to avoid extreme radii
+        console.log("radius", radius);
+        const ctrlPtDist = 0.552284749831 * radius; // (4/3) * tan(pi / 8) * radius
+
+        var perpSlope = (inkCoords.Y - startInkCoords.Y) / (inkCoords.X - startInkCoords.X);
+        if (perpSlope > 500) {
+            perpSlope = 500; // avoid ridiculously high/infinity slopes
+        } else if (perpSlope < -500) {
+            perpSlope = -500
+        } else if (perpSlope === 0) {
+            perpSlope = 0.002
+        }
+        const slope = - 1 / perpSlope
+        console.log("slope", slope);
+        var points: { X: number; Y: number }[]  = []
+        if (startInkCoords.X > inkCoords.X) {
+             points = [
+                { X: startInkCoords.X, Y: startInkCoords.Y },
+                { X: startInkCoords.X, Y: startInkCoords.Y },
+                // { X: startInkCoords.X + this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y + slope * this.coordWithSlope(ctrlPtDist, slope)},
+
+                { X: inkCoords.X + this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+
+                // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) + this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) },
+                // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) - this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+
+                { X: inkCoords.X - this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+
+                // { X: startInkCoords.X - this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y - slope * this.coordWithSlope(ctrlPtDist, slope) },
+                { X: startInkCoords.X, Y: startInkCoords.Y },
+                { X: startInkCoords.X, Y: startInkCoords.Y },
+            ];
+        } else {
+            points = [
+                { X: startInkCoords.X, Y: startInkCoords.Y },
+                { X: startInkCoords.X, Y: startInkCoords.Y },
+                // { X: startInkCoords.X + this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y + slope * this.coordWithSlope(ctrlPtDist, slope)},
+
+                { X: inkCoords.X + this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+
+                // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) + this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) },
+                { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) },
+                // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) - this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+
+                { X: inkCoords.X - this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) },
+                { X: inkCoords.X - this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
+
+                // { X: startInkCoords.X - this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y - slope * this.coordWithSlope(ctrlPtDist, slope) },
+                { X: startInkCoords.X, Y: startInkCoords.Y },
+                { X: startInkCoords.X, Y: startInkCoords.Y },
+            ];
+        }
+
         const eraserCircle = new InkField(points);
-        return points;
+        return eraserCircle;
     }
 
     _subContentView: ViewBoxInterface | undefined;
@@ -533,6 +589,9 @@ export function SetActiveArrowScale(value: number) {
 export function SetActiveDash(dash: string): void {
     !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
 }
+export function SetEraserWidth(radius: string): void {
+    !isNaN(parseInt(radius)) && ActiveInkPen() && (ActiveInkPen().eraserWidth = radius);
+}
 export function ActiveInkPen(): Doc {
     return Doc.UserDoc();
 }
@@ -566,3 +625,6 @@ export function ActiveInkWidth(): number {
 export function ActiveInkBezierApprox(): string {
     return StrCast(ActiveInkPen()?.activeInkBezier);
 }
+export function ActiveEraserWidth(): number {
+    return Number(ActiveInkPen()?.eraserWidth);
+}
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 58b8d255a..56db0c488 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -358,6 +358,9 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faCut,
                 fa.faEllipsisV,
                 fa.faEraser,
+                fa.faDeleteLeft,
+                fa.faXmarksLines,
+                fa.faCircleXmark,
                 fa.faExclamation,
                 fa.faFileAlt,
                 fa.faFileAudio,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 60f7662ff..78dae87c3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -749,10 +749,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     const segments =
                         Doc.ActiveTool === InkTool.SegmentEraser
                             ? 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
-                            : this.radiusErase(intersect.inkView, intersect.t, currPoint);
-                    // } else if (Doc.ActiveTool === InkTool.RadiusEraser) {
-                    //     segments = undefined;
-                    // }
+                            : this.radiusErase(intersect.inkView, { X: down[0], Y: down[1] }, currPoint);
                     segments?.forEach(segment =>
                         this.forceStrokeGesture(
                             e,
@@ -763,7 +760,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     setTimeout(() => this._eraserLock--);
                 }
                 // Lower ink opacity to give the user a visual indicator of deletion.
-                intersect.inkView.layoutDoc.opacity = 0.0;
+                intersect.inkView.layoutDoc.opacity = 0.2;
                 intersect.inkView.layoutDoc.dontIntersect = true;
             }
         });
@@ -831,63 +828,86 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    radiusErase = (ink: DocumentView, eraseT: number, screenEraserPt: { X: number; Y: number }): Segment[] => {
+    radiusErase = (ink: DocumentView, startPt: { X: number; Y: number }, screenEraserPt: { X: number; Y: number }): Segment[] => {
         const segments: Segment[] = [];
+        const startInkCoords = ink.ComponentView?.ptFromScreen?.(startPt);
         const inkCoords = ink.ComponentView?.ptFromScreen?.(screenEraserPt); // coordinates in ink space
         if (!inkCoords) return [];
 
-        var segment1: Segment = [];
-        var segment2: Segment = [];
-        const eraseRadius = ActiveInkWidth() / 2;
+        var eraseSegment: Segment = []; // for eraser visualization
         const inkStroke = ink?.ComponentView as InkingStroke;
-        const { inkData } = inkStroke.inkScaledData();
-
-        const eraserInkData = inkStroke.splitByEraser(inkCoords, eraseRadius);
 
-        const tVals: number[] = []; // should be the tvals of the intersections
+        const eraserStroke: InkData = inkStroke.splitByEraser(startInkCoords, inkCoords).inkData;
+        const strokeToTVals: Map<InkingStroke, number[]> = new Map<InkingStroke, number[]>();
+        for (var i = 0; i < eraserStroke.length - 3; i +=4) {
+            eraseSegment.push(InkField.Segment(eraserStroke, i)); // for eraser visualization
+            this.getOtherInkIntersections(i, eraserStroke, strokeToTVals);
+        }
+        strokeToTVals.forEach((tVals, inkStroke) => {
+            var segment1: Segment = [];
+            var segment2: Segment = [];
+            const { inkData } = inkStroke.inkScaledData();
+            tVals.sort();
+            console.log('TVALS', inkStroke, tVals);
 
-        for (var i = 0; i < eraserInkData.length - 3; i += 4) {
-            const eraserBezier: Bezier = InkField.Segment(eraserInkData, i);
-            segment1.push(eraserBezier);
-            for (var j = 0; j < inkData.length; j += 4) {
-                const inkSegment: Bezier = InkField.Segment(inkData, i);
-                // this.bintersects(inkSegment, eraserBezier).forEach((val: string | number, i: number) => {
-                //     // Converting the Bezier.js Split type to a t-value number.
-                //     const t = +val.toString().split('/')[0];
-                //     if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
-                // });
+            var hasSplit = false;
+            var continueErasing = false;
+            
+            // below is curve splitting logic
+            if (tVals.length) {
+                for (var i = 0; i < inkData.length - 3; i += 4) {
+                    const inkSegment: Bezier = InkField.Segment(inkData, i);
+                    const currCurveT = Math.floor(i / 4);
+
+                    if (tVals.length === 2) {
+                        if (tVals[0] > currCurveT && tVals[0] < currCurveT + 1) {
+                            segment1.push(inkSegment.split(0, tVals[0] - currCurveT));
+                            continueErasing = true;
+                            if (tVals[1] > currCurveT && tVals[1] < currCurveT + 1) {
+                                segment2.push(inkSegment.split(tVals[1] - currCurveT, 1));
+                                continueErasing = false;
+                                hasSplit = true;
+                            }
+                        } else if (tVals[1] > currCurveT && tVals[1] < currCurveT + 1) {
+                            segment2.push(inkSegment.split(tVals[1] - currCurveT, 1));
+                            continueErasing = false;
+                            hasSplit = true;
+                        } else if (!continueErasing) {
+                            if (hasSplit) {
+                                segment2.push(inkSegment);
+                            } else {
+                                segment1.push(inkSegment);
+                            }
+                        }
+                    } else if (tVals.length === 1) {
+                        if (tVals[0] > currCurveT && tVals[0] < currCurveT + 1) {
+                            if (tVals[0] < Math.floor(inkData.length / 4) - tVals[0]) {
+                                // if it's on the first end
+                                segment1.push(inkSegment.split(tVals[0] - currCurveT, 1));
+                                hasSplit = true;
+                            } else {
+                                segment1.push(inkSegment.split(0, tVals[0] - currCurveT));
+                                hasSplit = true;
+                            }
+                        } else {
+                            if (tVals[0] < Math.floor(inkData.length / 4) - tVals[0] && hasSplit) {
+                                segment1.push(inkSegment);
+                            } else if (tVals[0] >= Math.floor(inkData.length / 4) - tVals[0] && !hasSplit) {
+                                segment1.push(inkSegment);
+                            }
+                        }
+                    }
+                }
             }
-        }
-        // segment1.push(eraserBezier);
-
-        // for (var i = 0; i < inkData.length - 3; i += 4) {
-        //     const currCurveT = Math.floor(i/4);
-        //     const inkSegment: Bezier = InkField.Segment(inkData, i);
-        //     if (tVals[0] >= currCurveT && tVals[0] < currCurveT+1) {
-        //         tVals.shift()
-        //         i -= 4;
-        //         if (eraseT - eraseWidth > currCurveT && eraseT + eraseWidth < currCurveT + 1) {
-        //             segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
-        //             segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
-        //         } else if (eraseT - eraseWidth < currCurveT) {
-        //             segment2.push(inkSegment.split(eraseT - currCurveT + eraseWidth, 1));
-        //         } else if (eraseT + eraseWidth > currCurveT + 1) {
-        //             segment1.push(inkSegment.split(0, eraseT - currCurveT - eraseWidth));
-        //         }
-        //     } else if (eraseT > currCurveT + 1) {
-        //         segment1.push(inkSegment);
-        //     } else {
-        //         segment2.push(inkSegment);
-        //     }
-        // }
+            if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
+                segments.push(segment1);
+            }
+            if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
+                segments.push(segment2);
+            }
+        });
 
-        // push 1 or both segments if they are not empty
-        if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
-            segments.push(segment1);
-        }
-        if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
-            segments.push(segment2);
-        }
+        segments.push(eraseSegment); // for eraser visualization
         return segments;
     };
 
@@ -1114,6 +1134,47 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return tVals;
     };
 
+    /**
+     * For radius erase, a function to find an eraser's intersections with all ink strokes on the freeform canvas.
+     * @param i the index of the eraser's segment
+     * @param points the eraser's ink data points
+     * @param strokeToTVals Map of InkingStroke to the tVals of its intersections with the eraser
+     * @returns 
+     */
+    getOtherInkIntersections = (i: number, points: InkData, strokeToTVals: Map<InkingStroke, number[]>): Map<InkingStroke, number[]> => {
+        // const tVals: [InkingStroke, number[]] = [];
+        // Iterating through all ink strokes in the current freeform collection.
+        this.childDocs
+            .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect)
+            .forEach(doc => {
+                // InkingStroke of other ink strokes
+                const otherInk = DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())?.ComponentView as InkingStroke;
+                // ink Data of other ink strokes
+                const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
+                for (var j = 0; j < otherInkData.length - 3; j += 4) {
+                    const curve: Bezier = InkField.Segment(points, i); // eraser curve
+                    const otherCurve: Bezier = InkField.Segment(otherInkData, j); // other curve
+                    this.bintersects(otherCurve, curve).forEach((val: string | number, k: number) => {
+                        // Converting the Bezier.js Split type to a t-value number.
+                        const t = +val.toString().split('/')[0];
+                        if (k % 2 === 0) {
+                            // here, add to the map
+                            const inkList = strokeToTVals.get(otherInk);
+                            if (inkList !== undefined) {
+                                const inList = inkList.some(val => Math.abs(val - (t + Math.floor(j / 4))) <= 0.01);
+                                if (!inList) {
+                                    inkList.push(t + Math.floor(j/4));
+                                }
+                            } else {
+                                strokeToTVals.set(otherInk, [t + Math.floor(j/4)]);
+                            }
+                        }
+                    });
+                }
+            });
+        return strokeToTVals;
+    };
+
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
         if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 2a5732708..279d3c24c 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -12,7 +12,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
-import { ActiveFillColor, ActiveInkColor, ActiveInkHideTextLabels, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkHideTextLabels, SetActiveInkWidth, SetActiveIsInkMask } from '../InkingStroke';
+import { ActiveFillColor, ActiveInkColor, ActiveInkHideTextLabels, ActiveInkWidth, ActiveIsInkMask, ActiveEraserWidth, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkHideTextLabels, SetActiveInkWidth, SetActiveIsInkMask, SetEraserWidth } from '../InkingStroke';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 // import { InkTranscription } from '../InkTranscription';
 import { DocData } from '../../../fields/DocSymbols';
@@ -338,11 +338,19 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
 
 ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
 
+// ScriptingGlobals.add(function setEraserProperty(option: 'eraseWidth', value: any, checkResult?: boolean) {
+//     setInk: (doc: Doc) => (doc[DocData].eraserWidth = NumCast(value))
+//     InkingStroke.setEraserRadius(NumCast(value));
+// })
+
 // toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
+ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: any, checkResult?: boolean) {
     const selected = SelectionManager.Docs.lastElement() ?? Doc.UserDoc();
+    if (option === 'eraserWidth') {
+        console.log('eraserWidth', value)
+    }
     // prettier-ignore
-    const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
+    const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
         ['inkMask', {
             checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())),
             setInk: (doc: Doc) => (doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask),
@@ -368,6 +376,11 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fil
             setInk: (doc: Doc) => (doc[DocData].color = String(value)),
             setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK &&  setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
         }],
+        [ 'eraserWidth', {
+            checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].eraser_width) : ActiveEraserWidth()),
+            setInk: (doc: Doc) => (doc[DocData].eraserWidth = NumCast(value)),
+            setMode: () => { SetEraserWidth(value.toString());},
+        }]
     ]);
 
     if (checkResult) {
-- 
cgit v1.2.3-70-g09d2


From 6ddfdb20e6a0b2c896594ad2c20292c6cfea0c1a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 24 Apr 2024 00:36:30 -0400
Subject: several fixes to radius ink eraser to fix eraser shap, intersections
 with straight lines

---
 src/Utils.ts                                       |   4 +
 src/client/views/InkingStroke.tsx                  | 129 ++++++++-------------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  91 +++++++++------
 3 files changed, 105 insertions(+), 119 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 291d7c799..226051f7d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -348,6 +348,10 @@ export namespace Utils {
         return project(near.point.x, near.point.y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
     }
 
+    export function ptDistance(p1: { x: number; y: number }, p2: { x: number; y: number }) {
+        return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
+    }
+
     export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
         const r = l + w,
             b = t + h;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 1ae96edfd..03acd5393 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -331,87 +331,54 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     };
 
-    coordWithSlope = (distance: number, slope: number) =>  {
-        return Math.sqrt((distance * distance) / (1 + slope * slope))
-    }
-    
-
-    splitByEraser = (startInkCoords: {X: number; Y: number}, inkCoords: {X: number; Y: number}) => {
-
+    splitByEraser = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => {
         const radius = ActiveEraserWidth() / 2 + 3; // reduce values to avoid extreme radii
-        console.log("radius", radius);
-        const ctrlPtDist = 0.552284749831 * radius; // (4/3) * tan(pi / 8) * radius
-
-        var perpSlope = (inkCoords.Y - startInkCoords.Y) / (inkCoords.X - startInkCoords.X);
-        if (perpSlope > 500) {
-            perpSlope = 500; // avoid ridiculously high/infinity slopes
-        } else if (perpSlope < -500) {
-            perpSlope = -500
-        } else if (perpSlope === 0) {
-            perpSlope = 0.002
-        }
-        const slope = - 1 / perpSlope
-        console.log("slope", slope);
-        var points: { X: number; Y: number }[]  = []
-        if (startInkCoords.X > inkCoords.X) {
-             points = [
-                { X: startInkCoords.X, Y: startInkCoords.Y },
-                { X: startInkCoords.X, Y: startInkCoords.Y },
-                // { X: startInkCoords.X + this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y + slope * this.coordWithSlope(ctrlPtDist, slope)},
-
-                { X: inkCoords.X + this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-
-                // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) + this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) },
-                // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) - this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-
-                { X: inkCoords.X - this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-
-                // { X: startInkCoords.X - this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y - slope * this.coordWithSlope(ctrlPtDist, slope) },
-                { X: startInkCoords.X, Y: startInkCoords.Y },
-                { X: startInkCoords.X, Y: startInkCoords.Y },
-            ];
-        } else {
-            points = [
-                { X: startInkCoords.X, Y: startInkCoords.Y },
-                { X: startInkCoords.X, Y: startInkCoords.Y },
-                // { X: startInkCoords.X + this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y + slope * this.coordWithSlope(ctrlPtDist, slope)},
-
-                { X: inkCoords.X + this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-
-                // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) + this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) },
-                { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) },
-                // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) - this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-
-                { X: inkCoords.X - this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) },
-                { X: inkCoords.X - this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) },
-
-                // { X: startInkCoords.X - this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y - slope * this.coordWithSlope(ctrlPtDist, slope) },
-                { X: startInkCoords.X, Y: startInkCoords.Y },
-                { X: startInkCoords.X, Y: startInkCoords.Y },
-            ];
-        }
-
-        const eraserCircle = new InkField(points);
-        return eraserCircle;
-    }
+        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);
+        const direction = { x: (movement.x / moveLen) * radius, y: (movement.y / moveLen) * radius };
+        const normal    = { x: -direction.y, y: direction.x }; // prettier-ignore
+
+        const startCoords = { X: startInkCoordsIn.X - direction.x, Y: startInkCoordsIn.Y - direction.y };
+        const endCoords = { X: endInkCoordsIn.X + direction.x, Y: endInkCoordsIn.Y + direction.y };
+        return new InkField([
+            // left bot arc
+            { X: startCoords.X,                                            Y: startCoords.Y }, // prettier-ignore
+            { X: startCoords.X + normal.x * c,                             Y: startCoords.Y + normal.y * c }, // prettier-ignore
+            { X: startCoords.X + direction.x + normal.x - direction.x * c, Y: startCoords.Y + direction.y + normal.y - direction.y * c },
+            { X: startCoords.X + direction.x + normal.x,                   Y: startCoords.Y + direction.y + normal.y }, // prettier-ignore
+
+            // bot
+            { X: startCoords.X + direction.x + normal.x,                   Y: startCoords.Y + direction.y + normal.y }, // prettier-ignore
+            { X: startCoords.X + direction.x + normal.x + direction.x * c, Y: startCoords.Y + direction.y + normal.y + direction.y * c },
+            { X: endCoords.X - direction.x + normal.x - direction.x * c,   Y: endCoords.Y - direction.y + normal.y - direction.y * c }, // prettier-ignore
+            { X: endCoords.X - direction.x + normal.x,                     Y: endCoords.Y - direction.y + normal.y }, // prettier-ignore
+
+            // right bot arc
+            { X: endCoords.X - direction.x + normal.x,                    Y: endCoords.Y - direction.y + normal.y }, // prettier-ignore
+            { X: endCoords.X - direction.x + normal.x + direction.x * c,  Y: endCoords.Y - direction.y + normal.y + direction.y * c}, // prettier-ignore
+            { X: endCoords.X + normal.x * c,                              Y: endCoords.Y + normal.y * c }, // prettier-ignore
+            { X: endCoords.X,                                             Y: endCoords.Y  }, // prettier-ignore
+
+            // right top arc
+            { X: endCoords.X,                                             Y: endCoords.Y  }, // prettier-ignore
+            { X: endCoords.X - normal.x * c,                              Y: endCoords.Y - normal.y * c }, // prettier-ignore
+            { X: endCoords.X - direction.x - normal.x + direction.x * c,  Y: endCoords.Y - direction.y - normal.y + direction.y * c}, // prettier-ignore
+            { X: endCoords.X - direction.x - normal.x,                    Y: endCoords.Y - direction.y - normal.y }, // prettier-ignore
+
+            // top
+            { X: endCoords.X - direction.x - normal.x,                     Y: endCoords.Y - direction.y - normal.y }, // prettier-ignore
+            { X: endCoords.X - direction.x - normal.x - direction.x * c,   Y: endCoords.Y - direction.y - normal.y - direction.y * c}, // prettier-ignore
+            { X: startCoords.X + direction.x - normal.x + direction.x * c, Y: startCoords.Y + direction.y - normal.y + direction.y * c },
+            { X: startCoords.X + direction.x - normal.x,                   Y: startCoords.Y + direction.y - normal.y }, // prettier-ignore
+
+            // left top arc
+            { X: startCoords.X + direction.x - normal.x,                   Y: startCoords.Y + direction.y - normal.y }, // prettier-ignore
+            { X: startCoords.X + direction.x - normal.x - direction.x * c, Y: startCoords.Y + direction.y - normal.y - direction.y * c }, // prettier-ignore
+            { X: startCoords.X - normal.x * c,                             Y: startCoords.Y - normal.y * c }, // prettier-ignore
+            { X: startCoords.X,                                            Y: startCoords.Y }, // prettier-ignore
+        ]);
+    };
 
     _subContentView: ViewBoxInterface | undefined;
     setSubContentView = (doc: ViewBoxInterface) => (this._subContentView = doc);
@@ -627,4 +594,4 @@ export function ActiveInkBezierApprox(): string {
 }
 export function ActiveEraserWidth(): number {
     return Number(ActiveInkPen()?.eraserWidth);
-}
\ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 78dae87c3..9df9c5492 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -620,6 +620,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     case InkTool.SegmentEraser:
                     case InkTool.RadiusEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
+                        this._eraserPts.length = 0;
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
                     case InkTool.None:
@@ -728,6 +729,7 @@ 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,
@@ -737,6 +739,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
         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.
         this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
             if (!this._deleteList.includes(intersect.inkView)) {
@@ -749,7 +753,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     const segments =
                         Doc.ActiveTool === InkTool.SegmentEraser
                             ? 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
-                            : this.radiusErase(intersect.inkView, { X: down[0], Y: down[1] }, currPoint);
+                            : this.radiusErase(intersect.inkView, { X: this._eraserPts[0][0], Y: this._eraserPts[0][1] }, currPoint);
                     segments?.forEach(segment =>
                         this.forceStrokeGesture(
                             e,
@@ -832,16 +836,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const segments: Segment[] = [];
         const startInkCoords = ink.ComponentView?.ptFromScreen?.(startPt);
         const inkCoords = ink.ComponentView?.ptFromScreen?.(screenEraserPt); // coordinates in ink space
-        if (!inkCoords) return [];
+        if (!inkCoords || !startInkCoords) return [];
 
         var eraseSegment: Segment = []; // for eraser visualization
         const inkStroke = ink?.ComponentView as InkingStroke;
 
         const eraserStroke: InkData = inkStroke.splitByEraser(startInkCoords, inkCoords).inkData;
         const strokeToTVals: Map<InkingStroke, number[]> = new Map<InkingStroke, number[]>();
-        for (var i = 0; i < eraserStroke.length - 3; i +=4) {
+        for (var i = 0; i < eraserStroke.length - 3; i += 4) {
             eraseSegment.push(InkField.Segment(eraserStroke, i)); // for eraser visualization
-            this.getOtherInkIntersections(i, eraserStroke, strokeToTVals);
+            this.getOtherInkIntersections(ink, i, eraserStroke, strokeToTVals);
         }
         strokeToTVals.forEach((tVals, inkStroke) => {
             var segment1: Segment = [];
@@ -852,7 +856,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
             var hasSplit = false;
             var continueErasing = false;
-            
+
             // below is curve splitting logic
             if (tVals.length) {
                 for (var i = 0; i < inkData.length - 3; i += 4) {
@@ -881,12 +885,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         }
                     } else if (tVals.length === 1) {
                         if (tVals[0] > currCurveT && tVals[0] < currCurveT + 1) {
-                            if (tVals[0] < Math.floor(inkData.length / 4) - tVals[0]) {
+                            // this heuristic for determine which segment to keep is not quite right even though it will work most of the time.
+                            // We should really store the eraser intersection normal in getOtherInkIntersections,
+                            // and then test its dot product with the tangent of the stroke at the intersection point.
+                            // if the dot product is positive, then erase the first part of the stroke, otherwise the second.
+                            const leftDist = Utils.ptDistance({ x: inkCoords.X, y: inkCoords.Y }, inkSegment.points.lastElement());
+                            const rightDist = Utils.ptDistance({ x: inkCoords.X, y: inkCoords.Y }, inkSegment.points[0]);
+                            const splits = inkSegment.split(tVals[0] - currCurveT);
+                            if (leftDist < rightDist) {
                                 // if it's on the first end
-                                segment1.push(inkSegment.split(tVals[0] - currCurveT, 1));
+                                segment1.push(splits.left);
                                 hasSplit = true;
                             } else {
-                                segment1.push(inkSegment.split(0, tVals[0] - currCurveT));
+                                segment1.push(splits.right);
                                 hasSplit = true;
                             }
                         } else {
@@ -1084,6 +1095,15 @@ 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) => {
+        if ((curve as any)._linear) {
+            // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line
+            const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] });
+            if (intersections.length) {
+                const intPt = otherCurve.get(intersections[0]);
+                const intT = curve.project(intPt).t;
+                return intT ? [intT] : [];
+            }
+        }
         if ((otherCurve as any)._linear) {
             return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] });
         }
@@ -1135,43 +1155,38 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     /**
-     * For radius erase, a function to find an eraser's intersections with all ink strokes on the freeform canvas.
+     * For radius erase, a function to find an eraser's intersections with a single ink stroke
+     * @param otherInkDocVIew the ink document that is being erased
      * @param i the index of the eraser's segment
      * @param points the eraser's ink data points
      * @param strokeToTVals Map of InkingStroke to the tVals of its intersections with the eraser
-     * @returns 
+     * @returns
      */
-    getOtherInkIntersections = (i: number, points: InkData, strokeToTVals: Map<InkingStroke, number[]>): Map<InkingStroke, number[]> => {
-        // const tVals: [InkingStroke, number[]] = [];
-        // Iterating through all ink strokes in the current freeform collection.
-        this.childDocs
-            .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect)
-            .forEach(doc => {
-                // InkingStroke of other ink strokes
-                const otherInk = DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())?.ComponentView as InkingStroke;
-                // ink Data of other ink strokes
-                const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
-                for (var j = 0; j < otherInkData.length - 3; j += 4) {
-                    const curve: Bezier = InkField.Segment(points, i); // eraser curve
-                    const otherCurve: Bezier = InkField.Segment(otherInkData, j); // other curve
-                    this.bintersects(otherCurve, curve).forEach((val: string | number, k: number) => {
-                        // Converting the Bezier.js Split type to a t-value number.
-                        const t = +val.toString().split('/')[0];
-                        if (k % 2 === 0) {
-                            // here, add to the map
-                            const inkList = strokeToTVals.get(otherInk);
-                            if (inkList !== undefined) {
-                                const inList = inkList.some(val => Math.abs(val - (t + Math.floor(j / 4))) <= 0.01);
-                                if (!inList) {
-                                    inkList.push(t + Math.floor(j/4));
-                                }
-                            } else {
-                                strokeToTVals.set(otherInk, [t + Math.floor(j/4)]);
-                            }
+    getOtherInkIntersections = (otherInkDocView: DocumentView, i: number, points: InkData, strokeToTVals: Map<InkingStroke, number[]>): Map<InkingStroke, number[]> => {
+        // InkingStroke of other ink strokes
+        const otherInk = otherInkDocView.ComponentView as InkingStroke;
+        // ink Data of other ink strokes
+        const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
+        for (var j = 0; j < otherInkData.length - 3; j += 4) {
+            const curve: Bezier = InkField.Segment(points, i); // eraser curve
+            const otherCurve: Bezier = InkField.Segment(otherInkData, j); // other curve
+            this.bintersects(otherCurve, curve).forEach((val: string | number, k: number) => {
+                // Converting the Bezier.js Split type to a t-value number.
+                const t = +val.toString().split('/')[0];
+                if (k % 2 === 0) {
+                    // here, add to the map
+                    const inkList = strokeToTVals.get(otherInk);
+                    if (inkList !== undefined) {
+                        const inList = inkList.some(val => Math.abs(val - (t + Math.floor(j / 4))) <= 0.01);
+                        if (!inList) {
+                            inkList.push(t + Math.floor(j / 4));
                         }
-                    });
+                    } else {
+                        strokeToTVals.set(otherInk, [t + Math.floor(j / 4)]);
+                    }
                 }
             });
+        }
         return strokeToTVals;
     };
 
-- 
cgit v1.2.3-70-g09d2


From 82f8c4bac5c9d8a6aef4b42936bd1839fcb8939e Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sat, 27 Apr 2024 17:53:25 -0400
Subject: pull

---
 src/client/util/CurrentUserUtils.ts                |  5 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 95 +++++++++++++++++-----
 src/client/views/global/globalScripts.ts           |  5 +-
 3 files changed, 80 insertions(+), 25 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 59fc30635..d5b571141 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -752,21 +752,20 @@ pie title Minerals in my tap water
         return [
             { title: "Pen",    toolTip: "Pen (Ctrl+P)",                     btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                            scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",             btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
-            // { title: "Eraser", toolTip: "Eraser (Ctrl+E)", width: 50,       btnType: ButtonType.DropdownList, icon: "eraser",     toolType: "eraser", ignoreClick: true,      scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" },
-            //     btnList: new List<string>(["Stroke Erase", "Segment Erase", "Radius Erase"]) },
             { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.MultiToggleButton,                toolType:"eraser",                          scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'},
               subMenu: [
                     { title: "Stroke",   toolTip: "Stroke Erase",           btnType: ButtonType.ToggleButton, icon: "eraser",     toolType:"strokeeraser", ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
                     { title: "Segment",  toolTip: "Segment Erase",          btnType: ButtonType.ToggleButton, icon: "xmarks-lines",      toolType:"segmenteraser",ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
                     { title: "Radius",   toolTip: "Radius Erase",           btnType: ButtonType.ToggleButton, icon: "circle-xmark",     toolType:"radiuseraser", ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
             ]},
+            // { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.ToggleButton, icon: "eraser",     toolType:"eraser",                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+            { title: "Eraser Width",  toolTip: "Eraser Width",              btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
             { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType:GestureUtils.Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType:GestureUtils.Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType:GestureUtils.Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                             btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
             { title: "Labels", toolTip: "Lab els",                          btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
             { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberSliderButton,               toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
-            { title: "Eraser Width",  toolTip: "Eraser Width",              btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
             { title: "Ink",    toolTip: "Stroke color",                     btnType: ButtonType.ColorButton,  icon: "pen",        toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
         ];
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9df9c5492..eedee0b18 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -787,6 +787,55 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return false;
     };
 
+    createEraserOutline = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => {
+        const radius = ActiveEraserWidth() / 2 + 3; // reduce values to avoid extreme radii
+        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);
+        const direction = { x: (movement.x / moveLen) * radius, y: (movement.y / moveLen) * radius };
+        const normal    = { x: -direction.y, y: direction.x }; // prettier-ignore
+
+        const startCoords = { X: startInkCoordsIn.X - direction.x, Y: startInkCoordsIn.Y - direction.y };
+        const endCoords = { X: endInkCoordsIn.X + direction.x, Y: endInkCoordsIn.Y + direction.y };
+        return new InkField([
+            // left bot arc
+            { X: startCoords.X,                                            Y: startCoords.Y }, // prettier-ignore
+            { X: startCoords.X + normal.x * c,                             Y: startCoords.Y + normal.y * c }, // prettier-ignore
+            { X: startCoords.X + direction.x + normal.x - direction.x * c, Y: startCoords.Y + direction.y + normal.y - direction.y * c },
+            { X: startCoords.X + direction.x + normal.x,                   Y: startCoords.Y + direction.y + normal.y }, // prettier-ignore
+
+            // bot
+            { X: startCoords.X + direction.x + normal.x,                   Y: startCoords.Y + direction.y + normal.y }, // prettier-ignore
+            { X: startCoords.X + direction.x + normal.x + direction.x * c, Y: startCoords.Y + direction.y + normal.y + direction.y * c },
+            { X: endCoords.X - direction.x + normal.x - direction.x * c,   Y: endCoords.Y - direction.y + normal.y - direction.y * c }, // prettier-ignore
+            { X: endCoords.X - direction.x + normal.x,                     Y: endCoords.Y - direction.y + normal.y }, // prettier-ignore
+
+            // right bot arc
+            { X: endCoords.X - direction.x + normal.x,                    Y: endCoords.Y - direction.y + normal.y }, // prettier-ignore
+            { X: endCoords.X - direction.x + normal.x + direction.x * c,  Y: endCoords.Y - direction.y + normal.y + direction.y * c}, // prettier-ignore
+            { X: endCoords.X + normal.x * c,                              Y: endCoords.Y + normal.y * c }, // prettier-ignore
+            { X: endCoords.X,                                             Y: endCoords.Y  }, // prettier-ignore
+
+            // right top arc
+            { X: endCoords.X,                                             Y: endCoords.Y  }, // prettier-ignore
+            { X: endCoords.X - normal.x * c,                              Y: endCoords.Y - normal.y * c }, // prettier-ignore
+            { X: endCoords.X - direction.x - normal.x + direction.x * c,  Y: endCoords.Y - direction.y - normal.y + direction.y * c}, // prettier-ignore
+            { X: endCoords.X - direction.x - normal.x,                    Y: endCoords.Y - direction.y - normal.y }, // prettier-ignore
+
+            // top
+            { X: endCoords.X - direction.x - normal.x,                     Y: endCoords.Y - direction.y - normal.y }, // prettier-ignore
+            { X: endCoords.X - direction.x - normal.x - direction.x * c,   Y: endCoords.Y - direction.y - normal.y - direction.y * c}, // prettier-ignore
+            { X: startCoords.X + direction.x - normal.x + direction.x * c, Y: startCoords.Y + direction.y - normal.y + direction.y * c },
+            { X: startCoords.X + direction.x - normal.x,                   Y: startCoords.Y + direction.y - normal.y }, // prettier-ignore
+
+            // left top arc
+            { X: startCoords.X + direction.x - normal.x,                   Y: startCoords.Y + direction.y - normal.y }, // prettier-ignore
+            { X: startCoords.X + direction.x - normal.x - direction.x * c, Y: startCoords.Y + direction.y - normal.y - direction.y * c }, // prettier-ignore
+            { X: startCoords.X - normal.x * c,                             Y: startCoords.Y - normal.y * c }, // prettier-ignore
+            { X: startCoords.X,                                            Y: startCoords.Y }, // prettier-ignore
+        ]);
+    };
+
     /**
      * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
      * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected
@@ -1163,30 +1212,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * @returns
      */
     getOtherInkIntersections = (otherInkDocView: DocumentView, i: number, points: InkData, strokeToTVals: Map<InkingStroke, number[]>): Map<InkingStroke, number[]> => {
-        // InkingStroke of other ink strokes
-        const otherInk = otherInkDocView.ComponentView as InkingStroke;
-        // ink Data of other ink strokes
-        const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
-        for (var j = 0; j < otherInkData.length - 3; j += 4) {
-            const curve: Bezier = InkField.Segment(points, i); // eraser curve
-            const otherCurve: Bezier = InkField.Segment(otherInkData, j); // other curve
-            this.bintersects(otherCurve, curve).forEach((val: string | number, k: number) => {
-                // Converting the Bezier.js Split type to a t-value number.
-                const t = +val.toString().split('/')[0];
-                if (k % 2 === 0) {
-                    // here, add to the map
-                    const inkList = strokeToTVals.get(otherInk);
-                    if (inkList !== undefined) {
-                        const inList = inkList.some(val => Math.abs(val - (t + Math.floor(j / 4))) <= 0.01);
-                        if (!inList) {
-                            inkList.push(t + Math.floor(j / 4));
+        this.childDocs
+            .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect)
+            .forEach(doc => {
+                // InkingStroke of other ink strokes
+                const otherInk = otherInkDocView.ComponentView as InkingStroke;
+                // ink Data of other ink strokes
+                const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
+                for (var j = 0; j < otherInkData.length - 3; j += 4) {
+                    const curve: Bezier = InkField.Segment(points, i); // eraser curve
+                    const otherCurve: Bezier = InkField.Segment(otherInkData, j); // other curve
+                    this.bintersects(otherCurve, curve).forEach((val: string | number, k: number) => {
+                        // Converting the Bezier.js Split type to a t-value number.
+                        const t = +val.toString().split('/')[0];
+                        if (k % 2 === 0) {
+                            // here, add to the map
+                            const inkList = strokeToTVals.get(otherInk);
+                            if (inkList !== undefined) {
+                                const inList = inkList.some(val => Math.abs(val - (t + Math.floor(j / 4))) <= 0.01);
+                                if (!inList) {
+                                    inkList.push(t + Math.floor(j / 4));
+                                }
+                            } else {
+                                strokeToTVals.set(otherInk, [t + Math.floor(j / 4)]);
+                            }
                         }
-                    } else {
-                        strokeToTVals.set(otherInk, [t + Math.floor(j / 4)]);
-                    }
+                    });
                 }
             });
-        }
         return strokeToTVals;
     };
 
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 279d3c24c..86bd684be 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -324,7 +324,10 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
             }
         } else if (tool) {
             // pen or eraser
-            if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
+            if (Doc.ActiveTool === tool && tool === InkTool.Eraser) {
+                Doc.ActiveTool = InkTool.SegmentEraser;
+                console.log("erase click twice")
+            } else if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
             } else {
                 Doc.ActiveTool = tool as any;
-- 
cgit v1.2.3-70-g09d2


From 4a01680bd22a0652dbdd0da5c3a7167ca8117440 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 8 May 2024 22:22:29 -0400
Subject: close curve and bounding box fixes

---
 src/client/util/CurrentUserUtils.ts                |  10 +-
 src/client/views/MainView.tsx                      |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 347 ++++++++++++---------
 src/client/views/global/globalScripts.ts           |  10 +-
 4 files changed, 214 insertions(+), 154 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b1673ff1c..3ee1b42aa 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -752,13 +752,13 @@ pie title Minerals in my tap water
         return [
             { title: "Pen",    toolTip: "Pen (Ctrl+P)",                     btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                            scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",             btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
-            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.MultiToggleButton,                toolType:"eraser",                          scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'},
+            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.MultiToggleButton,                                                            scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {toolType:"activeEraserTool()"},
               subMenu: [
                     { title: "Stroke",   toolTip: "Stroke Erase",           btnType: ButtonType.ToggleButton, icon: "eraser",      toolType:InkTool.StrokeEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-                    { title: "Segment",  toolTip: "Segment Erase",          btnType: ButtonType.ToggleButton, icon: "xmarks-lines",toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+                    { title: "Segment",  toolTip: "Segment Erase",          btnType: ButtonType.ToggleButton, icon: "xmark",toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
                     { title: "Radius",   toolTip: "Radius Erase",           btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkTool.RadiusEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-            ]},  
-            { title: "Eraser Width",  toolTip: "Eraser Width",              btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"isRadiusEraser()" }},
+            ]},
+            { title: "Eraser Width",  toolTip: "Eraser Width",              btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"NotRadiusEraser()"}},
             { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType:GestureUtils.Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType:GestureUtils.Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType:GestureUtils.Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
@@ -1119,7 +1119,7 @@ pie title Minerals in my tap water
 ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs");
 ScriptingGlobals.add(function IsExploreMode() { return SnappingManager.ExploreMode; }, "is Dash in exploration mode");
 ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode");
-ScriptingGlobals.add(function isRadiusEraser() { return !(Doc.ActiveTool === InkTool.RadiusEraser); }, "is the eraser selected");
+ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.RadiusEraser; }, "is the active tool anything but the radius eraser");
 ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering");
 ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
 ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; });
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 56db0c488..d0b3221b4 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -361,6 +361,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faDeleteLeft,
                 fa.faXmarksLines,
                 fa.faCircleXmark,
+                fa.faXmark,
                 fa.faExclamation,
                 fa.faFileAlt,
                 fa.faFileAudio,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index eedee0b18..2e174be30 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,5 +1,7 @@
+import { inside } from '@turf/turf';
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
+import { validationResult } from 'express-validator';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
@@ -35,7 +37,7 @@ import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
 import { GestureOverlay } from '../../GestureOverlay';
 import { CtrlKey } from '../../GlobalKeyHandler';
-import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
+import { ActiveEraserWidth, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
@@ -618,11 +620,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
                     case InkTool.StrokeEraser:
                     case InkTool.SegmentEraser:
-                    case InkTool.RadiusEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         this._eraserPts.length = 0;
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
+                    case InkTool.RadiusEraser:
+                        this._batch = UndoManager.StartBatch('collectionErase');
+                        this._eraserPts.length = 0;
+                        setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction);
+                        break;
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
                             this._hitCluster = this.pickCluster(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
@@ -734,7 +740,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * 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.
-     *  However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety.
      */
     @action
     onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
@@ -750,10 +755,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // 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++;
-                    const segments =
-                        Doc.ActiveTool === InkTool.SegmentEraser
-                            ? 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
-                            : this.radiusErase(intersect.inkView, { X: this._eraserPts[0][0], Y: this._eraserPts[0][1] }, currPoint);
+                    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
                     segments?.forEach(segment =>
                         this.forceStrokeGesture(
                             e,
@@ -764,12 +766,44 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     setTimeout(() => this._eraserLock--);
                 }
                 // Lower ink opacity to give the user a visual indicator of deletion.
-                intersect.inkView.layoutDoc.opacity = 0.2;
+                intersect.inkView.layoutDoc.opacity = 0;
                 intersect.inkView.layoutDoc.dontIntersect = true;
             }
         });
         return false;
     };
+
+    @action
+    onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
+        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);
+        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());
+                segments?.forEach(segment =>
+                    this.forceStrokeGesture(
+                        e,
+                        GestureUtils.Gestures.Stroke,
+                        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;
+        });
+        return false;
+    };
+
     forceStrokeGesture = (e: PointerEvent, gesture: GestureUtils.Gestures, points: InkData, text?: any) => {
         this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text));
     };
@@ -788,7 +822,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     createEraserOutline = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => {
-        const radius = ActiveEraserWidth() / 2 + 3; // reduce values to avoid extreme radii
+        const radius = ActiveEraserWidth() + 3; // add 3 to avoid eraser being too thin
         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);
@@ -836,6 +870,39 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         ]);
     };
 
+    /**
+     * Ray-tracing algorithm to determine whether a point is inside the eraser outline
+     * @param eraserOutline 
+     * @param point 
+     * @returns 
+     */
+    insideEraserOutline = (eraserOutline: InkData, point: { X: number; Y: number }) => {
+        var isInside = false;
+        if (!isNaN(eraserOutline[0].X) && !isNaN(eraserOutline[0].Y)) {
+            var minX = eraserOutline[0].X, maxX = eraserOutline[0].X;
+            var minY = eraserOutline[0].Y, maxY = eraserOutline[0].Y;
+            for (var i = 1; i < eraserOutline.length; i++) {
+                const currPoint: {X: number, Y: number} = eraserOutline[i];
+                minX = Math.min(currPoint.X, minX);
+                maxX = Math.max(currPoint.X, maxX);
+                minY = Math.min(currPoint.Y, minY);
+                maxY = Math.max(currPoint.Y, maxY);
+            }
+
+            if (point.X < minX || point.X > maxX || point.Y < minY || point.Y > maxY) {
+                return false;
+            }
+
+            for (var i = 0, j = eraserOutline.length - 1; i < eraserOutline.length; j = i, i++) {
+                if ((eraserOutline[i].Y > point.Y) != (eraserOutline[j].Y > point.Y) &&
+                    point.X < (eraserOutline[j].X - eraserOutline[i].X) * (point.Y - eraserOutline[i].Y) / (eraserOutline[j].Y - eraserOutline[i].Y) + eraserOutline[i].X ) {
+                    isInside = !isInside;
+                }
+            }
+        }
+        return isInside;
+    };
+
     /**
      * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
      * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected
@@ -880,94 +947,141 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             );
     };
 
+    getRadiusEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => {
+        const eraserRadius = ActiveEraserWidth() + 3;
+        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[]>();
+        const intersectingStrokes = this.childDocs
+            .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.()))
+            .filter(inkView => inkView?.ComponentView instanceof InkingStroke) // filter to all inking strokes
+            .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
+            );
+        console.log("itersectnig strokes", intersectingStrokes);
+        intersectingStrokes.forEach(({ inkStroke, inkView }) => {
+            const { inkData } = inkStroke.inkScaledData();
+            const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
+            const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
+            const eraserInkData = this.createEraserOutline(prevPointInkSpace, currPointInkSpace).inkData;
+            // add the ends of the stroke in as "intersections"
+            if (this.insideEraserOutline(eraserInkData, inkData[0])) {
+                strokeToTVals.set(inkView, [0]);
+            }
+            if (this.insideEraserOutline(eraserInkData, inkData[inkData.length - 1])) {
+                const inkList = strokeToTVals.get(inkView);
+                if (inkList !== undefined) {
+                    inkList.push(Math.floor(inkData.length / 4) + 1);
+                } else {
+                    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) {
+                    const intersectCurve: Bezier = InkField.Segment(inkData, i); // other curve
+                    const eraserCurve: Bezier = InkField.Segment(eraserInkData, j); // eraser curve
+                    this.bintersects(intersectCurve, eraserCurve).forEach((val: string | number, k: number) => {
+                        // Converting the Bezier.js Split type to a t-value number.
+                        const t = +val.toString().split('/')[0];
+                        if (k % 2 === 0) {
+                            // 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 inList = inkList.some(val => Math.abs(val - (t + Math.floor(i / 4))) <= tValOffset);
+                                if (!inList) {
+                                    inkList.push(t + Math.floor(i / 4));
+                                }
+                            } else {
+                                strokeToTVals.set(inkView, [t + Math.floor(i / 4)]);
+                            }
+                        }
+                    });
+                }
+            }
+        });
+        console.log("strokeToTVals", strokeToTVals);
+        return strokeToTVals;
+    };
+
+    /**
+     * 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.
+     * @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, startPt: { X: number; Y: number }, screenEraserPt: { X: number; Y: number }): Segment[] => {
+    radiusErase = (ink: DocumentView, tVals: number[]): Segment[] => {
         const segments: Segment[] = [];
-        const startInkCoords = ink.ComponentView?.ptFromScreen?.(startPt);
-        const inkCoords = ink.ComponentView?.ptFromScreen?.(screenEraserPt); // coordinates in ink space
-        if (!inkCoords || !startInkCoords) return [];
-
-        var eraseSegment: Segment = []; // for eraser visualization
         const inkStroke = ink?.ComponentView as InkingStroke;
-
-        const eraserStroke: InkData = inkStroke.splitByEraser(startInkCoords, inkCoords).inkData;
-        const strokeToTVals: Map<InkingStroke, number[]> = new Map<InkingStroke, number[]>();
-        for (var i = 0; i < eraserStroke.length - 3; i += 4) {
-            eraseSegment.push(InkField.Segment(eraserStroke, i)); // for eraser visualization
-            this.getOtherInkIntersections(ink, i, eraserStroke, strokeToTVals);
+        const { inkData } = inkStroke.inkScaledData();
+        var currSegment: Segment = [];
+        if (tVals.length % 2 !== 0) { // should always have even tVals
+            for (var i = 0; i < inkData.length - 3; i +=4) {
+                currSegment.push(InkField.Segment(inkData, i));
+            }
+            if (currSegment.length > 0) {
+                segments.push(currSegment);
+                return segments;
+            }
         }
-        strokeToTVals.forEach((tVals, inkStroke) => {
-            var segment1: Segment = [];
-            var segment2: Segment = [];
-            const { inkData } = inkStroke.inkScaledData();
-            tVals.sort();
-            console.log('TVALS', inkStroke, tVals);
 
-            var hasSplit = false;
-            var continueErasing = false;
-
-            // below is curve splitting logic
-            if (tVals.length) {
-                for (var i = 0; i < inkData.length - 3; i += 4) {
-                    const inkSegment: Bezier = InkField.Segment(inkData, i);
-                    const currCurveT = Math.floor(i / 4);
+        var continueErasing = false;
+        var firstSegment: Segment = [];
+        // early return if nothing to split on
+        if (tVals.length === 0 || (tVals.length === 1 && tVals[0] === 0)) {
+            return segments;
+        }
 
-                    if (tVals.length === 2) {
-                        if (tVals[0] > currCurveT && tVals[0] < currCurveT + 1) {
-                            segment1.push(inkSegment.split(0, tVals[0] - currCurveT));
+        for (var i = 0; i < inkData.length - 3; i += 4) {
+            const currCurveT = Math.floor(i/4);
+            const inkBezier: Bezier = InkField.Segment(inkData, i);
+            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 ) {
+                        continueErasing = true;
+                    } else if (segmentTs[j] === Math.floor(inkData.length / 4) + 1) {
+                        break;
+                    }else {
+                        if (!continueErasing) {
+                            currSegment.push(inkBezier.split(0, segmentTs[j] - currCurveT));
                             continueErasing = true;
-                            if (tVals[1] > currCurveT && tVals[1] < currCurveT + 1) {
-                                segment2.push(inkSegment.split(tVals[1] - currCurveT, 1));
-                                continueErasing = false;
-                                hasSplit = true;
-                            }
-                        } else if (tVals[1] > currCurveT && tVals[1] < currCurveT + 1) {
-                            segment2.push(inkSegment.split(tVals[1] - currCurveT, 1));
-                            continueErasing = false;
-                            hasSplit = true;
-                        } else if (!continueErasing) {
-                            if (hasSplit) {
-                                segment2.push(inkSegment);
-                            } else {
-                                segment1.push(inkSegment);
-                            }
-                        }
-                    } else if (tVals.length === 1) {
-                        if (tVals[0] > currCurveT && tVals[0] < currCurveT + 1) {
-                            // this heuristic for determine which segment to keep is not quite right even though it will work most of the time.
-                            // We should really store the eraser intersection normal in getOtherInkIntersections,
-                            // and then test its dot product with the tangent of the stroke at the intersection point.
-                            // if the dot product is positive, then erase the first part of the stroke, otherwise the second.
-                            const leftDist = Utils.ptDistance({ x: inkCoords.X, y: inkCoords.Y }, inkSegment.points.lastElement());
-                            const rightDist = Utils.ptDistance({ x: inkCoords.X, y: inkCoords.Y }, inkSegment.points[0]);
-                            const splits = inkSegment.split(tVals[0] - currCurveT);
-                            if (leftDist < rightDist) {
-                                // if it's on the first end
-                                segment1.push(splits.left);
-                                hasSplit = true;
-                            } else {
-                                segment1.push(splits.right);
-                                hasSplit = true;
-                            }
                         } else {
-                            if (tVals[0] < Math.floor(inkData.length / 4) - tVals[0] && hasSplit) {
-                                segment1.push(inkSegment);
-                            } else if (tVals[0] >= Math.floor(inkData.length / 4) - tVals[0] && !hasSplit) {
-                                segment1.push(inkSegment);
+                            continueErasing = false;
+                            if (currSegment.length > 0) {
+                                segments.push(currSegment);
+                                if (firstSegment.length === 0) {
+                                    firstSegment = currSegment;
+                                }
+                                currSegment = [];
                             }
+                            currSegment.push(inkBezier.split(segmentTs[j] - currCurveT, 1));
                         }
                     }
                 }
+            } else {
+                if (!continueErasing) { // push the bezier piece if not in the eraser circle
+                    currSegment.push(inkBezier);
+                }
             }
-            if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
-                segments.push(segment1);
-            }
-            if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) {
-                segments.push(segment2);
+        }
+        if (currSegment.length > 0) {
+            if (InkingStroke.IsClosed(inkData)) {
+                currSegment = currSegment.concat(firstSegment);
             }
-        });
-
-        segments.push(eraseSegment); // for eraser visualization
+            segments.push(currSegment);
+        }
         return segments;
     };
 
@@ -993,7 +1107,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             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
-            currIntersects = currIntersects.map(tVal => tVal + Math.floor(i / 4));
+            currIntersects = currIntersects.filter(tVal => tVal > 0 && tVal < 1).map(tVal => tVal + Math.floor(i / 4));
             if (currIntersects.length) {
                 intersections = [...intersections, ...currIntersects];
                 for (var j = 0; j < currIntersects.length; j++) {
@@ -1002,19 +1116,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
 
-        // for closed curve bookkeeping
         var isClosedCurve = false;
-        if (intersections[0] === 0) {
+        if (InkingStroke.IsClosed(inkData)) {
             isClosedCurve = true;
-
-            // now reduce intersections list to the actual intersections because closed curves have additional ones with itself
-            const indices = intersections.map((value, index) => (value > 0.0001 && value < Math.floor(inkData.length / 4) ? index : -1)).filter(index => index !== -1);
-
-            // Filter intersections and segmentIndexes based on validIndices
-            intersections = indices.map(index => intersections[index]);
-            segmentIndexes = indices.map(index => segmentIndexes[index]);
-            // intersections = intersections.slice(1, intersections.length ); // take the 0 intersection out
-            // segmentIndexes = segmentIndexes.slice(1, segmentIndexes.length); // same for indexes
         }
 
         if (intersections.length) {
@@ -1068,7 +1172,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         }
                     }
                 } else if (splitSegment1 === -1) {
-                    // case where left end is erased
+                    // case where first end is erased
                     if (currCurveT === splitSegment2) {
                         segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
                         hasSplit = true;
@@ -1081,15 +1185,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         }
                     }
                 } else {
-                    // case where right end is erased
+                    // case where last end is erased
                     if (currCurveT === segmentIndexes[0] && isClosedCurve) {
                         segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
                         hasSplit = true;
                     } else if (currCurveT === splitSegment1) {
                         segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
-                        hasSplit = false; // although this is not technically true, we set to false so the segments after don't get added
-                        // } else if (isClosedCurve && currCurveT === segmentIndexes[1]) {
-                        //     segment1.push(inkSegment.split(intersections[0] - currCurveT, 1));
+                        hasSplit = true; 
                     } else {
                         if ((isClosedCurve && hasSplit) || (!isClosedCurve && !hasSplit)) {
                             segment1.push(inkSegment);
@@ -1099,6 +1201,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
 
+        if (isClosedCurve && segment1.length > 0 && segment2.length > 0) {
+            segment2 = segment2.concat(segment1);
+            segment1 = [];
+        }
+
         // push 1 or both segments if they are not empty
         if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) {
             segments.push(segment1);
@@ -1203,46 +1310,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return tVals;
     };
 
-    /**
-     * For radius erase, a function to find an eraser's intersections with a single ink stroke
-     * @param otherInkDocVIew the ink document that is being erased
-     * @param i the index of the eraser's segment
-     * @param points the eraser's ink data points
-     * @param strokeToTVals Map of InkingStroke to the tVals of its intersections with the eraser
-     * @returns
-     */
-    getOtherInkIntersections = (otherInkDocView: DocumentView, i: number, points: InkData, strokeToTVals: Map<InkingStroke, number[]>): Map<InkingStroke, number[]> => {
-        this.childDocs
-            .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect)
-            .forEach(doc => {
-                // InkingStroke of other ink strokes
-                const otherInk = otherInkDocView.ComponentView as InkingStroke;
-                // ink Data of other ink strokes
-                const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
-                for (var j = 0; j < otherInkData.length - 3; j += 4) {
-                    const curve: Bezier = InkField.Segment(points, i); // eraser curve
-                    const otherCurve: Bezier = InkField.Segment(otherInkData, j); // other curve
-                    this.bintersects(otherCurve, curve).forEach((val: string | number, k: number) => {
-                        // Converting the Bezier.js Split type to a t-value number.
-                        const t = +val.toString().split('/')[0];
-                        if (k % 2 === 0) {
-                            // here, add to the map
-                            const inkList = strokeToTVals.get(otherInk);
-                            if (inkList !== undefined) {
-                                const inList = inkList.some(val => Math.abs(val - (t + Math.floor(j / 4))) <= 0.01);
-                                if (!inList) {
-                                    inkList.push(t + Math.floor(j / 4));
-                                }
-                            } else {
-                                strokeToTVals.set(otherInk, [t + Math.floor(j / 4)]);
-                            }
-                        }
-                    });
-                }
-            });
-        return strokeToTVals;
-    };
-
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
         if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 5c5f8de03..19741e2e0 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -341,10 +341,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
                 Doc.UserDoc().activeEraserTool = tool;
             }
             // pen or eraser
-            if (Doc.ActiveTool === tool && tool === InkTool.Eraser) {
-                Doc.ActiveTool = InkTool.SegmentEraser;
-                console.log("erase click twice")
-            } else if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
+            if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
             } else {
                 Doc.ActiveTool = tool as any;
@@ -362,11 +359,6 @@ ScriptingGlobals.add(function activeEraserTool() {
     return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser);
 }, 'returns the current eraser tool');
 
-// ScriptingGlobals.add(function setEraserProperty(option: 'eraseWidth', value: any, checkResult?: boolean) {
-//     setInk: (doc: Doc) => (doc[DocData].eraserWidth = NumCast(value))
-//     InkingStroke.setEraserRadius(NumCast(value));
-// })
-
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: any, checkResult?: boolean) {
     const selected = SelectionManager.Docs.lastElement() ?? Doc.UserDoc();
-- 
cgit v1.2.3-70-g09d2


From 4a332476db24555260820b8c5e268acc2000041c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 9 May 2024 11:15:46 -0400
Subject: tweaks to make eraser deletion more efficient

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 67 ++++++++++++----------
 1 file changed, 38 insertions(+), 29 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 2e174be30..ca9a9974a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,7 +1,5 @@
-import { inside } from '@turf/turf';
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
-import { validationResult } from 'express-validator';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
@@ -693,7 +691,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
     @action
     onEraserUp = (e: PointerEvent): void => {
-        this._deleteList.forEach(ink => ink._props.removeDocument?.(ink.Document));
+        this._deleteList.lastElement()?._props.removeDocument?.(this._deleteList.map(ink => ink.Document));
         this._deleteList = [];
         this._batch?.end();
     };
@@ -756,13 +754,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 if (Doc.ActiveTool !== InkTool.StrokeEraser) {
                     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
-                    segments?.forEach(segment =>
-                        this.forceStrokeGesture(
-                            e,
-                            GestureUtils.Gestures.Stroke,
-                            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[])
-                        )
-                    );
+                    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[]);
+                        const bounds = GestureOverlay.getBounds(points);
+                        const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+                        const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+                        return Docs.Create.InkDocument(
+                            points,
+                            { title: 'stroke',
+                            x: B.x - inkWidth / 2,
+                            y: B.y - inkWidth / 2,
+                            _width: B.width + inkWidth,
+                            _height: B.height + inkWidth,
+                            stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                            inkWidth
+                        );
+                    });
+                    newStrokes && this.addDocument?.(newStrokes);
                     setTimeout(() => this._eraserLock--);
                 }
                 // Lower ink opacity to give the user a visual indicator of deletion.
@@ -872,17 +880,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     /**
      * Ray-tracing algorithm to determine whether a point is inside the eraser outline
-     * @param eraserOutline 
-     * @param point 
-     * @returns 
+     * @param eraserOutline
+     * @param point
+     * @returns
      */
     insideEraserOutline = (eraserOutline: InkData, point: { X: number; Y: number }) => {
         var isInside = false;
         if (!isNaN(eraserOutline[0].X) && !isNaN(eraserOutline[0].Y)) {
-            var minX = eraserOutline[0].X, maxX = eraserOutline[0].X;
-            var minY = eraserOutline[0].Y, maxY = eraserOutline[0].Y;
-            for (var i = 1; i < eraserOutline.length; i++) {
-                const currPoint: {X: number, Y: number} = eraserOutline[i];
+            let minX = eraserOutline[0].X, maxX = eraserOutline[0].X; // prettier-ignore
+            let minY = eraserOutline[0].Y, maxY = eraserOutline[0].Y; // prettier-ignore
+            for (let i = 1; i < eraserOutline.length; i++) {
+                const currPoint: { X: number; Y: number } = eraserOutline[i];
                 minX = Math.min(currPoint.X, minX);
                 maxX = Math.max(currPoint.X, maxX);
                 minY = Math.min(currPoint.Y, minY);
@@ -894,8 +902,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
 
             for (var i = 0, j = eraserOutline.length - 1; i < eraserOutline.length; j = i, i++) {
-                if ((eraserOutline[i].Y > point.Y) != (eraserOutline[j].Y > point.Y) &&
-                    point.X < (eraserOutline[j].X - eraserOutline[i].X) * (point.Y - eraserOutline[i].Y) / (eraserOutline[j].Y - eraserOutline[i].Y) + eraserOutline[i].X ) {
+                if (eraserOutline[i].Y > point.Y != eraserOutline[j].Y > point.Y && point.X < ((eraserOutline[j].X - eraserOutline[i].X) * (point.Y - eraserOutline[i].Y)) / (eraserOutline[j].Y - eraserOutline[i].Y) + eraserOutline[i].X) {
                     isInside = !isInside;
                 }
             }
@@ -950,7 +957,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getRadiusEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => {
         const eraserRadius = ActiveEraserWidth() + 3;
         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 eraserMax = { X: Math.max(lastPoint.X, currPoint.X) + eraserRadius, Y: Math.max(lastPoint.Y, currPoint.Y) + eraserRadius };
         const strokeToTVals = new Map<DocumentView, number[]>();
         const intersectingStrokes = this.childDocs
             .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.()))
@@ -964,7 +971,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     eraserMax.X >= inkViewBounds.left &&
                     eraserMax.Y >= inkViewBounds.top
             );
-        console.log("itersectnig strokes", intersectingStrokes);
+
         intersectingStrokes.forEach(({ inkStroke, inkView }) => {
             const { inkData } = inkStroke.inkScaledData();
             const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
@@ -1007,7 +1014,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }
             }
         });
-        console.log("strokeToTVals", strokeToTVals);
         return strokeToTVals;
     };
 
@@ -1024,8 +1030,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const inkStroke = ink?.ComponentView as InkingStroke;
         const { inkData } = inkStroke.inkScaledData();
         var currSegment: Segment = [];
-        if (tVals.length % 2 !== 0) { // should always have even tVals
-            for (var i = 0; i < inkData.length - 3; i +=4) {
+        if (tVals.length % 2 !== 0) {
+            // should always have even tVals
+            for (var i = 0; i < inkData.length - 3; i += 4) {
                 currSegment.push(InkField.Segment(inkData, i));
             }
             if (currSegment.length > 0) {
@@ -1042,18 +1049,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
 
         for (var i = 0; i < inkData.length - 3; i += 4) {
-            const currCurveT = Math.floor(i/4);
+            const currCurveT = Math.floor(i / 4);
             const inkBezier: Bezier = InkField.Segment(inkData, i);
             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) {
                         continueErasing = true;
                     } else if (segmentTs[j] === Math.floor(inkData.length / 4) + 1) {
                         break;
-                    }else {
+                    } else {
                         if (!continueErasing) {
                             currSegment.push(inkBezier.split(0, segmentTs[j] - currCurveT));
                             continueErasing = true;
@@ -1071,7 +1078,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     }
                 }
             } else {
-                if (!continueErasing) { // push the bezier piece if not in the eraser circle
+                if (!continueErasing) {
+                    // push the bezier piece if not in the eraser circle
                     currSegment.push(inkBezier);
                 }
             }
@@ -1191,7 +1199,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         hasSplit = true;
                     } else if (currCurveT === splitSegment1) {
                         segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
-                        hasSplit = true; 
+                        hasSplit = true;
                     } else {
                         if ((isClosedCurve && hasSplit) || (!isClosedCurve && !hasSplit)) {
                             segment1.push(inkSegment);
@@ -2128,6 +2136,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) {
                 this._renderCutoffData.set(layout_unrendered[i][Id] + '', true);
             }
+            layout_unrendered.forEach(lu => lu.stroke && this._renderCutoffData.set(lu[Id] + '', true));
         }
         this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
     });
-- 
cgit v1.2.3-70-g09d2


From d2c67049891493551e39f8c2423ee6e40109bb65 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 12 May 2024 01:00:42 -0400
Subject: trying out a circle around the cursor

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 35 ++++++++++++++++++----
 1 file changed, 29 insertions(+), 6 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 2e174be30..ab47c105e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -97,6 +97,8 @@ 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;
@@ -741,6 +743,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      *  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 };
@@ -775,6 +778,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
+        // this._eraserX = e.clientX;
+        // this._eraserY = e.clientY;
         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));
@@ -964,7 +969,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     eraserMax.X >= inkViewBounds.left &&
                     eraserMax.Y >= inkViewBounds.top
             );
-        console.log("itersectnig strokes", intersectingStrokes);
         intersectingStrokes.forEach(({ inkStroke, inkView }) => {
             const { inkData } = inkStroke.inkScaledData();
             const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
@@ -1007,7 +1011,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }
             }
         });
-        console.log("strokeToTVals", strokeToTVals);
         return strokeToTVals;
     };
 
@@ -1028,16 +1031,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             for (var i = 0; i < inkData.length - 3; i +=4) {
                 currSegment.push(InkField.Segment(inkData, i));
             }
-            if (currSegment.length > 0) {
-                segments.push(currSegment);
-                return segments;
-            }
+            segments.push(currSegment);
+            return segments;
         }
 
         var continueErasing = false;
         var firstSegment: Segment = [];
         // 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) {
+                currSegment.push(InkField.Segment(inkData, i));
+            }
+            segments.push(currSegment);
             return segments;
         }
 
@@ -1082,6 +1087,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
             segments.push(currSegment);
         }
+        if (segments.length === 0) {
+            console.log("segments is 0");
+        }
         return segments;
     };
 
@@ -1980,6 +1988,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     onCursorMove = (e: React.PointerEvent) => {
+        this._eraserX = e.clientX;
+        this._eraserY = e.clientY;
         //  super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     };
 
@@ -2265,6 +2275,19 @@ 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',
+                    }}
+                /> */}
                 {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


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


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

---
 src/client/views/InkingStroke.tsx                  |  49 -------
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 141 +++++++++++----------
 src/client/views/global/globalScripts.ts           |   3 -
 3 files changed, 73 insertions(+), 120 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 03acd5393..9e09c0aa9 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -331,55 +331,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     };
 
-    splitByEraser = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => {
-        const radius = ActiveEraserWidth() / 2 + 3; // reduce values to avoid extreme radii
-        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);
-        const direction = { x: (movement.x / moveLen) * radius, y: (movement.y / moveLen) * radius };
-        const normal    = { x: -direction.y, y: direction.x }; // prettier-ignore
-
-        const startCoords = { X: startInkCoordsIn.X - direction.x, Y: startInkCoordsIn.Y - direction.y };
-        const endCoords = { X: endInkCoordsIn.X + direction.x, Y: endInkCoordsIn.Y + direction.y };
-        return new InkField([
-            // left bot arc
-            { X: startCoords.X,                                            Y: startCoords.Y }, // prettier-ignore
-            { X: startCoords.X + normal.x * c,                             Y: startCoords.Y + normal.y * c }, // prettier-ignore
-            { X: startCoords.X + direction.x + normal.x - direction.x * c, Y: startCoords.Y + direction.y + normal.y - direction.y * c },
-            { X: startCoords.X + direction.x + normal.x,                   Y: startCoords.Y + direction.y + normal.y }, // prettier-ignore
-
-            // bot
-            { X: startCoords.X + direction.x + normal.x,                   Y: startCoords.Y + direction.y + normal.y }, // prettier-ignore
-            { X: startCoords.X + direction.x + normal.x + direction.x * c, Y: startCoords.Y + direction.y + normal.y + direction.y * c },
-            { X: endCoords.X - direction.x + normal.x - direction.x * c,   Y: endCoords.Y - direction.y + normal.y - direction.y * c }, // prettier-ignore
-            { X: endCoords.X - direction.x + normal.x,                     Y: endCoords.Y - direction.y + normal.y }, // prettier-ignore
-
-            // right bot arc
-            { X: endCoords.X - direction.x + normal.x,                    Y: endCoords.Y - direction.y + normal.y }, // prettier-ignore
-            { X: endCoords.X - direction.x + normal.x + direction.x * c,  Y: endCoords.Y - direction.y + normal.y + direction.y * c}, // prettier-ignore
-            { X: endCoords.X + normal.x * c,                              Y: endCoords.Y + normal.y * c }, // prettier-ignore
-            { X: endCoords.X,                                             Y: endCoords.Y  }, // prettier-ignore
-
-            // right top arc
-            { X: endCoords.X,                                             Y: endCoords.Y  }, // prettier-ignore
-            { X: endCoords.X - normal.x * c,                              Y: endCoords.Y - normal.y * c }, // prettier-ignore
-            { X: endCoords.X - direction.x - normal.x + direction.x * c,  Y: endCoords.Y - direction.y - normal.y + direction.y * c}, // prettier-ignore
-            { X: endCoords.X - direction.x - normal.x,                    Y: endCoords.Y - direction.y - normal.y }, // prettier-ignore
-
-            // top
-            { X: endCoords.X - direction.x - normal.x,                     Y: endCoords.Y - direction.y - normal.y }, // prettier-ignore
-            { X: endCoords.X - direction.x - normal.x - direction.x * c,   Y: endCoords.Y - direction.y - normal.y - direction.y * c}, // prettier-ignore
-            { X: startCoords.X + direction.x - normal.x + direction.x * c, Y: startCoords.Y + direction.y - normal.y + direction.y * c },
-            { X: startCoords.X + direction.x - normal.x,                   Y: startCoords.Y + direction.y - normal.y }, // prettier-ignore
-
-            // left top arc
-            { X: startCoords.X + direction.x - normal.x,                   Y: startCoords.Y + direction.y - normal.y }, // prettier-ignore
-            { X: startCoords.X + direction.x - normal.x - direction.x * c, Y: startCoords.Y + direction.y - normal.y - direction.y * c }, // prettier-ignore
-            { X: startCoords.X - normal.x * c,                             Y: startCoords.Y - normal.y * c }, // prettier-ignore
-            { X: startCoords.X,                                            Y: startCoords.Y }, // prettier-ignore
-        ]);
-    };
-
     _subContentView: ViewBoxInterface | undefined;
     setSubContentView = (doc: ViewBoxInterface) => (this._subContentView = doc);
     @computed get fillColor() {
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%)',
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 19741e2e0..0a8434148 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -362,9 +362,6 @@ ScriptingGlobals.add(function activeEraserTool() {
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: any, checkResult?: boolean) {
     const selected = SelectionManager.Docs.lastElement() ?? Doc.UserDoc();
-    if (option === 'eraserWidth') {
-        console.log('eraserWidth', value);
-    }
     // prettier-ignore
     const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
         ['inkMask', {
-- 
cgit v1.2.3-70-g09d2

