From f6775da5d14f35be2d05de13008f86fbae2cbf68 Mon Sep 17 00:00:00 2001
From: Lauren <victor_kalev@brown.edu>
Date: Fri, 29 Oct 2021 13:57:37 -0400
Subject: working on ink stroke intersection

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 27 +++++++++++++++++++++-
 1 file changed, 26 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index febccbfcc..f0064d15e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -5,7 +5,7 @@ import { DateField } from "../../../../fields/DateField";
 import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc";
 import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas";
 import { Id } from "../../../../fields/FieldSymbols";
-import { InkData, InkField, InkTool } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool, PointData } from "../../../../fields/InkField";
 import { List } from "../../../../fields/List";
 import { ObjectField } from "../../../../fields/ObjectField";
 import { RichTextField } from "../../../../fields/RichTextField";
@@ -51,6 +51,7 @@ import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import { ColorScheme } from "../../../util/SettingsManager";
+import { Bezier } from "bezier-js";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -111,6 +112,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     @observable _pullDirection: string = "";
     @observable _showAnimTimeline = false;
     @observable _clusterSets: (Doc[])[] = [];
+    @observable _prevPoint: PointData = { X: -1, Y: -1 };
+    @observable _currPoint: PointData = { X: -1, Y: -1 };
     @observable _timelineRef = React.createRef<Timeline>();
     @observable _marqueeRef = React.createRef<HTMLDivElement>();
     @observable _keyframeEditing = false;
@@ -450,6 +453,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             }
             // eraser plus anything else mode
             else {
+                if (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) {
+                    this._prevPoint = this._currPoint = { X: e.clientX, Y: e.clientY };
+                    const intersectStroke = this.getInkIntersection();
+                    if (intersectStroke) intersectStroke.props.removeDocument?.(intersectStroke.props.Document);
+                }
                 e.stopPropagation();
                 e.preventDefault();
             }
@@ -595,6 +603,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     onPointerUp = (e: PointerEvent): void => {
         if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
+            this._prevPoint = this._currPoint = { X: -1, Y: -1 };
             document.removeEventListener("pointermove", this.onPointerMove);
             document.removeEventListener("pointerup", this.onPointerUp);
             this.removeMoveListeners();
@@ -627,6 +636,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     onPointerMove = (e: PointerEvent): void => {
         if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
+        if (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) {
+            this._prevPoint = this._currPoint;
+            this._currPoint = { X: e.clientX, Y: e.clientY }
+        }
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             if (this.props.isContentActive(true)) e.stopPropagation();
         } else if (!e.cancelBubble) {
@@ -642,6 +655,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
     }
 
+    getInkIntersection = (): DocumentView | null => {
+        // this.children.filter((doc: Doc) => doc.type === DocumentType.INK)
+        // .forEach((doc: Doc) => {
+        //     const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
+        //     const ctrlPoints = inkView?.ComponentView?.inkScaledData().inkData.slice();
+        //     const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
+        //     const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects({p1: {x: this._prevPoint.X, y: this._prevPoint.Y}, p2: {x: this._currPoint.X, y: this._currPoint.Y}});
+        //     if (intersects) return inkView;
+        // });
+        return null;
+    }
+
     handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
         if (!e.cancelBubble) {
             const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
-- 
cgit v1.2.3-70-g09d2


From 2e6c1f2c1710548ebf86476e1476c22889f11819 Mon Sep 17 00:00:00 2001
From: Lauren <victor_kalev@brown.edu>
Date: Fri, 5 Nov 2021 18:27:07 -0400
Subject: ink document iteration not working

---
 src/client/views/InkingStroke.tsx                  |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 52 +++++++++++++++-------
 2 files changed, 36 insertions(+), 18 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 81a888256..ecb46a5b3 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -130,7 +130,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const inkPt = {
             X: (docPt[0] - inkStrokeWidth / 2) / inkScaleX + inkStrokeWidth / 2 + inkLeft,
             Y: (docPt[1] - inkStrokeWidth / 2) / inkScaleY + inkStrokeWidth / 2 + inkTop,
-        }
+        };
         return inkPt;
     }
     ptToScreen = (inkPt: { X: number, Y: number }) => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f0064d15e..0941db5f8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -114,6 +114,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     @observable _clusterSets: (Doc[])[] = [];
     @observable _prevPoint: PointData = { X: -1, Y: -1 };
     @observable _currPoint: PointData = { X: -1, Y: -1 };
+    @observable _deleteList: DocumentView[] = [];
     @observable _timelineRef = React.createRef<Timeline>();
     @observable _marqueeRef = React.createRef<HTMLDivElement>();
     @observable _keyframeEditing = false;
@@ -453,11 +454,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             }
             // eraser plus anything else mode
             else {
-                if (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) {
-                    this._prevPoint = this._currPoint = { X: e.clientX, Y: e.clientY };
-                    const intersectStroke = this.getInkIntersection();
-                    if (intersectStroke) intersectStroke.props.removeDocument?.(intersectStroke.props.Document);
-                }
+                this._prevPoint = { X: e.clientX, Y: e.clientY };
                 e.stopPropagation();
                 e.preventDefault();
             }
@@ -603,11 +600,17 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     onPointerUp = (e: PointerEvent): void => {
         if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
-            this._prevPoint = this._currPoint = { X: -1, Y: -1 };
             document.removeEventListener("pointermove", this.onPointerMove);
             document.removeEventListener("pointerup", this.onPointerUp);
             this.removeMoveListeners();
             this.removeEndListeners();
+            if (CurrentUserUtils.SelectedTool !== InkTool.None) {
+                this._deleteList.forEach(ink => {
+                    ink.props.removeDocument?.(ink.props.Document);
+                });
+                this._prevPoint = this._currPoint = { X: -1, Y: -1 };
+                this._deleteList = [];
+            }
         }
     }
 
@@ -636,9 +639,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     onPointerMove = (e: PointerEvent): void => {
         if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
-        if (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) {
-            this._prevPoint = this._currPoint;
-            this._currPoint = { X: e.clientX, Y: e.clientY }
+        if (CurrentUserUtils.SelectedTool !== InkTool.None) {
+            this._currPoint = { X: e.clientX, Y: e.clientY };
+            const intersectStroke = this.getInkIntersection();
+            if (intersectStroke) {
+                if (!this._deleteList.includes(intersectStroke)) {
+                    this._deleteList.push(intersectStroke);
+                    // lower intersectStroke opacity to give user a visual indicator
+                }
+            }
         }
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             if (this.props.isContentActive(true)) e.stopPropagation();
@@ -655,15 +664,24 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
     }
 
+    /**
+     * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
+     * @returns The DocumentView of the intersected stroke.
+     */
     getInkIntersection = (): DocumentView | null => {
-        // this.children.filter((doc: Doc) => doc.type === DocumentType.INK)
-        // .forEach((doc: Doc) => {
-        //     const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
-        //     const ctrlPoints = inkView?.ComponentView?.inkScaledData().inkData.slice();
-        //     const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
-        //     const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects({p1: {x: this._prevPoint.X, y: this._prevPoint.Y}, p2: {x: this._currPoint.X, y: this._currPoint.Y}});
-        //     if (intersects) return inkView;
-        // });
+        this.props.childDocuments?.filter(doc => doc.type === DocumentType.INK)
+            .forEach(doc => {
+                console.log("in for each");
+                const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
+                const ctrlPoints = Cast(inkView?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
+                for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
+                    const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
+                    const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects(
+                        { p1: { x: this._prevPoint.X, y: this._prevPoint.Y }, 
+                          p2: { x: this._currPoint.X, y: this._currPoint.Y } });
+                    if (intersects) return inkView;
+                }
+            });
         return null;
     }
 
-- 
cgit v1.2.3-70-g09d2


From 50f8a862bd27c212b13fcd1f626aafa5434e4842 Mon Sep 17 00:00:00 2001
From: Lauren <victor_kalev@brown.edu>
Date: Sat, 6 Nov 2021 15:32:47 -0400
Subject: from last

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx  | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0941db5f8..0b7bf92cc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -669,7 +669,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
      * @returns The DocumentView of the intersected stroke.
      */
     getInkIntersection = (): DocumentView | null => {
-        this.props.childDocuments?.filter(doc => doc.type === DocumentType.INK)
+        console.log("in intersection");
+        const currentCollection = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+        currentCollection?.childDocs
+            .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
                 console.log("in for each");
                 const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
-- 
cgit v1.2.3-70-g09d2


From b2b95106d21f03a0b2b46f972a36bbf00068ab20 Mon Sep 17 00:00:00 2001
From: Lauren <victor_kalev@brown.edu>
Date: Sat, 6 Nov 2021 16:49:36 -0400
Subject: eraser tool working

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 46 +++++++++++++---------
 1 file changed, 27 insertions(+), 19 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0b7bf92cc..f60af6355 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -605,9 +605,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             this.removeMoveListeners();
             this.removeEndListeners();
             if (CurrentUserUtils.SelectedTool !== InkTool.None) {
-                this._deleteList.forEach(ink => {
-                    ink.props.removeDocument?.(ink.props.Document);
-                });
+                if (this._deleteList.length > 0) {
+                    this._deleteList.forEach(ink => {
+                        ink.props.removeDocument?.(ink.props.Document);
+                    });
+                }
                 this._prevPoint = this._currPoint = { X: -1, Y: -1 };
                 this._deleteList = [];
             }
@@ -641,12 +643,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
         if (CurrentUserUtils.SelectedTool !== InkTool.None) {
             this._currPoint = { X: e.clientX, Y: e.clientY };
-            const intersectStroke = this.getInkIntersection();
-            if (intersectStroke) {
-                if (!this._deleteList.includes(intersectStroke)) {
-                    this._deleteList.push(intersectStroke);
-                    // lower intersectStroke opacity to give user a visual indicator
-                }
+            const intersections: DocumentView[] = this.getInkIntersection();
+            if (intersections.length > 0) {
+                intersections.forEach(ink =>{
+                    if (!this._deleteList.includes(ink)) {
+                        this._deleteList.push(ink);
+                        // Lowering ink opacity to give the user a visual indicator of deletion.
+                        ink.Document.opacity = 0.5;
+                    }
+                });
             }
         }
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
@@ -668,24 +673,27 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
      * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
      * @returns The DocumentView of the intersected stroke.
      */
-    getInkIntersection = (): DocumentView | null => {
-        console.log("in intersection");
-        const currentCollection = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
-        currentCollection?.childDocs
+    getInkIntersection = (): DocumentView[] => {
+        const inks: DocumentView[] = [];
+        this.childDocs
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
-                console.log("in for each");
                 const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
                 const ctrlPoints = Cast(inkView?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
                 for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
                     const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
-                    const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects(
-                        { p1: { x: this._prevPoint.X, y: this._prevPoint.Y }, 
-                          p2: { x: this._currPoint.X, y: this._currPoint.Y } });
-                    if (intersects) return inkView;
+                    // Converting from screen space to ink space for the intersection.
+                    const prevPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._prevPoint);
+                    const currPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._currPoint);
+                    if (prevPointInkSpace && currPointInkSpace) {
+                        const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects(
+                            { p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, 
+                              p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } });
+                        if (inkView && intersects.length > 0) inks.push(inkView);
+                    }
                 }
             });
-        return null;
+        return inks;
     }
 
     handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
-- 
cgit v1.2.3-70-g09d2


From dcd2fd6f3030f7b57000536005f6005a0abaa482 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 8 Nov 2021 12:14:31 -0500
Subject: fixed dragging snap lines to get recomputed properly (sometimes they
 didn't activate before) and fixed snapping to not be off by 1 screen space
 pixel

---
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx     | 8 +++-----
 src/client/views/nodes/DocumentView.tsx                           | 1 +
 2 files changed, 4 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index febccbfcc..37caf508f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -146,7 +146,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
     }
     @computed get cachedGetTransform(): Transform {
-        return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
+        return this.getContainerTransform().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
     }
 
     @action setKeyFrameEditing = (set: boolean) => this._keyframeEditing = set;
@@ -169,7 +169,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     getTransform = () => this.cachedGetTransform.copy();
     getLocalTransform = () => this.cachedGetLocalTransform.copy();
     getContainerTransform = () => this.cachedGetContainerTransform.copy();
-    getTransformOverlay = () => this.getContainerTransform().translate(1, 1);
     getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
     isAnyChildContentActive = () => this.props.isAnyChildContentActive();
     addLiveTextBox = (newBox: Doc) => {
@@ -221,7 +220,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         if (!de.embedKey && !this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
         if (!super.onInternalDrop(e, de)) return false;
         const refDoc = docDragData.droppedDocuments[0];
-        const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y);
+        const [xpo, ypo] = this.getContainerTransform().transformPoint(de.x, de.y);
         const z = NumCast(refDoc.z);
         const x = (z ? xpo : xp) - docDragData.offset[0];
         const y = (z ? ypo : yp) - docDragData.offset[1];
@@ -1010,7 +1009,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             rootSelected={childData ? this.rootSelected : returnFalse}
             onClick={this.onChildClickHandler}
             onDoubleClick={this.onChildDoubleClickHandler}
-            ScreenToLocalTransform={childLayout.z ? this.getTransformOverlay : this.getTransform}
+            ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform}
             PanelWidth={childLayout[WidthSym]}
             PanelHeight={childLayout[HeightSym]}
             docFilters={this.childDocFilters}
@@ -1403,7 +1402,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     }
 
     onPointerOver = (e: React.PointerEvent) => {
-        (DocumentDecorations.Instance.Interacting || (this.props.layerProvider?.(this.props.Document) !== false && SnappingManager.GetIsDragging())) && this.setupDragLines(e.ctrlKey || e.shiftKey);
         e.stopPropagation();
     }
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c9b246c10..6c058ff97 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -415,6 +415,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
             DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) },
                 () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed.
+            ffview?.setupDragLines(false);
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From f450c8760c0563cdadd414f169902faeace585ec Mon Sep 17 00:00:00 2001
From: vkalev <victor_kalev@brown.edu>
Date: Tue, 9 Nov 2021 16:08:01 -0500
Subject: working on undo/redo

---
 src/client/views/InkStrokeProperties.ts              | 20 +++++++++++---------
 .../collectionFreeForm/CollectionFreeFormView.tsx    |  8 +++++++-
 2 files changed, 18 insertions(+), 10 deletions(-)

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

diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 02288bbb5..58d2992e0 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -356,16 +356,18 @@ export class InkStrokeProperties {
             const oldHandlePoint = ink[handleIndex];
             const oppositeHandlePoint = ink[oppositeHandleIndex];
             const controlPoint = ink[controlIndex];
-            const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale };
             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 = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
-                inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
+            if (!Number.isNaN(deltaX / xScale) && !Number.isNaN(deltaY / yScale)) {
+                const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale };
+                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 = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
+                    inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
+                }
             }
             return inkCopy;
         })
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f60af6355..018f4f79a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -29,7 +29,7 @@ import { SearchUtil } from "../../../util/SearchUtil";
 import { SelectionManager } from "../../../util/SelectionManager";
 import { SnappingManager } from "../../../util/SnappingManager";
 import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
 import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariables.scss";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { ContextMenu } from "../../ContextMenu";
@@ -99,6 +99,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     private _layoutSizeData = observable.map<string, { width?: number, height?: number }>();
     private _cachedPool: Map<string, PoolData> = new Map();
     private _lastTap = 0;
