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/client/views/collections/TreeView.tsx | 49 +++++++++++++++++++------------
 1 file changed, 30 insertions(+), 19 deletions(-)

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

diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 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>;
-- 
cgit v1.2.3-70-g09d2


From dc3b329885785297b24f7f73e5d60dd56c370c7f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 13 Nov 2021 11:13:39 -0500
Subject: treeview title fixes - made it aformatted text box so clicking on it
 will select the document.  fixed margins.  fixed dragging over tree view to
 not highlight drop bullets in outline mode unless its a bullet point being
 moved.

---
 src/client/util/DragManager.ts                     |  2 ++
 .../views/collections/CollectionTreeView.tsx       | 30 ++++++++--------------
 src/client/views/collections/TreeView.tsx          |  8 +++---
 3 files changed, 17 insertions(+), 23 deletions(-)

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

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index f5704d2bf..ae3fa3170 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -335,8 +335,10 @@ export namespace DragManager {
     }
     export let docsBeingDragged: Doc[] = [];
     export let CanEmbed = false;
+    export let DocDragData: DocumentDragData | undefined;
     export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
         if (dragData.dropAction === "none") return;
+        DocDragData = dragData instanceof DocumentDragData ? dragData : undefined;
         const batch = UndoManager.StartBatch("dragging");
         eles = eles.filter(e => e);
         CanEmbed = dragData.canEmbed || false;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 6fb18d4c2..ea077ea40 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -7,7 +7,7 @@ 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 { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnOne } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { CurrentUserUtils } from '../../util/CurrentUserUtils';
 import { DocumentManager } from '../../util/DocumentManager';
@@ -47,7 +47,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     private _disposers: { [name: string]: IReactionDisposer } = {};
     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.
+    private observer: any; // observer for monitoring tree view items.
+    private static expandViewLabelSize = 20;
 
     @computed get doc() { return this.props.Document; }
     @computed get dataDoc() { return this.props.DataDoc || this.doc; }
@@ -187,36 +188,26 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
 
 
     get documentTitle() {
-        return <DocumentView
-            Document={this.doc}
-            DataDoc={undefined}
-            LayoutTemplateString={FormattedTextBox.LayoutString("text")}
+        return <FormattedTextBox
+            {...this.props}
+            fieldKey={"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}
+            scaling={returnOne}
             docFilters={returnEmptyFilter}
             docRangeFilters={returnEmptyFilter}
             searchFilterDocs={returnEmptyDoclist}
             ContainingCollectionDoc={this.doc}
             ContainingCollectionView={this.props.CollectionView}
-            addDocument={this.props.addDocument}
+            addDocument={returnFalse}
             moveDocument={returnFalse}
             removeDocument={returnFalse}
             whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-            addDocTab={this.props.addDocTab}
-            pinToPres={this.props.pinToPres}
             bringToFront={returnFalse}
         />;
     }
@@ -265,7 +256,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     @computed get titleBar() {
         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() } : {}}
+                style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}}
                 ref={r => this._titleRef = r}
                 onKeyDown={e => {
                     if (this.outlineMode) {
@@ -333,10 +324,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     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.marginX()) * (this.props.scaling?.() || 1);
+    panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (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;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index a266c301f..d8f984601 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -221,9 +221,11 @@ export class TreeView extends React.Component<TreeViewProps> {
         const before = pt[1] < rect.top + rect.height / 2;
         const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length);
         this._header.current!.className = "treeView-header";
-        if (inside) this._header.current!.className += " treeView-header-inside";
-        else if (before) this._header.current!.className += " treeView-header-above";
-        else if (!before) this._header.current!.className += " treeView-header-below";
+        if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) {
+            if (inside) this._header.current!.className += " treeView-header-inside";
+            else if (before) this._header.current!.className += " treeView-header-above";
+            else if (!before) this._header.current!.className += " treeView-header-below";
+        }
         e.stopPropagation();
     }
 
-- 
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/TreeView.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

