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.
---
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 5b435e44a..9f2a9b8e1 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -87,9 +87,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
     static _globalHighlightsCache: string = '';
     static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
-    static _highlightStyleSheet = addStyleSheet();
-    static _bulletStyleSheet = addStyleSheet();
-    static _userStyleSheet = addStyleSheet();
+    static _highlightStyleSheet: any = addStyleSheet();
+    static _bulletStyleSheet: any = addStyleSheet();
+    static _userStyleSheet: any = addStyleSheet();
     static _hadSelection: boolean = false;
     private _selectionHTML: string | undefined;
     private _sidebarRef = React.createRef<SidebarAnnos>();
@@ -384,7 +384,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     }
                 }
             } else {
-                const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data;
+                const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data!;
                 if (jsonstring) {
                     const json = JSON.parse(jsonstring);
                     json.selection = state.toJSON().selection;
@@ -1925,11 +1925,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     setHeight={this.setSidebarHeight}
                 />
             ) : (
-                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}>
+                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}>
                     <ComponentTag
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
-                        ref={this._sidebarTagRef}
+                        ref={this._sidebarTagRef as any}
                         setContentView={emptyFunction}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
-- 
cgit v1.2.3-70-g09d2


From fd5278045e8c2e280d81cb965c0b2cc5afb59be8 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 7 Aug 2024 18:09:47 -0400
Subject: problem with setting smooth amount

---
 package-lock.json                                  |   6 +
 package.json                                       |   1 +
 src/client/apis/gpt/GPT.ts                         |  45 +++++-
 src/client/cognitive_services/CognitiveServices.ts |  18 ++-
 src/client/views/GestureOverlay.tsx                | 150 ++++++++---------
 src/client/views/InkStrokeProperties.ts            |  43 ++++-
 src/client/views/InkTranscription.tsx              |   4 +-
 src/client/views/InkingStroke.tsx                  |   4 +-
 src/client/views/PropertiesButtons.tsx             |   4 +-
 src/client/views/PropertiesView.scss               |   6 +
 src/client/views/PropertiesView.tsx                | 180 +++++++++++++++++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 106 ++++++------
 .../collectionFreeForm/MarqueeOptionsMenu.tsx      |   2 +
 .../collections/collectionFreeForm/MarqueeView.tsx |  80 ++++++++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   8 +
 src/client/views/pdf/AnchorMenu.tsx                |  38 +++++
 src/client/views/pdf/PDFViewer.tsx                 |  10 +-
 src/client/views/smartdraw/AnnotationPalette.tsx   |  44 +----
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 155 ++++++++++++++----
 19 files changed, 669 insertions(+), 235 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/package-lock.json b/package-lock.json
index 70f53156c..7f6237ef5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -214,6 +214,7 @@
                 "sass-loader": "^14.2.0",
                 "serializr": "^3.0.2",
                 "shelljs": "^0.8.5",
+                "simplify-js": "^1.2.4",
                 "socket.io": "^4.7.2",
                 "socket.io-client": "^4.7.2",
                 "standard-http-error": "^2.0.1",
@@ -38407,6 +38408,11 @@
                 "node": ">=10"
             }
         },
+        "node_modules/simplify-js": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/simplify-js/-/simplify-js-1.2.4.tgz",
+            "integrity": "sha512-vITfSlwt7h/oyrU42R83mtzFpwYk3+mkH9bOHqq/Qw6n8rtR7aE3NZQ5fbcyCUVVmuMJR6ynsAhOfK2qoah8Jg=="
+        },
         "node_modules/skmeans": {
             "version": "0.9.7",
             "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz",
diff --git a/package.json b/package.json
index 8929bddf0..16b2841be 100644
--- a/package.json
+++ b/package.json
@@ -299,6 +299,7 @@
         "sass-loader": "^14.2.0",
         "serializr": "^3.0.2",
         "shelljs": "^0.8.5",
+        "simplify-js": "^1.2.4",
         "socket.io": "^4.7.2",
         "socket.io-client": "^4.7.2",
         "standard-http-error": "^2.0.1",
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index a780596fa..ee8d5e9b2 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -13,6 +13,7 @@ enum GPTCallType {
     MERMAID = 'mermaid',
     DATA = 'data',
     DRAW = 'draw',
+    COLOR = 'color',
 }
 
 type GPTCallOpts = {
@@ -57,9 +58,15 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
     draw: {
         model: 'gpt-4o',
         maxTokens: 1024,
-        temp: 0.5,
+        temp: 0.8,
         prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. Make sure every element has the stroke field filled out. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, polygon, and path with M, Q, C, and L so only use those.',
     },
+    color: {
+        model: 'gpt-4o',
+        maxTokens: 1024,
+        temp: 0.5,
+        prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.',
+    },
 };
 
 let lastCall = '';
@@ -187,4 +194,38 @@ const gptHandwriting = async (src: string): Promise<string> => {
     }
 };
 
-export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting };
+const gptDrawingColor = async (image: string, coords: string[]): Promise<string> => {
+    try {
+        const response = await openai.chat.completions.create({
+            model: 'gpt-4o',
+            temperature: 0,
+            messages: [
+                {
+                    role: 'user',
+                    content: [
+                        {
+                            type: 'text',
+                            text: `Identify what the drawing in the image represents in 1-5 words. Then, given a list of a list of coordinates, where each list is the coordinates for one stroke of the drawing, determine which part of the drawing it is. Return just what the item it is, followed by ~~~ then only your descriptions in a list like [description, description, ...]. Here are the coordinates: ${coords}`,
+                        },
+                        {
+                            type: 'image_url',
+                            image_url: {
+                                url: `${image}`,
+                                detail: 'low',
+                            },
+                        },
+                    ],
+                },
+            ],
+        });
+        if (response.choices[0].message.content) {
+            return response.choices[0].message.content;
+        }
+        return 'Missing labels';
+    } catch (err) {
+        console.log(err);
+        return 'Error connecting with API';
+    }
+};
+
+export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDrawingColor };
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 9f46b8685..9f7701c54 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -47,14 +47,14 @@ export namespace CognitiveServices {
     const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
         const apiKey = process.env[service.toUpperCase()];
         if (apiKey) {
-            console.log(data)
+            console.log(data);
             console.log(`No API key found for ${service}: ensure youe root directory has .env file with _CLIENT_${service.toUpperCase()}.`);
             return undefined;
         }
 
         let results: any;
         try {
-            results = await manager.requester("has", manager.converter(data), service).then(json => JSON.parse(json));
+            results = await manager.requester('has', manager.converter(data), service).then(json => JSON.parse(json));
         } catch (e) {
             throw e;
         }
@@ -138,12 +138,14 @@ export namespace CognitiveServices {
                     points: points.map(({ X: x, Y: y }) => `${x},${y}`).join(','),
                     language: 'en-US',
                 }));
-                console.log(JSON.stringify({
-                    version: 1,
-                    language: 'en-US',
-                    unit: 'mm',
-                    strokes,
-                }))
+                console.log(
+                    JSON.stringify({
+                        version: 1,
+                        language: 'en-US',
+                        unit: 'mm',
+                        strokes,
+                    })
+                );
                 return JSON.stringify({
                     version: 1,
                     language: 'en-US',
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 649208989..7eac583dd 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -141,7 +141,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
 
             // if any of the shape is activated in the CollectionFreeFormViewChrome
             if (this.InkShape) {
-                this.makeBezierPolygon(this.InkShape, false);
+                GestureOverlay.makeBezierPolygon(this._points, this.InkShape, false);
                 this.dispatchGesture(this.InkShape);
                 this.primCreated();
             }
@@ -158,7 +158,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                         case Gestures.Triangle:
                         case Gestures.Rectangle:
                         case Gestures.Circle:
-                            this.makeBezierPolygon(result.Name, true);
+                            GestureOverlay.makeBezierPolygon(this._points, result.Name, true);
                             actionPerformed = this.dispatchGesture(result.Name);
                             console.log(result.Name);
                             console.log();
@@ -202,17 +202,17 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         this._points.length = 0;
     };
 
-    makeBezierPolygon = (shape: string, gesture: boolean) => {
-        const xs = this._points.map(p => p.X);
-        const ys = this._points.map(p => p.Y);
+    public static makeBezierPolygon = (points: { X: number; Y: number }[], shape: string, gesture: boolean) => {
+        const xs = points.map(p => p.X);
+        const ys = points.map(p => p.Y);
         let right = Math.max(...xs);
         let left = Math.min(...xs);
         let bottom = Math.max(...ys);
         let top = Math.min(...ys);
-        const firstx = this._points[0].X;
-        const firsty = this._points[0].Y;
-        let lastx = this._points[this._points.length - 2].X;
-        let lasty = this._points[this._points.length - 2].Y;
+        const firstx = points[0].X;
+        const firsty = points[0].Y;
+        let lastx = points[points.length - 2].X;
+        let lasty = points[points.length - 2].Y;
         let fourth = (lastx - firstx) / 4;
         if (isNaN(fourth) || fourth === 0) {
             fourth = 0.01;
@@ -223,15 +223,15 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         }
         // const b = firsty - m * firstx;
         if (shape === 'noRec') {
-            return false;
+            return undefined;
         }
         if (!gesture) {
             // if shape options is activated in inkOptionMenu
             // take second to last point because _point[length-1] is _points[0]
-            right = this._points[this._points.length - 2].X;
-            left = this._points[0].X;
-            bottom = this._points[this._points.length - 2].Y;
-            top = this._points[0].Y;
+            right = points[points.length - 2].X;
+            left = points[0].X;
+            bottom = points[points.length - 2].Y;
+            top = points[0].Y;
             if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) {
                 if (left > right) {
                     const temp = right;
@@ -245,47 +245,47 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 }
             }
         }
-        this._points.length = 0;
+        points.length = 0;
         switch (shape) {
             case Gestures.Rectangle:
-                this._points.push({ X: left, Y: top }); // curr pt
-                this._points.push({ X: left, Y: top }); // curr first ctrl pt
-                this._points.push({ X: right, Y: top }); // next ctrl pt
-                this._points.push({ X: right, Y: top }); // next pt
-
-                this._points.push({ X: right, Y: top }); // next pt
-                this._points.push({ X: right, Y: top }); // next first ctrl pt
-                this._points.push({ X: right, Y: bottom }); // next next ctrl pt
-                this._points.push({ X: right, Y: bottom }); // next next pt
-
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
-
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: top });
-                this._points.push({ X: left, Y: top });
+                points.push({ X: left, Y: top }); // curr pt
+                points.push({ X: left, Y: top }); // curr first ctrl pt
+                points.push({ X: right, Y: top }); // next ctrl pt
+                points.push({ X: right, Y: top }); // next pt
+
+                points.push({ X: right, Y: top }); // next pt
+                points.push({ X: right, Y: top }); // next first ctrl pt
+                points.push({ X: right, Y: bottom }); // next next ctrl pt
+                points.push({ X: right, Y: bottom }); // next next pt
+
+                points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
+
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: top });
+                points.push({ X: left, Y: top });
 
                 break;
 
             case Gestures.Triangle:
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
 
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
 
-                this._points.push({ X: (right + left) / 2, Y: top });
-                this._points.push({ X: (right + left) / 2, Y: top });
-                this._points.push({ X: (right + left) / 2, Y: top });
-                this._points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: (right + left) / 2, Y: top });
 
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
 
                 break;
             case Gestures.Circle:
@@ -299,25 +299,25 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                     const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
 
                     // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
-                    this._points.push({ X: centerX, Y: centerY + radius }); // curr pt
-                    this._points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
-                    this._points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
-                    this._points.push({ X: centerX + radius, Y: centerY }); // next pt
-
-                    this._points.push({ X: centerX + radius, Y: centerY }); // next pt
-                    this._points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
-                    this._points.push({ X: centerX + c * radius, Y: centerY - radius });
-                    this._points.push({ X: centerX, Y: centerY - radius });
-
-                    this._points.push({ X: centerX, Y: centerY - radius });
-                    this._points.push({ X: centerX - c * radius, Y: centerY - radius });
-                    this._points.push({ X: centerX - radius, Y: centerY - c * radius });
-                    this._points.push({ X: centerX - radius, Y: centerY });
-
-                    this._points.push({ X: centerX - radius, Y: centerY });
-                    this._points.push({ X: centerX - radius, Y: centerY + c * radius });
-                    this._points.push({ X: centerX - c * radius, Y: centerY + radius });
-                    this._points.push({ X: centerX, Y: centerY + radius });
+                    points.push({ X: centerX, Y: centerY + radius }); // curr pt
+                    points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
+                    points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
+                    points.push({ X: centerX + radius, Y: centerY }); // next pt
+
+                    points.push({ X: centerX + radius, Y: centerY }); // next pt
+                    points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
+                    points.push({ X: centerX + c * radius, Y: centerY - radius });
+                    points.push({ X: centerX, Y: centerY - radius });
+
+                    points.push({ X: centerX, Y: centerY - radius });
+                    points.push({ X: centerX - c * radius, Y: centerY - radius });
+                    points.push({ X: centerX - radius, Y: centerY - c * radius });
+                    points.push({ X: centerX - radius, Y: centerY });
+
+                    points.push({ X: centerX - radius, Y: centerY });
+                    points.push({ X: centerX - radius, Y: centerY + c * radius });
+                    points.push({ X: centerX - c * radius, Y: centerY + radius });
+                    points.push({ X: centerX, Y: centerY + radius });
                 }
                 break;
 
@@ -328,11 +328,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 if (Math.abs(firsty - lasty) < 10 && Math.abs(firstx - lastx) > 10) {
                     lasty = firsty;
                 }
-                this._points.push({ X: firstx, Y: firsty });
-                this._points.push({ X: firstx, Y: firsty });
+                points.push({ X: firstx, Y: firsty });
+                points.push({ X: firstx, Y: firsty });
 
-                this._points.push({ X: lastx, Y: lasty });
-                this._points.push({ X: lastx, Y: lasty });
+                points.push({ X: lastx, Y: lasty });
+                points.push({ X: lastx, Y: lasty });
                 break;
             case Gestures.Arrow:
                 {
@@ -347,16 +347,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                     const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
                     const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
                     const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
-                    this._points.push({ X: x1, Y: y1 });
-                    this._points.push({ X: x2, Y: y2 });
-                    this._points.push({ X: x3, Y: y3 });
-                    this._points.push({ X: x4, Y: y4 });
-                    this._points.push({ X: x2, Y: y2 });
+                    points.push({ X: x1, Y: y1 });
+                    points.push({ X: x2, Y: y2 });
+                    points.push({ X: x3, Y: y3 });
+                    points.push({ X: x4, Y: y4 });
+                    points.push({ X: x2, Y: y2 });
                 }
                 break;
             default:
         }
-        return false;
+        return points;
     };
 
     dispatchGesture = (gesture: Gestures, stroke?: InkData, text?: any) => {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 3920ecc2a..35d628a4e 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -5,8 +5,8 @@ import { Doc, NumListCast, Opt } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
-import { Cast, NumCast } from '../../fields/Types';
-import { PointData } from '../../pen-gestures/GestureTypes';
+import { Cast, NumCast, toList } from '../../fields/Types';
+import { Gestures, PointData } from '../../pen-gestures/GestureTypes';
 import { Point } from '../../pen-gestures/ndollar';
 import { DocumentType } from '../documents/DocumentTypes';
 import { undoBatch } from '../util/UndoManager';
@@ -14,6 +14,9 @@ import { FitOneCurve } from '../util/bezierFit';
 import { InkingStroke } from './InkingStroke';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentView } from './nodes/DocumentView';
+import simplify from 'simplify-js';
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
+import { GestureOverlay } from './GestureOverlay';
 
 export class InkStrokeProperties {
     // eslint-disable-next-line no-use-before-define
@@ -487,4 +490,40 @@ export class InkStrokeProperties {
             }
             return inkCopy;
         });