+    private _batch: UndoManager.Batch | undefined = undefined;
 
     private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
     private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; }
@@ -449,6 +450,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             document.addEventListener("pointerup", this.onPointerUp);
             // if not using a pen and in no ink mode
             if (CurrentUserUtils.SelectedTool === InkTool.None) {
+                this._batch = UndoManager.StartBatch("collectionErase");
                 this._downX = this._lastX = e.pageX;
                 this._downY = this._lastY = e.pageY;
             }
@@ -598,6 +600,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
     }
 
+    // @undoBatch
+    @action
     onPointerUp = (e: PointerEvent): void => {
         if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             document.removeEventListener("pointermove", this.onPointerMove);
@@ -607,11 +611,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             if (CurrentUserUtils.SelectedTool !== InkTool.None) {
                 if (this._deleteList.length > 0) {
                     this._deleteList.forEach(ink => {
+                        ink.Document.opacity = 1;
                         ink.props.removeDocument?.(ink.props.Document);
                     });
                 }
                 this._prevPoint = this._currPoint = { X: -1, Y: -1 };
                 this._deleteList = [];
+                this._batch?.end();
             }
         }
     }
-- 
cgit v1.2.3-70-g09d2


From 44c3ed6af9516c7bb91785934a997c7ab054ec5f Mon Sep 17 00:00:00 2001
From: vkalev <victor_kalev@brown.edu>
Date: Fri, 12 Nov 2021 16:54:51 -0500
Subject: undo works with ink deletion

---
 package-lock.json                                              |  6 +++---
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx  | 10 ++++++----
 2 files changed, 9 insertions(+), 7 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index d03f8af6b..128d6fba8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7709,14 +7709,14 @@
             "resolved": "https://registry.npmjs.org/image-size-stream/-/image-size-stream-1.1.0.tgz",
             "integrity": "sha1-Ivou2mbG31AQh0bacUkmSy0l+Gs=",
             "requires": {
-                "image-size": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+                "image-size": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
                 "readable-stream": "^1.0.33",
                 "tryit": "^1.0.1"
             },
             "dependencies": {
                 "image-size": {
-                    "version": "git+ssh://git@github.com/netroy/image-size.git#da2c863807a3e9602617bdd357b0de3ab4a064c1",
-                    "from": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
+                    "version": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+                    "from": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
                 },
                 "isarray": {
                     "version": "0.0.1",
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 018f4f79a..bd72e4af9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -99,7 +99,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     private _layoutSizeData = observable.map<string, { width?: number, height?: number }>();
     private _cachedPool: Map<string, PoolData> = new Map();
     private _lastTap = 0;
-    private _batch: UndoManager.Batch | undefined = undefined;
 
     private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
     private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; }
@@ -450,7 +449,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             document.addEventListener("pointerup", this.onPointerUp);
             // if not using a pen and in no ink mode
             if (CurrentUserUtils.SelectedTool === InkTool.None) {
-                this._batch = UndoManager.StartBatch("collectionErase");
+                // this._batch = UndoManager.StartBatch("collectionErase");
                 this._downX = this._lastX = e.pageX;
                 this._downY = this._lastY = e.pageY;
             }
@@ -600,7 +599,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
     }
 
-    // @undoBatch
     @action
     onPointerUp = (e: PointerEvent): void => {
         if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
@@ -610,14 +608,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             this.removeEndListeners();
             if (CurrentUserUtils.SelectedTool !== InkTool.None) {
                 if (this._deleteList.length > 0) {
+                    // Ensuring ink opacity returns to normal if the user undos after deletion.
                     this._deleteList.forEach(ink => {
                         ink.Document.opacity = 1;
+                    });
+                    const batch = UndoManager.StartBatch("collectionErase");
+                    this._deleteList.forEach(ink => {
                         ink.props.removeDocument?.(ink.props.Document);
                     });
+                    batch.end();
                 }
                 this._prevPoint = this._currPoint = { X: -1, Y: -1 };
                 this._deleteList = [];
-                this._batch?.end();
             }
         }
     }
-- 
cgit v1.2.3-70-g09d2


From 3381bbb0ef5160707513f4bbbe551ca551b64b0d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 13 Nov 2021 10:38:53 -0500
Subject: change isContentActive to a tri-state to allow turning on/off and
 default - fixes issues with videobox and others so that content can be turned
 off reliably.   added annotation overlay for treeViews for ppt like slides. 
 lots of fixes to tree view to get layout to be more robust.

---
 src/Utils.ts                                       |   2 +-
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/CurrentUserUtils.ts                |  20 +-
 src/client/views/DocComponent.tsx                  |  18 +-
 src/client/views/DocumentDecorations.scss          |   6 +-
 src/client/views/DocumentDecorations.tsx           |  27 +--
 src/client/views/MainView.tsx                      |   6 +-
 src/client/views/OverlayView.tsx                   |   2 +-
 src/client/views/Palette.tsx                       |   2 +-
 src/client/views/PropertiesView.tsx                |   2 +-
 .../views/collections/CollectionStackingView.tsx   |   2 +-
 src/client/views/collections/CollectionSubView.tsx |   7 +-
 .../views/collections/CollectionTreeView.scss      |   9 +
 .../views/collections/CollectionTreeView.tsx       | 252 ++++++++++++---------
 src/client/views/collections/CollectionView.tsx    |  12 +-
 src/client/views/collections/TabDocView.tsx        |   3 +-
 src/client/views/collections/TreeView.scss         |   7 +-
 src/client/views/collections/TreeView.tsx          |  49 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  10 +-
 .../CollectionMulticolumnView.tsx                  |   4 +-
 .../CollectionMultirowView.tsx                     |   4 +-
 .../collectionMulticolumn/MulticolumnResizer.tsx   |   2 +-
 .../collectionMulticolumn/MultirowResizer.tsx      |   2 +-
 .../collectionSchema/CollectionSchemaHeaders.tsx   |   2 +-
 .../collections/collectionSchema/SchemaTable.tsx   |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  10 +-
 src/client/views/nodes/FieldView.tsx               |   2 +-
 src/client/views/nodes/FilterBox.tsx               |   2 +-
 src/client/views/nodes/LinkBox.tsx                 |   2 +-
 src/client/views/nodes/LinkDocPreview.tsx          |   2 +-
 src/client/views/nodes/ScreenshotBox.tsx           |   2 +-
 src/client/views/nodes/VideoBox.scss               |  10 +-
 src/client/views/nodes/VideoBox.tsx                |  11 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  18 +-
 src/mobile/AudioUpload.tsx                         |   2 +-
 src/mobile/MobileInterface.tsx                     |   2 +-
 36 files changed, 298 insertions(+), 221 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index bfb29fe8d..ca1432de2 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -448,7 +448,7 @@ export function returnEmptyDoclist() { return [] as any[]; }
 
 export let emptyPath = [];
 
-export function emptyFunction() { }
+export function emptyFunction() { return undefined; }
 
 export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index da5c8efa9..219f51a3a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -94,6 +94,7 @@ type DROPt = DAInfo | dropActionType;
 export class DocumentOptions {
     system?: BOOLt = new BoolInfo("is this a system created/owned doc");
     _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
+    allowOverlayDrop?: BOOLt = new BoolInfo("can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?");
     childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection ");
     targetDropAction?: DROPt = new DAInfo("what should happen to the source document when ??? ");
     color?: string; // foreground color data doc
@@ -289,6 +290,7 @@ export class DocumentOptions {
     treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed
     treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
     treeViewTruncateTitleWidth?: number;
+    treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations
     treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy
     sidebarColor?: string;  // background color of text sidebar
     sidebarViewType?: string; // collection type of text sidebar
@@ -821,7 +823,7 @@ export namespace Docs {
         }
 
         export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
         }
 
         export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9d06ad8a3..c9bef5924 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -339,7 +339,7 @@ export class CurrentUserUtils {
         const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc);
         if (doc.templateDocs === undefined) {
             doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], {
-                title: "template layouts", _xPadding: 0, system: true,
+                title: "template layouts", _xMargin: 0, system: true,
                 dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
             }));
         }
