From 42419cd85b7dc2aec0695f2b29b2f707ae7e36e2 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 27 Sep 2024 14:19:40 -0400
Subject: fixed linting.  cleaned up scribble erase code and fixed several
 issues with determining if cusp intersections amounted to a scribble.  also
 fixed recognition of lines to not recognize scribbles.

---
 src/client/views/GestureOverlay.tsx | 276 +++++++++++++-----------------------
 1 file changed, 96 insertions(+), 180 deletions(-)

(limited to 'src/client/views/GestureOverlay.tsx')

diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 1cd6de55c..e9e12fc3b 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,10 +1,9 @@
 import * as fitCurve from 'fit-curve';
-import { Bezier } from 'bezier-js';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
-import { emptyFunction } from '../../Utils';
+import { emptyFunction, intersectRect } from '../../Utils';
 import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { NumCast } from '../../fields/Types';
@@ -16,6 +15,7 @@ import { Docs } from '../documents/Documents';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { Transform } from '../util/Transform';
+import { undoable } from '../util/UndoManager';
 import './GestureOverlay.scss';
 import { InkingStroke } from './InkingStroke';
 import { ObservableReactComponent } from './ObservableReactComponent';
@@ -37,7 +37,6 @@ import {
     SetActiveInkColor,
     SetActiveInkWidth,
 } from './nodes/DocumentView';
-import { docs } from 'googleapis/build/src/apis/docs';
 export enum ToolglassTools {
     InkToText = 'inktotext',
     IgnoreGesture = 'ignoregesture',
@@ -53,7 +52,9 @@ interface GestureOverlayProps {
  * drew or perform the gesture's action
  */
 export class GestureOverlay extends ObservableReactComponent<React.PropsWithChildren<GestureOverlayProps>> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: GestureOverlay;
+    // eslint-disable-next-line no-use-before-define
     static Instances: GestureOverlay[] = [];
 
     @observable public InkShape: Opt<Gestures> = undefined;
@@ -79,10 +80,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     }
 
     private _overlayRef = React.createRef<HTMLDivElement>();
-    private _d1: Doc | undefined;
-    private _inkToTextDoc: Doc | undefined;
-    private thumbIdentifier?: number;
-    private pointerIdentifier?: number;
 
     constructor(props: GestureOverlayProps) {
         super(props);
@@ -136,119 +133,67 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         }
     }
     /**
-     * this method returns if what the user drew is a scribble. if it is, it will determine what documents need
-     * to be deleted and then it will delete them.
+     * If what the user drew is a scribble, this returns the documents that were scribbled over
      * I changed it so it doesnt use triangles. It will modify an intersect array, with its length being
      * how many sharp cusps there are. The first index will have a boolean that is based on if there is an
      * intersection in the first 1/length percent of the stroke. The second index will be if there is an intersection
-     * in the 2nd 1/length percent of the stroke. This array will be used in determineInScribble().
-     * @returns
+     * in the 2nd 1/length percent of the stroke. This array will be used in determineIfScribble().
+     * @param ffview freeform view where scribble is drawn
+     * @param scribbleStroke scribble stroke in screen space coordinats
+     * @returns array of documents scribbled over
      */
