From 647b66c965f5896d784de0f321d31cc712937e1c Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Fri, 26 Jul 2024 01:23:06 -0400
Subject: Revert "Merge branch 'master' into eleanor-gptdraw"

This reverts commit decbefe23a1da35c838222bafe8a2c029c6ea794, reversing
changes made to 8ca26551622d36b7856f5c1865498fa9e5d888b5.
---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 265 ++++++++++++---------
 1 file changed, 150 insertions(+), 115 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 62632e8c2..a298e6ac4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,7 +1,7 @@
 /* eslint-disable react/jsx-props-no-spreading */
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 /* eslint-disable jsx-a11y/no-static-element-interactions */
-import { Bezier } from 'bezier-js';
+import { Bezier, Point } from 'bezier-js';
 import { Colors } from 'browndash-components';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -27,6 +27,7 @@ import { aggregateBounds, clamp, emptyFunction, intersectRect, Utils } from '../
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocUtils } from '../../../documents/DocUtils';
+import { FitCurve, GenerateControlPoints } from '../../../util/bezierFit';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
 import { CompileScript } from '../../../util/Scripting';
@@ -90,10 +91,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     private _clusters = new CollectionFreeFormClusters(this);
-    private _oldWheel: HTMLDivElement | null = null;
-    private _panZoomTransitionTimer: NodeJS.Timeout | undefined = undefined;
-    private _brushtimer: NodeJS.Timeout | undefined = undefined;
-    private _brushtimer1: NodeJS.Timeout | undefined = undefined;
+    private _oldWheel: any;
+    private _panZoomTransitionTimer: any;
     private _lastX: number = 0;
     private _lastY: number = 0;
     private _downX: number = 0;
@@ -102,6 +101,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _renderCutoffData = observable.map<string, boolean>();
     private _batch: UndoManager.Batch | undefined = undefined;
+    private _brushtimer: any;
+    private _brushtimer1: any;
     private _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed.  Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
 
     private _presEaseFunc: string = 'ease';
@@ -120,6 +121,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
     @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.  could be used for performance improvement
     @observable _showAnimTimeline = false;
+    @observable _showDrawingEditor = false;
     @observable _deleteList: DocumentView[] = [];
     @observable _timelineRef = React.createRef<Timeline>();
     @observable _marqueeViewRef = React.createRef<MarqueeView>();
@@ -132,7 +134,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _eraserX: number = 0;
     @observable _eraserY: number = 0;
     @observable _showEraserCircle: boolean = false; // to determine whether the radius eraser should show
-    constructor(props: collectionFreeformViewProps) {
+    constructor(props: any) {
         super(props);
         makeObservable(this);
     }
@@ -499,28 +501,30 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             if (!this.Document.isGroup) {
                 // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
                 // prettier-ignore
+                const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                 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.Highlighter:
+                    case InkTool.Write:
+                    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:
                     case InkTool.SegmentEraser:
-                        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);
+                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1);
+                        e.stopPropagation();
                         break;
+                    case InkTool.SmartDraw:
+                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.showSmartDraw, hit !== -1);
+                        e.stopPropagation();
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
                             const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                             setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
                         }
                         break;
-                    default: 
+                    default:
                 }
             }
         }
@@ -571,6 +575,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
     };
+
     @action
     onEraserUp = (): void => {
         this._deleteList.lastElement()?._props.removeDocument?.(this._deleteList.map(ink => ink.Document));
@@ -611,60 +616,91 @@ 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[]) => {
+    erase = (e: PointerEvent, delta: number[]) => {
+        e.stopImmediatePropagation();
         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; // 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);
-                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 (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
-                    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 = InkField.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',
+        if (Doc.ActiveTool === InkTool.RadiusEraser) {
+            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());
+                    segments?.forEach(segment =>
+                        this.forceStrokeGesture(
+                            e,
+                            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[])
+                        )
+                    );
+                }
+                stroke.layoutDoc.opacity = 0;
+                stroke.layoutDoc.dontIntersect = true;
+            });
+        } else {
+            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);
+                    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 (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
+                        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 = InkField.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,
-                            ActiveInkColor(),
-                            ActiveInkBezierApprox(),
-                            ActiveFillColor(),
-                            ActiveArrowStart(),
-                            ActiveArrowEnd(),
-                            ActiveDash(),
-                            ActiveIsInkMask()
-                        );
-                    });
-                    newStrokes && this.addDocument?.(newStrokes);
-                    // setTimeout(() => this._eraserLock--);
+                                inkWidth,
+                                ActiveInkColor(),
+                                ActiveInkBezierApprox(),
+                                ActiveFillColor(),
+                                ActiveArrowStart(),
+                                ActiveArrowEnd(),
+                                ActiveDash(),
+                                ActiveIsInkMask()
+                            );
+                        });
+                        newStrokes && this.addDocument?.(newStrokes);
+                        // setTimeout(() => this._eraserLock--);
+                    }
                 }
-                // Lower ink opacity to give the user a visual indicator of deletion.
-                intersect.inkView.layoutDoc.opacity = 0;
-                intersect.inkView.layoutDoc.dontIntersect = true;
-            }
-        });
+            });
+        }
         return false;
     };
 