@@ -421,7 +421,11 @@ export class CurrentUserUtils {
             ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0;
         }
         if (doc.emptySlide === undefined) {
-            const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", _autoHeight: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) });
+            const textDoc = Docs.Create.TreeDocument([], {
+                title: "Slide", _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
+                allowOverlayDrop: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true,
+                backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"])
+            });
             Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text');
             FormattedTextBox.SelectOnLoad = textDoc[Id];
             doc.emptySlide = textDoc;
@@ -811,7 +815,7 @@ export class CurrentUserUtils {
             const newDashboardButton: Doc = Docs.Create.FontIconDocument({ onClick: newDashboard, _forceActive: true, toolTip: "Create new dashboard", _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
             doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], {
                 title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true, freezeChildren: "remove|add",
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+                treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true,
                 explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files."
@@ -840,7 +844,7 @@ export class CurrentUserUtils {
             const newTrailButton: Doc = Docs.Create.FontIconDocument({ onClick: newTrail, _forceActive: true, toolTip: "Create new trail", _stayInCollection: true, _hideContextMenu: true, title: "New trail", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
             doc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], {
                 title: "My Trails", _showTitle: "title", _height: 100,
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+                treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
                 explainer: "All of the trails that you have created will appear here."
@@ -865,7 +869,7 @@ export class CurrentUserUtils {
             });
             doc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([doc.myFileOrphans as Doc], {
                 title: "My Documents", _showTitle: "title", buttonMenu: true, buttonMenuDoc: newFolderButton, _height: 100,
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+                treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true,
                 isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true,
@@ -884,7 +888,7 @@ export class CurrentUserUtils {
             const clearDocsButton: Doc = Docs.Create.FontIconDocument({ onClick: clearAll, _forceActive: true, toolTip: "Empty recently closed", _stayInCollection: true, _hideContextMenu: true, title: "Empty", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "Empty", icon: "trash", system: true });
             doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], {
                 title: "My Recently Closed", _showTitle: "title", buttonMenu: true, buttonMenuDoc: clearDocsButton, childHideLinkButton: true,
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+                treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
                 explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
@@ -902,7 +906,7 @@ export class CurrentUserUtils {
         if (doc.currentFilter === undefined) {
             doc.currentFilter = Docs.Create.FilterDocument({
                 title: "Unnamed Filter", _height: 150,
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "none",
+                treeViewHideTitle: true, _xPadding: 5, _yPadding: 5, _gridGap: 5, _forceActive: true, childDropAction: "none",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, _autoHeight: true, _fitWidth: true
             });
@@ -918,7 +922,7 @@ export class CurrentUserUtils {
             doc.treeViewOpen = true;
             doc.treeViewExpandedView = "fields";
             doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], {
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title",
+                treeViewHideTitle: true, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true
             })) as any as Doc;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 32c351bf5..b9772fd57 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -44,7 +44,7 @@ interface ViewBoxBaseProps {
     fieldKey: string;
     layerProvider?: (doc: Doc, assign?: boolean) => boolean;
     isSelected: (outsideReaction?: boolean) => boolean;
-    isContentActive: () => boolean;
+    isContentActive: () => boolean | undefined;
     renderDepth: number;
     rootSelected: (outsideReaction?: boolean) => boolean;
 }
@@ -65,10 +65,12 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
 
         lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
 
-        isContentActive = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool !== InkTool.None ||
-            (this.props.isContentActive?.() || this.props.Document.forceActive ||
-                this.props.isSelected(outsideReaction) ||
-                this.props.rootSelected(outsideReaction)) ? true : false)
+        isContentActive = (outsideReaction?: boolean) => (
+            this.props.isContentActive?.() === false ? false :
+                (CurrentUserUtils.SelectedTool !== InkTool.None ||
+                    (this.props.isContentActive?.() || this.props.Document.forceActive ||
+                        this.props.isSelected(outsideReaction) ||
+                        this.props.rootSelected(outsideReaction)) ? true : undefined))
         protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     }
     return Component;
@@ -82,7 +84,7 @@ export interface ViewBoxAnnotatableProps {
     fieldKey: string;
     filterAddDocument?: (doc: Doc[]) => boolean;  // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
     layerProvider?: (doc: Doc, assign?: boolean) => boolean;
-    isContentActive: () => boolean;
+    isContentActive: () => boolean | undefined;
     select: (isCtrlPressed: boolean) => void;
     whenChildContentsActiveChanged: (isActive: boolean) => void;
     isSelected: (outsideReaction?: boolean) => boolean;
@@ -165,13 +167,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
         // otherwise, the document being moved must be able to be removed from its container before
         // moving it into the target.
         @action.bound
-        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string): boolean => {
+        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
             if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
                 return true;
             }
             const first = doc instanceof Doc ? doc : doc[0];
             if (!first?._stayInCollection && addDocument !== returnFalse) {
-                return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc));
+                return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey));
             }
             return false;
         }
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index d8ad47ecb..82dca1287 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -240,8 +240,10 @@ $linkGap: 3px;
         text-align: center;
         display: flex;
         margin-left: 5px;
-        height: 22px;
+        height: 20px;
         position: absolute;
+        border-radius: 8px;
+        background: rgba(159,159,159,0.1);
 
         .documentDecorations-titleSpan, 
         .documentDecorations-titleSpan-Dark  {
@@ -288,7 +290,7 @@ $linkGap: 3px;
     text-align: center;
     display: flex;
     margin-left: 5px;
-    height: 22px;
+    height: 20px;
     position: absolute;
     .documentDecorations-titleSpan {
       width: 100%;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 522995479..1c0b1b995 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -454,17 +454,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
             </Tooltip>);
 
         const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme);
-        const titleArea = this._edtingTitle ?
-            <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
-                style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }}
-                type="text" name="dynbox" autoComplete="on"
-                value={this._accumulatedTitle}
-                onBlur={e => this.titleBlur()}
-                onChange={action(e => this._accumulatedTitle = e.target.value)}
-                onKeyPress={this.titleEntered} /> :
-            <div className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
-                <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
-            </div>;
+        const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} style={{ width: "100%" }} key="title" /> :
+            this._edtingTitle ?
+                <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
+                    style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }}
+                    type="text" name="dynbox" autoComplete="on"
+                    value={this._accumulatedTitle}
+                    onBlur={e => this.titleBlur()}
+                    onChange={action(e => this._accumulatedTitle = e.target.value)}
+                    onKeyPress={this.titleEntered} /> :
+                <div className="documentDecorations-title" style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
+                    <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
+                </div>;
 
         let inMainMenuPanel = false;
         for (let node = seldoc.ContentDiv; node && !inMainMenuPanel; node = node?.parentNode as any) {
@@ -498,8 +499,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
                     top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
                 }}>
                     {!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")}
-                    {hideTitle ? (null) : titleArea}{!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
-
+                    {titleArea}
+                    {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
                     {hideResizers ? (null) :
                         <>
                             {SelectionManager.Views().length !== 1 || hideTitle ? (null) :
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 9a885fbf8..546b0e360 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -511,7 +511,7 @@ export class MainView extends React.Component {
                     bringToFront={emptyFunction}
                     select={emptyFunction}
                     isAnyChildContentActive={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     isSelected={returnFalse}
                     docViewPath={returnEmptyDoclist}
                     moveDocument={this.moveButtonDoc}
@@ -592,7 +592,7 @@ export class MainView extends React.Component {
                     pinToPres={returnFalse}
                     ScreenToLocalTransform={Transform.Identity}
                     bringToFront={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     whenChildContentsActiveChanged={returnFalse}
                     focus={returnFalse}
                     docViewPath={returnEmptyDoclist}
@@ -669,7 +669,7 @@ export class MainView extends React.Component {
                             pinToPres={returnFalse}
                             ScreenToLocalTransform={Transform.Identity}
                             bringToFront={returnFalse}
-                            isContentActive={returnFalse}
+                            isContentActive={emptyFunction}
                             whenChildContentsActiveChanged={returnFalse}
                             focus={returnFalse}
                             PanelWidth={() => 500}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index af04b967a..7cf388872 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -191,7 +191,7 @@ export class OverlayView extends React.Component {
                     ScreenToLocalTransform={Transform.Identity}
                     renderDepth={1}
                     isDocumentActive={returnTrue}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     whenChildContentsActiveChanged={emptyFunction}
                     focus={DocUtils.DefaultFocus}
                     styleProvider={DefaultStyleProvider}
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 86ab881bb..529697f71 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -50,7 +50,7 @@ export default class Palette extends React.Component<PaletteProps> {
                             PanelHeight={() => window.screen.height}
                             renderDepth={0}
                             isDocumentActive={returnTrue}
-                            isContentActive={returnFalse}
+                            isContentActive={emptyFunction}
                             focus={emptyFunction}
                             docViewPath={returnEmptyDoclist}
                             styleProvider={returnEmptyString}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ab9022a84..1083e0075 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -295,7 +295,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     freezeDimensions={true}
                     dontCenter={"y"}
                     isDocumentActive={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined}
                     NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
                     PanelWidth={panelWidth}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 648ff5087..bffaf86b1 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -226,7 +226,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
             layerProvider={this.props.layerProvider}
             docViewPath={this.props.docViewPath}
             fitWidth={this.props.childFitWidth}
-            isContentActive={returnFalse}
+            isContentActive={emptyFunction}
             isDocumentActive={this.isContentActive}
             LayoutTemplate={this.props.childLayoutTemplate}
             LayoutTemplateString={this.props.childLayoutString}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5dffc65fc..fc1bcb8b9 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -22,7 +22,6 @@ import ReactLoading from 'react-loading';
 
 export interface SubCollectionViewProps extends CollectionViewProps {
     CollectionView: Opt<CollectionView>;
-    SetSubView?: (subView: any) => void;
     isAnyChildContentActive: () => boolean;
 }
 
@@ -49,10 +48,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
             this.createDashEventsTarget(ele);
         }
 
-        componentDidMount() {
-            this.props.SetSubView?.(this);
-        }
-
         componentWillUnmount() {
             this.gestureDisposer?.();
             this._multiTouchDisposer?.();
@@ -220,7 +215,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                     const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
                     const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
                     if (movedDocs.length) {
-                        const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay ||
+                        const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || (!this.props.isAnnotationOverlay || this.props.Document.allowOverlayDrop) ||
                             Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
                         added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
                     } else {
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index d370d21ab..b664d9d82 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -71,6 +71,15 @@
     display: none;
 }
 
+.collectionTreeView-titleBar {
+    display: inline-block;
+    width: 100%;
+    height: max-content;
+    .contentFittingDocumentView {
+        display: block;  // makes titleBar take up full width of the treeView (flex doesn't for some reason)
+    }
+}
+
 .collectionTreeView-keyHeader:hover {
     background: #797777;
     cursor: pointer;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3852987b9..6fb18d4c2 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,4 +1,3 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
@@ -8,13 +7,14 @@ import { Document, listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyFunction } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { CurrentUserUtils } from '../../util/CurrentUserUtils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from "../../util/DragManager";
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
@@ -22,11 +22,11 @@ import { EditableView } from "../EditableView";
 import { DocumentView } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
+import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView } from "./CollectionSubView";
 import "./CollectionTreeView.scss";
 import { TreeView } from "./TreeView";
 import React = require("react");
-import { Transform } from '../../util/Transform';
 const _global = (window /* browser */ || global /* node */) as any;
 
 export type collectionTreeViewProps = {
@@ -41,10 +41,13 @@ export type collectionTreeViewProps = {
 
 @observer
 export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) {
-    private treedropDisposer?: DragManager.DragDropDisposer;
+    private _treedropDisposer?: DragManager.DragDropDisposer;
     private _mainEle?: HTMLDivElement;
+    private _titleRef?: HTMLDivElement | HTMLInputElement | null;
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    MainEle = () => this._mainEle;
+    private _isDisposing = false; // notes that instance is in process of being disposed
+    private refList: Set<any> = new Set(); // list of tree view items to monitor for height changes
+    private observer: any; // observer for mnonitoring tree view items.
 
     @computed get doc() { return this.props.Document; }
     @computed get dataDoc() { return this.props.DataDoc || this.doc; }
@@ -54,6 +57,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     @computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; }
     @computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; }
 
+    @observable _explainerHeight = 0; // height of the description of the tree view
+
+    MainEle = () => this._mainEle;
+
     // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
     @observable _isAnyChildContentActive = false;
     whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
@@ -62,11 +69,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
             this.props.isSelected(outsideReaction) || this._isAnyChildContentActive ||
             this.props.rootSelected(outsideReaction)) ? true : false)
 
-    isDisposing = false;
     componentWillUnmount() {
-        this.isDisposing = true;
+        this._isDisposing = true;
         super.componentWillUnmount();
-        this.treedropDisposer?.();
+        this._treedropDisposer?.();
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
@@ -76,13 +82,13 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
             { fireImmediately: true });
     }
 
-    refList: Set<any> = new Set();
-    observer: any;
     computeHeight = () => {
-        if (this.isDisposing) return;
-        const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.paddingTop() + this.paddingBot());
-        this.layoutDoc._autoHeightMargins = bodyHeight;
-        this.props.setHeight(this.documentTitleHeight() + bodyHeight);
+        if (!this._isDisposing) {
+            const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace("px", ""));
+            const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.marginBot());
+            this.layoutDoc._autoHeightMargins = bodyHeight;
+            this.props.setHeight(bodyHeight + titleHeight);
+        }
     }
     unobserveHeight = (ref: any) => {
         this.refList.delete(ref);
@@ -101,8 +107,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         }
     }
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
-        this.treedropDisposer?.();
-        if (this._mainEle = ele) this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
+        this._treedropDisposer?.();
+        if (this._mainEle = ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
     }
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
@@ -165,60 +171,54 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true);
     }
 
-    editableTitle = (childDocs: Doc[]) => {
-        return !this.dataDoc ? (null) :
-            <EditableView
-                contents={this.dataDoc.title}
-                display={"block"}
-                maxHeight={72}
-                height={"auto"}
-                GetValue={() => StrCast(this.dataDoc.title)}
-                SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
-                    if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(childDocs);
-                    this.dataDoc.title = value;
-                    return true;
-                })} />;
+    get editableTitle() {
+        return <EditableView
+            contents={this.dataDoc.title}
+            display={"block"}
+            maxHeight={72}
+            height={"auto"}
+            GetValue={() => StrCast(this.dataDoc.title)}
+            SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
+                if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(this.treeChildren);
+                this.dataDoc.title = value;
+                return true;
+            })} />;
     }
 
 
-    documentTitle = (childDocs: Doc[]) => {
-        return <div style={{ display: "inline-block", width: "100%", height: this.documentTitleHeight() }} key={this.doc[Id]}
-            onKeyDown={e => {
-                e.stopPropagation();
-                e.key === "Enter" && this.makeTextCollection(childDocs);
-            }}>
-            <DocumentView
-                Document={this.doc}
-                DataDoc={undefined}
-                LayoutTemplateString={FormattedTextBox.LayoutString("text")}
-                renderDepth={this.props.renderDepth + 1}
-                isContentActive={this.isContentActive}
-                isDocumentActive={this.isContentActive}
-                rootSelected={returnTrue}
-                docViewPath={this.props.docViewPath}
-                styleProvider={this.props.styleProvider}
-                layerProvider={this.props.layerProvider}
-                PanelWidth={this.documentTitleWidth}
-                PanelHeight={this.documentTitleHeight}
-                NativeWidth={this.documentTitleWidth}
-                NativeHeight={this.documentTitleHeight}
-                focus={this.props.focus}
-                treeViewDoc={this.props.Document}
-                ScreenToLocalTransform={this.titleTransform}
-                docFilters={returnEmptyFilter}
-                docRangeFilters={returnEmptyFilter}
-                searchFilterDocs={returnEmptyDoclist}
-                ContainingCollectionDoc={this.doc}
-                ContainingCollectionView={this.props.CollectionView}
-                addDocument={this.props.addDocument}
-                moveDocument={returnFalse}
-                removeDocument={returnFalse}
-                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                addDocTab={this.props.addDocTab}
-                pinToPres={this.props.pinToPres}
-                bringToFront={returnFalse}
-            />
-        </div>;
+    get documentTitle() {
+        return <DocumentView
+            Document={this.doc}
+            DataDoc={undefined}
+            LayoutTemplateString={FormattedTextBox.LayoutString("text")}
+            renderDepth={this.props.renderDepth + 1}
+            isContentActive={this.isContentActive}
+            isDocumentActive={this.isContentActive}
+            rootSelected={returnTrue}
+            forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight
+            docViewPath={this.props.docViewPath}
+            styleProvider={this.props.styleProvider}
+            layerProvider={this.props.layerProvider}
+            PanelWidth={this.documentTitleWidth}
+            PanelHeight={this.documentTitleHeight}
+            NativeWidth={this.documentTitleWidth}
+            NativeHeight={this.documentTitleHeight}
+            focus={this.props.focus}
+            treeViewDoc={this.props.Document}
+            ScreenToLocalTransform={this.titleTransform}
+            docFilters={returnEmptyFilter}
+            docRangeFilters={returnEmptyFilter}
+            searchFilterDocs={returnEmptyDoclist}
+            ContainingCollectionDoc={this.doc}
+            ContainingCollectionView={this.props.CollectionView}
+            addDocument={this.props.addDocument}
+            moveDocument={returnFalse}
+            removeDocument={returnFalse}
+            whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+            addDocTab={this.props.addDocTab}
+            pinToPres={this.props.pinToPres}
+            bringToFront={returnFalse}
+        />;
     }
     childContextMenuItems = () => {
         const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -263,21 +263,31 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         );
     }
     @computed get titleBar() {
-        const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle;
-        return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren);
+        return this.dataDoc === null ? (null) :
+            <div className="collectionTreeView-titleBar" key={this.doc[Id]}
+                style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingRight: this.marginX(), paddingTop: this.marginTop() } : {}}
+                ref={r => this._titleRef = r}
+                onKeyDown={e => {
+                    if (this.outlineMode) {
+                        e.stopPropagation();
+                        e.key === "Enter" && this.makeTextCollection(this.treeChildren);
+                    }
+                }}>
+                {this.outlineMode ? this.documentTitle : this.editableTitle}
+            </div>;
+    }
+
+    @computed get noviceExplainer() {
+        return !Doc.UserDoc().noviceMode || !this.rootDoc.explainer ? (null) :
+            <div className="documentExplanation"> {this.rootDoc.explainer} </div>;
     }
 
     return35 = () => 35;
     @computed get buttonMenu() {
-        const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+        const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
         // To create a multibutton menu add a CollectionLinearView
-        if (menuDoc) {
-
-            const width: number = NumCast(menuDoc._width, 30);
-            const height: number = NumCast(menuDoc._height, 30);
-            console.log(menuDoc.title, width, height);
-            return (<div className="buttonMenu-docBtn"
-                style={{ width: width, height: height }}>
+        return !menuDoc ? null :
+            (<div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}>
                 <DocumentView
                     Document={menuDoc}
                     DataDoc={menuDoc}
@@ -306,11 +316,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
                     ContainingCollectionDoc={undefined}
                 />
             </div>);
-        }
     }
 
-    @observable _explainerHeight: number = 0;
-
     @computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); }
     @computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); }
 
@@ -321,47 +328,82 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         const wscale = nw ? this.props.PanelWidth() / nw : 1;
         return wscale < hscale ? wscale : hscale;
     }
-    paddingX = () => NumCast(this.doc._xPadding, 15);
-    paddingTop = () => NumCast(this.doc._yPadding, 20);
-    paddingBot = () => NumCast(this.doc._yPadding, 20);
+    marginX = () => NumCast(this.doc._xMargin);
+    marginTop = () => NumCast(this.doc._yMargin);
+    marginBot = () => NumCast(this.doc._yMargin);
     documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth());
     documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins);
     titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20));
     truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
     onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
-    panelWidth = () => (this.props.PanelWidth() - 2 * this.paddingX()) * (this.props.scaling?.() || 1);
-    render() {
-        TraceMobx();
+    panelWidth = () => (this.props.PanelWidth() - 2 * this.marginX()) * (this.props.scaling?.() || 1);
+
+    addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+    remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+    moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) =>
+        this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false
+
+    contentFunc = () => {
         const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
         const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined;
-        const buttonMenu = this.rootDoc.buttonMenu;
-        const noviceExplainer = this.rootDoc.explainer;
-
-        return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
-            <>
-                {this.titleBar}
+        const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : this.titleBar;
+        return [
+            <div className="collectionTreeView-contents" key="tree" style={{
+                ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
+                overflow: "auto",
+                height: this.layoutDoc._autoHeight ? "max-content" : "100%"
+            }} >
+                {titleBar}
                 <div className="collectionTreeView-container"
-                    style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}}
+                    style={{
+                        transform: this.outlineMode ? `scale(${this.contentScaling})` : "",
+                        paddingLeft: `${this.marginX()}px`,
+                        height: "max-content",
+                        width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : ""
+                    }}
                     onContextMenu={this.onContextMenu}>
-                    {buttonMenu || noviceExplainer ? <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
-                        {buttonMenu ? this.buttonMenu : null}
-                        {Doc.UserDoc().noviceMode && noviceExplainer ?
-                            <div className="documentExplanation">
-                                {noviceExplainer}
-                            </div>
-                            : null
-                        }
-                    </div> : null}
+                    {!this.buttonMenu && !this.noviceExplainer ? (null) :
+                        <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
+                            {this.buttonMenu}
+                            {this.noviceExplainer}
+                        </div>
+                    }
                     <div className="collectionTreeView-dropTarget"