+
+    @undoBatch
+    smoothInkStrokes = (inkDocs: Doc[], tolerance: number = 5) => {
+        inkDocs.forEach(inkDoc => {
+            const inkView = DocumentView.getDocumentView(inkDoc);
+            const inkStroke = inkView?.ComponentView as InkingStroke;
+            const { inkData } = inkStroke.inkScaledData();
+
+            const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]);
+            console.log(result);
+            let polygonPoints: { X: number; Y: number }[] | undefined = undefined;
+            if (result && (result.Name === 'line' ? result.Score > 0.92 : result.Score > 0.85)) {
+                switch (result.Name) {
+                    case Gestures.Line:
+                    case Gestures.Triangle:
+                    case Gestures.Rectangle:
+                    case Gestures.Circle:
+                        GestureOverlay.makeBezierPolygon(inkData, result.Name, true);
+                        break;
+                    default:
+                }
+            } else {
+                const polylinePoints = inkData.filter((pt, index) => { return index % 4 === 0 || pt === inkData.lastElement()}).map(pt => { return { x: pt.X, y: pt.Y }; }); // prettier-ignore
+                if (polylinePoints.length > 2) {
+                    const toKeep = simplify(polylinePoints, tolerance).map(pt => {return { X: pt.x, Y: pt.y }}); // prettier-ignore
+                    for (var i = 4; i < inkData.length - 3; i += 4) {
+                        const contains = toKeep.find(pt => pt.X === inkData[i].X && pt.Y === inkData[i].Y);
+                        if (!contains) {
+                            this._currentPoint = i;
+                            inkView && this.deletePoints(inkView, false);
+                        }
+                    }
+                }
+            }
+        });
+    };
 }
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 3f90df7d1..1277a6fe7 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -6,7 +6,7 @@ import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { Cast, DateCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
 import { aggregateBounds } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
+import { CollectionFreeFormView, MarqueeView } from './collections/collectionFreeForm';
 import { InkingStroke } from './InkingStroke';
 import './InkTranscription.scss';
 import { Docs } from '../documents/Documents';
@@ -387,7 +387,7 @@ export class InkTranscription extends React.Component {
         );
         docView.props.removeDocument?.(selected);
         // Gets a collection based on the selected nodes using a marquee view ref
-        const newCollection = marqViewRef?.getCollection(selected, undefined, true);
+        const newCollection = MarqueeView.getCollection(selected, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
         if (newCollection) {
             newCollection.width = NumCast(newCollection._width);
             newCollection.height = NumCast(newCollection._height);
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index ce1c07f2f..48b26d903 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -104,10 +104,10 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field,
      * and the recognized words to the 'handwriting'
      */
-    analyzeStrokes=()=> {
+    analyzeStrokes = () => {
         const data: InkData = this.inkScaledData().inkData ?? [];
         CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]);
-    }
+    };
 
     /**
      * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index edf6df2b9..de1800700 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -15,7 +15,7 @@ import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtit
 import { RxWidth } from 'react-icons/rx';
 import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb';
 import { TfiBarChart } from 'react-icons/tfi';
-import { Doc, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast } from '../../fields/Types';
@@ -30,6 +30,8 @@ import { Colors } from './global/globalEnums';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { MarqueeOptionsMenu } from './collections/collectionFreeForm';
+import { InkStrokeProperties } from './InkStrokeProperties';
 
 @observer
 export class PropertiesButtons extends React.Component<{}, {}> {
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 840df41e7..aa825a6e9 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -638,3 +638,9 @@
     padding-left: 8px;
     background-color: rgb(51, 51, 51);
 }
+
+.smooth,
+.color,
+.smooth-slider {
+    margin-top: 3px;
+}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 024db82a4..ac2625f32 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -5,7 +5,7 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core';
 import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Checkbox, Tooltip } from '@mui/material';
-import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components';
+import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from 'browndash-components';
 import { concat } from 'lodash';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -14,13 +14,13 @@ import { ColorResult, SketchPicker } from 'react-color';
 import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
 import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
 import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { ComputedField } from '../../fields/ScriptField';
-import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { GroupManager } from '../util/GroupManager';
@@ -43,6 +43,10 @@ import { DocumentView } from './nodes/DocumentView';
 import { StyleProviderFuncType } from './nodes/FieldView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
+import { InkingStroke } from './InkingStroke';
+import { SettingsManager } from '../util/SettingsManager';
+import { MarqueeOptionsMenu } from './collections/collectionFreeForm';
+import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
 
 const _global = (window /* browser */ || global) /* node */ as any;
 
@@ -117,6 +121,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @observable openAddSlide: boolean = false;
     @observable openSlideOptions: boolean = false;
 
+    // For ink groups
+    @observable containsInkDoc: boolean = false;
+    @observable inkDoc: Doc | undefined = undefined;
+
     @observable _controlButton: boolean = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
 
@@ -794,7 +802,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     };
 
     getField(key: string) {
-        return Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
+        return this.containsInkDoc ? Field.toString(this.inkDoc?.[DocData][key] as FieldType) : Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
     }
 
     @computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore
@@ -805,8 +813,17 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     set shapeWid(value)  { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore
     @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore
     set shapeHgt(value)  { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore
-    @computed get strokeThk(){ return NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
-    set strokeThk(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); } // prettier-ignore
+    @computed get strokeThk(){ return this.containsInkDoc ?  NumCast(this.inkDoc?.[DocData].stroke_width) : NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
+    set strokeThk(value) { 
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_width = Math.round(value * 100) / 100;
+            })
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); 
+        }
+    } // prettier-ignore
 
     @computed get hgtInput() {
         return this.inputBoxDuo(
@@ -843,10 +860,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     private _lastDash: any = '2';
 
-    @computed get colorFil() { return StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore
-    set colorFil(value) { this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined); } // prettier-ignore
-    @computed get colorStk() { return StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
-    set colorStk(value) { this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); } // prettier-ignore
+    @computed get colorFil() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].fillColor) : StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore
+    set colorFil(value) {
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                const inkStroke = DocumentView.getDocumentView(doc)?.ComponentView as InkingStroke;
+                const { inkData } = inkStroke.inkScaledData();
+                if (InkingStroke.IsClosed(inkData)) {
+                    doc[DocData].fillColor = value || undefined;
+                }
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined);
+        }
+    }
+    @computed get colorStk() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].color) : StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
+    set colorStk(value) {
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].color = value || undefined;
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined);
+        }
+    }
 
     colorButton(value: string, type: string, setter: () => void) {
         return (
@@ -917,10 +956,69 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         );
     }
 
-    @computed get dashdStk() {  return this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
+    @computed get smoothAndColor() {
+        const targetDoc = this.selectedLayoutDoc;
+        return (
+            <div>
+                <div className="smooth">
+                    <Toggle
+                        text={'Smooth Ink Strokes'}
+                        color={SettingsManager.userColor}
+                        icon={<FontAwesomeIcon icon="bezier-curve" />}
+                        iconPlacement="left"
+                        align="flex-start"
+                        fillWidth
+                        toggleType={ToggleType.BUTTON}
+                        onClick={undoable(() => {
+                            InkStrokeProperties.Instance.smoothInkStrokes(this.containsInkDoc ? DocListCast(targetDoc.data) : [targetDoc], this.smoothAmt);
+                        }, 'smoothStrokes')}
+                    />
+                </div>
+                <div className="smooth-slider">
+                    {this.getNumber(
+                        'Smooth Amount',
+                        '',
+                        1,
+                        Math.max(20, this.smoothAmt),
+                        this.smoothAmt,
+                        (val: number) => {
+                            !isNaN(val) && (this.smoothAmt = val);
+                        },
+                        20,
+                        1
+                    )}
+                </div>
+                {!targetDoc.layout_isSvg && (
+                    <div className="color">
+                        <Toggle
+                            text={'Color with GPT'}
+                            color={SettingsManager.userColor}
+                            icon={<FontAwesomeIcon icon="fill-drip" />}
+                            iconPlacement="left"
+                            align="flex-start"
+                            fillWidth
+                            toggleType={ToggleType.BUTTON}
+                            onClick={undoable(() => {
+                                SmartDrawHandler.Instance.colorWithGPT(targetDoc);
+                            }, 'smoothStrokes')}
+                        />
+                    </div>
+                )}
+            </div>
+        );
+    }
+
+    @computed get dashdStk() {  return this.containsInkDoc? this.inkDoc?.stroke_dash || '' : this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
     set dashdStk(value) {
         value && (this._lastDash = value);
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_dash = value ? this._lastDash : undefined;
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
+        }
     }
     @computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore
     set widthStk(value) {
@@ -930,13 +1028,31 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     set markScal(value) {
         this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
     }
+    @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '1'); } // prettier-ignore
+    set smoothAmt(value) {
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_smoothAmount = Number(value));
+    }
     @computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore
     set markHead(value) {
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_startMarker = value;
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+        }
     }
     @computed get markTail() {  return this.getField('stroke_endMarker') || ''; } // prettier-ignore
     set markTail(value) {
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_endMarker = value;
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+        }
     }
 
     regInput = (key: string, value: any, setter: (val: string) => {}) => (
@@ -1036,6 +1152,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             <div className="appearance-editor">
                 {this.widthAndDash}
                 {this.strokeAndFill}
+                {this.smoothAndColor}
+            </div>
+        );
+    }
+
+    @computed get inkEditor() {
+        return (
+            <div className="ink-editor">
+                {this.widthAndDash}
+                {this.strokeAndFill}
             </div>
         );
     }
@@ -1164,6 +1290,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get inkSubMenu() {
+        this.containsInkDoc = false;
         return (
             // prettier-ignore
             <>
@@ -1177,6 +1304,30 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         );
     }
 
+    containsInk = (selectedDoc: Doc) => {
+        const childDocs: Doc[] = DocListCast(selectedDoc[DocData].data);
+        for (var i = 0; i < childDocs.length; i++) {
+            if (DocumentView.getDocumentView(childDocs[i])?.layoutDoc?.layout_isSvg) {
+                this.inkDoc = childDocs[i];
+                this.containsInkDoc = true;
+                return true;
+            }
+        }
+        this.containsInkDoc = false;
+        return false;
+    };
+
+    @computed get inkCollectionSubMenu() {
+        return (
+            // prettier-ignore
+            <>
+                <PropertiesSection title="Ink Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
+                    {this.isGroup && this.containsInk(this.selectedDoc) ? this.appearanceEditor : null}
+                </PropertiesSection>
+            </>
+        );
+    }
+
     @computed get fieldsSubMenu() {
         return (
             <PropertiesSection
@@ -1737,6 +1888,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     {this.linksSubMenu}
                     {!this.selectedLink || !this.openLinks ? null : this.linkProperties}
                     {this.inkSubMenu}
+                    {this.inkCollectionSubMenu}
                     {this.contextsSubMenu}
                     {isNovice ? null : this.sharingSubMenu}
                     {this.filtersSubMenu}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a298e6ac4..8575807b3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -548,24 +548,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             default: {
                 const { points } = ge;
                 const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
-                const inkDoc = Docs.Create.InkDocument(
-                    points,
-                    { title: ge.gesture.toString(),
-                      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()
-                );
+                const inkDoc = this.createInkDoc(points, B);
                 if (Doc.ActiveTool === InkTool.Write) {
                     this.unprocessedDocs.push(inkDoc);
                     CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
@@ -652,26 +635,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         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()
-                            );
+                            return this.createInkDoc(points);
                         });
                         newStrokes && this.addDocument?.(newStrokes);
                         // setTimeout(() => this._eraserLock--);
@@ -1276,18 +1240,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return tVals;
     };
 
+    createInkDoc = (points: InkData, transformedBounds?: { x: number; y: number; width: number; height: number }) => {
+        const bounds = InkField.getBounds(points);
+        const B = transformedBounds || 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()
+        );
+    };
+
     @action
-    showSmartDraw = (e: PointerEvent, doubleTap?: boolean) => {
-        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing);
+    showSmartDraw = (e: PointerEvent) => {
+        SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+        SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+        SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY);
     };
 
     _drawing: Doc[] = [];
     _drawingContainer: Doc | undefined = undefined;
     @undoBatch
-    createDrawing = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => {
+    createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
         this._drawing = [];
         const xf = this.screenToFreeformContentsXf;
-        // this._drawingContainer = undefined;
         strokeData.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
             const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
@@ -1310,20 +1299,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 ActiveIsInkMask()
             );
             this._drawing.push(inkDoc);
-            this.addDocument(inkDoc);
         });
-        const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
-        if (collection) {
-            const docData = collection[DocData];
-            docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
-            docData.drawingInput = opts.text;
-            docData.drawingComplexity = opts.complexity;
-            docData.drawingColored = opts.autoColor;
-            docData.drawingSize = opts.size;
-            docData.drawingData = gptRes;
-            this._drawingContainer = collection;
-        }
-        this._batch?.end();
+        const collection = MarqueeView.getCollection(this._drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+        return collection;
     };
 
     removeDrawing = (doc?: Doc) => {
@@ -1339,6 +1317,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._drawing = [];
     };
 
+    addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
+        const docData = doc[DocData];
+        docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+        docData.drawingInput = opts.text;
+        docData.drawingComplexity = opts.complexity;
+        docData.drawingColored = opts.autoColor;
+        docData.drawingSize = opts.size;
+        docData.drawingData = gptRes;
+        this._drawingContainer = doc;
+        this.addDocument(doc);
+        this._batch?.end();
+    };
+
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
         if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
@@ -2045,7 +2036,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         optionItems.push({
             description: 'Show Drawing Editor',
             event: action(() => {
-                !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate();
+                SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
             }),
             icon: 'pen-to-square',
         });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index b3fdd9379..76c37dff0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -21,6 +21,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
     public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
     public classifyImages: (e: React.MouseEvent | undefined) => void = unimplementedFunction;
     public groupImages: () => void = unimplementedFunction;
+    public smoothStrokes: (docs?: Doc[]) => void = unimplementedFunction;
     public isShown = () => this._opacity > 0;
     constructor(props: any) {
         super(props);
@@ -41,6 +42,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
                 <IconButton tooltip="Delete Documents" onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} />
                 <IconButton tooltip="Pin selected region" onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} />
                 <IconButton tooltip="Classify Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
+                <IconButton tooltip="Smooth Strokes" onPointerDown={() => this.smoothStrokes} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
             </>
         );
         return this.getElement(buttons);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 8560323c9..92c0da983 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -4,14 +4,14 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils';
-import { intersectRect, numberRange } from '../../../../Utils';
+import { intersectRect, numberRange, unimplementedFunction } from '../../../../Utils';
 import { Doc, NumListCast, Opt } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkField, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
-import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { GetEffectiveAcl } from '../../../../fields/util';
 import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT';
@@ -26,7 +26,7 @@ import { ContextMenu } from '../../ContextMenu';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
 import { PreviewCursor } from '../../PreviewCursor';
-import { DocumentView } from '../../nodes/DocumentView';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../../nodes/DocumentView';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -36,7 +36,12 @@ import { CollectionFreeFormView } from './CollectionFreeFormView';
 import { ImageLabelHandler } from './ImageLabelHandler';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './MarqueeView.scss';
-import { collectionOf } from '@turf/turf';
+import { collectionOf, points } from '@turf/turf';
+import { InkingStroke } from '../../InkingStroke';
+import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
+import { Gestures } from '../../../../pen-gestures/GestureTypes';
+import { GestureOverlay } from '../../GestureOverlay';
+import { InkStrokeProperties } from '../../InkStrokeProperties';
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -89,6 +94,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         return bounds;
     }
 
+    public AddInkDoc: (points: InkData) => Doc | void = unimplementedFunction;
+
     componentDidMount() {
         this._props.setPreviewCursor?.(this.setPreviewCursor);
     }
@@ -277,6 +284,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView;
             MarqueeOptionsMenu.Instance.classifyImages = this.classifyImages;
             MarqueeOptionsMenu.Instance.groupImages = this.groupImages;
+            MarqueeOptionsMenu.Instance.smoothStrokes = this.smoothStrokes;
             document.addEventListener('pointerdown', hideMarquee, true);
             document.addEventListener('wheel', hideMarquee, true);
         } else {
@@ -489,6 +497,70 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this._props.Document.pivotField = 'data_label';
     });
 
+    @undoBatch
+    smoothStrokes = action((docs?: Doc[]) => {
+        docs && docs.length > 0 ? (this._selectedDocs = docs) : (this._selectedDocs = this.marqueeSelect(false, DocumentType.INK));
+        if (this._selectedDocs.length == 0) return;
+
+        this._selectedDocs.forEach(stroke => {
+            const docView = DocumentView.getDocumentView(stroke);
+            const inkStroke = docView?.ComponentView as InkingStroke;
+            const { inkData } = inkStroke.inkScaledData();
+
+            const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]);
+            console.log(result);
+            let polygonPoints: { X: number; Y: number }[] | undefined = undefined;
+            if (result && (result.Name === 'line' ? result.Score > 0.9 : result.Score > 0.8)) {
+                switch (result.Name) {
+                    case Gestures.Line:
+                    case Gestures.Triangle:
+                    case Gestures.Rectangle:
+                    case Gestures.Circle:
+                        GestureOverlay.makeBezierPolygon(inkData, result.Name, true);
+                        break;
+                    default:
+                }
+            } else {
+                const distances: number[] = [];
+                for (var i = 0; i < inkData.length - 3; i += 4) {
+                    distances.push(Math.sqrt((inkData[i].X - inkData[i + 3].X) ** 2 + (inkData[i].Y - inkData[i + 3].Y) ** 2));
+                }
+                const avgDist = (NumCast(stroke.width) + NumCast(stroke.height)) / 2;
+                // const avgDist = distances.reduce((a, b) => a + b) / distances.length;
+                if (Math.sqrt((inkData.lastElement().X - inkData[0].X) ** 2 + (inkData.lastElement().Y - inkData[0].Y) ** 2) < avgDist) {
+                    inkData.pop();
+                    inkData.push({ X: inkData[0].X, Y: inkData[0].Y });
+                }
+                // const editedPoints: InkData = [];
+                // const toDelete: number[] = [];
+
+                // distances.forEach((dist, i) => {
+                //     if (dist < avgDist / 3) {
+                //         toDelete.unshift(i * 4);
+                //     }
+                // });
+                // toDelete.forEach(pt => {
+                //     InkStrokeProperties.Instance._currentPoint = pt;
+                //     docView && InkStrokeProperties.Instance.deletePoints(docView, false);
+                // });
+
+                // for (var i = 0; i < distances.length; i++) {
+                // if (distances[i] > avgDist / 3) {
+                //     editedPoints.push(...inkData.slice(i * 4, i * 4 + 4));
+                // } else {
+                //     if (i !== distances.length) {
+                //         editedPoints.push(...inkData.slice(i * 4, i * 4 + 2));
+                //         editedPoints.push(...inkData.slice(i * 4 + 6, i * 4 + 8));
+                //         i++;
+                //     }
+                // }
+                // }
+                // inkData.length = 0;
+                // inkData.push(...editedPoints);
+            }
+        });
+    });
+
     @undoBatch
     syntaxHighlight = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
         const selected = this.marqueeSelect(false);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9f2a9b8e1..1e700d240 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -295,6 +295,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
         });
 