-    isScribble(inkData: InkData) {
-        const ffView = DocumentView.allViews().find(view => view.ComponentView instanceof CollectionFreeFormView);
-        console.log(inkData.map(ink => ({ x: ink.X, y: ink.Y })));
-        let intersectArray: boolean[] = [];
-        const cuspArray = this.getCusps(inkData);
-        console.log(cuspArray.length);
-        for (let i = 0; i < cuspArray.length; i++) {
-            intersectArray[i] = false;
-        }
-        const docsToDelete: Doc[] = [];
-        let childDocs = (ffView?.ComponentView as CollectionFreeFormView).childDocs.slice(0, -1);
-        childDocs.filter(doc => doc.type === 'ink').map(doc => DocumentView.getDocumentView(doc, DocumentView.getDocumentView(doc)));
-        if ((ffView?.ComponentView as CollectionFreeFormView).childDocs) {
-            for (const doc of childDocs) {
-                const otherInk = DocumentView.getDocumentView(doc)?.ComponentView as InkingStroke;
-                const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
-                const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
-                if (this.isRectangleOverlap(this.getExtremeCoordinates(otherScreenPts), this.getExtremeCoordinates(inkData))) {
-                    const intersects = this.doInksIntersect(inkData, otherScreenPts);
-                    intersects.forEach(intersect => {
-                        let percentage = '';
-                        if (intersect.includes('/')) {
-                            const leftOfSlash = intersect.split('/')[0];
-                            percentage = leftOfSlash;
-                        } else {
-                            percentage = intersect;
-                        }
-                        intersectArray[Math.floor((percentage as unknown as number) * cuspArray.length)] = true;
-                        const docsInBoundingBox = this.docsInBoundingBox(doc, childDocs);
-                        childDocs = childDocs.filter(doc => !docsInBoundingBox.includes(doc));
-                        docsToDelete.push(...docsInBoundingBox);
-                        docsToDelete.push(doc);
+    isScribble = (ffView: CollectionFreeFormView, cuspArray: { X: number; Y: number }[], scribbleStroke: { X: number; Y: number }[]) => {
+        const intersectArray = cuspArray.map(() => false);
+        const scribbleBounds = InkField.getBounds(scribbleStroke);
+        const docsToDelete = ffView.childDocs
+            .map(doc => DocumentView.getDocumentView(doc))
+            .filter(dv => dv?.ComponentView instanceof InkingStroke)
+            .map(dv => dv?.ComponentView as InkingStroke)
+            .filter(otherInk => {
+                const otherScreenPts = otherInk.inkScaledData?.().inkData.map(otherInk.ptToScreen);
+                if (intersectRect(InkField.getBounds(otherScreenPts), scribbleBounds)) {
+                    const intersects = this.findInkIntersections(scribbleStroke, otherScreenPts).map(intersect => {
+                        const percentage = intersect.split('/')[0];
+                        intersectArray[Math.floor(Number(percentage) * cuspArray.length)] = true;
                     });
+                    return intersects.length > 0;
                 }
-            }
-            console.log(intersectArray);
-            if (intersectArray.length > 3 && this.determineIfScribble(intersectArray)) {
-                const uniqueArray = Array.from(new Set(docsToDelete));
-                console.log(uniqueArray.length);
-                console.log('is a scribble');
-                docsToDelete.forEach(doc => {
-                    ffView?.ComponentView?.removeDocument?.(doc);
-                });
-                this._points = [];
-                return true;
-            }
-        }
-        return false;
-    }
-    /**
-     * this will return all the docs overlapping with the maindocs bounding box
-     * @param mainDoc the bounding box of this doc will be used
-     * @param childDocs the array of all docs in collection
-     * @returns
-     */
-    docsInBoundingBox(mainDoc: Doc, childDocs: Doc[]): Doc[] {
-        return childDocs.filter(
-            doc =>
-                typeof doc.x === 'number' &&
-                typeof doc.y === 'number' &&
-                typeof doc.width === 'number' &&
-                typeof doc.height === 'number' &&
-                typeof mainDoc.x === 'number' &&
-                typeof mainDoc.y === 'number' &&
-                typeof mainDoc.width === 'number' &&
-                typeof mainDoc.height === 'number' &&
-                doc.x < mainDoc.x + mainDoc.width &&
-                doc.x + doc.width > mainDoc.x &&
-                doc.y < mainDoc.y + mainDoc.height &&
-                doc.y + doc.height > mainDoc.y
-        );
-    }
+            });
+        return !this.determineIfScribble(intersectArray) ? undefined : 
+            [ ...docsToDelete.map(stroke => stroke.Document),
+               // bcz: NOTE: docsInBoundingBox test should be replaced with a docsInConvexHull test
+              ...this.docsInBoundingBox({ topLeft    : ffView.ScreenToContentsXf().transformPoint(scribbleBounds.left, scribbleBounds.top),
+                                          bottomRight: ffView.ScreenToContentsXf().transformPoint(scribbleBounds.right,scribbleBounds.bottom)},
+                                        ffView.childDocs.filter(doc => !docsToDelete.map(s => s.Document).includes(doc)) )]; // prettier-ignore
+    };
     /**
-     * this method determines if what the user drew is a scribble based on certain criteria.
-     * @param cuspBooleanArray will take in an array of booleans tht represent what sections(seperated by a cusp) in the scribble
-     * has an object in it.
-     * @returns
+     * Returns all docs in array that overlap bounds.  Note that the bounds should be given in screen space coordinates.
+     * @param boundingBox screen space bounding box
+     * @param childDocs array of docs to test against bounding box
+     * @returns list of docs that overlap rect
      */
-    determineIfScribble(cuspBooleanArray: boolean[]) {
-        if (!cuspBooleanArray) {
-            return false;
-        }
-        const quarterArrayLength = Math.ceil((cuspBooleanArray.length - 2) * 0.3);
-        let hasObjectInFirstAndLast25 = true;
-        for (let i = 0; i < quarterArrayLength; i++) {
-            if (cuspBooleanArray[i] == false || cuspBooleanArray[cuspBooleanArray.length - 1 - i] == false) {
-                hasObjectInFirstAndLast25 = false;
-            }
-        }
-        const trueCount = cuspBooleanArray.filter(value => value).length;
-        const percentageTrues = trueCount / cuspBooleanArray.length;
-        return percentageTrues >= 0.5 || hasObjectInFirstAndLast25;
-    }
+    docsInBoundingBox = (boundingBox: { topLeft: number[]; bottomRight: number[] }, childDocs: Doc[]): Doc[] => {
+        const rect = { left: boundingBox.topLeft[0], top: boundingBox.topLeft[1], width: boundingBox.bottomRight[0] - boundingBox.topLeft[0], height: boundingBox.bottomRight[1] - boundingBox.topLeft[1] };
+        return childDocs.filter(doc => intersectRect(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
+    };
     /**
-     * determines if two rectangles are overlapping each other
-     * @param rect1 the rectangle object has has a minX,maxX,minY, and maxY
-     * @param rect2
+     * Determines if what the array of cusp/intersection data corresponds to a scribble.
+     * true if there are at least 4 cusps and either:
+     *    1) the initial and final quarters of the array contain objects
+     *    2) or half of the cusps contain objects
+     * @param intersectArray array of booleans coresponding to which scribble sections (regions separated by a cusp) contain Docs
      * @returns
      */
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    isRectangleOverlap(rect1: any, rect2: any): boolean {
-        const noOverlap = rect1.maxX < rect2.minX || rect1.minX > rect2.maxX || rect1.maxY < rect2.minY || rect1.minY > rect2.maxY;
-
-        return !noOverlap;
-    }
+    determineIfScribble = (intersectArray: boolean[]) => {
+        const quarterArrayLength = Math.ceil(intersectArray.length / 3.9); // use 3.9 instead of 4 to work better with strokes with only 4 cusps
+        const { start, end } = intersectArray.reduce((res, val, i) => ({   // test for scribbles at start and end of scribble stroke
+                start: res.start || (val && i <= quarterArrayLength), 
+                end:   res.end   || (val && i >= intersectArray.length - quarterArrayLength) 
+            }), { start: false, end: false }); // prettier-ignore
+
+        const percentCuspsWithContent = intersectArray.filter(value => value).length / intersectArray.length;
+        return intersectArray.length > 3 && (percentCuspsWithContent >= 0.5 || (start && end));
+    };
     /**
      * determines if inks intersect
      * @param line is pointData
@@ -260,32 +205,24 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
      * it will return two numbers, one for each curve, seperated by a comma. Sometimes, the percentage it returns is inaccurate,
      * espcially in the beginning and end parts of the stroke. dont know why. hope this makes sense
      */
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    doInksIntersect(scribble: InkData, inkStroke: InkData): string[] {
-        let intersectArray: string[] = [];
-        const extremeScribble = this.getExtremeCoordinates(scribble);
-        const ffView = DocumentView.allViews().find(view => view.ComponentView instanceof CollectionFreeFormView);
-        if (ffView && ffView.ComponentView instanceof CollectionFreeFormView) {
-            for (let i = 0; i < scribble.length - 3; i += 4) {
-                // iterate over each segment of bezier curve
-                for (let j = 0; j < inkStroke.length - 3; j += 4) {
-                    const scribbleCurve: Bezier = InkField.Segment(scribble, i);
-                    const strokeCurve: Bezier = InkField.Segment(inkStroke, j);
-                    const points = strokeCurve.points.map(point => ({
-                        X: point.x,
-                        Y: point.y,
-                    }));
-                    if (ffView && ffView.ComponentView instanceof CollectionFreeFormView && this.isRectangleOverlap(extremeScribble, this.getExtremeCoordinates(points))) {
-                        const result = (ffView.ComponentView as CollectionFreeFormView).bintersects(scribbleCurve, strokeCurve)[0];
-                        if (result !== undefined) {
-                            intersectArray.push(result.toString());
-                        }
+    findInkIntersections = (scribble: InkData, inkStroke: InkData): string[] => {
+        const intersectArray: string[] = [];
+        const scribbleBounds = InkField.getBounds(scribble);
+        for (let i = 0; i < scribble.length - 3; i += 4) {  // for each segment of scribble
+            for (let j = 0; j < inkStroke.length - 3; j += 4) { // for each segment of ink stroke
+                const scribbleSeg = InkField.Segment(scribble, i);
+                const strokeSeg = InkField.Segment(inkStroke, j);
+                const strokeBounds = InkField.getBounds(strokeSeg.points.map(pt => ({ X: pt.x, Y: pt.y })));
+                if (intersectRect(scribbleBounds, strokeBounds)) {
+                    const result = InkField.bintersects(scribbleSeg, strokeSeg)[0];
+                    if (result !== undefined) {
+                        intersectArray.push(result.toString());
                     }
                 }
-            }
-        }
+            } // prettier-ignore
+        } // prettier-ignore
         return intersectArray;
-    }
+    };
     dryInk = () => {
         const newPoints = this._points.reduce((p, pts) => {
             p.push([pts.X, pts.Y]);
@@ -309,6 +246,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     };
     @action
     onPointerUp = () => {
+        const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView;
         DocumentView.DownDocView = undefined;
         if (this._points.length > 1) {
             const B = this.svgBounds;
@@ -320,31 +258,34 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                       ? GestureUtils.GestureRecognizer.Recognize([points])
                       : undefined) ?? 
                 new Result(Gestures.Stroke, 1, Date.now); // prettier-ignore
+
+            const cuspArray = this.getCusps(points);
             // if any of the shape is activated in the CollectionFreeFormViewChrome
             // need to decide when to turn gestures back on
             const actionPerformed = ((name: Gestures) => {
                 switch (name) {
                     case Gestures.Line:
+                        if (cuspArray.length > 2) return undefined;
+                    // eslint-disable-next-line no-fallthrough
                     case Gestures.Triangle:
                     case Gestures.Rectangle:
                     case Gestures.Circle:
                         this.makeBezierPolygon(Name, true);
                         return this.dispatchGesture(name);
                     case Gestures.RightAngle:
-                        return this.convertToText().length > 0;
+                        return ffView && this.convertToText(ffView).length > 0;
                     default:
                 }
             })(Score < 0.7 ? Gestures.Stroke : (Name as Gestures));
             // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document
+
             if (!actionPerformed) {
-                this.dryInk();
-                setTimeout(() => {
-                    const ffView = DocumentView.allViews().find(view => view.ComponentView instanceof CollectionFreeFormView);
-                    const scribbleInk = (ffView?.ComponentView as CollectionFreeFormView).childDocs[(ffView?.ComponentView as CollectionFreeFormView).childDocs.length - 1];
-                    if (this.isScribble((DocumentView.getDocumentView(scribbleInk)?.ComponentView as InkingStroke).inkScaledData().inkData)) {
-                        ffView?.ComponentView?.removeDocument?.(scribbleInk);
-                    }
-                }, 1);
+                const scribbledOver = ffView && this.isScribble(ffView, cuspArray, this._points);
+                if (scribbledOver) {
+                    undoable(() => ffView.removeDocument(scribbledOver), 'scribble erase')();
+                } else {
+                    this.dryInk();
+                }
             }
         }
         this._points.length = 0;
@@ -353,21 +294,19 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
      * used in the rightAngle gesture to convert handwriting into text. will only work on collections
      * TODO: make it work on individual ink docs.
      */
-    convertToText() {
-        const ffView = DocumentView.allViews().find(view => view.ComponentView instanceof CollectionFreeFormView);
+    convertToText = (ffView: CollectionFreeFormView) => {
         let minX = 999999999;
         let maxX = -999999999;
         let minY = 999999999;
         let maxY = -999999999;
         const textDocs: Doc[] = [];
-        (ffView?.ComponentView as CollectionFreeFormView).childDocs
+        ffView.childDocs
             .filter(doc => doc.type === DocumentType.COL)
             .forEach(doc => {
                 if (typeof doc.width === 'number' && typeof doc.height === 'number' && typeof doc.x === 'number' && typeof doc.y === 'number') {
                     const bounds = DocumentView.getDocumentView(doc)?.getBounds;
                     if (bounds) {
-                        const rect1 = { minX: bounds.left, maxX: bounds.right, minY: bounds.top, maxY: bounds.bottom };
-                        if (this.isRectangleOverlap(rect1, this.getExtremeCoordinates(this._points))) {
+                        if (intersectRect({ ...bounds, width: bounds.right - bounds.left, height: bounds.bottom - bounds.top }, InkField.getBounds(this._points))) {
                             if (doc.x < minX) {
                                 minX = doc.x;
                             }
@@ -383,9 +322,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                             const newDoc = Docs.Create.TextDocument(doc.transcription as string, { title: '', x: doc.x as number, y: minY });
                             newDoc.height = doc.height;
                             newDoc.width = doc.width;
-                            if (ffView?.ComponentView?.addDocument && ffView?.ComponentView?.removeDocument) {
-                                ffView.ComponentView.addDocument(newDoc);
-                                ffView.ComponentView.removeDocument(doc);
+                            if (ffView.addDocument && ffView.removeDocument) {
+                                ffView.addDocument(newDoc);
+                                ffView.removeDocument(doc);
                             }
                             textDocs.push(newDoc);
                         }
@@ -393,10 +332,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 }
             });
         return textDocs;
-    }
+    };
     /**
-     * used to determine how many cusps and where the cusps are in order
-     * @returns will return an array containing the coordinates of the sharp cusps
+     * Returns array of coordinates corresponding to the sharp cusps in an input stroke
+     * @param points array of X,Y stroke coordinates
+     * @returns array containing the coordinates of the sharp cusps
      */
     getCusps(points: InkData) {
         const arrayOfPoints: { X: number; Y: number }[] = [];
@@ -406,40 +346,13 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             const point2 = points[i + 1];
             const point3 = points[i + 2];
             if (this.find_angle(point1, point2, point3) < 90) {
+                // NOTE: this is not an accurate way to find cusps -- it is highly dependent on sampling rate and doesn't work well with slowly drawn scribbles
                 arrayOfPoints.push(point2);
             }
         }
         arrayOfPoints.push(points[points.length - 1]);
         return arrayOfPoints;
     }
-    /**
-     * will look through an array of point data and return the coordinates of the smallest box that can fit all the points
-     * @returns the minX,maxX,minY,maxY of the box
-     */
-    getExtremeCoordinates(points: { X: number; Y: number }[]) {
-        const coordinates = points;
-        if (coordinates.length === 0) {
-            throw new Error('Coordinates array is empty');
-        }
-
-        let minX = coordinates[0].X;
-        let maxX = coordinates[0].X;
-        let minY = coordinates[0].Y;
-        let maxY = coordinates[0].Y;
-
-        coordinates.forEach(coord => {
-            if (coord.X < minX) minX = coord.X;
-            if (coord.X > maxX) maxX = coord.X;
-            if (coord.Y < minY) minY = coord.Y;
-            if (coord.Y > maxY) maxY = coord.Y;
-        });
-        return {
-            minX,
-            maxX,
-            minY,
-            maxY,
-        };
-    }
     /**
      * takes in three points and then determines the angle of the points. used to determine if the cusp
      * is sharp enoug
@@ -770,6 +683,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
 }
 
 ScriptingGlobals.add('GestureOverlay', GestureOverlay);
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setPen(width: string, color: string, fill: string, arrowStart: string, arrowEnd: string, dash: string) {
     runInAction(() => {
         GestureOverlay.Instance.SavedColor = ActiveInkColor();
@@ -782,6 +696,7 @@ ScriptingGlobals.add(function setPen(width: string, color: string, fill: string,
         SetActiveDash(dash);
     });
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function resetPen() {
     runInAction(() => {
         SetActiveInkColor(GestureOverlay.Instance.SavedColor ?? 'rgb(0, 0, 0)');
@@ -789,6 +704,7 @@ ScriptingGlobals.add(function resetPen() {
     });
 }, 'resets the pen tool');
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function createText(text: string, X: number, Y: number) {
         GestureOverlay.Instance.dispatchGesture(Gestures.Text, [{ X, Y }], text);
     },
-- 
cgit v1.2.3-70-g09d2