-                        style={{ background: background(), height: `calc(100% - ${this._explainerHeight}px)`, paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingBottom: `${this.paddingBot()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }}
+                        style={{
+                            background: background(),
+                            height: `calc(100% - ${this._explainerHeight}px)`,
+                            pointerEvents: pointerEvents()
+                        }}
                         onWheel={e => e.stopPropagation()}
                         onDrop={this.onTreeDrop}
-                        ref={this.createTreeDropTarget}>
+                        ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
                         <ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} >
                             {this.treeViewElements}
                         </ul>
                     </div >
                 </div>
-            </>;
+            </div>
+        ];
+    }
+    render() {
+        TraceMobx();
+
+        return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
+            this.doc.treeViewHasOverlay ?
+                <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+                    isAnnotationOverlay={true}
+                    isAnnotationOverlayScrollable={true}
+                    childDocumentsActive={this.props.isDocumentActive}
+                    fieldKey={this.props.fieldKey + "-annotations"}
+                    dropAction={"move"}
+                    select={emptyFunction}
+                    addDocument={this.addAnnotationDocument}
+                    removeDocument={this.remAnnotationDocument}
+                    moveDocument={this.moveAnnotationDocument}
+                    bringToFront={emptyFunction}
+                    renderDepth={this.props.renderDepth + 1} >
+                    {this.contentFunc}
+                </CollectionFreeFormView> :
+                this.contentFunc();
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 38e027fb3..8e84b59de 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -63,6 +63,7 @@ export enum CollectionViewType {
 }
 export interface CollectionViewProps extends FieldViewProps {
     isAnnotationOverlay?: boolean;  // is the collection an annotation overlay (eg an overlay on an image/video/etc)
+    isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
     layoutEngine?: () => string;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
 
@@ -125,8 +126,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
     }
 
     screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth());
-    private SubView = (type: CollectionViewType, props: SubCollectionViewProps) => {
+    private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
         TraceMobx();
+        if (type === undefined) return null;
         switch (type) {
             default:
             case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
@@ -246,17 +248,13 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
     childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
     @computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); }
 
-
-    @observable _subView: any = undefined;
-
     isContentActive = (outsideReaction?: boolean) => {
-        return this.props.isContentActive() ? true : false;
+        return this.props.isContentActive();
     }
     render() {
         TraceMobx();
         const props: SubCollectionViewProps = {
             ...this.props,
-            SetSubView: action((subView: any) => this._subView = subView),
             addDocument: this.addDocument,
             moveDocument: this.moveDocument,
             removeDocument: this.removeDocument,
@@ -273,7 +271,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
         return (<div className={"collectionView"} onContextMenu={this.onContextMenu}
             style={{ pointerEvents: this.props.layerProvider?.(this.rootDoc) === false ? "none" : undefined }}>
             {this.showIsTagged()}
-            {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+            {this.renderSubView(this.collectionViewType, props)}
         </div>);
     }
 }
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index eb95bb913..5ba019698 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -476,7 +476,6 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
             <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
                 <CollectionFreeFormView
                     Document={this.props.document}
-                    SetSubView={() => this}
                     CollectionView={undefined}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined}
@@ -484,7 +483,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
                     childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
                     noOverlay={true} // don't render overlay Docs since they won't scale
                     setHeight={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     isAnyChildContentActive={returnFalse}
                     select={emptyFunction}
                     dropAction={undefined}
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 1ebc5873e..2e33d3564 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -53,14 +53,11 @@
     }
 }
 
+.treeView-container-outline-active
 .treeView-container-active {
     z-index: 100;
     position: relative;
-
-    .formattedTextbox-sidebar {
-        background-color: #ffff001f !important;
-        height: 500px !important;
-    }
+    pointer-events: all;
 }
 
 .treeView-openRight {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 7f2128230..a266c301f 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -81,7 +81,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     static _openLevelScript: Opt<ScriptField | undefined>;
     private _header: React.RefObject<HTMLDivElement> = React.createRef();
     private _tref = React.createRef<HTMLDivElement>();
-    private _docRef: Opt<DocumentView>;
+    @observable _docRef: Opt<DocumentView>;
     private _selDisposer: Opt<IReactionDisposer>;
     private _editTitleScript: (() => ScriptField) | undefined;
     private _openScript: (() => ScriptField) | undefined;
@@ -116,7 +116,8 @@ export class TreeView extends React.Component<TreeViewProps> {
     @computed get childLinks() { return this.childDocList("links"); }
     @computed get childAliases() { return this.childDocList("aliases"); }
     @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); }
-    @computed get selected() { return SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
+    @computed get selected() { return SelectionManager.IsSelected(this._docRef); }
+    //      SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
 
     childDocList(field: string) {
         const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
@@ -125,7 +126,12 @@ export class TreeView extends React.Component<TreeViewProps> {
             DocListCastOrNull(this.doc[field]); // otherwise use the document's data field
     }
     @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
-        return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc);
+        if (this.doc !== target && addDoc !== returnFalse) { // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse
+            if (this.props.removeDoc?.(doc) === true) {
+                return addDoc(doc);
+            }
+        }
+        return false;
     }
     @undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
         this.props.treeView.props.select(false);
@@ -141,8 +147,10 @@ export class TreeView extends React.Component<TreeViewProps> {
             this._editTitle = false;
         }
         else if (docView.isSelected()) {
+            const doc = docView.Document;
+            SelectionManager.SelectSchemaViewDoc(doc);
             this._editTitle = true;
-            this._selDisposer = reaction(() => docView.isSelected(), sel => !sel && this.setEditTitle(undefined));
+            this._selDisposer = reaction(() => SelectionManager.SelectedSchemaDoc(), seldoc => seldoc !== doc && this.setEditTitle(undefined));
         } else {
             docView.select(false);
         }
@@ -222,7 +230,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     public static makeTextBullet() {
         const bullet = Docs.Create.TextDocument("-text-", {
             layout: CollectionView.LayoutString("data"),
-            title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform,
+            title: "-title-",
             treeViewExpandedViewLock: true, treeViewExpandedView: "data",
             _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline",
             x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, backgroundColor: "transparent", _width: 1000, _height: 10
@@ -266,23 +274,25 @@ export class TreeView extends React.Component<TreeViewProps> {
             e.stopPropagation();
         }
         const docDragData = de.complete.docDragData;
-        if (docDragData) {
-            e.stopPropagation();
+        if (docDragData && pt[0] < rect.left + rect.width) {
             if (docDragData.draggedDocuments[0] === this.doc) return true;
-            this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document);
+            if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) {
+                e.stopPropagation();
+            }
         }
     }
 
     dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
         const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
-        const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd;
+        const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add")) || forceAdd;
         const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false;
         const addDoc = !inside ? parentAddDoc :
             (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
         const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument;
         if (canAdd) {
-            UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
+            return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
         }
+        return false;
     }
 
     refTransform = (ref: HTMLDivElement | undefined | null) => {
@@ -432,7 +442,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 </div>
             </ul>;
         }
-        return <ul>{this.renderEmbeddedDocument(false)}</ul>; // "layout"
+        return <ul onPointerDown={e => { e.preventDefault(); e.stopPropagation(); }}>{this.renderEmbeddedDocument(false, returnFalse)}</ul>; // "layout"
     }
 
     get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); }
@@ -581,6 +591,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     }
     titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth()));
 
+    return18 = () => 18;
     /**
      * Renders the EditableView title element for placement into the tree.
      */
@@ -636,10 +647,10 @@ export class TreeView extends React.Component<TreeViewProps> {
                 moveDocument={this.move}
                 removeDocument={this.props.removeDoc}
                 ScreenToLocalTransform={this.getTransform}
-                NativeHeight={() => 18}
+                NativeHeight={this.return18}
                 NativeWidth={this.titleWidth}
                 PanelWidth={this.titleWidth}
-                PanelHeight={() => 18}
+                PanelHeight={this.return18}
                 contextMenuItems={this.contextMenuItems}
                 renderDepth={1}
                 isContentActive={this.props.isContentActive}
@@ -679,6 +690,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     renderBulletHeader = (contents: JSX.Element, editing: boolean) => {
         return <>
             <div className={`treeView-header` + (editing ? "-editing" : "")} key="titleheader"
+                style={{ width: "max-content" }}
                 ref={this._header}
                 onClick={this.ignoreEvent}
                 onPointerDown={this.ignoreEvent}
@@ -691,7 +703,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     }
 
 
-    renderEmbeddedDocument = (asText: boolean) => {
+    renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
         const layout = StrCast(Doc.LayoutField(this.layoutDoc));
         const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name);
         const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth;
@@ -704,8 +716,8 @@ export class TreeView extends React.Component<TreeViewProps> {
             NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined}
             NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined}
             LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined}
-            isContentActive={asText ? this.props.isContentActive : returnFalse}
-            isDocumentActive={asText ? this.props.isContentActive : returnFalse}
+            isContentActive={isActive}
+            isDocumentActive={isActive}
             styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
             hideTitle={asText}
             fitContentsToDoc={returnTrue}
@@ -749,7 +761,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     @computed get renderDocumentAsHeader() {
         return <>
             {this.renderBullet}
-            {this.renderEmbeddedDocument(true)}
+            {this.renderEmbeddedDocument(true, this.props.isContentActive)}
         </>;
     }
 
@@ -776,13 +788,12 @@ export class TreeView extends React.Component<TreeViewProps> {
         return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
             <div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`}
                 ref={this.createTreeDropTarget}
-
                 onDrop={this.onTreeDrop}
                 //onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
                 onKeyDown={this.onKeyDown}>
                 <li className="collection-child">
                     {hideTitle && this.doc.type !== DocumentType.RTF ?
-                        this.renderEmbeddedDocument(false) :
+                        this.renderEmbeddedDocument(false, returnFalse) :
                         this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)}
                 </li>
             </div>;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 37caf508f..190d0e76a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -165,7 +165,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX);
     panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY);
     zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1));
-    contentTransform = () => `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+    contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
     getTransform = () => this.cachedGetTransform.copy();
     getLocalTransform = () => this.cachedGetLocalTransform.copy();
     getContainerTransform = () => this.cachedGetContainerTransform.copy();
@@ -802,7 +802,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         else if (this.props.isContentActive(true) && !this.Document._isGroup) {
             e.stopPropagation();
             e.preventDefault();
-            this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+            !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
         }
     }
 
@@ -1015,7 +1015,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             docFilters={this.childDocFilters}
             docRangeFilters={this.childDocRangeFilters}
             searchFilterDocs={this.searchFilterDocs}
-            isContentActive={returnFalse}
+            isContentActive={emptyFunction}
             isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
             focus={this.focusDocument}
             addDocTab={this.addDocTab}
@@ -1034,7 +1034,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             bringToFront={this.bringToFront}
             showTitle={this.props.childShowTitle}
             dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
-            pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" :
+            pointerEvents={this.props.isContentActive() === false ? "none" : this.backgroundActive || this.props.childPointerEvents ? "all" :
                 (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents}
             jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
         //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
@@ -1474,6 +1474,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 {this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)}
                 <CollectionFreeFormViewPannableContents
                     isAnnotationOverlay={this.isAnnotationOverlay}
+                    isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
                     transform={this.contentTransform}
                     zoomScaling={this.zoomScaling}
                     presPaths={BoolCast(this.Document.presPathView)}
@@ -1582,6 +1583,7 @@ interface CollectionFreeFormViewPannableContentsProps {
     progressivize?: boolean;
     presPinView?: boolean;
     isAnnotationOverlay: boolean | undefined;
+    isAnnotationOverlayScrollable: boolean | undefined;
 }
 
 @observer
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 65c345547..ec1cbadd5 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
 import { List } from '../../../../fields/List';
 import { makeInterface } from '../../../../fields/Schema';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
@@ -228,7 +228,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
             LayoutTemplateString={this.props.childLayoutString}
             freezeDimensions={this.props.childFreezeDimensions}
             renderDepth={this.props.renderDepth + 1}
-            isContentActive={returnFalse}
+            isContentActive={emptyFunction}
             PanelWidth={width}
             PanelHeight={height}
             rootSelected={this.rootSelected}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 30836854a..a2d51e2e7 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
 import { List } from '../../../../fields/List';
 import { makeInterface } from '../../../../fields/Schema';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
@@ -237,7 +237,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
             ScreenToLocalTransform={dxf}
             focus={this.props.focus}
             docFilters={this.childDocFilters}
-            isContentActive={returnFalse}
+            isContentActive={emptyFunction}
             docRangeFilters={this.childDocRangeFilters}
             searchFilterDocs={this.searchFilterDocs}
             ContainingCollectionDoc={this.props.CollectionView?.props.Document}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 9fe18d118..273e609ca 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProp } from "../../StyleProvider";
 interface ResizerProps {
     width: number;
     styleProvider?: StyleProviderFunc;
-    isContentActive?: () => boolean;
+    isContentActive?: () => boolean | undefined;
     columnUnitLength(): number | undefined;
     toLeft?: Doc;
     toRight?: Doc;
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 5478bf709..006ef4df6 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProviderFunc } from "../../nodes/DocumentView";
 interface ResizerProps {
     height: number;
     styleProvider?: StyleProviderFunc;
-    isContentActive?: () => boolean;
+    isContentActive?: () => boolean | undefined;
     columnUnitLength(): number | undefined;
     toTop?: Doc;
     toBottom?: Doc;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index 1306b79cb..dc35b5749 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -225,7 +225,7 @@ export interface KeysDropdownProps {
     fieldKey: string;
     ContainingCollectionDoc: Doc | undefined;
     ContainingCollectionView: Opt<CollectionView>;
-    active?: (outsideReaction?: boolean) => boolean;
+    active?: (outsideReaction?: boolean) => boolean | undefined;
     openHeader: (column: any, screenx: number, screeny: number) => void;
     col: SchemaHeaderField;
     icon: IconProp;
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index bc5a9559f..2219345f6 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -68,7 +68,7 @@ export interface SchemaTableProps {
     addDocument?: (document: Doc | Doc[]) => boolean;
     moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
     ScreenToLocalTransform: () => Transform;
-    active: (outsideReaction: boolean | undefined) => boolean;
+    active: (outsideReaction: boolean | undefined) => boolean | undefined;
     onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
     addDocTab: (document: Doc, where: string) => boolean;
     pinToPres: (document: Doc) => void;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6c058ff97..c8a32a911 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -138,6 +138,7 @@ export interface DocumentViewSharedProps {
     hideLinkButton?: boolean;
     hideCaptions?: boolean;
     ignoreAutoHeight?: boolean;
+    forceAutoHeight?: boolean;
     disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
     pointerEvents?: string;
     scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
@@ -224,7 +225,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     componentWillUnmount() { this.cleanupHandlers(true); }
     componentDidMount() { this.setupHandlers(); }
-    componentDidUpdate() { this.setupHandlers(); }
+    //componentDidUpdate() { this.setupHandlers(); }
     setupHandlers() {
         this.cleanupHandlers(false);
         if (this._mainCont.current) {
@@ -825,13 +826,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     }
     setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view);
     isContentActive = (outsideReaction?: boolean) => {
-        return CurrentUserUtils.SelectedTool !== InkTool.None ||
+        return this.props.isContentActive() === false ? false : (
+            CurrentUserUtils.SelectedTool !== InkTool.None ||
             SnappingManager.GetIsDragging() ||
             this.props.rootSelected() ||
             this.props.Document.forceActive ||
             this.props.isSelected(outsideReaction) ||
             this._componentView?.isAnyChildContentActive?.() ||
-            this.props.isContentActive() ? true : false;
+            this.props.isContentActive()) ? true : undefined;
     }
     @computed get contents() {
         TraceMobx();
@@ -1256,7 +1258,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         position: this.props.Document.isInkMask ? "absolute" : undefined,
                         transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
                         width: isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
-                        height: isButton ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
+                        height: isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
                             `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
                     }}>
                     <DocumentViewInternal {...this.props}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index ee81e106a..943b9f153 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -18,7 +18,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
     scrollOverflow?: boolean; // bcz: would like to think this can be avoided -- need to look at further
 
     select: (isCtrlPressed: boolean) => void;