+        AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
+            const container = DocCast(this._props.Document.embedContainer);
+            const docView = DocumentView.getDocumentView?.(container);
+            docView?.ComponentView?._props.addDocument?.(drawing);
+            drawing.x = NumCast(this._props.Document.x) + (this._props.Document.width as number);
+            drawing.y = NumCast(this._props.Document.y);
+        };
+
         AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
         const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
         this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index df990b0c0..ea574493a 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -15,6 +15,11 @@ import { LinkPopup } from '../linking/LinkPopup';
 import { DocumentView } from '../nodes/DocumentView';
 import './AnchorMenu.scss';
 import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
+import { InkData, InkField } from '../../../fields/InkField';
+import { DocData } from '../../../fields/DocSymbols';
+import { undoBatch } from '../../util/UndoManager';
+import ReactLoading from 'react-loading';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -38,6 +43,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     // GPT additions
     @observable private _selectedText: string = '';
+    @observable private _isLoading: boolean = false;
     @action
     public setSelectedText = (txt: string) => {
         this._selectedText = txt.trim();
@@ -61,6 +67,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     public get Active() {
         return this._left > 0;
     }
+    public AddDrawingAnnotation: (doc: Doc) => void = unimplementedFunction;
     public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
 
     componentWillUnmount() {
@@ -137,6 +144,29 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         this.addToCollection?.(newCol);
     };
 
+    gptDraw = async (e: React.PointerEvent) => {
+        try {
+            SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
+            this._isLoading = true;
+            await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true);
+            this._isLoading = false;
+        } catch (err) {
+            console.error(err);
+        }
+    };
+
+    @undoBatch
+    createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => {
+        this.AddDrawingAnnotation(drawing);
+        const docData = drawing[DocData];
+        docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+        docData.drawingInput = opts.text;
+        docData.drawingComplexity = opts.complexity;
+        docData.drawingColored = opts.autoColor;
+        docData.drawingSize = opts.size;
+        docData.drawingData = gptRes;
+    });
+
     pointerDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(
             this,
@@ -239,6 +269,14 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                         icon={<FontAwesomeIcon icon="id-card" size="lg" />}
                         color={SettingsManager.userColor}
                     />
+                    {this._selectedText && (
+                        <IconButton
+                            tooltip="Create drawing"
+                            onPointerDown={e => this.gptDraw(e)}
+                            icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon="paintbrush" size="lg" />}
+                            color={SettingsManager.userColor}
+                        />
+                    )}
                     {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
                         <IconButton
                             tooltip="Click to Record Annotation" //
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index fbe3518ec..5d526c1bf 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -33,7 +33,7 @@ import './PDFViewer.scss';
 
 // pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
 // The workerSrc property shall be specified.
-Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.3.136/build/pdf.worker.mjs';
+Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.mjs';
 
 interface IViewerProps extends FieldViewProps {
     pdfBox: PDFBox;
@@ -435,6 +435,14 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         GPTPopup.Instance.addDoc = this._props.sidebarAddDoc;
         // allows for creating collection
         AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
+        AnchorMenu.Instance.AddDrawingAnnotation = this.addDrawingAnnotation;
+    };
+
+    addDrawingAnnotation = (drawing: Doc) => {
+        // drawing[DocData].x = this._props.pdfBox.ScreenToLocalBoxXf().TranslateX
+        // const scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
+        drawing.y = (drawing.y as number) + (this._props.Document.data_sidebar_panY as number);
+        this._props.addDocument?.(drawing);
     };
 
     @action
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index ec4279e3e..7e4d46204 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -64,7 +64,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
     @action
     handleKeyPress = async (event: React.KeyboardEvent) => {
         if (event.key === 'Enter') {
-            await this.generateDrawing();
+            await this.generateDrawings();
         }
     };
 
@@ -130,15 +130,14 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
     }
 
     @undoBatch
-    generateDrawing = action(async () => {
+    generateDrawings = action(async () => {
         this._isLoading = true;
         this._props.Document[DocData].data = undefined;
         for (var i = 0; i < 3; i++) {
             try {
-                SmartDrawHandler.Instance._addFunc = this.createDrawing;
+                SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
                 this._canInteract = false;
                 if (this._showRegenerate) {
-                    SmartDrawHandler.Instance._deleteFunc = unimplementedFunction;
                     await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
                 } else {
                     await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
@@ -154,39 +153,10 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
     });
 
     @action
-    createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
-        this._opts = opts;
+    addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
         this._gptRes.push(gptRes);
-        const drawing: Doc[] = [];
-
-        strokeList.forEach((stroke: [InkData, string, string]) => {
-            const bounds = InkField.getBounds(stroke[0]);
-            const inkWidth = Math.min(5, ActiveInkWidth());
-            const inkDoc = Docs.Create.InkDocument(
-                stroke[0],
-                { title: 'stroke',
-                    x: bounds.left - inkWidth / 2,
-                    y: bounds.top - inkWidth / 2,
-                    _width: bounds.width + inkWidth,
-                    _height: bounds.height + inkWidth,
-                    stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                inkWidth,
-                opts.autoColor ? stroke[1] : ActiveInkColor(),
-                ActiveInkBezierApprox(),
-                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
-                ActiveArrowStart(),
-                ActiveArrowEnd(),
-                ActiveDash(),
-                ActiveIsInkMask()
-            );
-            drawing.push(inkDoc);
-        });
-
-        const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
-        if (collection) {
-            collection[DocData].freeform_fitContentsToBox = true;
-            Doc.AddDocToList(this._props.Document, 'data', collection);
-        }
+        drawing[DocData].freeform_fitContentsToBox = true;
+        Doc.AddDocToList(this._props.Document, 'data', drawing);
     };
 
     saveDrawing = async () => {
@@ -262,7 +232,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                                 icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
                                 iconPlacement="right"
                                 color={SettingsManager.userColor}
-                                onClick={this.generateDrawing}
+                                onClick={this.generateDrawings}
                             />
                         </div>
                         <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '170px', marginTop: '5px' }}>
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index c842551c3..52df598ee 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -7,17 +7,23 @@ import { ObservableReactComponent } from '../ObservableReactComponent';
 import { Button, IconButton } from 'browndash-components';
 import ReactLoading from 'react-loading';
 import { AiOutlineSend } from 'react-icons/ai';
-import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
-import { InkData, InkTool } from '../../../fields/InkField';
+import { gptAPICall, GPTCallType, gptDrawingColor } from '../../apis/gpt/GPT';
+import { InkData, InkField, InkTool } from '../../../fields/InkField';
 import { SVGToBezier } from '../../util/bezierFit';
 const { parse } = require('svgson');
 import { Slider, Switch } from '@mui/material';
-import { Doc } from '../../../fields/Doc';
+import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { DocumentView } from '../nodes/DocumentView';
-import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import './SmartDrawHandler.scss';
 import { unimplementedFunction } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { MarqueeView } from '../collections/collectionFreeForm';
+import { ImageField, URLField } from '../../../fields/URLField';
+import { CollectionCardView } from '../collections/CollectionCardDeckView';
+import { InkingStroke } from '../InkingStroke';
+import { undoBatch } from '../../util/UndoManager';
 
 export interface DrawingOptions {
     text: string;
@@ -46,13 +52,43 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _autoColor: boolean = true;
     @observable private _regenInput: string = '';
     @observable private _canInteract: boolean = true;
-    public _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
-    public _deleteFunc: (doc?: Doc) => void = () => {};
+
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
     private _selectedDoc: Doc | undefined = undefined;
     private _errorOccurredOnce = false;
 
+    public RemoveDrawing: (doc?: Doc) => void = unimplementedFunction;
+    public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+        const drawing: Doc[] = [];
+        strokeList.forEach((stroke: [InkData, string, string]) => {
+            const bounds = InkField.getBounds(stroke[0]);
+            const inkWidth = Math.min(5, ActiveInkWidth());
+            const inkDoc = Docs.Create.InkDocument(
+                stroke[0],
+                { title: 'stroke',
+                        x: bounds.left - inkWidth / 2,
+                        y: bounds.top - inkWidth / 2,
+                        _width: bounds.width + inkWidth,
+                        _height: bounds.height + inkWidth,
+                        stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                inkWidth,
+                opts.autoColor ? stroke[1] : ActiveInkColor(),
+                ActiveInkBezierApprox(),
+                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+                ActiveArrowStart(),
+                ActiveArrowEnd(),
+                ActiveDash(),
+                ActiveIsInkMask()
+            );
+            drawing.push(inkDoc);
+        });
+
+        const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+        return collection;
+    };
+    public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
+
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -90,25 +126,26 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    displaySmartDrawHandler = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+    setEdit = () => {
+        this._showEditBox = !this._showEditBox;
+    };
+
+    @action
+    displaySmartDrawHandler = (x: number, y: number) => {
         this._pageX = x;
         this._pageY = y;
         this._display = true;
-        this._addFunc = addFunc;
-        this._deleteFunc = deleteFunc;
     };
 
     @action
-    displayRegenerate = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+    displayRegenerate = (x: number, y: number) => {
         this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
-        const docData = this._selectedDoc[DocData];
-        this._addFunc = addFunc;
-        this._deleteFunc = deleteFunc;
         this._pageX = x;
         this._pageY = y;
         this._display = false;
         this._showRegenerate = true;
         this._showEditBox = false;
+        const docData = this._selectedDoc[DocData];
         this._lastResponse = StrCast(docData.drawingData);
         this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
     };
@@ -155,8 +192,8 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             this._showOptions = false;
             try {
                 await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
-                this._showRegenerate = true;
                 this.hideSmartDrawHandler();
+                this._showRegenerate = true;
             } catch (err) {
                 if (this._errorOccurredOnce) {
                     console.error('GPT call failed', err);
@@ -181,16 +218,14 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             return;
         }
         console.log(res);
-        const strokeData = await this.parseResponse(res, startPt, false, autoColor);
+        const strokeData = await this.parseSvg(res, startPt, false, autoColor);
+        const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
+        drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+
         this._errorOccurredOnce = false;
         return strokeData;
     };
 
-    @action
-    edit = () => {
-        this._showEditBox = !this._showEditBox;
-    };
-
     @action
     regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
         if (lastInput) this._lastInput = lastInput;
@@ -211,14 +246,18 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log(res);
-            await this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+            const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+            this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(this._selectedDoc);
+            const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
+            drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+            return strokeData;
         } catch (err) {
-            console.error('GPT call failed', err);
+            console.error('Error regenerating drawing', err);
         }
     };
 
     @action
-    parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
+    parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
         const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
         if (svg) {
             this._lastResponse = svg[0];
@@ -235,16 +274,70 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                     (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined,
                 ]);
             });
-            if (regenerate) {
-                if (this._deleteFunc !== unimplementedFunction) this._deleteFunc(this._selectedDoc);
-                this._addFunc(strokeData, this._lastInput, svg[0]);
-            } else {
-                this._addFunc(strokeData, this._lastInput, svg[0]);
-            }
             return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
         }
     };
 