+    /**
+     * 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[]) => {
+        this.erase(e, delta);
+        // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future
+        return false;
+    };
+
+    @action
+    onEraserClick = (e: PointerEvent, doubleTap?: boolean) => {
+        e.preventDefault();
+        e.stopImmediatePropagation();
+        this.erase(e, [0, 0]);
+    };
+
     /**
      * 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
@@ -674,35 +710,35 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * @param delta
      * @returns
      */
-    @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));
-        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());
-                segments?.forEach(segment =>
-                    this.forceStrokeGesture(
-                        e,
-                        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[])
-                    )
-                );
-            }
-            stroke.layoutDoc.opacity = 0;
-            stroke.layoutDoc.dontIntersect = true;
-        });
-        return false;
-    };
-
-    forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData) => {
-        this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points)));
+    // @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));
+    //     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());
+    //             segments?.forEach(segment =>
+    //                 this.forceStrokeGesture(
+    //                     e,
+    //                     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[])
+    //                 )
+    //             );
+    //         }
+    //         stroke.layoutDoc.opacity = 0;
+    //         stroke.layoutDoc.dontIntersect = true;
+    //     });
+    //     return false;
+    // };
+
+    forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
+        this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
     };
 
     onPointerMove = (e: PointerEvent) => {
@@ -730,7 +766,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // increase radius slightly based on the erased stroke's width, added to make eraser look more realistic
         const 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 movement = { x: Math.max(endInkCoordsIn.X - startInkCoordsIn.X, 1), y: Math.max(endInkCoordsIn.Y - startInkCoordsIn.Y, 1) };
         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
@@ -1719,7 +1755,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * since rendering a large collection of documents can be slow, at startup, docs are rendered in batches.
      * each doc's render() method will call the cutoff provider which will let the doc know if it should render itself yet, or wait
      */
-    renderCutoffProvider = computedFn((doc: Doc) => (this.Document.isTemplateDoc || this.Document.isTemplateForField ? false : !this._renderCutoffData.get(doc[Id] + '')));
+    renderCutoffProvider = computedFn((doc: Doc) => (this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + '')));
 
     doEngineLayout(
         poolData: Map<string, PoolData>,
@@ -1753,7 +1789,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             .forEach(entry =>
                 elements.push({
                     ele: this.getChildDocView(entry[1]),
-                    bounds: entry[1].opacity === 0 ? { payload: undefined, type: '', ...entry[1], width: 0, height: 0 } : { payload: undefined, type: '', ...entry[1] },
+                    bounds: (entry[1].opacity === 0 ? { ...entry[1], width: 0, height: 0 } : { ...entry[1] }) as any,
                     inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1,
                 })
             );
@@ -1875,32 +1911,31 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
-    updateIcon = (usePanelDimensions?: boolean) => {
-        const contentDiv = this.DocumentView?.().ContentDiv;
-        contentDiv &&
-            UpdateIcon(
-                this.layoutDoc[Id] + '-icon' + new Date().getTime(),
-                contentDiv,
-                usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
-                usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
-                this._props.PanelWidth(),
-                this._props.PanelHeight(),
-                0,
-                1,
-                false,
-                '',
-                (iconFile, nativeWidth, nativeHeight) => {
-                    this.dataDoc.icon = new ImageField(iconFile);
-                    this.dataDoc.icon_nativeWidth = nativeWidth;
-                    this.dataDoc.icon_nativeHeight = nativeHeight;
-                }
-            );
-    };
+    updateIcon = (usePanelDimensions?: boolean) =>
+        UpdateIcon(
+            this.layoutDoc[Id] + '-icon' + new Date().getTime(),
+            this.DocumentView?.().ContentDiv!,
+            usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+            usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+            this._props.PanelWidth(),
+            this._props.PanelHeight(),
+            0,
+            1,
+            false,
+            '',
+            (iconFile, nativeWidth, nativeHeight) => {
+                this.dataDoc.icon = new ImageField(iconFile);
+                this.dataDoc.icon_nativeWidth = nativeWidth;
+                this.dataDoc.icon_nativeHeight = nativeHeight;
+            }
+        );
 
     @action
     onCursorMove = (e: React.PointerEvent) => {
-        this._eraserX = e.clientX;
-        this._eraserY = e.clientY;
+        const locPt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY);
+        this._eraserX = locPt[0];
+        this._eraserY = locPt[1];
+        // Doc.ActiveTool === InkTool.RadiusEraser ? this._childPointerEvents = 'none' : this._childPointerEvents = 'all'
         //  super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     };
 
@@ -2083,7 +2118,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     incrementalRender = action(() => {
         if (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())) {
             const layoutUnrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
-            const loadIncrement = this.Document.isTemplateDoc || this.Document.isTemplateForField ? Number.MAX_VALUE : 5;
+            const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5;
             for (let i = 0; i < Math.min(layoutUnrendered.length, loadIncrement); i++) {
                 this._renderCutoffData.set(layoutUnrendered[i][Id] + '', true);
             }
@@ -2218,8 +2253,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         onPointerMove={this.onCursorMove}
                         style={{
                             position: 'fixed',
-                            left: this._eraserX - 60,
-                            top: this._eraserY - 100,
+                            left: this._eraserX,
+                            top: this._eraserY,
                             width: (ActiveEraserWidth() + 5) * 2,
                             height: (ActiveEraserWidth() + 5) * 2,
                             borderRadius: '50%',
-- 
cgit v1.2.3-70-g09d2