-    isContentActive: (outsideReaction?: boolean) => boolean;
+    isContentActive: (outsideReaction?: boolean) => boolean | undefined;
     isDocumentActive?: () => boolean;
     isSelected: (outsideReaction?: boolean) => boolean;
     scaling?: () => number;
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 2041c7399..fb8e89da9 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -225,7 +225,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
                     treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, ignoreClick: true
                 });
                 Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
-                newFacet._textBoxPadding = 4;
+                newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4;
                 const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`;
                 newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" });
             } else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) {
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index b82d16677..879a63248 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -15,7 +15,7 @@ const LinkDocument = makeInterface(documentSchema);
 @observer
 export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>(LinkDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
-    isContentActiveFunc = () => this.isContentActive() ? true : false;
+    isContentActiveFunc = () => this.isContentActive();
     render() {
         if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
         return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 424083dac..2e29c0656 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -166,7 +166,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                             docViewPath={returnEmptyDoclist}
                             ScreenToLocalTransform={Transform.Identity}
                             isDocumentActive={returnFalse}
-                            isContentActive={returnFalse}
+                            isContentActive={emptyFunction}
                             addDocument={returnFalse}
                             removeDocument={returnFalse}
                             addDocTab={returnFalse}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 7ad96bf05..0c631e5f9 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -313,7 +313,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                             PanelHeight={this.formattedPanelHeight}
                             isAnnotationOverlay={true}
                             select={emptyFunction}
-                            isContentActive={returnFalse}
+                            isContentActive={emptyFunction}
                             scaling={returnOne}
                             xPadding={25}
                             yPadding={10}
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 4871599b8..f0d7bd2f3 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -48,10 +48,18 @@
     width: 100%;
     z-index: -1; // 0;  // logically this should be 0 (or unset) which would give us transparent brush strokes over videos.  However, this makes Chrome crawl to a halt
     position: absolute;
+    video {
+        width: auto;
+        height: 100%; 
+        display: flex; 
+        margin: auto;
+    }
 }
 
 .videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
-    height: Auto; 
+    width: 100%;
+    height: 100%; 
+    left: 0px;
 }
 
 .videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen  {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 440ccf638..615d595c0 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -307,12 +307,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @computed get content() {
         const field = Cast(this.dataDoc[this.fieldKey], VideoField);
         const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
-        const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+        const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div key="loading">Loading</div> :
-            <div className="container" key="container" style={{ mixBlendMode: "multiply", pointerEvents: this.props.isContentActive() ? "all" : "none" }}>
-                <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}>
+            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
+                <div className={classname}>
                     <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
-                        style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }}
                         onCanPlay={this.videoLoad}
                         controls={VideoBox._nativeControls}
                         onPlay={() => this.Play()}
@@ -457,11 +456,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
         this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
-        const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+        const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
         const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0)));
         return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
             onPointerLeave={this.updateTimecode}
-            onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
+            onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
             src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
     }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index aa53f751d..1170528df 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -123,7 +123,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
 
     @computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
     @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
-    @computed get autoHeight() { return this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight; }
+    @computed get autoHeight() { return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight; }
     @computed get textHeight() { return NumCast(this.rootDoc[this.fieldKey + "-height"]); }
     @computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); }
     @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + "-height"]); }
@@ -1565,7 +1565,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     setHeight={this.setSidebarHeight}
                     fitContentsToDoc={this.fitToBox}
                     noSidebar={true}
-                    fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : this.SidebarKey} />;
+                    fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} />;
         };
         return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.SelectedTool !== InkTool.None ? "-inking" : "")}
             style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
@@ -1581,10 +1581,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false);
         if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide);
         const minimal = this.props.ignoreAutoHeight;
-        const margins = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
-        const selPad = Math.min(margins, 10);
-        const padding = Math.max(margins + ((selected && !this.layoutDoc._singleLine) || minimal ? -selPad : 0), 0);
-        const selPaddingClass = selected && !this.layoutDoc._singleLine && margins >= 10 ? "-selected" : "";
+        const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0);
+        const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
+        const selPad = ((selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0);
+        const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? "-selected" : "";
         const styleFromString = this.styleFromLayoutString(scale);   // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._headerHeight}px' >
         return (styleFromString?.height === "0px" ? (null) :
             <div className="formattedTextBox-cont"
@@ -1628,7 +1628,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                         onScroll={this.onScroll} onDrop={this.ondrop} >
                         <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget}
                             style={{
-                                padding: StrCast(this.layoutDoc._textBoxPadding, `${padding}px`),
+                                padding: StrCast(this.layoutDoc._textBoxPadding),
+                                paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
+                                paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
+                                paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
+                                paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
                                 pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? "none" : undefined) : undefined
                             }}
                         />
diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx
index 71ddda866..88221732e 100644
--- a/src/mobile/AudioUpload.tsx
+++ b/src/mobile/AudioUpload.tsx
@@ -94,7 +94,7 @@ export class AudioUpload extends React.Component {
                         PanelHeight={() => 400}
                         renderDepth={0}
                         isDocumentActive={returnTrue}
-                        isContentActive={returnFalse}
+                        isContentActive={emptyFunction}
                         focus={emptyFunction}
                         layerProvider={undefined}
                         styleProvider={() => "rgba(0,0,0,0)"}
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 652804126..d732a6e2f 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -211,7 +211,7 @@ export class MobileInterface extends React.Component {
                     PanelHeight={this.returnHeight}
                     renderDepth={0}
                     isDocumentActive={returnTrue}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     focus={DocUtils.DefaultFocus}
                     styleProvider={this.whitebackground}
                     layerProvider={undefined}
-- 
cgit v1.2.3-70-g09d2


From 9b5c863f7b88db579fba3ea8f85c43c2562f4289 Mon Sep 17 00:00:00 2001
From: vkalev <victor_kalev@brown.edu>
Date: Sat, 20 Nov 2021 14:11:50 -0500
Subject: fixing undo batch

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 26 ++++++++++------------
 1 file changed, 12 insertions(+), 14 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bd72e4af9..4447b7624 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -99,6 +99,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     private _layoutSizeData = observable.map<string, { width?: number, height?: number }>();
     private _cachedPool: Map<string, PoolData> = new Map();
     private _lastTap = 0;
+    private _batch: UndoManager.Batch | undefined = undefined;
 
     private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
     private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; }
@@ -449,12 +450,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             document.addEventListener("pointerup", this.onPointerUp);
             // if not using a pen and in no ink mode
             if (CurrentUserUtils.SelectedTool === InkTool.None) {
-                // this._batch = UndoManager.StartBatch("collectionErase");
                 this._downX = this._lastX = e.pageX;
                 this._downY = this._lastY = e.pageY;
             }
             // eraser plus anything else mode
             else {
+                this._batch = UndoManager.StartBatch("collectionErase");
                 this._prevPoint = { X: e.clientX, Y: e.clientY };
                 e.stopPropagation();
                 e.preventDefault();
@@ -608,18 +609,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             this.removeEndListeners();
             if (CurrentUserUtils.SelectedTool !== InkTool.None) {
                 if (this._deleteList.length > 0) {
-                    // Ensuring ink opacity returns to normal if the user undos after deletion.
-                    this._deleteList.forEach(ink => {
-                        ink.Document.opacity = 1;
-                    });
-                    const batch = UndoManager.StartBatch("collectionErase");
                     this._deleteList.forEach(ink => {
                         ink.props.removeDocument?.(ink.props.Document);
                     });
-                    batch.end();
                 }
                 this._prevPoint = this._currPoint = { X: -1, Y: -1 };
                 this._deleteList = [];
+                this._batch?.end();
             }
         }
     }
@@ -653,7 +649,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             this._currPoint = { X: e.clientX, Y: e.clientY };
             const intersections: DocumentView[] = this.getInkIntersection();
             if (intersections.length > 0) {
-                intersections.forEach(ink =>{
+                intersections.forEach(ink => {
                     if (!this._deleteList.includes(ink)) {
                         this._deleteList.push(ink);
                         // Lowering ink opacity to give the user a visual indicator of deletion.
@@ -695,8 +691,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     const currPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._currPoint);
                     if (prevPointInkSpace && currPointInkSpace) {
                         const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects(
-                            { p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, 
-                              p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } });
+                            {
+                                p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+                                p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
+                            });
                         if (inkView && intersects.length > 0) inks.push(inkView);
                     }
                 }
@@ -878,10 +876,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 .filter(({ pos, size }) => pos && size).map(({ pos, size }) => ({ pos: pos!, size: size! }));
             if (measuredDocs.length) {
                 const ranges = measuredDocs.reduce(({ xrange, yrange }, { pos, size }) =>  // computes range of content
-                    ({
-                        xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) },
-                        yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) }
-                    })
+                ({
+                    xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) },
+                    yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) }
+                })
                     , {
                         xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
                         yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }
-- 
cgit v1.2.3-70-g09d2


From 7b3e1614fa645014c7de69a0f2057e389074c0ed Mon Sep 17 00:00:00 2001
From: vkalev <victor_kalev@brown.edu>
Date: Sat, 20 Nov 2021 15:38:54 -0500
Subject: updating intersections to map t-values to inks

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 38 +++++++++++++---------
 1 file changed, 23 insertions(+), 15 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4447b7624..6f79c5eab 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -647,15 +647,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
         if (CurrentUserUtils.SelectedTool !== InkTool.None) {
             this._currPoint = { X: e.clientX, Y: e.clientY };
-            const intersections: DocumentView[] = this.getInkIntersection();
-            if (intersections.length > 0) {
-                intersections.forEach(ink => {
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
-                });
+            const intersections: { [id: number]: DocumentView[] } = this.getEraserIntersections();
+            if (intersections) {
+                for (const t in intersections) {
+                    const inks = intersections[t];
+                    inks.forEach(ink => {
+                        if (!this._deleteList.includes(ink)) {
+                            this._deleteList.push(ink);
+                            // Lowering ink opacity to give the user a visual indicator of deletion.
+                            ink.Document.opacity = 0.5;
+                        }
+                    });
+                }
             }
         }
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
@@ -675,10 +678,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     /**
      * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
-     * @returns The DocumentView of the intersected stroke.
+     * @returns A dictionary mapping the t-value intersection of the eraser with a list of ink DocumentViews.
      */
-    getInkIntersection = (): DocumentView[] => {
-        const inks: DocumentView[] = [];
+    getEraserIntersections = (): { [id: number]: DocumentView[] } => {
+        const intersections: { [id: number]: DocumentView[] } = {};
         this.childDocs
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
@@ -690,16 +693,21 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     const prevPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._prevPoint);
                     const currPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._currPoint);
                     if (prevPointInkSpace && currPointInkSpace) {
-                        const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects(
+                        const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
+                        const t = curve.intersects(
                             {
                                 p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
                                 p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
                             });
-                        if (inkView && intersects.length > 0) inks.push(inkView);
+                        if (inkView && t) {
+                            t.forEach(val => {
+                                intersections[+t[0]] ? intersections[+t[0]].push(inkView) : intersections[+t[0]] = [inkView];
+                            });
+                        }
                     }
                 }
             });
-        return inks;
+        return intersections;
     }
 
     handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
-- 
cgit v1.2.3-70-g09d2


From cc5cdf6882e25826ae00220927d31201a3bb997e Mon Sep 17 00:00:00 2001
From: vkalev <victor_kalev@brown.edu>
Date: Sun, 21 Nov 2021 19:37:51 -0500
Subject: splitting/deletion occurs but too many t-values are stored

---
 src/client/views/GestureOverlay.tsx                |   7 --
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 136 +++++++++++++++++----
 src/fields/InkField.ts                             |   6 +
 3 files changed, 117 insertions(+), 32 deletions(-)

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

diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 99829139f..e2193c9ac 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -489,19 +489,12 @@ export class GestureOverlay extends Touchable {
         e.stopPropagation();
     }
 
-    @action
-    checkInkIntersection = (point: PointData) => {
-        
-    }
-
     @action
     onPointerDown = (e: React.PointerEvent) => {
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
             // if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) SetActiveInkColor("rgba(245, 230, 95, 0.75)");
-        } else if (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) {
-            this.checkInkIntersection({ X: e.clientX, Y: e.clientY });
         }
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 6f79c5eab..ce4acb7ad 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -5,7 +5,7 @@ import { DateField } from "../../../../fields/DateField";
 import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc";
 import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas";
 import { Id } from "../../../../fields/FieldSymbols";
-import { InkData, InkField, InkTool, PointData } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool, PointData, Intersection } from "../../../../fields/InkField";
 import { List } from "../../../../fields/List";
 import { ObjectField } from "../../../../fields/ObjectField";
 import { RichTextField } from "../../../../fields/RichTextField";
@@ -52,6 +52,7 @@ import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import { ColorScheme } from "../../../util/SettingsManager";
 import { Bezier } from "bezier-js";
+import { GestureOverlay } from "../../GestureOverlay";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -647,19 +648,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
         if (CurrentUserUtils.SelectedTool !== InkTool.None) {
             this._currPoint = { X: e.clientX, Y: e.clientY };
-            const intersections: { [id: number]: DocumentView[] } = this.getEraserIntersections();
-            if (intersections) {
-                for (const t in intersections) {
-                    const inks = intersections[t];
-                    inks.forEach(ink => {
-                        if (!this._deleteList.includes(ink)) {
-                            this._deleteList.push(ink);
-                            // Lowering ink opacity to give the user a visual indicator of deletion.
-                            ink.Document.opacity = 0.5;
-                        }
-                    });
-                }
-            }
+            const eraserIntersections: Intersection[] = this.getEraserIntersections();
+            if (eraserIntersections) this.eraseInkStrokes(eraserIntersections);
         }
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             if (this.props.isContentActive(true)) e.stopPropagation();
@@ -676,12 +666,79 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
     }
 
+    @action
+    eraseInkStrokes = (eraserIntersections: Intersection[]) => {
+        eraserIntersections.forEach(intersect => {
+            var t1, t2;
+            var distT1: number, distT2: number;
+            distT1 = distT2 = Number.MAX_SAFE_INTEGER;
+            const t = intersect.t;
+            const ink = intersect.ink;
+            const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
+            for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
+                const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
+                const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
+                const inkIntersections: number[] = this.getInkIntersections(curve);
+                inkIntersections.forEach(currentT => {
+                    const diff = t - currentT;
+                    if (diff > 0 && diff < distT1) {
+                        distT1 = diff;
+                        t1 = currentT;
+                    } else if (diff < 0 && diff < distT2) {
+                        distT2 = -diff;
+                        t2 = currentT;
+                    }
+                });
+
+                // Normal case --> deletion of entire stroke
+                if (!t1 && !t2) {
+                    if (!this._deleteList.includes(ink)) {
+                        this._deleteList.push(ink);
+                        // Lowering ink opacity to give the user a visual indicator of deletion.
+                        ink.Document.opacity = 0.5;
+                    }
+                } else if (t1 && !t2) {
+                    const splitCurve = curve.split(t1);
+                    const leftInkData: PointData[] = splitCurve.left.points.map(p => ({ X: p.x, Y: p.y }));
+                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData);
+                    if (!this._deleteList.includes(ink)) {
+                        this._deleteList.push(ink);
+                        // Lowering ink opacity to give the user a visual indicator of deletion.
+                        ink.Document.opacity = 0.5;
+                    }
+                } else if (!t1 && t2) {
+                    const splitCurve = curve.split(t2);
+                    const rightInkData: PointData[] = splitCurve.right.points.map(p => ({ X: p.x, Y: p.y }));
+                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData);
+                    if (!this._deleteList.includes(ink)) {
+                        this._deleteList.push(ink);
+                        // Lowering ink opacity to give the user a visual indicator of deletion.
+                        ink.Document.opacity = 0.5;
+                    }
+                } else if (t1 && t2) {
+                    const splitCurve1 = curve.split(t1);
+                    const leftInkData: PointData[] = splitCurve1.left.points.map(p => ({ X: p.x, Y: p.y }));
+                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData);
+                    const splitCurve2 = curve.split(t2);
+                    const rightInkData: PointData[] = splitCurve2.right.points.map(p => ({ X: p.x, Y: p.y }));
+                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData);
+
+                    if (!this._deleteList.includes(ink)) {
+                        this._deleteList.push(ink);
+                        // Lowering ink opacity to give the user a visual indicator of deletion.
+                        ink.Document.opacity = 0.5;
+                    }
+                }
+            }
+        });
+    }
+
     /**
      * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
-     * @returns A dictionary mapping the t-value intersection of the eraser with a list of ink DocumentViews.
+     * @returns A dictionary mapping the t-value intersection of the eraser with the corresponding ink DocumentView.
      */
-    getEraserIntersections = (): { [id: number]: DocumentView[] } => {
-        const intersections: { [id: number]: DocumentView[] } = {};
+    getEraserIntersections = (): Intersection[] => {
+        const intersections: Intersection[] = [];
         this.childDocs
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
@@ -694,14 +751,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     const currPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._currPoint);
                     if (prevPointInkSpace && currPointInkSpace) {
                         const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
-                        const t = curve.intersects(
-                            {
-                                p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
-                                p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
-                            });
-                        if (inkView && t) {
-                            t.forEach(val => {
-                                intersections[+t[0]] ? intersections[+t[0]].push(inkView) : intersections[+t[0]] = [inkView];
+                        const intersects = curve.intersects({
+                            p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+                            p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
+                        });
+                        if (inkView && intersects) {
+                            intersects.forEach(t => {
+                                intersections.push({ t: +t.toString(), ink: inkView });
                             });
                         }
                     }
@@ -710,6 +766,36 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         return intersections;
     }
 
+    getInkIntersections = (curve: Bezier): number[] => {
+        const intersections: number[] = [];
+        const selfIntersect = curve.selfintersects();
+        if (selfIntersect) {
+            selfIntersect.forEach(t => {
+                intersections.push(+t.toString());
+            });
+        }
+
+        this.childDocs
+            .filter(doc => doc.type === DocumentType.INK)
+            .forEach(doc => {
+                const currInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
+                const currCtrls = Cast(currInk?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
+                for (var i = 0; i < currCtrls.length - 3; i += 4) {
+                    const currArray = [currCtrls[i], currCtrls[i + 1], currCtrls[i + 2], currCtrls[i + 3]];
+                    const currCurve = new Bezier(currArray.map(p => ({ x: p.X, y: p.Y })));
+                    const intersect = curve.intersects(currCurve);
+                    if (intersect) {
+                        intersect.forEach(t => {
+                            intersections.push(+this.parseTValue(t.toString()));
+                        });
+                    }
+                }
+            });
+        return intersections;
+    }
+
+    parseTValue = (t: string) => t.split("/", 1)[0];
+
     handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
         if (!e.cancelBubble) {
             const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index f16e143d8..1d50b5e0d 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -3,6 +3,7 @@ import { Scripting } from "../client/util/Scripting";
 import { Deserializable } from "../client/util/SerializationHelper";
 import { Copy, ToScriptString, ToString } from "./FieldSymbols";
 import { ObjectField } from "./ObjectField";
+import { DocumentView } from "../client/views/nodes/DocumentView";
 
 // Helps keep track of the current ink tool in use.
 export enum InkTool {
@@ -20,6 +21,11 @@ export interface PointData {
     Y: number;
 }
 
+export interface Intersection {
+    t: number;
+    ink: DocumentView;
+}
+
 // Defines an ink as an array of points.
 export type InkData = Array<PointData>;
 
-- 
cgit v1.2.3-70-g09d2


From cc7c677399d1aafa4078741922ffcd0b81ad720f Mon Sep 17 00:00:00 2001
From: vkalev <victor_kalev@brown.edu>
Date: Wed, 24 Nov 2021 12:45:50 -0500
Subject: segmentation of ink strokes prior to deletion

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 192 ++++++++++++---------
 src/fields/InkField.ts                             |   9 +-
 2 files changed, 121 insertions(+), 80 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ce4acb7ad..4aac49469 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -5,7 +5,7 @@ import { DateField } from "../../../../fields/DateField";
 import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc";
 import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas";
 import { Id } from "../../../../fields/FieldSymbols";
-import { InkData, InkField, InkTool, PointData, Intersection } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool, PointData, Intersection, Segment } from "../../../../fields/InkField";
 import { List } from "../../../../fields/List";
 import { ObjectField } from "../../../../fields/ObjectField";
 import { RichTextField } from "../../../../fields/RichTextField";
@@ -53,6 +53,7 @@ import React = require("react");
 import { ColorScheme } from "../../../util/SettingsManager";
 import { Bezier } from "bezier-js";
 import { GestureOverlay } from "../../GestureOverlay";
+import { constants } from "perf_hooks";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -648,6 +649,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
         if (CurrentUserUtils.SelectedTool !== InkTool.None) {
             this._currPoint = { X: e.clientX, Y: e.clientY };
+            // Erasing ink strokes if intersections occur.
             const eraserIntersections: Intersection[] = this.getEraserIntersections();
             if (eraserIntersections) this.eraseInkStrokes(eraserIntersections);
         }
@@ -666,68 +668,28 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
     }
 
-    @action
+    /**
+     * Iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
+     * and deletes the original stroke.
+     * @param eraserIntersections The intersections made by the eraser.
+     */
     eraseInkStrokes = (eraserIntersections: Intersection[]) => {
         eraserIntersections.forEach(intersect => {
-            var t1, t2;
-            var distT1: number, distT2: number;
-            distT1 = distT2 = Number.MAX_SAFE_INTEGER;
-            const t = intersect.t;
             const ink = intersect.ink;
-            const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
-            for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
-                const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
-                const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
-                const inkIntersections: number[] = this.getInkIntersections(curve);
-                inkIntersections.forEach(currentT => {
-                    const diff = t - currentT;
-                    if (diff > 0 && diff < distT1) {
-                        distT1 = diff;
-                        t1 = currentT;
-                    } else if (diff < 0 && diff < distT2) {
-                        distT2 = -diff;
-                        t2 = currentT;
-                    }
-                });
-
-                // Normal case --> deletion of entire stroke
-                if (!t1 && !t2) {
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
-                } else if (t1 && !t2) {
-                    const splitCurve = curve.split(t1);
-                    const leftInkData: PointData[] = splitCurve.left.points.map(p => ({ X: p.x, Y: p.y }));
-                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData);
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
-                } else if (!t1 && t2) {
-                    const splitCurve = curve.split(t2);
-                    const rightInkData: PointData[] = splitCurve.right.points.map(p => ({ X: p.x, Y: p.y }));
-                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData);
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
-                } else if (t1 && t2) {
-                    const splitCurve1 = curve.split(t1);
-                    const leftInkData: PointData[] = splitCurve1.left.points.map(p => ({ X: p.x, Y: p.y }));
-                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData);
-                    const splitCurve2 = curve.split(t2);
-                    const rightInkData: PointData[] = splitCurve2.right.points.map(p => ({ X: p.x, Y: p.y }));
-                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData);
-
-                    if (!this._deleteList.includes(ink)) {
-                        this._deleteList.push(ink);
-                        // Lowering ink opacity to give the user a visual indicator of deletion.
-                        ink.Document.opacity = 0.5;
-                    }
+            if (ink) {
+                if (!this._deleteList.includes(ink)) {
+                    const segments: Segment[] = this.segmentInkStroke(ink, intersect.index ?? 0);
+                    segments.forEach(segment => {
+                        const newInkData: PointData[] = [];
+                        // Appending all curves of the current segment together in order to render a single new stroke.
+                        segment.forEach(curve => {
+                            newInkData.push(...curve.points.map(p => ({ X: p.x, Y: p.y })));
+                        });
+                        GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, newInkData);
+                    });
+                    this._deleteList.push(ink);
+                    // Lowering ink opacity to give the user a visual indicator of deletion.
+                    ink.Document.opacity = 0.5;
                 }
             }
         });
@@ -756,9 +718,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                             p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
                         });
                         if (inkView && intersects) {
-                            intersects.forEach(t => {
-                                intersections.push({ t: +t.toString(), ink: inkView });
-                            });
+                            for (const val of intersects) {
+                                // Casting t-value from type: (string | number) to number for comparisons.
+                                const t = +val.toString();
+                                var unique: boolean = true;
+                                // Ensuring there are no duplicate intersections in the list returned.
+                                for (const prevIntersect of intersections) {
+                                    if (prevIntersect.t === t) {
+                                        unique = false;
+                                        break;
+                                    }
+                                }
+                                if (unique) intersections.push({ t: +t.toString(), ink: inkView, curve: curve, index: i });
+                            }
                         }
                     }
                 }
@@ -766,36 +738,100 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         return intersections;
     }
 
-    getInkIntersections = (curve: Bezier): number[] => {
-        const intersections: number[] = [];
-        const selfIntersect = curve.selfintersects();
-        if (selfIntersect) {
-            selfIntersect.forEach(t => {
-                intersections.push(+t.toString());
-            });
+    /**
+     * Performs segmentation of the ink stroke - creates "segments" or subsections of the current ink stroke at points in which the
+     * ink stroke intersects any other ink stroke (including itself).
+     * @param ink The ink DocumentView intersected by the eraser.
+     * @param excludeCurve The index of the curve in the ink document that the eraser intersection occurred.
+     * @returns The ink stroke represented as a list of segments, excluding the segment in which the eraser intersection occurred.
+     */
+    @action
+    segmentInkStroke = (ink: DocumentView, excludeCurve: number): Segment[] => {
+        const segments: Segment[] = [];
+        var segment: Segment = [];
+        var excludeSegment: boolean = false;
+        const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
+        // Iterating through all of the curves of the intersected ink stroke.
+        for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
+            // Performing "deletion" of a segment by excluding the segment in which intersection with the eraser occurred.
+            if (i === excludeCurve) excludeSegment = true;
+            const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
+            const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
+            // Getting all t-value intersections of the current curve with all other curves.
+            const tVals: number[] = this.getInkIntersections(i, ink, curve);
+            if (tVals.length > 0) {
+                tVals.sort();
+                const curveCopy = new Bezier(curve.points);
+                // Splitting only by the first t-value.
+                const pair = curveCopy.split(tVals[0]);
+                segment.push(pair.left);
+                if (!excludeSegment) {
+                    segments.push(segment);
+                    segment = [pair.right];
+                    excludeSegment = false;
+                }
+                // Splitting by the number of t-values returned.
+                // for (var index = 0; index < tVals.length; index++) {
+                //     const curveCopy = new Bezier(curve.points);
+                //     if (index === 0) {
+                //         const pair = curveCopy.split(tVals[index]);
+                //         segment.push(pair.left);
+                //         segments.push(segment);
+                //         segment = [pair.right];
+                //     } else if (index === tVals.length - 1) {
+                //         segments.push(segment);
+                //         const pair = curveCopy.split(tVals[index]);
+                //         segment = [pair.right];
+                //     } else {
+                //         segments.push(segment);
+                //         const curve = curveCopy.split(index, index + 1);
+                //         segment = [curve];
+                //     }
+                // }
+            } else {
+                segment.push(curve);
+            }
         }
+        if (!excludeSegment) segments.push(segment);
+        return segments;
+    }
 
+    /**
+     * Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all
+     * ink strokes in the current collection.
+     * @param i The index of the current curve within the inkData of the intersected ink stroke.
+     * @param ink The intersected DocumentView of the ink stroke.
+     * @param curve The current curve of the intersected ink stroke.
+     * @returns A list of all t-values at which intersections occur at the current curve of the intersected ink stroke.
+     */
+    getInkIntersections = (i: number, ink: DocumentView, curve: Bezier): number[] => {
+        const tVals: number[] = [];
+        // Iterating through all ink strokes in the current freeform collection.
         this.childDocs
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
                 const currInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
                 const currCtrls = Cast(currInk?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
-                for (var i = 0; i < currCtrls.length - 3; i += 4) {
-                    const currArray = [currCtrls[i], currCtrls[i + 1], currCtrls[i + 2], currCtrls[i + 3]];
+                for (var j = 0; j < currCtrls.length - 3; j += 4) {
+                    const exclude = i === j || i === j - 4 || i === j + 4;
+                    const sameDoc = ink?.Document === currInk?.Document;
+                    // Ensuring that the curve intersected by the eraser is not checked for further ink intersections.
+                    if (sameDoc && exclude) continue;
+                    const currArray = [currCtrls[j], currCtrls[j + 1], currCtrls[j + 2], currCtrls[j + 3]];
                     const currCurve = new Bezier(currArray.map(p => ({ x: p.X, y: p.Y })));
                     const intersect = curve.intersects(currCurve);
-                    if (intersect) {
-                        intersect.forEach(t => {
-                            intersections.push(+this.parseTValue(t.toString()));
+                    if (intersect.length > 0) {
+                        intersect.forEach(val => {
+                            // Converting the Bezier.js Split type to a t-value number.
+                            const t = +val.toString().split("/")[0];
+                            if (!tVals.includes(t)) tVals.push(t);
                         });
                     }
                 }
             });
-        return intersections;
+        return tVals;
     }
 
-    parseTValue = (t: string) => t.split("/", 1)[0];
-
     handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
         if (!e.cancelBubble) {
             const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 1d50b5e0d..f61313674 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -4,6 +4,7 @@ import { Deserializable } from "../client/util/SerializationHelper";
 import { Copy, ToScriptString, ToString } from "./FieldSymbols";
 import { ObjectField } from "./ObjectField";
 import { DocumentView } from "../client/views/nodes/DocumentView";
+import { Bezier } from "bezier-js";
 
 // Helps keep track of the current ink tool in use.
 export enum InkTool {
@@ -22,10 +23,14 @@ export interface PointData {
 }
 
 export interface Intersection {
-    t: number;
-    ink: DocumentView;
+    t?: number;
+    ink?: DocumentView;
+    curve?: Bezier;
+    index?: number;
 }
 
+export type Segment = Array<Bezier>;
+
 // Defines an ink as an array of points.
 export type InkData = Array<PointData>;
 
-- 
cgit v1.2.3-70-g09d2


From 8405ed2e3ebaf7ba7842a5619e9b252bf5eb9c4c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 25 Nov 2021 21:45:06 -0500
Subject: fixed up erasing ink by segments

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 156 +++++++++------------
 1 file changed, 65 insertions(+), 91 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4aac49469..df690da49 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -34,7 +34,7 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariable
 import { Timeline } from "../../animationtimeline/Timeline";
 import { ContextMenu } from "../../ContextMenu";
 import { DocumentDecorations } from "../../DocumentDecorations";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkWidth, SetActiveFillColor, SetActiveInkColor } from "../../InkingStroke";
 import { LightboxView } from "../../LightboxView";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
@@ -610,11 +610,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             this.removeMoveListeners();
             this.removeEndListeners();
             if (CurrentUserUtils.SelectedTool !== InkTool.None) {
-                if (this._deleteList.length > 0) {
-                    this._deleteList.forEach(ink => {
-                        ink.props.removeDocument?.(ink.props.Document);
-                    });
-                }
+                this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
                 this._prevPoint = this._currPoint = { X: -1, Y: -1 };
                 this._deleteList = [];
                 this._batch?.end();
@@ -650,8 +646,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         if (CurrentUserUtils.SelectedTool !== InkTool.None) {
             this._currPoint = { X: e.clientX, Y: e.clientY };
             // Erasing ink strokes if intersections occur.
-            const eraserIntersections: Intersection[] = this.getEraserIntersections();
-            if (eraserIntersections) this.eraseInkStrokes(eraserIntersections);
+            this.eraseInkStrokes(this.getEraserIntersections());
         }
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             if (this.props.isContentActive(true)) e.stopPropagation();
@@ -676,21 +671,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     eraseInkStrokes = (eraserIntersections: Intersection[]) => {
         eraserIntersections.forEach(intersect => {
             const ink = intersect.ink;
-            if (ink) {
-                if (!this._deleteList.includes(ink)) {
-                    const segments: Segment[] = this.segmentInkStroke(ink, intersect.index ?? 0);
-                    segments.forEach(segment => {
-                        const newInkData: PointData[] = [];
-                        // Appending all curves of the current segment together in order to render a single new stroke.
-                        segment.forEach(curve => {
-                            newInkData.push(...curve.points.map(p => ({ X: p.x, Y: p.y })));
-                        });
-                        GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, newInkData);
-                    });
-                    this._deleteList.push(ink);
-                    // Lowering ink opacity to give the user a visual indicator of deletion.
-                    ink.Document.opacity = 0.5;
-                }
+            if (ink && !this._deleteList.includes(ink)) {
+                this._deleteList.push(ink);
+                SetActiveInkWidth(StrCast(ink.rootDoc.strokeWidth?.toString()) || "1");
+                SetActiveInkColor(StrCast(ink.rootDoc.color?.toString()) || "black");
+                // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
+                this.segmentInkStroke(ink, intersect.t ?? 0).forEach(segment =>
+                    GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke,
+                        segment.reduce((data, curve) => [...data, ...curve.points
+                            .map(p => ink.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })
+                        ], [] as PointData[])));
+                // Lower ink opacity to give the user a visual indicator of deletion.
+                ink.layoutDoc.opacity = 0.5;
             }
         });
     }
@@ -705,12 +697,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
                 const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
-                const ctrlPoints = Cast(inkView?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
-                for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
-                    const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
+                const inkStroke = inkView?.ComponentView as InkingStroke;
+                const { inkData } = inkStroke?.inkScaledData();
+                for (var i = 0; i < inkData.length - 3; i += 4) {
+                    const array = inkData.slice(i, i + 4);
                     // Converting from screen space to ink space for the intersection.
-                    const prevPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._prevPoint);
-                    const currPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._currPoint);
+                    const prevPointInkSpace = inkStroke?.ptFromScreen?.(this._prevPoint);
+                    const currPointInkSpace = inkStroke?.ptFromScreen?.(this._currPoint);
                     if (prevPointInkSpace && currPointInkSpace) {
                         const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
                         const intersects = curve.intersects({
@@ -720,7 +713,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                         if (inkView && intersects) {
                             for (const val of intersects) {
                                 // Casting t-value from type: (string | number) to number for comparisons.
-                                const t = +val.toString();
+                                const t = +(Number(val) + Math.floor(i / 4)).toString(); // add start of curve segment to convert from local t value to t value along complete curve
                                 var unique: boolean = true;
                                 // Ensuring there are no duplicate intersections in the list returned.
                                 for (const prevIntersect of intersections) {
@@ -729,7 +722,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                                         break;
                                     }
                                 }
-                                if (unique) intersections.push({ t: +t.toString(), ink: inkView, curve: curve, index: i });
+                                if (unique) intersections.push({ t: +t.toString(), ink: inkView, curve: curve });
                             }
                         }
                     }
@@ -742,57 +735,40 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
      * Performs segmentation of the ink stroke - creates "segments" or subsections of the current ink stroke at points in which the
      * ink stroke intersects any other ink stroke (including itself).
      * @param ink The ink DocumentView intersected by the eraser.
-     * @param excludeCurve The index of the curve in the ink document that the eraser intersection occurred.
+     * @param excludeT The index of the curve in the ink document that the eraser intersection occurred.
      * @returns The ink stroke represented as a list of segments, excluding the segment in which the eraser intersection occurred.
      */
     @action
-    segmentInkStroke = (ink: DocumentView, excludeCurve: number): Segment[] => {
+    segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => {
         const segments: Segment[] = [];
         var segment: Segment = [];
-        var excludeSegment: boolean = false;
-        const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
-        // Iterating through all of the curves of the intersected ink stroke.
-        for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
-            // Performing "deletion" of a segment by excluding the segment in which intersection with the eraser occurred.
-            if (i === excludeCurve) excludeSegment = true;
-            const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
-            const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
+        var startSegmentT = 0;
+        const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
+        // This iterates through all segments of the curve and splits them where they intersect another curve.
+        // if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted)
+        for (var i = 0; i < inkData.length - 3; i += 4) {
+            const curve = new Bezier(inkData.slice(i, i + 4).map(p => ({ x: p.X, y: p.Y })));
             // Getting all t-value intersections of the current curve with all other curves.
-            const tVals: number[] = this.getInkIntersections(i, ink, curve);
-            if (tVals.length > 0) {
-                tVals.sort();
-                const curveCopy = new Bezier(curve.points);
-                // Splitting only by the first t-value.
-                const pair = curveCopy.split(tVals[0]);
-                segment.push(pair.left);
-                if (!excludeSegment) {
-                    segments.push(segment);
-                    segment = [pair.right];
-                    excludeSegment = false;
-                }
-                // Splitting by the number of t-values returned.
-                // for (var index = 0; index < tVals.length; index++) {
-                //     const curveCopy = new Bezier(curve.points);
-                //     if (index === 0) {
-                //         const pair = curveCopy.split(tVals[index]);
-                //         segment.push(pair.left);
-                //         segments.push(segment);
-                //         segment = [pair.right];
-                //     } else if (index === tVals.length - 1) {
-                //         segments.push(segment);
-                //         const pair = curveCopy.split(tVals[index]);
-                //         segment = [pair.right];
-                //     } else {
-                //         segments.push(segment);
-                //         const curve = curveCopy.split(index, index + 1);
-                //         segment = [curve];
-                //     }
-                // }
+            const tVals = this.getInkIntersections(i, ink, curve).sort();
+            if (tVals.length) {
+                tVals.forEach((t, index) => {
+                    const docCurveTVal = t + Math.floor(i / 4);
+                    if (excludeT < startSegmentT || excludeT > docCurveTVal) {
+                        const localStartTVal = startSegmentT - Math.floor(i / 4);
+                        segment.push(curve.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+                        segment.length && segments.push(segment);
+                    }
+                    // start a new segment from the intersection t value
+                    segment = tVals.length - 1 === index ? [curve.split(t).right] : [];
+                    startSegmentT = docCurveTVal;
+                });
             } else {
                 segment.push(curve);
             }
         }
-        if (!excludeSegment) segments.push(segment);
+        if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) {
+            segment.length && segments.push(segment);
+        }
         return segments;
     }
 
@@ -810,23 +786,21 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         this.childDocs
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
-                const currInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
-                const currCtrls = Cast(currInk?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? [];
-                for (var j = 0; j < currCtrls.length - 3; j += 4) {
-                    const exclude = i === j || i === j - 4 || i === j + 4;
-                    const sameDoc = ink?.Document === currInk?.Document;
+                const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke;
+                const { inkData: otherInkData } = otherInk.inkScaledData();
+                const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
+                const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt));
+                for (var j = 0; j < otherCtrlPts.length - 3; j += 4) {
+                    const neighboringSegment = i === j || i === j - 4 || i === j + 4;
                     // Ensuring that the curve intersected by the eraser is not checked for further ink intersections.
-                    if (sameDoc && exclude) continue;
-                    const currArray = [currCtrls[j], currCtrls[j + 1], currCtrls[j + 2], currCtrls[j + 3]];
-                    const currCurve = new Bezier(currArray.map(p => ({ x: p.X, y: p.Y })));
-                    const intersect = curve.intersects(currCurve);
-                    if (intersect.length > 0) {
-                        intersect.forEach(val => {
-                            // Converting the Bezier.js Split type to a t-value number.
-                            const t = +val.toString().split("/")[0];
-                            if (!tVals.includes(t)) tVals.push(t);
-                        });
-                    }
+                    if (ink?.Document === otherInk.props.Document && neighboringSegment) continue;
+
+                    const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
+                    curve.intersects(otherCurve).forEach((val: string | number, i: number) => {
+                        // Converting the Bezier.js Split type to a t-value number.
+                        const t = +val.toString().split("/")[0];
+                        if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).  
+                    });
                 }
             });
         return tVals;
@@ -1006,10 +980,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 .filter(({ pos, size }) => pos && size).map(({ pos, size }) => ({ pos: pos!, size: size! }));
             if (measuredDocs.length) {
                 const ranges = measuredDocs.reduce(({ xrange, yrange }, { pos, size }) =>  // computes range of content
-                ({
-                    xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) },
-                    yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) }
-                })
+                    ({
+                        xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) },
+                        yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) }
+                    })
                     , {
                         xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
                         yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }
-- 
cgit v1.2.3-70-g09d2


From 49e9103721ecfd6d5c900c754f2d88362f64a3ab Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 2 Dec 2021 00:22:05 -0500
Subject: added shift erase stroke to delete full strokes.  added shift drag
 end of stroke to scale uniformly.  added ctrl+p,ctrl+e to switch between
 eraser/pen.  added delete to menu options for tree view items.  cleaned up a
 lot of ink code.

---
 src/client/views/GlobalKeyHandler.ts               |   4 +
 src/client/views/InkControlPtHandles.tsx           |  18 +-
 src/client/views/InkStrokeProperties.ts            |  13 +-
 src/client/views/collections/TreeView.tsx          |  13 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 219 ++++++++++-----------
 src/fields/InkField.ts                             |  16 +-
 6 files changed, 136 insertions(+), 147 deletions(-)

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

diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d5e0ed962..1a4080d81 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -230,6 +230,10 @@ export class KeyManager {
                     }
                 }
                 break;
+            case "e": CurrentUserUtils.SelectedTool = InkTool.Eraser;
+                break;
+            case "p": CurrentUserUtils.SelectedTool = InkTool.Pen;
+                break;
             case "o":
                 const target = SelectionManager.Docs().lastElement();
                 target && CollectionDockingView.OpenFullScreen(target);
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 76ce73b0d..0996e75d4 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -191,16 +191,16 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
         setupMoveUpEvents(this, e, (e) => {
             if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink");
             // compute stretch factor by finding scaling along axis between start and end points
-            const v1 = { x: p1().X - p2().X, y: p1().Y - p2().Y };
-            const v2 = { x: e.clientX - p2().X, y: e.clientY - p2().Y };
-            const v1len = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
-            const v2len = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
+            const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y };
+            const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y };
+            const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y);
+            const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y);
             const scaling = v2len / v1len;
-            const v1n = { x: v1.x / v1len, y: v1.y / v1len };
-            const v2n = { x: v2.x / v2len, y: v2.y / v2len };
-            const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y);
-            InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n);
-            InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y });
+            const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
+            const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
+            const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
+            InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, p2(), v1n, e.shiftKey);
+            InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, p2());
             return false;
         }, action(() => {
             this.controlUndo?.end();
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 9634e6e83..695bdcc5a 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -205,18 +205,17 @@ export class InkStrokeProperties {
      */
     @undoBatch
     @action
-    stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: { x: number, y: number }, scrVec: { x: number, y: number }) => {
+    stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
         this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
             const ptFromScreen = view.ComponentView?.ptFromScreen;
             const ptToScreen = view.ComponentView?.ptToScreen;
             return !ptToScreen || !ptFromScreen ? ink :
                 ink.map(ptToScreen).map(i => {
-                    const pvec = { X: i.X - scrpt.x, Y: i.Y - scrpt.y };
-                    const svec = pvec.X * scrVec.x * scaling + pvec.Y * scrVec.y * scaling;
-                    const ovec = -pvec.X * scrVec.y + pvec.Y * (scrVec.x);
-                    const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x };
-                    const newpt = ptFromScreen(newscrpt);
-                    return newpt;
+                    const pvec = { X: i.X - scrpt.X, Y: i.Y - scrpt.Y };
+                    const svec = pvec.X * scrVec.X * scaling + pvec.Y * scrVec.Y * scaling;
+                    const ovec = -pvec.X * scrVec.Y * (scaleUniformly ? scaling : 1) + pvec.Y * scrVec.X * (scaleUniformly ? scaling : 1);
+                    const newscrpt = { X: scrpt.X + svec * scrVec.X - ovec * scrVec.Y, Y: scrpt.Y + svec * scrVec.Y + ovec * scrVec.X };
+                    return ptFromScreen(newscrpt);
                 });
         });
     }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index d8f984601..eedb353e3 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -254,9 +254,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView };
         return this.props.addDocument(folder);
     }
-    deleteFolder = () => {
-        return this.props.removeDoc?.(this.doc);
-    }
+    deleteItem = () => this.props.removeDoc?.(this.doc);
 
     preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
         const dragData = de.complete.docDragData;
@@ -531,16 +529,16 @@ export class TreeView extends React.Component<TreeViewProps> {
     }
     contextMenuItems = () => {
         const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "New Folder" };
-        const deleteFolder = { script: ScriptField.MakeFunction(`scriptContext.deleteFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete Folder" };
-        const folderOp = this.childDocs?.length ? makeFolder : deleteFolder;
+        const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete" };
+        const folderOp = this.childDocs?.length ? [makeFolder] : [];
         const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: "copy", label: "Open Alias" };
         const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: "eye", label: "Focus or Open" };