+    colorWithGPT = async (drawing: Doc) => {
+        const img = await this.getIcon(drawing);
+        const { href } = (img as URLField).url;
+        const hrefParts = href.split('.');
+        const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+        try {
+            const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
+            const strokes = DocListCast(drawing[DocData].data);
+            const coords: string[] = [];
+            strokes.forEach((stroke, i) => {
+                const inkingStroke = DocumentView.getDocumentView(stroke)?.ComponentView as InkingStroke;
+                const { inkData } = inkingStroke.inkScaledData();
+                coords.push(
+                    `${i + 1}. ${inkData
+                        .filter((point, index) => {
+                            return index % 4 === 0 || index == inkData.length - 1;
+                        })
+                        .map(point => {
+                            return `(${point.X.toString()}, ${point.Y.toString()})`;
+                        })}`
+                );
+            });
+            const response = await gptDrawingColor(hrefBase64, coords);
+            console.log(response);
+            const colorResponse = await gptAPICall(response, GPTCallType.COLOR, undefined);
+            console.log(colorResponse);
+            this.colorStrokes(colorResponse, drawing);
+        } catch (error) {
+            console.log('GPT call failed');
+        }
+    };
+
+    @undoBatch
+    colorStrokes = (res: string, drawing: Doc) => {
+        const colorList = res.match(/\{.*?\}/g);
+        const strokes = DocListCast(drawing[DocData].data);
+        colorList?.forEach((colors, index) => {
+            const strokeAndFill = colors.match(/#[0-9A-Fa-f]{6}/g);
+            if (strokeAndFill && strokeAndFill.length == 2) {
+                strokes[index][DocData].color = strokeAndFill[0];
+                const inkStroke = DocumentView.getDocumentView(strokes[index])?.ComponentView as InkingStroke;
+                const { inkData } = inkStroke.inkScaledData();
+                if (InkingStroke.IsClosed(inkData)) {
+                    strokes[index][DocData].fillColor = strokeAndFill[1];
+                }
+            }
+        });
+    };
+
+    async getIcon(doc: Doc) {
+        const docView = DocumentView.getDocumentView(doc);
+        console.log(doc);
+        if (docView) {
+            console.log(docView);
+            docView.ComponentView?.updateIcon?.();
+            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+        }
+        return undefined;
+    }
+
     render() {
         if (this._display) {
             return (
@@ -397,7 +490,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             color={SettingsManager.userColor}
                             onClick={this.handleSendClick}
                         />
-                        <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} />
+                        <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.setEdit} />
                         {this._showEditBox && (
                             <div
                                 style={{
-- 
cgit v1.2.3-70-g09d2


From 0ac79ba6a7ab19b4aafbc11dac9bab4781d4bd40 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 29 Aug 2024 13:07:53 -0400
Subject: merge cleanup cleanup

---
 eslint.config.mjs                                  |  67 +++++-
 src/ClientUtils.ts                                 |   9 +-
 src/client/cognitive_services/CognitiveServices.ts |  15 +-
 src/client/util/Scripting.ts                       |   4 +-
 src/client/util/SettingsManager.tsx                |   6 +-
 src/client/util/bezierFit.ts                       |  18 +-
 src/client/views/DocumentButtonBar.tsx             |  10 +-
 src/client/views/GestureOverlay.tsx                |  10 +-
 src/client/views/InkStrokeProperties.ts            | 268 +++++++++++----------
 src/client/views/PropertiesButtons.tsx             |   4 +-
 .../collections/CollectionStackedTimeline.tsx      |   2 +-
 .../views/collections/CollectionTreeView.tsx       |   4 -
 src/client/views/collections/TreeView.tsx          |   6 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  22 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  12 +-
 src/client/views/pdf/Annotation.tsx                |   1 +
 src/client/views/pdf/PDFViewer.tsx                 |   4 +-
 src/client/views/smartdraw/AnnotationPalette.tsx   |  20 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |   2 +-
 20 files changed, 256 insertions(+), 230 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/eslint.config.mjs b/eslint.config.mjs
index 12ad3300a..619966f20 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,14 +1,57 @@
-import globals from "globals";
-import pluginJs from "@eslint/js";
-import tseslint from "typescript-eslint";
-import pluginReact from "eslint-plugin-react";
-
+import pluginJs from '@eslint/js';
+import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js';
+import globals from 'globals';
+import tseslint from 'typescript-eslint';
 
 export default [
-  {files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
-  {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}},
-  {languageOptions: { globals: globals.browser }},
-  pluginJs.configs.recommended,
-  ...tseslint.configs.recommended,
-  pluginReact.configs.flat.recommended,
-];
\ No newline at end of file
+    {
+        languageOptions: { globals: { ...globals.browser, ...globals.node } },
+    },
+    pluginJs.configs.recommended,
+    ...tseslint.configs.recommended,
+    {
+        rules: {
+            'node/no-missing-import': 0,
+            'no-console': 'off',
+            'func-names': 'off',
+            'no-process-exit': 'off',
+            'object-shorthand': 'off',
+            'class-methods-use-this': 'off',
+            'single-quote': 'off',
+            'max-classes-per-file': 0,
+
+            'react/jsx-filename-extension': [
+                2,
+                {
+                    extensions: ['.js', '.jsx', '.ts', '.tsx'],
+                },
+            ],
+
+            'import/prefer-default-export': 'off',
+            'no-unused-expressions': 'off',
+            '@typescript-eslint/no-unused-expressions': 'off',
+            'prefer-template': 'off',
+            'no-inner-declarations': 'off',
+            'no-plusplus': 'off',
+            'no-multi-assign': 'off',
+            'no-underscore-dangle': 'off',
+            'no-nested-ternary': 'off',
+            'lines-between-class-members': 'off',
+            'no-shadow': 'off',
+            '@typescript-eslint/no-shadow': 'warn',
+            'no-unused-vars': 'off',
+            '@typescript-eslint/no-unused-vars': 'error',
+            '@typescript-eslint/no-namespace': 'off',
+            'react/destructuring-assignment': 0,
+            'prefer-arrow-callback': 'error',
+            'no-return-assign': 'error',
+            'no-await-in-loop': 'error',
+            'no-loop-func': 'error',
+            'no-cond-assign': 'error',
+            'no-use-before-define': 'error',
+            'no-explicit-any': 'error',
+            'no-restricted-globals': ['error', 'event'],
+        },
+    },
+    pluginReactConfig,
+];
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index dc52218c5..55801df81 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -82,10 +82,6 @@ export function returnEmptyFilter() {
     return [] as string[];
 }
 
-export function returnEmptyDoclist() {
-    return [] as any[];
-}
-
 export namespace ClientUtils {
     export const CLICK_TIME = 300;
     export const DRAG_THRESHOLD = 4;
@@ -449,11 +445,10 @@ export function smoothScrollHorizontal(duration: number, element: HTMLElement |
     animateScroll();
 }
 
-export function addStyleSheet(styleType: string = 'text/css') {
+export function addStyleSheet() {
     const style = document.createElement('style');
-    style.type = styleType;
     const sheets = document.head.appendChild(style);
-    return (sheets as any).sheet;
+    return sheets.sheet;
 }
 export function addStyleSheetRule(sheet: CSSStyleSheet | null, selector: string, css: string | { [key: string]: string }, selectorPrefix = '.') {
     const propText =
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 9f7701c54..3ee61cbfb 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -46,15 +46,14 @@ export enum Confidence {
 export namespace CognitiveServices {
     const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
         const apiKey = process.env[service.toUpperCase()];
-        if (apiKey) {
-            console.log(data);
+        if (!apiKey) {
             console.log(`No API key found for ${service}: ensure youe root directory has .env file with _CLIENT_${service.toUpperCase()}.`);
             return undefined;
         }
 
         let results: any;
         try {
-            results = await manager.requester('has', manager.converter(data), service).then(json => JSON.parse(json));
+            results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json));
         } catch (e) {
             throw e;
         }
@@ -138,14 +137,6 @@ export namespace CognitiveServices {
                     points: points.map(({ X: x, Y: y }) => `${x},${y}`).join(','),
                     language: 'en-US',
                 }));
-                console.log(
-                    JSON.stringify({
-                        version: 1,
-                        language: 'en-US',
-                        unit: 'mm',
-                        strokes,
-                    })
-                );
                 return JSON.stringify({
                     version: 1,
                     language: 'en-US',
@@ -345,7 +336,7 @@ export namespace CognitiveServices {
                         'Ocp-Apim-Subscription-Key': apiKey,
                     },
                 };
-                return request.post(options);
+                return rp.post(options);
             },
         };
 
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index cb314e3f1..c63d3d7cb 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,7 +1,7 @@
 // export const ts = (window as any).ts;
 // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
 // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
-// import typescriptlib from 'type_decls.d';
+import typescriptlib from 'type_decls.d';
 import * as ts from 'typescript';
 import { Doc, FieldType } from '../../fields/Doc';
 import { RefField } from '../../fields/RefField';
@@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     const funcScript = `(function(${paramString})${reqTypes} { ${body} })`;
     host.writeFile('file.ts', funcScript);
 
-    // if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
+    if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
     const program = ts.createProgram(['file.ts'], {}, host);
     const testResult = program.emit();
     const outputText = host.readFile('file.js');
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 9e49117a7..9200d68db 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -27,7 +27,7 @@ export enum ColorScheme {
 }
 
 @observer
-export class SettingsManager extends React.Component<{}> {
+export class SettingsManager extends React.Component<object> {
     // eslint-disable-next-line no-use-before-define
     public static Instance: SettingsManager;
     static _settingsStyle = addStyleSheet();
@@ -85,7 +85,7 @@ export class SettingsManager extends React.Component<{}> {
             if (this._playgroundMode) {
                 DocServer.Control.makeReadOnly();
                 addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' });
-            } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable();
+            } else if (ClientUtils.CurrentUserEmail() !== 'guest') DocServer.Control.makeEditable();
         }),
         'set playgorund mode'
     );
@@ -121,7 +121,7 @@ export class SettingsManager extends React.Component<{}> {
         'change color scheme'
     );
 
-    constructor(props: {}) {
+    constructor(props: object) {
         super(props);
         makeObservable(this);
         SettingsManager.Instance = this;
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 693676bc3..4c7f4a0ba 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -2,7 +2,6 @@
 /* eslint-disable prefer-destructuring */
 /* eslint-disable no-param-reassign */
 /* eslint-disable camelcase */
-import e from 'cors';
 import { Point } from '../../pen-gestures/ndollar';
 
 export enum SVGType {
@@ -628,7 +627,7 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
 
 export function SVGToBezier(name: SVGType, attributes: any): Point[] {
     switch (name) {
-        case 'line':
+        case 'line': {
             const x1 = parseInt(attributes.x1);
             const x2 = parseInt(attributes.x2);
             const y1 = parseInt(attributes.y1);
@@ -639,8 +638,9 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 { X: x2, Y: y2 },
                 { X: x2, Y: y2 },
             ];
+        }
         case 'circle':
-        case 'ellipse':
+        case 'ellipse': {
             const c = 0.551915024494;
             const centerX = parseInt(attributes.cx);
             const centerY = parseInt(attributes.cy);
@@ -664,7 +664,8 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 { X: centerX - c * radiusX, Y: centerY + radiusY },
                 { X: centerX, Y: centerY + radiusY },
             ];
-        case 'rect':
+        }
+        case 'rect': {
             const x = parseInt(attributes.x);
             const y = parseInt(attributes.y);
             const width = parseInt(attributes.width);
@@ -687,14 +688,15 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 { X: x, Y: y },
                 { X: x, Y: y },
             ];
-        case 'path':
+        }
+        case 'path': {
             const coordList: Point[] = [];
             const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/);
             coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
             const matches: RegExpMatchArray[] = Array.from(
                 attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
             );
-            let lastPt: Point;
+            let lastPt: Point = { X: 0, Y: 0 };
             matches.forEach(match => {
                 if (match[0].startsWith('Q')) {
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
@@ -725,7 +727,8 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 coordList.pop();
             }
             return coordList;
-        case 'polygon':
+        }
+        case 'polygon': {
             const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g));
             let list: Point[] = [];
             coords.forEach(coord => {
@@ -737,6 +740,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
             const firstPts = list.splice(0, 2);
             list = list.concat(firstPts);
             return list;
+        }
         default:
             return [];
     }
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 8aa4c2093..096f058ad 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -9,7 +9,7 @@ import * as React from 'react';
 import { FaEdit } from 'react-icons/fa';
 import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
 import { Cast, DocCast } from '../../fields/Types';
 import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils';
 import { CalendarManager } from '../util/CalendarManager';
@@ -17,7 +17,7 @@ import { DictationManager } from '../util/DictationManager';
 import { DragManager } from '../util/DragManager';
 import { dropActionType } from '../util/DropActionTypes';
 import { SharingManager } from '../util/SharingManager';
-import { UndoManager, undoable, undoBatch } from '../util/UndoManager';
+import { UndoManager, undoable } from '../util/UndoManager';
 import './DocumentButtonBar.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { PinProps } from './PinFuncs';
@@ -29,7 +29,6 @@ import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
 import { AnnotationPalette } from './smartdraw/AnnotationPalette';
-import { DocData } from '../../fields/DocSymbols';
 
 @observer
 export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: unknown }> {
@@ -240,10 +239,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         );
     }
 
-    @undoBatch
-    saveAnno = action(async (targetDoc: Doc) => {
-        await AnnotationPalette.addToPalette(targetDoc);
-    });
+    saveAnno = undoable(async (targetDoc: Doc) => await AnnotationPalette.addToPalette(targetDoc), 'save to palette');
 
     @computed
     get saveAnnoButton() {
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 63b472faf..befd19f6e 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -129,7 +129,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     }
     @action
     onPointerUp = () => {
-        console.log('pointer up');
         DocumentView.DownDocView = undefined;
         if (this._points.length > 1) {
             const B = this.svgBounds;
@@ -145,9 +144,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             else {
                 // need to decide when to turn gestures back on
                 const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize([points]);
-                console.log(points);
                 let actionPerformed = false;
-                console.log(result);
                 if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
                     switch (result.Name) {
                         case Gestures.Line:
@@ -156,15 +153,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                         case Gestures.Circle:
                             GestureOverlay.makeBezierPolygon(this._points, result.Name, true);
                             actionPerformed = this.dispatchGesture(result.Name);
-                            console.log(result.Name);
-                            console.log();
                             break;
                         case Gestures.Scribble:
                             console.log('scribble');
                             break;
                         case Gestures.RightAngle:
                             console.log('RightAngle');
-                        default:
+                            break;
+                        default: {
+                            /* empty */
+                        }
                     }
                 }
 
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index ffda126f4..b5cd72fa7 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -9,7 +9,7 @@ import { Cast, NumCast, toList } from '../../fields/Types';
 import { Gestures, PointData } from '../../pen-gestures/GestureTypes';
 import { Point } from '../../pen-gestures/ndollar';
 import { DocumentType } from '../documents/DocumentTypes';
-import { undoBatch } from '../util/UndoManager';
+import { undoBatch, undoable } from '../util/UndoManager';
 import { FitOneCurve } from '../util/bezierFit';
 import { InkingStroke } from './InkingStroke';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
@@ -92,8 +92,7 @@ export class InkStrokeProperties {
      * @param i index of first control point of segment being split
      * @param control The list of all control points of the ink.
      */
-    @undoBatch
-    addPoints = (inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
+    addPoints = undoable((inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
         this.applyFunction(inkView, (view: DocumentView /* , ink: InkData */) => {
             const doc = view.Document;
             const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]];
@@ -109,7 +108,7 @@ export class InkStrokeProperties {
 
             return controls;
         });
-    };
+    }, 'add ink points');
 
     /**
      * Scales a handle point of a control point that is adjacent to a newly added one.
@@ -164,46 +163,48 @@ export class InkStrokeProperties {
     /**
      * Deletes the current control point of the selected ink instance.
      */
-    @undoBatch
-    deletePoints = (inkView: DocumentView, preserve: boolean) =>
-        this.applyFunction(
-            inkView,
-            (view: DocumentView, ink: InkData) => {
-                const doc = view.Document;
-                const newPoints = ink.slice();
-                const brokenIndices = NumListCast(doc.brokenInkIndices);
-                if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
-                    newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
-                } else {
-                    const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
-                    const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
-                    const samples: Point[] = [];
-                    let startDir = { x: 0, y: 0 };
-                    let endDir = { x: 0, y: 0 };
-                    for (let i = 0; i < splicedPoints.length / 4; i++) {
-                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
-                        if (i === 0) startDir = bez.derivative(0);
-                        if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
-                        for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
-                            const pt = bez.compute(t);
-                            samples.push(new Point(pt.x, pt.y));
-                        }
-                    }
-                    const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                    if (error < 100) {
-                        newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
+    deletePoints = undoable(
+        (inkView: DocumentView, preserve: boolean) =>
+            this.applyFunction(
+                inkView,
+                (view: DocumentView, ink: InkData) => {
+                    const doc = view.Document;
+                    const newPoints = ink.slice();
+                    const brokenIndices = NumListCast(doc.brokenInkIndices);
+                    if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
+                        newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
                     } else {
-                        newPoints.splice(this._currentPoint - 2, 4);
+                        const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+                        const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+                        const samples: Point[] = [];
+                        let startDir = { x: 0, y: 0 };
+                        let endDir = { x: 0, y: 0 };
+                        for (let i = 0; i < splicedPoints.length / 4; i++) {
+                            const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                            if (i === 0) startDir = bez.derivative(0);
+                            if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+                            for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
+                                const pt = bez.compute(t);
+                                samples.push(new Point(pt.x, pt.y));
+                            }
+                        }
+                        const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                        if (error < 100) {
+                            newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
+                        } else {
+                            newPoints.splice(this._currentPoint - 2, 4);
+                        }
                     }
-                }
-                doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
-                runInAction(() => {
-                    this._currentPoint = -1;
-                });
-                return newPoints.length < 4 ? undefined : newPoints;
-            },
-            true
-        );
+                    doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
+                    runInAction(() => {
+                        this._currentPoint = -1;
+                    });
+                    return newPoints.length < 4 ? undefined : newPoints;
+                },
+                true
+            ),
+        'delete ink points'
+    );
 
     /**
      * Rotates ink stroke(s) about a point
@@ -211,8 +212,7 @@ export class InkStrokeProperties {
      * @param angle The angle at which to rotate the ink in radians.
      * @param scrpt The center point of the rotation in screen coordinates
      */
-    @undoBatch
-    rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
+    rotateInk = undoable((inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
         this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number /* , inkStrokeWidth: number */) => {
             const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
             return !inkCenterPt
@@ -224,7 +224,7 @@ export class InkStrokeProperties {
                       return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
                   });
         });
-    };
+    }, 'rotate ink');
 
     /**
      * Rotates ink stroke(s) about a point
@@ -232,8 +232,7 @@ export class InkStrokeProperties {
      * @param angle The angle at which to rotate the ink in radians.
      * @param scrpt The center point of the rotation in screen coordinates
      */
-    @undoBatch
-    stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
+    stretchInk = undoable((inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
         this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData) => {
             const ptFromScreen = view.ComponentView?.ptFromScreen;
             const ptToScreen = view.ComponentView?.ptToScreen;
@@ -247,77 +246,79 @@ export class InkStrokeProperties {
                       return ptFromScreen(newscrpt);
                   });
         });
-    };
+    }, 'stretch ink');
 
     /**
      * Handles the movement/scaling of a control point.
      */
-    @undoBatch
-    moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
-        inkView &&
-        this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
-            const order = controlIndex % 4;
-            const closed = InkingStroke.IsClosed(ink);
-            const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
-            if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
-                const cptBefore = ink[controlIndex];
-                const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
-                const newink = origInk.slice();
-                const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
-                const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
-                const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
-                if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice();
-                const samplesLeft: Point[] = [];
-                const samplesRight: Point[] = [];
-                let startDir = { x: 0, y: 0 };
-                let endDir = { x: 0, y: 0 };
-                for (let i = 0; i < nearestSeg / 4 + 1; i++) {
-                    const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
-                    if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
-                    if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
-                    for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) {
-                        const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
-                        samplesLeft.push(new Point(pt.x, pt.y));
+    moveControlPtHandle = undoable(
+        (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
+            inkView &&
+            this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+                const order = controlIndex % 4;
+                const closed = InkingStroke.IsClosed(ink);
+                const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
+                if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
+                    const cptBefore = ink[controlIndex];
+                    const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
+                    const newink = origInk.slice();
+                    const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+                    const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+                    const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
+                    if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice();
+                    const samplesLeft: Point[] = [];
+                    const samplesRight: Point[] = [];
+                    let startDir = { x: 0, y: 0 };
+                    let endDir = { x: 0, y: 0 };
+                    for (let i = 0; i < nearestSeg / 4 + 1; i++) {
+                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                        if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
+                        if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
+                        for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) {
+                            const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
+                            samplesLeft.push(new Point(pt.x, pt.y));
+                        }
                     }
-                }
-                let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
-                    const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
-                    if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
-                    if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
-                    for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) {
-                        const pt = bez.compute(Math.min(1, t));
-                        samplesRight.push(new Point(pt.x, pt.y));
+                    let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                    for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
+                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                        if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
+                        if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
+                        for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) {
+                            const pt = bez.compute(Math.min(1, t));
+                            samplesRight.push(new Point(pt.x, pt.y));
+                        }
                     }
+                    const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                    finalCtrls = finalCtrls.concat(rightCtrls);
+                    newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
+                    return newink;
                 }
-                const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                finalCtrls = finalCtrls.concat(rightCtrls);
-                newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
-                return newink;
-            }
 
-            return ink.map((pt, i) => {
-                const leftHandlePoint = order === 0 && i === controlIndex + 1;
-                const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
-                if (controlIndex === i || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1)) {
-                    return { X: pt.X + deltaX, Y: pt.Y + deltaY };
-                }
-                if (
-                    controlIndex === i ||
-                    leftHandlePoint ||
-                    rightHandlePoint ||
-                    (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
-                    ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) ||
-                    (order === 3 && i === controlIndex - 1) ||
-                    (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
-                    (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
-                    (ink[0].X === ink[ink.length - 1].X && ink[0].Y === ink[ink.length - 1].Y && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))
-                ) {
-                    return { X: pt.X + deltaX, Y: pt.Y + deltaY };
-                }
-                return pt;
-            });
-        });
+                return ink.map((pt, i) => {
+                    const leftHandlePoint = order === 0 && i === controlIndex + 1;
+                    const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
+                    if (controlIndex === i || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1)) {
+                        return { X: pt.X + deltaX, Y: pt.Y + deltaY };
+                    }
+                    if (
+                        controlIndex === i ||
+                        leftHandlePoint ||
+                        rightHandlePoint ||
+                        (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
+                        ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) ||
+                        (order === 3 && i === controlIndex - 1) ||
+                        (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
+                        (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
+                        (ink[0].X === ink[ink.length - 1].X && ink[0].Y === ink[ink.length - 1].Y && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))
+                    ) {
+                        return { X: pt.X + deltaX, Y: pt.Y + deltaY };
+                    }
+                    return pt;
+                });
+            }),
+        'move ink ctrl pt'
+    );
 
     public static nearestPtToStroke(ctrlPoints: { X: number; Y: number }[], refInkSpacePt: { X: number; Y: number }, excludeSegs?: number[]) {
         let distance = Number.MAX_SAFE_INTEGER;
@@ -470,26 +471,28 @@ export class InkStrokeProperties {
     /**
      * Handles the movement/scaling of a handle point.
      */
-    @undoBatch
-    moveTangentHandle = (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
-        this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
-            const doc = view.Document;
-            const closed = InkingStroke.IsClosed(ink);
-            const oldHandlePoint = ink[handleIndex];
-            const oppositeHandlePoint = ink[oppositeHandleIndex];
-            const controlPoint = ink[controlIndex];
-            const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY };
-            const inkCopy = ink.slice();
-            inkCopy[handleIndex] = newHandlePoint;
-            const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'));
-            const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
-            // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
-            if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
-                const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
-                inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
-            }
-            return inkCopy;
-        });
+    moveTangentHandle = undoable(
+        (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
+            this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+                const doc = view.Document;
+                const closed = InkingStroke.IsClosed(ink);
+                const oldHandlePoint = ink[handleIndex];
+                const oppositeHandlePoint = ink[oppositeHandleIndex];
+                const controlPoint = ink[controlIndex];
+                const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY };
+                const inkCopy = ink.slice();
+                inkCopy[handleIndex] = newHandlePoint;
+                const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'));
+                const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
+                // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
+                if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
+                    const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
+                    inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
+                }
+                return inkCopy;
+            }),
+        'move ink tangent'
+    );
 
     /**
      * Function that "smooths" ink strokes by using the gesture recognizer to detect shapes and
@@ -497,8 +500,7 @@ export class InkStrokeProperties {
      * @param inkDocs
      * @param tolerance Determines how strong the smooth effect will be
      */
