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/collections/TreeView.tsx | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index f69aea2a7..b82421e6b 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -469,12 +469,14 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         return false;
     }
 
-    refTransform = (ref: HTMLElement | undefined | null) => {
+    refTransform = (ref: HTMLDivElement | undefined | null) => {
         if (!ref) return this.ScreenToLocalTransform();
-        const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(ref);
-        return new Transform(-translateX, -translateY, 1).scale(1/scale);
+        const { translateX, translateY } = ClientUtils.GetScreenTransform(ref);
+        const outerXf = ClientUtils.GetScreenTransform(this.treeView.MainEle());
+        const offset = this.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+        return this.ScreenToLocalTransform().translate(offset[0], offset[1]);
     };
-    docTransform = () => this.refTransform(this._dref?.ContentDiv);
+    docTransform = () => this.refTransform(this._dref?.ContentRef?.current);
     getTransform = () => this.refTransform(this._tref.current);
     embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1);
     embeddedPanelHeight = () => {
-- 
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/collections/TreeView.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