-        return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [folderOp] :
+        return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? folderOp :
             Doc.IsSystem(this.doc) ? [] :
                 this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
                     [openAlias, makeFolder] :
                     this.doc.viewType === CollectionViewType.Docking ? [] :
-                        [openAlias, focusDoc])];
+                        [deleteItem, openAlias, focusDoc])];
     }
     childContextMenuItems = () => {
         const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -784,6 +782,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, "copy", undefined, false));
     }
 
+
     render() {
         TraceMobx();
         const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ceee4051b..19c3bf745 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,11 +1,12 @@
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx";
+import { Bezier } from "bezier-js";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { computedFn } from "mobx-utils";
 import { DateField } from "../../../../fields/DateField";
 import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc";
 import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas";
 import { Id } from "../../../../fields/FieldSymbols";
-import { InkData, InkField, InkTool, PointData, Intersection, Segment } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fields/InkField";
 import { List } from "../../../../fields/List";
 import { ObjectField } from "../../../../fields/ObjectField";
 import { RichTextField } from "../../../../fields/RichTextField";
@@ -27,14 +28,15 @@ import { InteractionUtils } from "../../../util/InteractionUtils";
 import { LinkManager } from "../../../util/LinkManager";
 import { SearchUtil } from "../../../util/SearchUtil";
 import { SelectionManager } from "../../../util/SelectionManager";
+import { ColorScheme } from "../../../util/SettingsManager";
 import { SnappingManager } from "../../../util/SnappingManager";
 import { Transform } from "../../../util/Transform";
 import { undoBatch, UndoManager } from "../../../util/UndoManager";
 import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariables.scss";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { ContextMenu } from "../../ContextMenu";
-import { DocumentDecorations } from "../../DocumentDecorations";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkWidth, SetActiveFillColor, SetActiveInkColor } from "../../InkingStroke";
+import { GestureOverlay } from "../../GestureOverlay";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "../../InkingStroke";
 import { LightboxView } from "../../LightboxView";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