-    @undoBatch
-    smoothInkStrokes = (inkDocs: Doc[], tolerance: number = 5) => {
+    smoothInkStrokes = undoable((inkDocs: Doc[], tolerance: number = 5) => {
         inkDocs.forEach(inkDoc => {
             const inkView = DocumentView.getDocumentView(inkDoc);
             const inkStroke = inkView?.ComponentView as InkingStroke;
@@ -530,5 +532,5 @@ export class InkStrokeProperties {
                 }
             }
         });
-    };
+    }, 'smooth ink stroke');
 }
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index b94041642..f346d4ba8 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -13,7 +13,7 @@ import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtit
 import { RxWidth } from 'react-icons/rx';
 import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb';
 import { TfiBarChart } from 'react-icons/tfi';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
@@ -28,8 +28,6 @@ import { Colors } from './global/globalEnums';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { MarqueeOptionsMenu } from './collections/collectionFreeForm';
-import { InkStrokeProperties } from './InkStrokeProperties';
 
 @observer
 export class PropertiesButtons extends React.Component {
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 98ddc61f9..486c826b6 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -847,7 +847,7 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
                     styleProvider={this._props.styleProvider}
                     renderDepth={this._props.renderDepth + 1}
                     LayoutTemplate={undefined}
-                    LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())}
+                    LayoutTemplateString={LabelBox.LayoutString('data')}
                     isDocumentActive={this._props.isDocumentActive}
                     PanelWidth={width}
                     PanelHeight={height}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index a0e079158..a60cd98ac 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -50,7 +50,6 @@ export type collectionTreeViewProps = {
 export class CollectionTreeView extends CollectionSubView<Partial<collectionTreeViewProps>>() {
     public static AddTreeFunc = 'addTreeFolder(this.embedContainer)';
     private _treedropDisposer?: DragManager.DragDropDisposer;
-    private _mainEle?: HTMLDivElement;
     private _titleRef?: HTMLDivElement | HTMLInputElement | null;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _isDisposing = false; // notes that instance is in process of being disposed
@@ -81,8 +80,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
 
     @observable _titleHeight = 0; // height of the title bar
 
-    MainEle = () => this._mainEle;
-
     // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
     @observable _isAnyChildContentActive = false;
     whenChildContentsActiveChanged = action((isActive: boolean) => {
@@ -132,7 +129,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     };
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this._treedropDisposer?.();
-        this._mainEle = ele;
         if (ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this));
     };
 
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 847ff5491..015f77ffd 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -472,7 +472,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(ref);
         return new Transform(-translateX, -translateY, 1).scale(1 / scale);
     };
-    docTransform = () => this.refTransform(this._dref?.ContentRef?.current);
+    docTransform = () => this.refTransform(this._dref?.ContentDiv);
     getTransform = () => this.refTransform(this._tref.current);
     embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1);
     embeddedPanelHeight = () => {
@@ -754,7 +754,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     }
 
     get onCheckedClick() {
-        return this.Document.type === DocumentType.COL ? undefined : this._props.onCheckedClick?.() ?? ScriptCast(this.Document.onCheckedClick);
+        return this.Document.type === DocumentType.COL ? undefined : (this._props.onCheckedClick?.() ?? ScriptCast(this.Document.onCheckedClick));
     }
 
     @action
@@ -779,7 +779,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         TraceMobx();
         const iconType = (this.treeView._props.styleProvider?.(this.Document, this.treeView._props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':treeOpen' : !this.childDocs.length ? ':empty' : '')) as string) || 'question';
         const color = SettingsManager.userColor;
-        const checked = this.onCheckedClick ? this.Document.treeView_Checked ?? 'unchecked' : undefined;
+        const checked = this.onCheckedClick ? (this.Document.treeView_Checked ?? 'unchecked') : undefined;
         return (
             <div
                 className={`bullet${this.treeView.outlineMode ? '-outline' : ''}`}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d95bc528d..d51b1cd3a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -328,6 +328,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 }
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) {
+ScriptingGlobals.add(function gotoFrame(doc: Doc, newFrame: number) {
     CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame);
 });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3ff3f49dc..4cc7eb0ff 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -156,13 +156,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     /// disable pointer events on content when there's an enabled onClick script (and not in explore mode) and the contents aren't forced active, or if contents are marked inactive
     @computed get _contentPointerEvents() {
         TraceMobx();
-        return this._props.contentPointerEvents ??
+        return (this._props.contentPointerEvents ??
             ((!this.disableClickScriptFunc && //
                 this.onClickHdlr &&
                 !SnappingManager.ExploreMode &&
                 !this.layoutDoc.layout_isSvg &&
                 this.isContentActive() !== true) ||
-                this.isContentActive() === false)
+                this.isContentActive() === false))
             ? 'none'
             : this._pointerEvents;
     }
@@ -892,7 +892,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const showTitle = this.showTitle?.split(':')[0];
         return !DocCast(this.Document) || GetEffectiveAcl(this.dataDoc) === AclPrivate
             ? null
-            : this.docContents ?? (
+            : (this.docContents ?? (
                   <div
                       className="documentView-node"
                       id={this.Document.type !== DocumentType.LINK ? this._docView?.DocUniqueId : undefined}
@@ -918,7 +918,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                       )}
                       {this.widgetDecorations ?? null}
                   </div>
-              );
+              ));
     };
 
     render() {
@@ -994,7 +994,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             bottom: dir === PresEffectDirection.Bottom,
             opposite: true,
             delay: 0,
-            duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
+            duration,
         };
 
         const timing = StrCast(presEffectDoc?.presentation_effectTiming);
@@ -1083,13 +1083,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done.
     ) => Promise<void>;
     public static linkCommonAncestor: (link: Doc) => DocumentView | undefined;
-    // pin func
+    /**
+     * Pins a Doc to the current presentation trail. (see TabDocView for implementation)
+     */
     public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void;
-    // gesture
+    /**
+     * The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event).  Used by GestureOverlay to determine the doc a gesture should apply to.
+     */
     public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event.  used by GestureOverlay to determine the doc a gesture should apply to.
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
-    public ContentRef = React.createRef<HTMLDivElement>();
     private _htmlOverlayEffect: Opt<Doc>;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _viewTimer: NodeJS.Timeout | undefined;
@@ -1468,11 +1471,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                 {!this.Document || !this._props.PanelWidth() ? null : (
                     <div
                         className="contentFittingDocumentView-previewDoc"
-                        ref={this.ContentRef}
                         style={{
                             transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
                             width: xshift ?? `${this._props.PanelWidth() - this.Xshift * 2}px`,
-                            height: this._props.forceAutoHeight ? undefined : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(this.effectiveNativeHeight / this.effectiveNativeWidth) * this._props.PanelWidth()}px`),
+                            height: this._props.forceAutoHeight ? undefined : (yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(this.effectiveNativeHeight / this.effectiveNativeWidth) * this._props.PanelWidth()}px`)),
                         }}>
                         <DocumentViewInternal
                             {...this._props}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0c0667575..38b22fad2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -101,9 +101,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
     static _globalHighlightsCache: string = '';
     static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
-    static _highlightStyleSheet: any = addStyleSheet();
-    static _bulletStyleSheet: any = addStyleSheet();
-    static _userStyleSheet: any = addStyleSheet();
+    static _highlightStyleSheet = addStyleSheet();
+    static _bulletStyleSheet = addStyleSheet();
+    static _userStyleSheet = addStyleSheet();
     static _hadSelection: boolean = false;
     private _oldWheel: HTMLDivElement | null = null;
     private _selectionHTML: string | undefined;