@@ -50,10 +52,6 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
 import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
-import { ColorScheme } from "../../../util/SettingsManager";
-import { Bezier } from "bezier-js";
-import { GestureOverlay } from "../../GestureOverlay";
-import { constants } from "perf_hooks";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -115,8 +113,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     @observable _pullDirection: string = "";
     @observable _showAnimTimeline = false;
     @observable _clusterSets: (Doc[])[] = [];
-    @observable _prevPoint: PointData = { X: -1, Y: -1 };
-    @observable _currPoint: PointData = { X: -1, Y: -1 };
     @observable _deleteList: DocumentView[] = [];
     @observable _timelineRef = React.createRef<Timeline>();
     @observable _marqueeRef = React.createRef<HTMLDivElement>();
@@ -439,28 +435,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) ||
-            ([InkTool.Pen, InkTool.Highlighter].includes(CurrentUserUtils.SelectedTool))) {
-            return;
-        }
-        this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+        this._downX = this._lastX = e.pageX;
+        this._downY = this._lastY = e.pageY;
         if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) {
-            document.removeEventListener("pointermove", this.onPointerMove);
-            document.removeEventListener("pointerup", this.onPointerUp);
-            document.addEventListener("pointermove", this.onPointerMove);
-            document.addEventListener("pointerup", this.onPointerUp);
-            // if not using a pen and in no ink mode
-            if (CurrentUserUtils.SelectedTool === InkTool.None) {
-                this._downX = this._lastX = e.pageX;
-                this._downY = this._lastY = e.pageY;
-            }
-            // eraser plus anything else mode
-            else {
-                this._batch = UndoManager.StartBatch("collectionErase");
-                this._prevPoint = { X: e.clientX, Y: e.clientY };
-                e.stopPropagation();
-                e.preventDefault();
-            }
+            if (!e.nativeEvent.cancelBubble &&
+                !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
+                !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) &&
+                !InteractionUtils.IsType(e, InteractionUtils.PENTYPE))
+                switch (CurrentUserUtils.SelectedTool) {
+                    case InkTool.Highlighter:
+                    case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
+                    case InkTool.Eraser:
+                        document.addEventListener("pointermove", this.onEraserMove);
+                        document.addEventListener("pointerup", this.onEraserUp);
+                        this._batch = UndoManager.StartBatch("collectionErase");
+                        e.stopPropagation();
+                        e.preventDefault();
+                        break;
+                    case InkTool.None:
+                        this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+                        document.addEventListener("pointermove", this.onPointerMove);
+                        document.addEventListener("pointerup", this.onPointerUp);
+                        break;
+                }
         }
     }
 
@@ -600,6 +597,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 }
         }
     }
+    @action
+    onEraserUp = (e: PointerEvent): void => {
+        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
+            document.removeEventListener("pointermove", this.onEraserMove);
+            document.removeEventListener("pointerup", this.onEraserUp);
+            this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
+            this._deleteList = [];
+            this._batch?.end();
+        }
+    }
 
     @action
     onPointerUp = (e: PointerEvent): void => {
@@ -608,12 +615,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             document.removeEventListener("pointerup", this.onPointerUp);
             this.removeMoveListeners();
             this.removeEndListeners();
-            if (CurrentUserUtils.SelectedTool !== InkTool.None) {
-                this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
-                this._prevPoint = this._currPoint = { X: -1, Y: -1 };
-                this._deleteList = [];
-                this._batch?.end();
-            }
         }
     }
 
@@ -638,96 +639,82 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         this._lastY = e.clientY;
     }
 
-    @action
-    onPointerMove = (e: PointerEvent): void => {
-        if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
-        if (CurrentUserUtils.SelectedTool !== InkTool.None) {
-            this._currPoint = { X: e.clientX, Y: e.clientY };
-            // Erasing ink strokes if intersections occur.
-            this.eraseInkStrokes(this.getEraserIntersections());
-        }
-        if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
-            if (this.props.isContentActive(true)) e.stopPropagation();
-        } else if (!e.cancelBubble) {
-            if (CurrentUserUtils.SelectedTool === InkTool.None) {
-                if (this.tryDragCluster(e, this._hitCluster)) {
-                    document.removeEventListener("pointermove", this.onPointerMove);
-                    document.removeEventListener("pointerup", this.onPointerUp);
-                }
-                else this.pan(e);
-            }
-            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-            e.preventDefault();
-        }
-    }
-
     /**
-     * Iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
+     * Erases strokes by intersecting them with an invisible "eraser stroke".
+     *  By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
      * and deletes the original stroke.
-     * @param eraserIntersections The intersections made by the eraser.
+     *  However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety.
      */
-    eraseInkStrokes = (eraserIntersections: Intersection[]) => {
-        eraserIntersections.forEach(intersect => {
-            const ink = intersect.ink;
-            if (ink && !this._deleteList.includes(ink)) {
-                this._deleteList.push(ink);
-                SetActiveInkWidth(StrCast(ink.rootDoc.strokeWidth?.toString()) || "1");
-                SetActiveInkColor(StrCast(ink.rootDoc.color?.toString()) || "black");
+    @action
+    onEraserMove = (e: PointerEvent) => {
+        const currPoint = { X: e.clientX, Y: e.clientY };
+        this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => {
+            if (!this._deleteList.includes(intersect.inkView)) {
+                this._deleteList.push(intersect.inkView);
+                SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || "1");
+                SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || "black");
                 // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
-                this.segmentInkStroke(ink, intersect.t ?? 0).forEach(segment =>
+                !e.shiftKey && this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment =>
                     GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke,
                         segment.reduce((data, curve) => [...data, ...curve.points
-                            .map(p => ink.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })
+                            .map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })
                         ], [] as PointData[])));
                 // Lower ink opacity to give the user a visual indicator of deletion.
-                ink.layoutDoc.opacity = 0.5;
+                intersect.inkView.layoutDoc.opacity = 0.5;
             }
         });
+        this._lastX = currPoint.X;
+        this._lastY = currPoint.Y;
+
+        e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
+        e.preventDefault();
+    }
+
+    @action
+    onPointerMove = (e: PointerEvent): void => {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
+        if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
+            if (this.props.isContentActive(true)) e.stopPropagation();
+        } else if (!e.cancelBubble) {
+            if (this.tryDragCluster(e, this._hitCluster)) {
+                document.removeEventListener("pointermove", this.onPointerMove);
+                document.removeEventListener("pointerup", this.onPointerUp);
+            }
+            else this.pan(e);
+            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
+            e.preventDefault();
+        }
     }
 
     /**
      * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
-     * @returns A dictionary mapping the t-value intersection of the eraser with the corresponding ink DocumentView.
+     * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected
      */
-    getEraserIntersections = (): Intersection[] => {
-        const intersections: Intersection[] = [];
-        this.childDocs
-            .filter(doc => doc.type === DocumentType.INK)
-            .forEach(doc => {
-                const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
-                const inkStroke = inkView?.ComponentView as InkingStroke;
-                const { inkData } = inkStroke?.inkScaledData();
+    getEraserIntersections = (lastPoint: { X: number, Y: number }, currPoint: { X: number, Y: number }) => {
+        const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) };
+        const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
+        return this.childDocs
+            .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView))
+            .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
+            .map(inkView => ({ inkViewBounds: inkView!.getBounds(), inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! }))
+            .filter(({ inkViewBounds }) => inkViewBounds && // bounding box of eraser segment and ink stroke overlap
+                eraserMin.X <= inkViewBounds.right && eraserMin.Y <= inkViewBounds.bottom &&
+                eraserMax.X >= inkViewBounds.left && eraserMax.Y >= inkViewBounds.top)
+            .reduce((intersections, { inkStroke, inkView }) => {
+                const { inkData } = inkStroke.inkScaledData();
+                // Convert from screen space to ink space for the intersection.
+                const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
+                const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
                 for (var i = 0; i < inkData.length - 3; i += 4) {
-                    const array = inkData.slice(i, i + 4);
-                    // Converting from screen space to ink space for the intersection.
-                    const prevPointInkSpace = inkStroke?.ptFromScreen?.(this._prevPoint);
-                    const currPointInkSpace = inkStroke?.ptFromScreen?.(this._currPoint);
-                    if (prevPointInkSpace && currPointInkSpace) {
-                        const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
-                        const intersects = curve.intersects({
-                            p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
-                            p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
-                        });
-                        if (inkView && intersects) {
-                            for (const val of intersects) {
-                                // Casting t-value from type: (string | number) to number for comparisons.
-                                const t = +(Number(val) + Math.floor(i / 4)).toString(); // add start of curve segment to convert from local t value to t value along complete curve
-                                var unique: boolean = true;
-                                // Ensuring there are no duplicate intersections in the list returned.
-                                for (const prevIntersect of intersections) {
-                                    if (prevIntersect.t === t) {
-                                        unique = false;
-                                        break;
-                                    }
-                                }
-                                if (unique) intersections.push({ t: +t.toString(), ink: inkView, curve: curve });
-                            }
-                        }
-                    }
+                    const intersects = Array.from(new Set(InkField.Segment(inkData, i).intersects({  // compute all unique intersections
+                        p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+                        p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
+                    }) as (number | string)[])); // convert to more manageable union array type
+                    // return tuples of the inkingStroke intersected, and the t value of the intersection
+                    intersections.push(...intersects.map(t => ({ inkView, t: (+t) + Math.floor(i / 4) })));// convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve
                 }
-            });
-        return intersections;
+                return intersections;
+            }, [] as { t: number, inkView: DocumentView }[]);
     }
 
     /**
@@ -746,23 +733,23 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         // This iterates through all segments of the curve and splits them where they intersect another curve.
         // if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted)
         for (var i = 0; i < inkData.length - 3; i += 4) {
-            const curve = new Bezier(inkData.slice(i, i + 4).map(p => ({ x: p.X, y: p.Y })));
+            const inkSegment = InkField.Segment(inkData, i);
             // Getting all t-value intersections of the current curve with all other curves.
-            const tVals = this.getInkIntersections(i, ink, curve).sort();
+            const tVals = this.getInkIntersections(i, ink, inkSegment).sort();
             if (tVals.length) {
                 tVals.forEach((t, index) => {
                     const docCurveTVal = t + Math.floor(i / 4);
                     if (excludeT < startSegmentT || excludeT > docCurveTVal) {
                         const localStartTVal = startSegmentT - Math.floor(i / 4);
-                        segment.push(curve.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+                        segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
                         segment.length && segments.push(segment);
                     }
                     // start a new segment from the intersection t value
-                    segment = tVals.length - 1 === index ? [curve.split(t).right] : [];
+                    segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : [];
                     startSegmentT = docCurveTVal;
                 });
             } else {
-                segment.push(curve);
+                segment.push(inkSegment);
             }
         }
         if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) {
@@ -786,7 +773,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
                 const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke;
-                const { inkData: otherInkData } = otherInk.inkScaledData();
+                const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
                 const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
                 const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt));
                 for (var j = 0; j < otherCtrlPts.length - 3; j += 4) {
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index f61313674..560cf3d63 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -3,7 +3,6 @@ import { Scripting } from "../client/util/Scripting";
 import { Deserializable } from "../client/util/SerializationHelper";
 import { Copy, ToScriptString, ToString } from "./FieldSymbols";
 import { ObjectField } from "./ObjectField";
-import { DocumentView } from "../client/views/nodes/DocumentView";
 import { Bezier } from "bezier-js";
 
 // Helps keep track of the current ink tool in use.
@@ -22,13 +21,6 @@ export interface PointData {
     Y: number;
 }
 
-export interface Intersection {
-    t?: number;
-    ink?: DocumentView;
-    curve?: Bezier;
-    index?: number;
-}
-
 export type Segment = Array<Bezier>;
 
 // Defines an ink as an array of points.
@@ -78,6 +70,14 @@ export class InkField extends ObjectField {
         this.inkData = data;
     }
 
+    /**
+     * Extacts a simple segment from a compound Bezier curve
+     * @param segIndex the start index of the simple bezier segment to extact (eg., 0, 4, 8, ...)
+     */
+    public static Segment(inkData: InkData, segIndex: number) {
+        return new Bezier(inkData.slice(segIndex, segIndex + 4).map(pt => ({ x: pt.X, y: pt.Y })));
+    }
+
     [Copy]() {
         return new InkField(this.inkData);
     }
-- 
cgit v1.2.3-70-g09d2


From f0495cc1a050abfbaffe96b73bed24c8afe4b7b4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 2 Dec 2021 00:37:54 -0500
Subject: fixed filled strokes when erasing to render properly.  fixed stretch
 ink to work with multiple selections.

---
 src/client/views/InkControlPtHandles.tsx                             | 5 +++--
 src/client/views/InkingStroke.tsx                                    | 2 +-
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx  | 3 ++-
 3 files changed, 6 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 0996e75d4..ef457cac4 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -13,6 +13,7 @@ import { Colors } from "./global/globalEnums";
 import { InkingStroke } from "./InkingStroke";
 import { InkStrokeProperties } from "./InkStrokeProperties";
 import { DocumentView } from "./nodes/DocumentView";
+import { SelectionManager } from "../util/SelectionManager";
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -199,8 +200,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
             const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
             const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
-            InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, p2(), v1n, e.shiftKey);
-            InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, p2());
+            InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2(), v1n, e.shiftKey);
+            InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, p2());
             return false;
         }, action(() => {
             this.controlUndo?.end();
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index fc1727055..f5dd66949 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -314,7 +314,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
         // Invisible polygonal line that enables the ink to be selected by the user.
         const clickableLine = (downHdlr?: (e: React.PointerEvent) => void) => InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
-            inkStrokeWidth, inkStrokeWidth + (CurrentUserUtils.SelectedTool === InkTool.Eraser ? 0 : highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+            inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
             StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
             StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
             undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 19c3bf745..aeda71d01 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -441,7 +441,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             if (!e.nativeEvent.cancelBubble &&
                 !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
                 !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) &&
-                !InteractionUtils.IsType(e, InteractionUtils.PENTYPE))
+                !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
                 switch (CurrentUserUtils.SelectedTool) {
                     case InkTool.Highlighter:
                     case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
@@ -458,6 +458,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                         document.addEventListener("pointerup", this.onPointerUp);
                         break;
                 }
+            }
         }
     }
 
-- 
cgit v1.2.3-70-g09d2