@@ -361,7 +361,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     }
                 }
             } else {
-                const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data!;
+                const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data;
                 if (jsonstring) {
                     const json = JSON.parse(jsonstring);
                     json.selection = state.toJSON().selection;
@@ -1897,11 +1897,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     setHeight={this.setSidebarHeight}
                 />
             ) : (
-                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}>
+                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}>
                     <ComponentTag
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
-                        ref={this._sidebarTagRef as any}
+                        ref={this._sidebarTagRef}
                         setContentView={emptyFunction}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 7dd4047c1..3bd42873c 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -111,6 +111,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> {
     outline = () => (this.linkHighlighted ? 'solid 1px lightBlue' : undefined);
     background = () => (this._props.annoDoc[Highlight] ? 'orange' : StrCast(this._props.annoDoc.backgroundColor));
     render() {
+        const forceRenderHack = [this.background(), this.outline(), this.opacity()]; // forces a re-render when these change -- because RegionAnnotation doesn't do this internally..
         return (
             <div style={{ display: this._props.annoDoc.textCopied && !Doc.GetBrushHighlightStatus(this._props.annoDoc) ? 'none' : undefined }}>
                 {StrListCast(this._props.annoDoc.text_inlineAnnotations)
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5984905d0..20719442a 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -52,7 +52,7 @@ interface IViewerProps extends FieldViewProps {
  */
 @observer
 export class PDFViewer extends ObservableReactComponent<IViewerProps> {
-    static _annotationStyle: any = addStyleSheet();
+    static _annotationStyle = addStyleSheet();
 
     constructor(props: IViewerProps) {
         super(props);
@@ -522,7 +522,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
             if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none';
             const isInk = doc.layout_isSvg && !props?.LayoutTemplateString;
-            return isInk ? 'visiblePainted' : 'all';
+            if (isInk) return 'visiblePainted';
         }
         return this._props.styleProvider?.(doc, props, property);
     };
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index c296138a8..22b8b7b67 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -6,25 +6,25 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
-import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
+import { Copy } from '../../../fields/FieldSymbols';
 import { ImageCast } from '../../../fields/Types';
-import { emptyFunction } from '../../../Utils';
+import { ImageField } from '../../../fields/URLField';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { makeUserTemplateImage } from '../../util/DropConverter';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
-import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DefaultStyleProvider } from '../StyleProvider';
 import './AnnotationPalette.scss';
 import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { ImageField } from '../../../fields/URLField';
-import { Copy } from '../../../fields/FieldSymbols';
 
 interface AnnotationPaletteProps {
     Document: Doc;
@@ -204,7 +204,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                             addDocument={undefined}
                             addDocTab={DocumentViewInternal.addDocTabFunc}
                             pinToPres={DocumentView.PinDoc}
-                            containerViewPath={returnEmptyDoclist}
+                            containerViewPath={returnEmptyDocViewList}
                             styleProvider={DefaultStyleProvider}
                             removeDocument={returnFalse}
                             ScreenToLocalTransform={Transform.Identity}
@@ -328,7 +328,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                             addDocument={undefined}
                             addDocTab={DocumentViewInternal.addDocTabFunc}
                             pinToPres={DocumentView.PinDoc}
-                            containerViewPath={returnEmptyDoclist}
+                            containerViewPath={returnEmptyDocViewList}
                             styleProvider={DefaultStyleProvider}
                             removeDocument={returnFalse}
                             ScreenToLocalTransform={Transform.Identity}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 368970f75..4679941fb 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -279,7 +279,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             const svgStrokes: INode[] = svgObject.children;
             const strokeData: [InkData, string, string][] = [];
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            svgStrokes.forEach((child: any) => {
+            svgStrokes.forEach((child) => {
                 const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
                 strokeData.push([
                     convertedBezier.map(point => {
-- 
cgit v1.2.3-70-g09d2


From b1692a7435ae9698eff618bef0e370fe3eb89572 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 29 Aug 2024 13:59:00 -0400
Subject: from last cleanup

---
 src/client/views/InkStrokeProperties.ts            | 140 +++++------
 src/client/views/LightboxView.tsx                  |   3 +-
 src/client/views/MainView.tsx                      |   4 +-
 src/client/views/MarqueeAnnotator.tsx              |   9 +-
 src/client/views/PropertiesView.tsx                |  70 ++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  24 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  10 +-
 src/client/views/global/globalScripts.ts           |   4 -
 src/client/views/nodes/LabelBigText.js             | 270 ---------------------
 .../views/nodes/formattedText/FormattedTextBox.tsx |   6 +-
 src/client/views/pdf/AnchorMenu.tsx                |  15 +-
 src/client/views/pdf/Annotation.tsx                |   3 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  10 +-
 13 files changed, 127 insertions(+), 441 deletions(-)
 delete mode 100644 src/client/views/nodes/LabelBigText.js

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index b5cd72fa7..13807c25f 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,22 +1,22 @@
 import { Bezier } from 'bezier-js';
 import * as _ from 'lodash';
 import { action, makeObservable, observable, reaction, runInAction } from 'mobx';
+import simplify from 'simplify-js';
 import { Doc, NumListCast, Opt } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
-import { Cast, NumCast, toList } from '../../fields/Types';
+import { Cast, NumCast } from '../../fields/Types';
 import { Gestures, PointData } from '../../pen-gestures/GestureTypes';
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
 import { Point } from '../../pen-gestures/ndollar';
 import { DocumentType } from '../documents/DocumentTypes';
-import { undoBatch, undoable } from '../util/UndoManager';
+import { undoable } from '../util/UndoManager';
 import { FitOneCurve } from '../util/bezierFit';
+import { GestureOverlay } from './GestureOverlay';
 import { InkingStroke } from './InkingStroke';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentView } from './nodes/DocumentView';
-import simplify from 'simplify-js';
-import { GestureUtils } from '../../pen-gestures/GestureUtils';
-import { GestureOverlay } from './GestureOverlay';
 
 export class InkStrokeProperties {
     // eslint-disable-next-line no-use-before-define
@@ -163,48 +163,46 @@ export class InkStrokeProperties {
     /**
      * Deletes the current control point of the selected ink instance.
      */
-    deletePoints = undoable(
-        (inkView: DocumentView, preserve: boolean) =>
-            this.applyFunction(
-                inkView,
-                (view: DocumentView, ink: InkData) => {
-                    const doc = view.Document;
-                    const newPoints = ink.slice();
-                    const brokenIndices = NumListCast(doc.brokenInkIndices);
-                    if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
-                        newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
-                    } else {
-                        const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
-                        const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
-                        const samples: Point[] = [];
-                        let startDir = { x: 0, y: 0 };
-                        let endDir = { x: 0, y: 0 };
-                        for (let i = 0; i < splicedPoints.length / 4; i++) {
-                            const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
-                            if (i === 0) startDir = bez.derivative(0);
-                            if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
-                            for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
-                                const pt = bez.compute(t);
-                                samples.push(new Point(pt.x, pt.y));
-                            }
-                        }
-                        const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                        if (error < 100) {
-                            newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
-                        } else {
-                            newPoints.splice(this._currentPoint - 2, 4);
+    deletePoints = undoable((inkView: DocumentView, preserve: boolean) => {
+        this.applyFunction(
+            inkView,
+            (view: DocumentView, ink: InkData) => {
+                const doc = view.Document;
+                const newPoints = ink.slice();
+                const brokenIndices = NumListCast(doc.brokenInkIndices);
+                if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
+                    newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
+                } else {
+                    const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+                    const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+                    const samples: Point[] = [];
+                    let startDir = { x: 0, y: 0 };
+                    let endDir = { x: 0, y: 0 };
+                    for (let i = 0; i < splicedPoints.length / 4; i++) {
+                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                        if (i === 0) startDir = bez.derivative(0);
+                        if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+                        for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
+                            const pt = bez.compute(t);
+                            samples.push(new Point(pt.x, pt.y));
                         }
                     }
-                    doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
-                    runInAction(() => {
-                        this._currentPoint = -1;
-                    });
-                    return newPoints.length < 4 ? undefined : newPoints;
-                },
-                true
-            ),
-        'delete ink points'
-    );
+                    const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                    if (error < 100) {
+                        newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
+                    } else {
+                        newPoints.splice(this._currentPoint - 2, 4);
+                    }
+                }
+                doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
+                runInAction(() => {
+                    this._currentPoint = -1;
+                });
+                return newPoints.length < 4 ? undefined : newPoints;
+            },
+            true
+        );
+    }, 'delete ink points');
 
     /**
      * Rotates ink stroke(s) about a point
@@ -251,9 +249,8 @@ export class InkStrokeProperties {
     /**
      * Handles the movement/scaling of a control point.
      */
-    moveControlPtHandle = undoable(
-        (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
-            inkView &&
+    moveControlPtHandle = undoable((inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) => {
+        inkView &&
             this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
                 const order = controlIndex % 4;
                 const closed = InkingStroke.IsClosed(ink);
@@ -316,9 +313,8 @@ export class InkStrokeProperties {
                     }
                     return pt;
                 });
-            }),
-        'move ink ctrl pt'
-    );
+            });
+    }, 'move ink ctrl pt');
 
     public static nearestPtToStroke(ctrlPoints: { X: number; Y: number }[], refInkSpacePt: { X: number; Y: number }, excludeSegs?: number[]) {
         let distance = Number.MAX_SAFE_INTEGER;
@@ -471,28 +467,26 @@ export class InkStrokeProperties {
     /**
      * Handles the movement/scaling of a handle point.
      */
-    moveTangentHandle = undoable(
-        (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
-            this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
-                const doc = view.Document;
-                const closed = InkingStroke.IsClosed(ink);
-                const oldHandlePoint = ink[handleIndex];
-                const oppositeHandlePoint = ink[oppositeHandleIndex];
-                const controlPoint = ink[controlIndex];
-                const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY };
-                const inkCopy = ink.slice();
-                inkCopy[handleIndex] = newHandlePoint;
-                const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'));
-                const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
-                // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
-                if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
-                    const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
-                    inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
-                }
-                return inkCopy;
-            }),
-        'move ink tangent'
-    );
+    moveTangentHandle = undoable((inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => {
+        this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+            const doc = view.Document;
+            const closed = InkingStroke.IsClosed(ink);
+            const oldHandlePoint = ink[handleIndex];
+            const oppositeHandlePoint = ink[oppositeHandleIndex];
+            const controlPoint = ink[controlIndex];
+            const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY };
+            const inkCopy = ink.slice();
+            inkCopy[handleIndex] = newHandlePoint;
+            const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'));
+            const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
+            // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
+            if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
+                const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
+                inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
+            }
+            return inkCopy;
+        });
+    }, 'move ink tangent');
 
     /**
      * Function that "smooths" ink strokes by using the gesture recognizer to detect shapes and
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 651fb5888..77a2c1b86 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -22,7 +22,6 @@ import { DefaultStyleProvider, returnEmptyDocViewList, wavyBorderPath } from './
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { AnnotationPalette } from './smartdraw/AnnotationPalette';
-import { DocData } from '../../fields/DocSymbols';
 
 interface LightboxViewProps {
     PanelWidth: number;
@@ -58,6 +57,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     }[] = [];
     private _savedState: LightboxSavedState = {};
     private _history: { doc: Doc; target?: Doc }[] = [];
+    private _annoPaletteView: AnnotationPalette | null = null;
     @observable private _future: Doc[] = [];
     @observable private _layoutTemplate: Opt<Doc> = undefined;
     @observable private _layoutTemplateString: Opt<string> = undefined;
@@ -65,7 +65,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     @observable private _docTarget: Opt<Doc> = undefined;
     @observable private _docView: Opt<DocumentView> = undefined;
     @observable private _showPalette: boolean = false;
-    private _annoPaletteView: AnnotationPalette | null = null;
 
     @computed get leftBorder() { return Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]); } // prettier-ignore
     @computed get topBorder()  { return Math.min(this._props.PanelHeight / 4, this._props.maxBorder[1]); } // prettier-ignore
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2a983a29f..6118cee92 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -580,9 +580,7 @@ export class MainView extends ObservableReactComponent<object> {
         DocumentManager.removeOverlayViews();
         Doc.linkFollowUnhighlight();
         const targets = document.elementsFromPoint(e.x, e.y);
-        const targetClasses: string[] = targets.map(target => {
-            return target.className.toString();
-        });
+        const targetClasses = targets.map(target => target.className.toString());
         if (targets.length) {
             let targClass = targets[0].className.toString();
             for (let i = 0; i < targets.length - 1; i++) {
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 0d2a11ec4..90323086c 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -27,7 +27,7 @@ export interface MarqueeAnnotatorProps {
     containerOffset?: () => number[];
     marqueeContainer: HTMLDivElement;
     docView: () => DocumentView;
-    savedAnnotations: () => ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>;
+    savedAnnotations: () => ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>;
     selectionText: () => string;
     annotationLayer: HTMLDivElement;
     addDocument: (doc: Doc) => boolean;
@@ -60,7 +60,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
     });
 
     @undoBatch
-    makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>): Opt<Doc> => {
+    makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>): Opt<Doc> => {
         const savedAnnoMap = savedAnnotations?.values() && Array.from(savedAnnotations?.values()).length ? savedAnnotations : this.props.savedAnnotations();
         if (savedAnnoMap.size === 0) return undefined;
         const savedAnnos = Array.from(savedAnnoMap.values())[0];
@@ -128,7 +128,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         savedAnnoMap.clear();
         return textRegionAnno;
     };
-
     @action
     highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
         // creates annotation documents for current highlights
@@ -138,7 +137,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         return annotationDoc as Doc;
     };
 
-    public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>, annotationLayer: HTMLDivElement & { marqueeing?: boolean}, div: HTMLDivElement, page: number) => {
+    public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>, annotationLayer: HTMLDivElement & { marqueeing?: boolean }, div: HTMLDivElement, page: number) => {
         div.style.backgroundColor = '#ACCEF7';
         div.style.opacity = '0.5';
         annotationLayer.append(div);
@@ -266,7 +265,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         if (!this.isEmpty && marqueeStyle) {
             // configure and show the annotation/link menu if a the drag region is big enough
             // copy the temporary marquee to allow for multiple selections (not currently available though).
-            const copy: (HTMLDivElement & {marqueeing?: boolean}) = document.createElement('div');
+            const copy: HTMLDivElement & { marqueeing?: boolean } = document.createElement('div');
             const scale = (this.props.scaling?.() || 1) * NumCast(this.props.Document._freeform_scale, 1);
             ['border', 'opacity', 'top', 'left', 'width', 'height'].forEach(prop => {
                 copy.style[prop as unknown as number] = marqueeStyle[prop as unknown as number]; // bcz: hack to get around TS type checking for array index with strings
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index bbe422e9e..6383d4947 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -813,6 +813,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
     }
 
+    @computed get selectedStrokes() {
+        return this.containsInkDoc ? DocListCast(this.selectedDoc[DocData].data) : this.selectedDoc ? [this.selectedDoc] : [];
+    }
     @computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore
     set shapeXps(value)  { this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100); } // prettier-ignore
     @computed get shapeYps() { return NumCast(this.selectedDoc?.y); } // prettier-ignore
@@ -822,16 +825,11 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore
     set shapeHgt(value)  { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore
     @computed get strokeThk(){ return this.containsInkDoc ?  NumCast(this.inkDoc?.[DocData].stroke_width) : NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
-    set strokeThk(value) { 
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_width = Math.round(value * 100) / 100;
-            })
-        } else {
-            this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); 
-        }
-    } // prettier-ignore
+    set strokeThk(value) {
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_width = Math.round(value * 100) / 100;
+        });
+    }
 
     @computed get hgtInput() {
         return this.inputBoxDuo(
@@ -885,14 +883,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     @computed get colorStk() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].color) : StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
     set colorStk(value) {
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].color = value || undefined;
-            });
-        } else {
-            this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined);
-        }
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].color = value || undefined;
+        });
     }
 
     colorButton(value: string, type: string, setter: () => void) {
@@ -1019,14 +1012,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @computed get dashdStk() {  return this.containsInkDoc? this.inkDoc?.stroke_dash || '' : this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
     set dashdStk(value) {
         value && (this._lastDash = value as string);
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_dash = value ? this._lastDash : undefined;
-            });
-        } else {
-            this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
-        }
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_dash = value ? this._lastDash : undefined;
+        });
     }
     @computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore
     set widthStk(value) {
@@ -1034,13 +1022,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     @computed get markScal() { return Number(this.getField('stroke_markerScale') || '1'); } // prettier-ignore
     set markScal(value) {
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_markerScale = Number(value);
-            });
-        }
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_markerScale = Number(value);
+        });
     }
     @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '10'); } // prettier-ignore
     set smoothAmt(value) {
@@ -1048,23 +1032,15 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     @computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore
     set markHead(value) {
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_startMarker = value;
-            });
-        }
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_startMarker = value;
+        });
     }
     @computed get markTail() {  return this.getField('stroke_endMarker') || ''; } // prettier-ignore
     set markTail(value) {
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_endMarker = value;
-            });
-        }
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_endMarker = value;
+        });
     }
 
     regInput = (key: string, value: string | number | undefined, setter: (val: string) => void) => (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2963a8f53..7c7764f1d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -89,8 +89,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     private _clusters = new CollectionFreeFormClusters(this);
-    private _oldWheel: any;
-    private _panZoomTransitionTimer: any;
+    private _oldWheel: HTMLDivElement | null = null;
+    private _panZoomTransitionTimer: NodeJS.Timeout | undefined = undefined;
+    private _brushtimer: NodeJS.Timeout | undefined = undefined;
+    private _brushtimer1: NodeJS.Timeout | undefined = undefined;
     private _lastX: number = 0;
     private _lastY: number = 0;
     private _downX: number = 0;
@@ -99,8 +101,6 @@ 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';
@@ -142,12 +142,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @computed get childPointerEvents() {
         return SnappingManager.IsResizing
             ? 'none'
-            : this._props.childPointerEvents?.() ??
+            : (this._props.childPointerEvents?.() ??
                   (this._props.viewDefDivClick || //
                   (this.layoutEngine === computePassLayout.name && !this._props.isSelected()) ||
                   this.isContentActive() === false
                       ? 'none'
-                      : this._props.pointerEvents?.());
+                      : this._props.pointerEvents?.()));
     }
     @computed get contentViews() {
         const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele);
@@ -376,14 +376,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const xfToCollection = options?.docTransform ?? Transform.Identity();
         const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
         const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
-        const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale ?? 0.75 : undefined);
+        const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? (options?.zoomScale ?? 0.75) : undefined);
 
         // focus on the document in the collection
         const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
         if (didMove) options.didMove = true;
         // glr: freeform transform speed can be set by adjusting presentation_transition field - needs a way of knowing when presentation is not active...
         if (didMove) {
-            const focusTime = options?.instant ? 0 : options.zoomTime ?? 500;
+            const focusTime = options?.instant ? 0 : (options.zoomTime ?? 500);
             (options.zoomScale ?? options.willZoomCentered) && scale && (this.Document[this.scaleFieldKey] = scale);
             this.setPan(panX, panY, focusTime); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
             return focusTime;
@@ -1546,7 +1546,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return ret;
     };
     childPointerEventsFunc = () => this._childPointerEvents;
-    childContentsActive = () => (this._props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)();
+    childContentsActive = () => ((this._props.childContentsActive ?? this.isContentActive() === false) ? returnFalse : emptyFunction)();
     getChildDocView(entry: PoolData) {
         const childLayout = entry.pair.layout;
         const childData = entry.pair.data;
@@ -1722,7 +1722,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 ? false : !this._renderCutoffData.get(doc[Id] + '')));
+    renderCutoffProvider = computedFn((doc: Doc) => (this.Document.isTemplateDoc || this.Document.isTemplateForField ? false : !this._renderCutoffData.get(doc[Id] + '')));
 
     doEngineLayout(
         poolData: Map<string, PoolData>,
@@ -1982,7 +1982,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 icon: 'eye',
             });
         appearanceItems.push({ description: `Pin View`, event: () => this._props.pinToPres(this.Document, { pinViewport: MarqueeView.CurViewBounds(this.dataDoc, this._props.PanelWidth(), this._props.PanelHeight()) }), icon: 'map-pin' });
-        !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' });
+        !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: () => this.updateIcon(), icon: 'compress-arrows-alt' });
         this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
 
         this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
@@ -2080,7 +2080,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 ? Number.MAX_VALUE : 5;
+            const loadIncrement = this.Document.isTemplateDoc || this.Document.isTemplateForField ? Number.MAX_VALUE : 5;
             for (let i = 0; i < Math.min(layoutUnrendered.length, loadIncrement); i++) {
                 this._renderCutoffData.set(layoutUnrendered[i][Id] + '', true);
             }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index f85727661..bfb1d12cb 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -8,7 +8,7 @@ import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSy
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
-import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { GetEffectiveAcl } from '../../../../fields/util';
 import { DocUtils } from '../../../documents/DocUtils';
@@ -22,7 +22,7 @@ import { MainView } from '../../MainView';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
 import { PreviewCursor } from '../../PreviewCursor';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../../nodes/DocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -30,12 +30,6 @@ import { SubCollectionViewProps } from '../CollectionSubView';
 import { ImageLabelBoxData } from './ImageLabelBox';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './MarqueeView.scss';
-import { collectionOf, points } from '@turf/turf';
-import { InkingStroke } from '../../InkingStroke';
-import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { Gestures } from '../../../../pen-gestures/GestureTypes';
-import { GestureOverlay } from '../../GestureOverlay';
-import { InkStrokeProperties } from '../../InkStrokeProperties';
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 50319c466..68d287ac8 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -36,9 +36,6 @@ import { ImageBox } from '../nodes/ImageBox';
 import { VideoBox } from '../nodes/VideoBox';
 import { WebBox } from '../nodes/WebBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-// import { InkTranscription } from '../InkTranscription';
-
-// import { InkTranscription } from '../InkTranscription';
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function IsNoneSelected() {
@@ -367,7 +364,6 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
 export function createInkGroup(/* inksToGroup?: Doc[], isSubGroup?: boolean */) {
     // TODO nda - if document being added to is a inkGrouping then we can just add to that group
     if (Doc.ActiveTool === InkTool.Write) {
-        console.log('create inking group ');
         CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
             // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
             const selected = ffView.unprocessedDocs;
diff --git a/src/client/views/nodes/LabelBigText.js b/src/client/views/nodes/LabelBigText.js
deleted file mode 100644
index 290152cd0..000000000
--- a/src/client/views/nodes/LabelBigText.js
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
-Brorlandi/big-text.js v1.0.0, 2017
-Adapted from DanielHoffmann/jquery-bigtext, v1.3.0, May 2014
-And from Jetroid/bigtext.js v1.0.0, September 2016
-
-Usage:
-BigText("#myElement",{
-    rotateText: {Number}, (null)
-    fontSizeFactor: {Number}, (0.8)
-    maximumFontSize: {Number}, (null)
-    limitingDimension: {String}, ("both")
-    horizontalAlign: {String}, ("center")
-    verticalAlign: {String}, ("center")
-    textAlign: {String}, ("center")
-    whiteSpace: {String}, ("nowrap")
-});
-
-
-Original Projects: 
-https://github.com/DanielHoffmann/jquery-bigtext
-https://github.com/Jetroid/bigtext.js
-
-Options:
-
-rotateText: Rotates the text inside the element by X degrees.
-
-fontSizeFactor: This option is used to give some vertical spacing for letters that overflow the line-height (like 'g', 'Á' and most other accentuated uppercase letters). This does not affect the font-size if the limiting factor is the width of the parent div. The default is 0.8
-
-maximumFontSize: maximum font size to use.
-
-minimumFontSize: minimum font size to use.  if font is calculated smaller than this, text will be rendered at this size and wrapped
-
-limitingDimension: In which dimension the font size should be limited. Possible values: "width", "height" or "both". Defaults to both. Using this option with values different than "both" overwrites the element parent width or height.
-
-horizontalAlign: Where to align the text horizontally. Possible values: "left", "center", "right". Defaults to "center".
-
-verticalAlign: Where to align the text vertically. Possible values: "top", "center", "bottom". Defaults to "center".
-
-textAlign: Sets the text align of the element. Possible values: "left", "center", "right". Defaults to "center". This option is only useful if there are linebreaks (<br> tags) inside the text.
-
-whiteSpace: Sets whitespace handling. Possible values: "nowrap", "pre". Defaults to "nowrap". (Can also be set to enable wrapping but this doesn't work well.)
-
-Bruno Orlandi - 2017
-
-Copyright (C) 2013 Daniel Hoffmann Bernardes, Ícaro Technologies
-Copyright (C) 2016 Jet Holt
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
-
-function _calculateInnerDimensions(computedStyle) {
-    //Calculate the inner width and height
-    var innerWidth;
-    var innerHeight;
-
-    var width = parseInt(computedStyle.getPropertyValue("width"));
-    var height = parseInt(computedStyle.getPropertyValue("height"));
-    var paddingLeft = parseInt(computedStyle.getPropertyValue("padding-left"));
-    var paddingRight = parseInt(computedStyle.getPropertyValue("padding-right"));
-    var paddingTop = parseInt(computedStyle.getPropertyValue("padding-top"));
-    var paddingBottom = parseInt(computedStyle.getPropertyValue("padding-bottom"));
-    var borderLeft = parseInt(computedStyle.getPropertyValue("border-left-width"));
-    var borderRight = parseInt(computedStyle.getPropertyValue("border-right-width"));
-    var borderTop = parseInt(computedStyle.getPropertyValue("border-top-width"));
-    var borderBottom = parseInt(computedStyle.getPropertyValue("border-bottom-width"));
-
-    //If box-sizing is border-box, we need to subtract padding and border.
-    var parentBoxSizing = computedStyle.getPropertyValue("box-sizing");
-    if (parentBoxSizing == "border-box") {
-        innerWidth = width - (paddingLeft + paddingRight + borderLeft + borderRight);
-        innerHeight = height - (paddingTop + paddingBottom + borderTop + borderBottom);
-    } else {
-        innerWidth = width;
-        innerHeight = height;
-    }
-    var obj = {};
-    obj["width"] = innerWidth;
-    obj["height"] = innerHeight;
-    return obj;
-}
-
-export default function BigText(element, options) {
-
-    if (typeof element === 'string') {
-        element = document.querySelector(element);
-    } else if (element.length) {
-        // Support for array based queries (such as jQuery)
-        element = element[0];
-    }
-
-    var defaultOptions = {
-        rotateText: null,
-        fontSizeFactor: 0.8,
-        maximumFontSize: null,
-        limitingDimension: "both",
-        horizontalAlign: "center",
-        verticalAlign: "center",
-        textAlign: "center",
-        whiteSpace: "nowrap",
-        singleLine: true
-    };
-
-    //Merge provided options and default options
-    options = options || {};
-    for (var opt in defaultOptions)
-        if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt))
-            options[opt] = defaultOptions[opt];
-
-    //Get variables which we will reference frequently
-    var style = element.style;
-    var parent = element.parentNode;
-    var parentStyle = parent.style;
-    var parentComputedStyle = document.defaultView.getComputedStyle(parent);
-
-    //hides the element to prevent "flashing"
-    style.visibility = "hidden";
-    //Set some properties
-    style.display = "inline-block";
-    style.clear = "both";
-    style.float = "left";
-    var fontSize = options.maximumFontSize;
-    if (options.singleLine) {
-        style.fontSize = (fontSize * options.fontSizeFactor) + "px";
-        style.lineHeight = fontSize + "px";
-    } else {
-        for (; fontSize > options.minimumFontSize; fontSize = fontSize - Math.min(fontSize / 2, Math.max(0, fontSize - 48) + 2)) {
-            style.fontSize = (fontSize * options.fontSizeFactor) + "px";
-            style.lineHeight = "1";
-            if (element.offsetHeight <= +parentComputedStyle.height.replace("px", "")) {
-                break;
-            }
-        }
-    }
-    style.whiteSpace = options.whiteSpace;
-    style.textAlign = options.textAlign;
-    style.position = "relative";
-    style.padding = 0;
-    style.margin = 0;
-    style.left = "50%";
-    style.top = "50%";
-    var computedStyle = document.defaultView.getComputedStyle(element);
-
-    //Get properties of parent to allow easier referencing later.
-    var parentPadding = {
-        top: parseInt(parentComputedStyle.getPropertyValue("padding-top")),
-        right: parseInt(parentComputedStyle.getPropertyValue("padding-right")),
-        bottom: parseInt(parentComputedStyle.getPropertyValue("padding-bottom")),
-        left: parseInt(parentComputedStyle.getPropertyValue("padding-left")),
-    };
-    var parentBorder = {
-        top: parseInt(parentComputedStyle.getPropertyValue("border-top")),
-        right: parseInt(parentComputedStyle.getPropertyValue("border-right")),
-        bottom: parseInt(parentComputedStyle.getPropertyValue("border-bottom")),
-        left: parseInt(parentComputedStyle.getPropertyValue("border-left")),
-    };
-
-    //Calculate the parent inner width and height
-    var parentInnerDimensions = _calculateInnerDimensions(parentComputedStyle);
-    var parentInnerWidth = parentInnerDimensions["width"];
-    var parentInnerHeight = parentInnerDimensions["height"];
-
-    var box = {
-        width: element.offsetWidth, //Note: This is slightly larger than the jQuery version
-        height: element.offsetHeight,
-    };
-    if (!box.width || !box.height) return element;
-
-
-    if (options.rotateText !== null) {
-        if (typeof options.rotateText !== "number")
-            throw "bigText error: rotateText value must be a number";
-        var rotate = "rotate(" + options.rotateText + "deg)";
-        style.webkitTransform = rotate;
-        style.msTransform = rotate;
-        style.MozTransform = rotate;
-        style.OTransform = rotate;
-        style.transform = rotate;
-        //calculating bounding box of the rotated element
-        var sine = Math.abs(Math.sin(options.rotateText * Math.PI / 180));
-        var cosine = Math.abs(Math.cos(options.rotateText * Math.PI / 180));
-        box.width = element.offsetWidth * cosine + element.offsetHeight * sine;
-        box.height = element.offsetWidth * sine + element.offsetHeight * cosine;
-    }
-
-    var parentWidth = (parentInnerWidth - parentPadding.left - parentPadding.right);
-    var parentHeight = (parentInnerHeight - parentPadding.top - parentPadding.bottom);
-    var widthFactor = parentWidth / box.width;
-    var heightFactor = parentHeight / box.height;
-    var lineHeight;
-
-    if (options.limitingDimension.toLowerCase() === "width") {
-        lineHeight = Math.floor(widthFactor * fontSize);
-    } else if (options.limitingDimension.toLowerCase() === "height") {
-        lineHeight = Math.floor(heightFactor * fontSize);
-    } else if (widthFactor < heightFactor)
-        lineHeight = Math.floor(widthFactor * fontSize);
-    else if (widthFactor >= heightFactor)
-        lineHeight = Math.floor(heightFactor * fontSize);
-
-    var fontSize = lineHeight * options.fontSizeFactor;
-    if (fontSize < options.minimumFontSize) {
-        parentStyle.display = "flex";
-        parentStyle.alignItems = "center";
-        style.textAlign = "center";
-        style.visibility = "";
-        style.fontSize = options.minimumFontSize + "px";
-        style.lineHeight = "";
-        style.overflow = "hidden";
-        style.textOverflow = "ellipsis";
-        style.top = "";
-        style.left = "";
-        style.margin = "";
-        return element;
-    }
-    if (options.maximumFontSize && fontSize > options.maximumFontSize) {
-        fontSize = options.maximumFontSize;
-        lineHeight = fontSize / options.fontSizeFactor;
-    }
-
-    style.fontSize = Math.floor(fontSize) + "px";
-    style.lineHeight = Math.ceil(lineHeight) + "px";
-    style.marginBottom = "0px";
-    style.marginRight = "0px";
-
-    // if (options.limitingDimension.toLowerCase() === "height") {
-    //     //this option needs the font-size to be set already so computedStyle.getPropertyValue("width") returns the right size
-    //     //this +4 is to compensate the rounding erros that can occur due to the calls to Math.floor in the centering code
-    //     parentStyle.width = (parseInt(computedStyle.getPropertyValue("width")) + 4) + "px";
-    // }
-
-    //Calculate the inner width and height
-    var innerDimensions = _calculateInnerDimensions(computedStyle);
-    var innerWidth = innerDimensions["width"];
-    var innerHeight = innerDimensions["height"];
-
-    switch (options.verticalAlign.toLowerCase()) {
-        case "top":
-            style.top = "0%";
-            break;
-        case "bottom":
-            style.top = "100%";
-            style.marginTop = Math.floor(-innerHeight) + "px";
-            break;
-        default:
-            style.marginTop = Math.ceil((-innerHeight / 2)) + "px";
-            break;
-    }
-
-    switch (options.horizontalAlign.toLowerCase()) {
-        case "left":
-            style.left = "0%";
-            break;
-        case "right":
-            style.left = "100%";
-            style.marginLeft = Math.floor(-innerWidth) + "px";
-            break;
-        default:
-            style.marginLeft = Math.ceil((-innerWidth / 2)) + "px";
-            break;
-    }
-
-    //shows the element after the work is done
-    style.visibility = "visible";
-
-    return element;
-}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 38b22fad2..996c6843e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -265,11 +265,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         });
 
         AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
-            const container = DocCast(this._props.Document.embedContainer);
+            const container = DocCast(this.Document.embedContainer);
             const docView = DocumentView.getDocumentView?.(container);
             docView?.ComponentView?._props.addDocument?.(drawing);
-            drawing.x = NumCast(this._props.Document.x) + (this._props.Document.width as number);
-            drawing.y = NumCast(this._props.Document.y);
+            drawing.x = NumCast(this.Document.x) + (this.Document.width as number);
+            drawing.y = NumCast(this.Document.y);
         };
 
         AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 4e0c73b02..1a79bbbfe 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -1,25 +1,24 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ColorResult } from 'react-color';
+import ReactLoading from 'react-loading';
 import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction, unimplementedFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
 import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
 import { SettingsManager } from '../../util/SettingsManager';
+import { undoBatch } from '../../util/UndoManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
 import { LinkPopup } from '../linking/LinkPopup';
 import { DocumentView } from '../nodes/DocumentView';
+import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import './AnchorMenu.scss';
 import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
-import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
-import { InkData, InkField } from '../../../fields/InkField';
-import { DocData } from '../../../fields/DocSymbols';
-import { undoBatch } from '../../util/UndoManager';
-import ReactLoading from 'react-loading';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -149,9 +148,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     gptDraw = async (e: React.PointerEvent) => {
         try {
             SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
-            this._isLoading = true;
+            runInAction(() => (this._isLoading = true));
             await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true);
-            this._isLoading = false;
+            runInAction(() => (this._isLoading = false));
         } catch (err) {
             console.error(err);
         }
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 3bd42873c..1891cfd4c 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -13,6 +13,7 @@ import { FieldViewProps } from '../nodes/FieldView';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { AnchorMenu } from './AnchorMenu';
 import './Annotation.scss';
+import { Property } from 'csstype';
 
 interface IRegionAnnotationProps {
     x: number;
@@ -45,7 +46,7 @@ interface IAnnotationProps extends FieldViewProps {
     annoDoc: Doc;
     containerDataDoc: Doc;
     fieldKey: string;
-    pointerEvents?: () => Opt<string>;
+    pointerEvents?: () => Opt<Property.PointerEvents>;
 }
 @observer
 export class Annotation extends ObservableReactComponent<IAnnotationProps> {
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 4679941fb..aa10dcead 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -9,7 +9,7 @@ import ReactLoading from 'react-loading';
 import { AiOutlineSend } from 'react-icons/ai';
 import { gptAPICall, GPTCallType, gptDrawingColor } from '../../apis/gpt/GPT';
 import { InkData, InkField, InkTool } from '../../../fields/InkField';
-import { SVGToBezier } from '../../util/bezierFit';
+import { SVGToBezier, SVGType } from '../../util/bezierFit';
 import { INode, parse } from 'svgson';
 import { Slider, Switch } from '@mui/material';
 import { Doc, DocListCast } from '../../../fields/Doc';
@@ -279,14 +279,14 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             const svgStrokes: INode[] = svgObject.children;
             const strokeData: [InkData, string, string][] = [];
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            svgStrokes.forEach((child) => {
-                const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
+            svgStrokes.forEach(child => {
+                const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes);
                 strokeData.push([
                     convertedBezier.map(point => {
                         return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
                     }),
-                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : undefined,
-                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined,
+                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '',
+                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '',
                 ]);
             });
             return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
-- 
cgit v1.2.3-70-g09d2


From b56d2c2ae115e81fbf7aeaae0d2fed9ba073f11d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Oct 2024 15:19:52 -0400
Subject: more annopalette related cleanup

---
 src/client/util/DropConverter.ts                   | 39 ++++------------------
 src/client/views/DocumentDecorations.tsx           | 14 ++++----
 src/client/views/InkingStroke.tsx                  |  3 --
 .../views/collections/CollectionCarouselView.tsx   |  3 +-
 src/client/views/nodes/DocumentView.tsx            |  4 +--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 src/client/views/smartdraw/AnnotationPalette.tsx   |  4 +--
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 23 +++++++++----
 8 files changed, 37 insertions(+), 55 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 0ede44298..71cdaa58b 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -5,7 +5,7 @@ import { RichTextField } from '../../fields/RichTextField';
 import { ComputedField, ScriptField } from '../../fields/ScriptField';
 import { StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
-import { Docs } from '../documents/Documents';
+import { Docs, DocumentOptions } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
 import { ButtonType, FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
 import { DragManager } from './DragManager';
@@ -64,49 +64,24 @@ export function MakeTemplate(doc: Doc) {
     return doc;
 }
 
-export function makeUserTemplateButton(doc: Doc) {
-    const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
-    if (layoutDoc.type !== DocumentType.FONTICON) {
-        !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
-    }
-    layoutDoc.isTemplateDoc = true;
-    const dbox = Docs.Create.FontIconDocument({
-        _nativeWidth: 100,
-        _nativeHeight: 100,
-        _width: 100,
-        _height: 100,
-        backgroundColor: StrCast(doc.backgroundColor),
-        title: StrCast(layoutDoc.title),
-        btnType: ButtonType.ClickButton,
-        icon: 'bolt',
-        isSystem: false,
-    });
-    dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
-    dbox.dragFactory = layoutDoc;
-    dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
-    dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)');
-    return dbox;
-}
-
 /**
- * Similar to makeUserTemplateButton, but rather than creating a draggable button for the template, it takes in
- * an ImageField that will display.
+ * Makes a draggable button or image that will create a template doc Instance
  */
-export function makeUserTemplateImage(doc: Doc, imageHref: string | undefined) {
-    const image = imageHref ?? 'http://www.cs.brown.edu/~bcz/noImage.png';
+export function makeUserTemplateButtonOrImage(doc: Doc, image: string | undefined) {
     const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
     if (layoutDoc.type !== DocumentType.FONTICON) {
         !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
     }
     layoutDoc.isTemplateDoc = true;
-    const dbox = Docs.Create.ImageDocument(image, {
+    const docOptions: DocumentOptions = {
         _nativeWidth: 100,
         _nativeHeight: 100,
         _width: 100,
         _height: 100,
         title: StrCast(layoutDoc.title),
         isSystem: false,
-    });
+    };
+    const dbox = image ? Docs.Create.ImageDocument(image, docOptions) : Docs.Create.FontIconDocument({ ...docOptions, backgroundColor: StrCast(doc.backgroundColor), btnType: ButtonType.ClickButton, icon: 'bolt' });
     dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
     dbox.dragFactory = layoutDoc;
     dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
@@ -129,7 +104,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
                 });
             }
         } else if (!doc.onDragStart && !doc.isButtonBar) {
-            dbox = makeUserTemplateButton(doc);
+            dbox = makeUserTemplateButtonOrImage(doc);
         } else if (doc.isButtonBar) {
             dbox.ignoreClick = true;
         }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index df9ec1bbf..e569c7dc8 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -231,15 +231,17 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                     views.forEach(iconView => {
                         const iconViewDoc = iconView.Document;
                         Doc.setNativeView(iconViewDoc);
+                        // bcz: hacky ... when closing a Doc do different things depending on the contet ...
                         if (iconViewDoc.activeFrame) {
-                            iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
+                            iconViewDoc.opacity = 0; // if in an animation collection, set opacity to 0 to allow inkMasks and other documents to remain in the collection and to smoothly animate when they are activated in a different animation frame
                         } else {
-                            // to mark annotations as no longer saved if they're deleted from the palette
-                            const dragFactory: Doc = DocCast(iconView.Document.dragFactory);
-                            if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) {
-                                DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
-                            }
+                            // if Doc is in the annotation palette, remove the flag indicating that it's saved
+                            const dragFactory = DocCast(iconView.Document.dragFactory);
+                            if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
+
+                            // if this is a face Annotation doc, then just hide it.
                             if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true;
+                            // otherwise actually remove the Doc from its parent collection
                             else iconView._props.removeDocument?.(iconView.Document);
                         }
                     });
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index bc2b4badb..f44c1720d 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -30,7 +30,6 @@ import { InkData, InkField } from '../../fields/InkField';
 import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { Gestures } from '../../pen-gestures/GestureTypes';
-import { CognitiveServices } from '../cognitive_services/CognitiveServices';
 import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
 import { InteractionUtils } from '../util/InteractionUtils';
@@ -122,8 +121,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
             InkTranscription.Instance.transcribeInk(newCollection, selected, false);
         }
-        // const data: InkData = this.inkScaledData().inkData ?? [];
-        // CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]);
     };
 
     /**
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 143cfe0e8..db75c9e8b 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -144,7 +144,6 @@ export class CollectionCarouselView extends CollectionSubView() {
         revealItems.push({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;},     icon: 'pencil',}); // prettier-ignore
         !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
     };
-    childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null));
 
     isChildContentActive = () =>
         this._props.isContentActive?.() === false
@@ -165,7 +164,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                 Document={doc}
                 NativeWidth={returnZero}
                 NativeHeight={returnZero}
-                fitWidth={undefined}
+                fitWidth={this._props.childLayoutFitWidth}
                 showTags={true}
                 containerViewPath={this.childContainerViewPath}
                 setContentViewBox={undefined}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 85fd42ddf..80b61b6a9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -27,7 +27,7 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
+import { MakeTemplate, makeUserTemplateButtonOrImage } from '../../util/DropConverter';
 import { UPDATE_SERVER_CACHE } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SearchUtil } from '../../util/SearchUtil';
@@ -1348,7 +1348,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                     tempDoc = view.Document;
                     MakeTemplate(tempDoc);
                     Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc);
-                    Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc));
+                    Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc));
                     tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc);
                 } else {
                     tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 84d3fc748..d1b0dc7bf 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -268,7 +268,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const container = DocCast(this.Document.embedContainer);
             const docView = DocumentView.getDocumentView?.(container);
             docView?.ComponentView?._props.addDocument?.(drawing);
-            drawing.x = NumCast(this.Document.x) + (this.Document.width as number);
+            drawing.x = NumCast(this.Document.x) + NumCast(this.Document.width);
             drawing.y = NumCast(this.Document.y);
         };
 
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index 0c8dbf12d..4421547be 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -13,7 +13,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { ImageCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { makeUserTemplateImage } from '../../util/DropConverter';
+import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
@@ -123,7 +123,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
             const { clone } = await Doc.MakeClone(doc);
             clone.title = doc.title;
             const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href;
-            Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateImage(clone, image));
+            Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateButtonOrImage(clone, image));
             doc.savedAsAnno = true;
         }
     };
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 6d1277a08..76bfe2a19 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Slider, Switch } from '@mui/material';
 import { Button, IconButton } from 'browndash-components';
-import { action, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
@@ -194,14 +194,21 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._canInteract = false;
         if (this.ShowRegenerate) {
             await this.regenerate();
-            this._regenInput = '';
-            this._showEditBox = false;
+            runInAction(() => {
+                this._regenInput = '';
+                this._showEditBox = false;
+            });
         } else {
-            this._showOptions = false;
+            runInAction(() => {
+                this._showOptions = false;
+            });
             try {
                 await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
                 this.hideSmartDrawHandler();
-                this.ShowRegenerate = true;
+
+                runInAction(() => {
+                    this.ShowRegenerate = true;
+                });
             } catch (err) {
                 if (this._errorOccurredOnce) {
                     console.error('GPT call failed', err);
@@ -212,8 +219,10 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 }
             }
         }
-        this._isLoading = false;
-        this._canInteract = true;
+        runInAction(() => {
+            this._isLoading = false;
+            this._canInteract = true;
+        });
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From 8d7bf0588ca63c2d505a494865dcf9921eb1383d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Oct 2024 15:57:57 -0400
Subject: more lint fixes

---
 src/client/util/CurrentUserUtils.ts                   |  2 +-
 src/client/util/DropConverter.ts                      |  2 +-
 src/client/util/Scripting.ts                          |  5 -----
 src/client/views/DocumentDecorations.tsx              |  2 +-
 src/client/views/InkTranscription.tsx                 |  1 +
 src/client/views/InkingStroke.tsx                     |  4 +---
 src/client/views/LightboxView.tsx                     |  2 --
 src/client/views/Main.tsx                             |  1 -
 src/client/views/MainView.tsx                         | 19 +++++++------------
 .../views/collections/CollectionCarousel3DView.tsx    |  2 +-
 .../views/collections/CollectionCarouselView.tsx      |  4 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx     | 11 ++++++-----
 .../collections/collectionFreeForm/MarqueeView.tsx    |  3 +--
 src/client/views/nodes/FontIconBox/FontIconBox.tsx    |  1 -
 src/client/views/nodes/ImageBox.tsx                   |  1 -
 src/client/views/nodes/PDFBox.tsx                     |  5 +----
 .../views/nodes/formattedText/FormattedTextBox.tsx    |  8 --------
 src/client/views/nodes/formattedText/RichTextMenu.tsx |  1 -
 src/client/views/pdf/PDFViewer.tsx                    |  2 --
 src/client/views/smartdraw/AnnotationPalette.tsx      |  4 +---
 src/client/views/smartdraw/SmartDrawHandler.tsx       |  3 ++-
 src/fields/SchemaHeaderField.ts                       |  1 -
 22 files changed, 26 insertions(+), 58 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 59760ba32..96d69e7a1 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -2,7 +2,7 @@
 import { reaction, runInAction } from "mobx";
 import * as rp from 'request-promise';
 import { ClientUtils, OmitKeys } from "../../ClientUtils";
-import { Doc, DocListCast, DocListCastAsync, FieldType, Opt, StrListCast } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc";
 import { DocData } from "../../fields/DocSymbols";
 import { InkTool } from "../../fields/InkField";
 import { List } from "../../fields/List";
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 71cdaa58b..b5d29be4c 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -67,7 +67,7 @@ export function MakeTemplate(doc: Doc) {
 /**
  * Makes a draggable button or image that will create a template doc Instance
  */
-export function makeUserTemplateButtonOrImage(doc: Doc, image: string | undefined) {
+export function makeUserTemplateButtonOrImage(doc: Doc, image?: string) {
     const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
     if (layoutDoc.type !== DocumentType.FONTICON) {
         !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c63d3d7cb..c7b86815a 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -60,7 +60,6 @@ function Run(script: string | undefined, customParams: string[], diagnostics: ts
     // let params: any[] = [Docs, ...fieldTypes];
     const compiledFunction = (() => {
         try {
-            // eslint-disable-next-line no-new-func
             return new Function(...paramNames, `return ${script}`);
         } catch (e) {
             console.log(e);
@@ -69,10 +68,8 @@ function Run(script: string | undefined, customParams: string[], diagnostics: ts
     })();
     if (!compiledFunction) return { compiled: false, errors };
     const { capturedVariables = {} } = options;
-    // eslint-disable-next-line default-param-last
     const run = (args: { [name: string]: unknown } = {}, onError?: (e: string) => void, errorVal?: ts.Diagnostic): ScriptResult => {
         const argsArray: unknown[] = [];
-        // eslint-disable-next-line no-restricted-syntax
         for (const name of customParams) {
             if (name !== 'this') {
                 argsArray.push(name in args ? args[name] : capturedVariables[name]);
@@ -224,7 +221,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     if ('this' in params || 'this' in capturedVariables) {
         paramNames.push('this');
     }
-    // eslint-disable-next-line no-restricted-syntax
     for (const key in params) {
         if (key !== 'this') {
             paramNames.push(key);
@@ -234,7 +230,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
         const val = params[key];
         return `${key}: ${val}`;
     });
-    // eslint-disable-next-line no-restricted-syntax
     for (const key in capturedVariables) {
         if (key !== 'this') {
             const val = capturedVariables[key];
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e569c7dc8..1c0d51e17 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { IconButton } from 'browndash-components';
-import { action, computed, makeObservable, observable, runInAction, trace } from 'mobx';
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { FaUndo } from 'react-icons/fa';
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index b4f4ebf63..24d53a8c8 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -18,6 +18,7 @@ import { URLField } from '../../fields/URLField';
  * Class component that handles inking in writing mode
  */
 export class InkTranscription extends React.Component {
+    // eslint-disable-next-line no-use-before-define
     static Instance: InkTranscription;
 
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index f44c1720d..270266a94 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -51,7 +51,7 @@ import { InkTranscription } from './InkTranscription';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentView } from './nodes/DocumentView';
 
-// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 
 @observer
@@ -473,7 +473,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
                         cursor: this._props.isSelected() ? 'default' : undefined,
                     }}
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...interactions}>
                     {clickableLine(this.onPointerDown, isInkMask)}
                     {isInkMask ? null : inkLine}
@@ -490,7 +489,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             // top: (this._props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this._props.NativeDimScaling?.() || 1)) / 2,
                         }}>
                         <FormattedTextBox
-                            // eslint-disable-next-line react/jsx-props-no-spreading
                             {...this._props}
                             setHeight={undefined}
                             setContentViewBox={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 331b3db64..a543b4875 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -46,7 +46,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         ); 
     } // prettier-ignore
     public static LightboxDoc = () => LightboxView.Instance?._doc;
-    // eslint-disable-next-line no-use-before-define
     static Instance: LightboxView;
     private _path: {
         doc: Opt<Doc>; //
@@ -341,7 +340,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
 }
 interface LightboxTourBtnProps {
     navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: IconProp, display: boolean, click: () => void, color?: string) => JSX.Element;
-    // eslint-disable-next-line react/no-unused-prop-types
     future: () => Opt<Doc[]>;
     stepInto: () => void;
     lightboxDoc: () => Opt<Doc>;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 94294e97a..73d2872d1 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable no-new */
 // if ((module as any).hot) {
 //     (module as any).hot.accept();
 // }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8e921ca5e..fa2e94d12 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -3,7 +3,7 @@ import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons';
 import * as far from '@fortawesome/free-regular-svg-icons';
 import * as fa from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ResizeObserver from 'resize-observer-polyfill';
@@ -77,7 +77,7 @@ import { TopBar } from './topbar/TopBar';
 import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
 import { InkTranscription } from './InkTranscription';
 
-// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 
 @observer
@@ -977,13 +977,11 @@ export class MainView extends ObservableReactComponent<object> {
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
                     {[
-                        ...SnappingManager.HorizSnapLines.map((l, i) => (
-                            // eslint-disable-next-line react/no-array-index-key
-                            <line key={'horiz' + i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                        ...SnappingManager.HorizSnapLines.map(l => (
+                            <line key={'horiz' + l} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
                         )),
-                        ...SnappingManager.VertSnapLines.map((l, i) => (
-                            // eslint-disable-next-line react/no-array-index-key
-                            <line key={'vert' + i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                        ...SnappingManager.VertSnapLines.map(l => (
+                            <line key={'vert' + l} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
                         )),
                     ]}
                 </svg>
@@ -1071,10 +1069,7 @@ export class MainView extends ObservableReactComponent<object> {
                         docView={DocButtonState.Instance.LinkEditorDocView}
                     />
                 ) : null}
-                {LinkInfo.Instance?.LinkInfo ? (
-                    // eslint-disable-next-line react/jsx-props-no-spreading
-                    <LinkDocPreview {...LinkInfo.Instance.LinkInfo} />
-                ) : null}
+                {LinkInfo.Instance?.LinkInfo ? <LinkDocPreview {...LinkInfo.Instance.LinkInfo} /> : null}
                 {((page: string) => {
                     // prettier-ignore
                     switch (page) {
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index cf86a0a4e..c5da8e037 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -16,6 +16,7 @@ import './CollectionCarousel3DView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { Transform } from '../../util/Transform';
 
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
 
 @observer
@@ -88,7 +89,6 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         const currentIndex = NumCast(this.layoutDoc._carousel_index);
         const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => (
             <DocumentView
-                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props}
                 Document={childPair.layout}
                 TemplateDataDocument={childPair.data}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index db75c9e8b..74cf580c9 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -3,8 +3,8 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { Doc, Opt } from '../../../fields/Doc';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { ContextMenu } from '../ContextMenu';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cdcc2328f..0cc63d632 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -510,12 +510,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         e.stopPropagation();
                         break;
                     case InkTool.SmartDraw:
-                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, e => this.showSmartDraw(e.pageX, e.pageY), hit !== -1);
+                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, () => this.showSmartDraw(e.pageX, e.pageY), hit !== -1);
                         e.stopPropagation();
+                        break;
                     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);
+                            const ahit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, ahit !== -1, false);
                         }
                         break;
                     default:
@@ -655,13 +656,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    onEraserClick = (e: PointerEvent, doubleTap?: boolean) => {
+    onEraserClick = (e: PointerEvent) => {
         e.preventDefault();
         e.stopImmediatePropagation();
         this.erase(e, [0, 0]);
     };
 
-    forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
+    forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: string) => {
         this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
     };
 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 7614b5337..b1f6815b3 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -57,6 +57,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps };
     }
 
+    // eslint-disable-next-line no-use-before-define
     static Instance: MarqueeView;
 
     constructor(props: SubCollectionViewProps & MarqueeViewProps) {
@@ -548,7 +549,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     };
 
     touchesLine(r1: { left: number; top: number; width: number; height: number }) {
-        // eslint-disable-next-line no-restricted-syntax
         for (const lassoPt of this._lassoPts) {
             const topLeft = this.Transform.transformPoint(lassoPt[0], lassoPt[1]);
             if (r1.left < topLeft[0] && topLeft[0] < r1.left + r1.width && r1.top < topLeft[1] && topLeft[1] < r1.top + r1.height) {
@@ -569,7 +569,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             let hasLeft = false;
             let hasBottom = false;
             let hasRight = false;
-            // eslint-disable-next-line no-restricted-syntax
             for (const lassoPt of this._lassoPts) {
                 const truePoint = this.Transform.transformPoint(lassoPt[0], lassoPt[1]);
                 hasLeft = hasLeft || (truePoint[0] > tl[0] && truePoint[0] < r1.left && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height);
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index b7e1350ca..feaf84b7b 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index aa4376bb2..ec5e062c8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -493,7 +493,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 }}>
                 <CollectionFreeFormView
                     ref={this._ffref}
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     setContentViewBox={emptyFunction}
                     NativeWidth={returnZero}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 42ac51107..7ef431885 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, FieldValue, ImageCast, NumCast, StrCast, toList } from '../../../fields/Types';
+import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
@@ -538,7 +538,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             return ComponentTag === CollectionStackingView ? (
                 <SidebarAnnos
                     ref={this._sidebarRef}
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     Document={this.Document}
                     layoutDoc={this.layoutDoc}
@@ -554,7 +553,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this._props.select(false), true)}>
                     <ComponentTag
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         setContentViewBox={emptyFunction} // override setContentView to do nothing
                         NativeWidth={this.sidebarNativeWidthFunc}
@@ -608,7 +606,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         top: 0,
                     }}>
                     <PDFViewer
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         pdfBox={this}
                         sidebarAddDoc={this.sidebarAddDocument}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d1b0dc7bf..d3bc08bd3 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -655,7 +655,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         textContent += node.child(i).textContent;
                         i++;
                     }
-                    // eslint-disable-next-line no-cond-assign
                     while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) {
                         const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1));
                         ret.push(sel);
@@ -714,7 +713,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const hr = Math.round(Date.now() / 1000 / 60 / 60);
             numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
         }
-        // eslint-disable-next-line operator-assignment
         this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx.  so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView)
     };
 
@@ -1131,7 +1129,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const nodes: Node[] = [];
             let hadStart = start !== 0;
             frag.forEach((node, index) => {
-                // eslint-disable-next-line no-use-before-define
                 const examinedNode = findAnchorNode(node, editor);
                 if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
                     nodes.push(examinedNode.node);
@@ -1285,7 +1282,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             action(selected => {
                 this.prepareForTyping();
                 if (FormattedTextBox._globalHighlights.has('Bold Text')) {
-                    // eslint-disable-next-line operator-assignment
                     this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
                 }
                 if (RichTextMenu.Instance?.view === this._editorView && !selected) {
@@ -1711,7 +1707,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/);
         if (match) {
             this.dataDoc.title_custom = true;
-            // eslint-disable-next-line prefer-destructuring
             this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc
             this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1));
         }
@@ -1793,7 +1788,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
         const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
         if (children && !SnappingManager.IsDragging) {
-            // eslint-disable-next-line no-use-before-define
             const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
             const toNum = (val: string) => Number(val.replace('px', ''));
             const toHgt = (node: Element): number => {
@@ -1880,7 +1874,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             return ComponentTag === CollectionStackingView ? (
                 <SidebarAnnos
                     ref={this._sidebarRef}
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     Document={this.Document}
                     layoutDoc={this.layoutDoc}
@@ -1900,7 +1893,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}>
                     <ComponentTag
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         ref={this._sidebarTagRef}
                         setContentView={emptyFunction}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 1560d51a1..88e2e4248 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -697,7 +697,6 @@ interface RichTextMenuPluginProps {
     editorProps: FormattedTextBoxProps;
 }
 export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> {
-    // eslint-disable-next-line react/no-unused-class-component-methods
     update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) {
         RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc);
     }
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 01242ba48..7a86ee802 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -504,7 +504,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         return (
             <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this._props.Document), transform: `scale(${NumCast(this._props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}>
                 {inlineAnnos.map(anno => (
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     <Annotation {...this._props} fieldKey={this._props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} containerDataDoc={this._props.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} />
                 ))}
             </div>
@@ -537,7 +536,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
                 pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
             }}>
             <CollectionFreeFormView
-                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props}
                 NativeWidth={returnZero}
                 NativeHeight={returnZero}
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index 1c17469c3..f1e2e4f41 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -11,6 +11,7 @@ import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { ImageCast, NumCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
@@ -23,8 +24,6 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
 import './AnnotationPalette.scss';
 import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
-import { ImageField } from '../../../fields/URLField';
-import { Copy } from '../../../fields/FieldSymbols';
 
 interface AnnotationPaletteProps {
     Document: Doc;
@@ -158,7 +157,6 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                 console.log('Error generating drawing', e);
             }
         }
-        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
         this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
         this._userInput = '';
         this._isLoading = false;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 76bfe2a19..75ef55060 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -50,6 +50,7 @@ export interface DrawingOptions {
 
 @observer
 export class SmartDrawHandler extends ObservableReactComponent<object> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: SmartDrawHandler;
 
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
@@ -395,7 +396,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                                 defaultChecked={true}
                                 value={this._autoColor}
                                 size="small"
-                                onChange={action(e => this._canInteract && (this._autoColor = !this._autoColor))}
+                                onChange={action(() => this._canInteract && (this._autoColor = !this._autoColor))}
                             />
                         </div>
                         <div className="complexity">
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 0a8dd1d9e..5f4d59cf9 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -79,7 +79,6 @@ export class SchemaHeaderField extends ObjectField {
     @serializable(primitive())
     desc: boolean | undefined; // boolean determines sort order, undefined when no sort
 
-    // eslint-disable-next-line default-param-last
     constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
         super();
 
-- 
cgit v1.2.3-70-g09d2

