From 21cd63dc9536deea17814231605ff22ea59e26b4 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Mon, 18 May 2020 14:24:28 -0700
Subject: fixed some bugs with webbox and hacked some problems with cognitive
 services

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx  | 5 +++--
 1 file changed, 3 insertions(+), 2 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 c6bae66a4..c3a77841e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -438,9 +438,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 console.log("end");
                 if (this._inkToTextStartX && this._inkToTextStartY) {
                     const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
-                    const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "text" && s.color);
+                    const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "rtf" && s.color);
                     const sets = setDocs.map((sd) => {
-                        return Cast(sd.data, RichTextField)?.Text as string;
+                        return Cast(sd.text, RichTextField)?.Text as string;
                     });
                     if (sets.length && sets[0]) {
                         this._wordPalette.clear();
@@ -476,6 +476,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                         }
                     });
 
+                    console.log(this._wordPalette)
                     CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => {
                         console.log(results);
                         const wordResults = results.filter((r: any) => r.category === "inkWord");
-- 
cgit v1.2.3-70-g09d2


From 43818b137486f3d4431e1c3b9f4de0b0aefba9ca Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 23 May 2020 14:59:01 -0400
Subject: several fixes to progressivization and added UI for editing by
 suspending transparency

---
 .../collectionFreeForm/CollectionFreeFormView.scss |  9 ++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 50 +++++++++++++++-------
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 14 +++---
 src/client/views/nodes/KeyValuePair.tsx            |  4 +-
 .../views/presentationview/PresElementBox.tsx      |  5 +--
 src/fields/ScriptField.ts                          | 17 ++++----
 src/fields/util.ts                                 |  2 +-
 8 files changed, 66 insertions(+), 39 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 7a84fcde1..5478a1c4a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -51,12 +51,21 @@
     }
     .backKeyframe {
         right:45;
+        svg {
+            display:block;
+            margin:auto;
+        }
     }
     .numKeyframe {
         right:25;
+        text-align:center;
     }
     .fwdKeyframe {
         right:5;
+        svg {
+            display:block;
+            margin:auto;
+        }
     }
 
     .collectionfreeformview-placeholder {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4b218bc18..d0415f77d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -126,21 +126,39 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
         this.addDocument(newBox);
     }
-    addDocument = (newBox: Doc | Doc[]) => {
-        if (this.Document.currentTimecode !== undefined && !this.props.isAnnotationOverlay) {
-            CollectionFreeFormDocumentView.setupKeyframes((newBox instanceof Doc) ? [newBox] : newBox, this.Document.currentTimecode, this.props.Document);
-        }
-
+    addDocument = action((newBox: Doc | Doc[]) => {
+        let retVal = false;
         if (newBox instanceof Doc) {
-            const added = this.props.addDocument(newBox);
-            added && this.bringToFront(newBox);
-            added && this.updateCluster(newBox);
-            return added;
+            retVal = this.props.addDocument(newBox);
+            retVal && this.bringToFront(newBox);
+            retVal && this.updateCluster(newBox);
         } else {
-            return this.props.addDocument(newBox);
+            retVal = this.props.addDocument(newBox);
             // bcz: deal with clusters
         }
-    }
+        if (retVal) {
+            const newBoxes = (newBox instanceof Doc) ? [newBox] : newBox;
+            for (let i = 0; i < newBoxes.length; i++) {
+                const newBox = newBoxes[i];
+                if (newBox.displayTimecode !== undefined) {
+                    const x = newBox.x;
+                    const y = newBox.y;
+                    delete newBox["x-indexed"];
+                    delete newBox["y-indexed"];
+                    delete newBox["opacity-indexed"];
+                    delete newBox.x;
+                    delete newBox.y;
+                    delete newBox.displayTimecode;
+                    newBox.x = x;
+                    newBox.y = y;
+                }
+            }
+            if (this.Document.currentTimecode !== undefined && !this.props.isAnnotationOverlay) {
+                CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document.currentTimecode);
+            }
+        }
+        return retVal;
+    })
 
     @undoBatch
     @action
@@ -148,7 +166,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         const currentTimecode = this.Document.currentTimecode;
         if (currentTimecode === undefined) {
             this.Document.currentTimecode = 0;
-            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0, this.props.Document);
+            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
         CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentTimecode || 0);
         this.Document.currentTimecode = Math.max(0, (currentTimecode || 0) + 1);
@@ -160,7 +178,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         const currentTimecode = this.Document.currentTimecode;
         if (currentTimecode === undefined) {
             this.Document.currentTimecode = 0;
-            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0, this.props.Document);
+            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
         CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
         this.Document.currentTimecode = Math.max(0, (currentTimecode || 0) - 1);
@@ -992,7 +1010,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         const { z, color, zIndex } = params.pair.layout;
         return {
             x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"),
-            transition: StrCast(layoutDoc.transition), opacity: Cast(opacity, "number", null),
+            transition: StrCast(layoutDoc.transition), opacity: this.Document.editing ? 1 : Cast(opacity, "number", null),
             width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number"), pair: params.pair, replica: ""
         };
     }
@@ -1376,8 +1394,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     <div key="back" className="backKeyframe" onClick={this.prevKeyframe}>
                         <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
                     </div>
-                    <div key="num" className="numKeyframe" >
-                        {NumCast(this.props.Document.currentTimecode)}
+                    <div key="num" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }} onClick={action(() => this.Document.editing = !this.Document.editing)} >
+                        {NumCast(this.Document.currentTimecode)}
                     </div>
                     <div key="fwd" className="fwdKeyframe" onClick={this.nextKeyframe}>
                         <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index c99e74ccd..ed70ac9e8 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -334,9 +334,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             _LODdisable: true,
             title: "a nested collection",
         });
-        // const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
-        // dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
-        // this.marqueeInkDelete(inkData);
+        selected.forEach(d => d.context = newCollection);
         this.hideMarquee();
         return newCollection;
     }
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index a4120f958..88fbdd589 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -77,6 +77,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     public static setValues(timecode: number, d: Doc, x?: number, y?: number, opacity?: number) {
+        Cast(d["x-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = x as any as number;
+        Cast(d["y-indexed"], listSpec("number"), null)[Math.max(0, timecode - 1)] = y as any as number;
         Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number;
         Cast(d["y-indexed"], listSpec("number"), null)[timecode] = y as any as number;
         Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number;
@@ -99,16 +101,18 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
     }
 
-    public static setupKeyframes(docs: Doc[], timecode: number, collection: Doc) {
+    public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) {
         docs.forEach((doc, i) => {
+            const curTimecode = progressivize ? i : timecode;
             const xlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]);
             const ylist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]);
-            xlist[Math.max(i - 1)] = xlist[timecode + 1] = NumCast(doc.x);
-            ylist[Math.max(i - 1)] = ylist[timecode + 1] = NumCast(doc.y);
+            const olist = new List<number>(numberRange(timecode + 1).map(t => progressivize && t < i ? 0 : 1));
+            xlist[Math.max(curTimecode - 1, 0)] = xlist[curTimecode] = NumCast(doc.x);
+            ylist[Math.max(curTimecode - 1, 0)] = ylist[curTimecode] = NumCast(doc.y);
             doc["x-indexed"] = xlist;
             doc["y-indexed"] = ylist;
-            doc["opacity-indexed"] = new List<number>(numberRange(timecode).map(i => 1));
-            doc.displayTimecode = ComputedField.MakeFunction("collection ? collection.currentTimecode : 0", {}, { collection });
+            doc["opacity-indexed"] = olist;
+            doc.displayTimecode = ComputedField.MakeFunction("self.context ? (self.context.currentTimecode||0) : 0");
             doc.x = ComputedField.MakeInterpolated("x", "displayTimecode");
             doc.y = ComputedField.MakeInterpolated("y", "displayTimecode");
             doc.opacity = ComputedField.MakeInterpolated("opacity", "displayTimecode");
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 956d6556b..3cbe3e494 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -99,9 +99,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
                     <div className="keyValuePair-td-key-container">
                         <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => {
                             if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
-                                props.Document[props.fieldKey] = undefined;
+                                delete props.Document[props.fieldKey];
                             }
-                            else props.Document.proto![props.fieldKey] = undefined;
+                            else delete props.Document.proto![props.fieldKey];
                         })}>
                             X
                         </button>
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 526a3dbf4..364c1d060 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -101,12 +101,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
         this.rootDoc.presProgressivize = !this.rootDoc.presProgressivize;
         const rootTarget = Cast(this.rootDoc.presentationTargetDoc, Doc, null);
         const docs = DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]);
-        if (this.rootDoc.presProgressivize && !rootTarget?.lastTimecode) {
+        if (this.rootDoc.presProgressivize) {
             rootTarget.currentTimecode = 0;
-            CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, this.presBox);
+            CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true);
             rootTarget.lastTimecode = docs.length - 1;
         }
-        docs.forEach((d, i) => i && numberRange(i).forEach(f => Cast(d["opacity-indexed"], listSpec("number"), [])[f] = 0));
     }
 
     /**
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 503c60790..5192af407 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -65,14 +65,14 @@ export class ScriptField extends ObjectField {
     @serializable(autoObject())
     private captures?: ProxyField<Doc>;
 
-    constructor(script: CompiledScript, setterscript?: CompileResult) {
+    constructor(script: CompiledScript, setterscript?: CompiledScript) {
         super();
 
         if (script?.options.capturedVariables) {
             const doc = Doc.assign(new Doc, script.options.capturedVariables);
             this.captures = new ProxyField(doc);
         }
-        this.setterscript = setterscript?.compiled ? setterscript : undefined;
+        this.setterscript = setterscript;
         this.script = script;
     }
 
@@ -98,10 +98,10 @@ export class ScriptField extends ObjectField {
     //     }
 
     [Copy](): ObjectField {
-        return new ScriptField(this.script);
+        return new ScriptField(this.script, this.setterscript);
     }
     toString() {
-        return `${this.script.originalScript}`;
+        return `${this.script.originalScript} + ${this.setterscript?.originalScript}`;
     }
 
     [ToScriptString]() {
@@ -141,22 +141,21 @@ export class ComputedField extends ScriptField {
 
 
     [Copy](): ObjectField {
-        return new ComputedField(this.script);
+        return new ComputedField(this.script, this.setterscript);
     }
 
     public static MakeScript(script: string, params: object = {}) {
         const compiled = ScriptField.CompileScript(script, params, false);
         return compiled.compiled ? new ComputedField(compiled) : undefined;
     }
-    public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }, setterScript?: string) {
+    public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) {
         const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
-        const setCompiled = setterScript ? ScriptField.CompileScript(setterScript, params, true, capturedVariables) : undefined;
-        return compiled.compiled ? new ComputedField(compiled, setCompiled?.compiled ? setCompiled : undefined) : undefined;
+        return compiled.compiled ? new ComputedField(compiled) : undefined;
     }
     public static MakeInterpolated(fieldKey: string, interpolatorKey: string) {
         const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
         const setField = ScriptField.CompileScript(`(self['${fieldKey}-indexed'])[self.${interpolatorKey}] = value`, { value: "any" }, true, {});
-        return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
+        return getField.compiled && setField.compiled ? new ComputedField(getField, setField) : undefined;
     }
 }
 
diff --git a/src/fields/util.ts b/src/fields/util.ts
index a287b0210..024c0f80e 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -116,7 +116,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
             return true;
         }
     }
-    if (target.__fields[prop] instanceof ComputedField && target.__fields[prop].setterscript) {
+    if (target.__fields[prop] instanceof ComputedField && target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) {
         return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false;
     }
     return _setter(target, prop, value, receiver);
-- 
cgit v1.2.3-70-g09d2


From 2ba063ff94a04e89ee1fa1fde7d71a839f5d6856 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 23 May 2020 23:25:22 -0400
Subject: switched frame animations to use currentFrame and activeFrame to fix
 aliasing issue of progressive slides at different frame codes

---
 src/client/documents/Documents.ts                  |  2 ++
 src/client/views/MainView.tsx                      |  6 +---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 41 +++++++++++-----------
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 25 +++++++------
 src/client/views/nodes/PresBox.tsx                 |  6 ++--
 .../views/presentationview/PresElementBox.tsx      |  2 +-
 src/fields/documentSchemas.ts                      |  6 ++--
 7 files changed, 46 insertions(+), 42 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 6b17b4303..2be961108 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -126,6 +126,8 @@ export interface DocumentOptions {
     curPage?: number;
     currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video)  value is in seconds
     displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
+    currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide)
+    activeFrame?: number; // the active frame of a document in a frame base collection
     borderRounding?: string;
     boxShadow?: string;
     dontRegisterChildViews?: boolean;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b7a75dfb9..358de2333 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -139,10 +139,7 @@ export class MainView extends React.Component {
 
     initEventListeners = () => {
         window.addEventListener("drop", (e) => { e.preventDefault(); }, false); // drop event handler
-        window.addEventListener("dragover", (e) => {
-            console.log("MDRAG");
-            e.preventDefault();
-        }, false); // drag event handler
+        window.addEventListener("dragover", (e) => { e.preventDefault(); }, false); // drag event handler
         // click interactions for the context menu
         document.addEventListener("pointerdown", this.globalPointerDown);
         document.addEventListener("pointerup", this.globalPointerUp);
@@ -246,7 +243,6 @@ export class MainView extends React.Component {
     onDrop = (e: React.DragEvent<HTMLDivElement>) => {
         e.preventDefault();
         e.stopPropagation();
-        console.log("Drop");
     }
 
     @action
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d0415f77d..91e257f80 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -56,6 +56,7 @@ export const panZoomSchema = createSchema({
     scale: "number",
     currentTimecode: "number",
     displayTimecode: "number",
+    currentFrame: "number",
     arrangeScript: ScriptField,
     arrangeInit: ScriptField,
     useClusters: "boolean",
@@ -140,7 +141,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             const newBoxes = (newBox instanceof Doc) ? [newBox] : newBox;
             for (let i = 0; i < newBoxes.length; i++) {
                 const newBox = newBoxes[i];
-                if (newBox.displayTimecode !== undefined) {
+                if (newBox.activeFrame !== undefined) {
                     const x = newBox.x;
                     const y = newBox.y;
                     delete newBox["x-indexed"];
@@ -148,13 +149,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     delete newBox["opacity-indexed"];
                     delete newBox.x;
                     delete newBox.y;
-                    delete newBox.displayTimecode;
+                    delete newBox.activeFrame;
                     newBox.x = x;
                     newBox.y = y;
                 }
             }
-            if (this.Document.currentTimecode !== undefined && !this.props.isAnnotationOverlay) {
-                CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document.currentTimecode);
+            if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) {
+                CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document.currentFrame);
             }
         }
         return retVal;
@@ -163,25 +164,25 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     @undoBatch
     @action
     nextKeyframe = (): void => {
-        const currentTimecode = this.Document.currentTimecode;
-        if (currentTimecode === undefined) {
-            this.Document.currentTimecode = 0;
+        const currentFrame = this.Document.currentFrame;
+        if (currentFrame === undefined) {
+            this.Document.currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentTimecode || 0);
-        this.Document.currentTimecode = Math.max(0, (currentTimecode || 0) + 1);
-        this.Document.lastTimecode = Math.max(NumCast(this.Document.currentTimecode), NumCast(this.Document.lastTimecode));
+        CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+        this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1);
+        this.Document.lastTimecode = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastTimecode));
     }
     @undoBatch
     @action
     prevKeyframe = (): void => {
-        const currentTimecode = this.Document.currentTimecode;
-        if (currentTimecode === undefined) {
-            this.Document.currentTimecode = 0;
+        const currentFrame = this.Document.currentFrame;
+        if (currentFrame === undefined) {
+            this.Document.currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
         CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
-        this.Document.currentTimecode = Math.max(0, (currentTimecode || 0) - 1);
+        this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1);
     }
 
     private selectDocuments = (docs: Doc[]) => {
@@ -232,9 +233,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                         for (let i = 0; i < droppedDocs.length; i++) {
                             const d = droppedDocs[i];
                             const layoutDoc = Doc.Layout(d);
-                            if (this.Document.currentTimecode !== undefined && !this.props.isAnnotationOverlay) {
-                                const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.displayTimecode, 1000));
-                                CollectionFreeFormDocumentView.setValues(this.Document.currentTimecode, d, x + vals.x - dropX, y + vals.y - dropY, vals.opacity);
+                            if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) {
+                                const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000));
+                                CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropX, y + vals.y - dropY, vals.opacity);
                             } else {
                                 d.x = x + NumCast(d.x) - dropX;
                                 d.y = y + NumCast(d.y) - dropY;
@@ -1005,8 +1006,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             return { x: 0, y: 0, transition: "transform 1s", ...result, pair: params.pair, replica: "" };
         }
         const layoutDoc = Doc.Layout(params.pair.layout);
-        const { x, y, opacity } = this.Document.currentTimecode === undefined ? params.pair.layout :
-            CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.currentTimecode || 0);
+        const { x, y, opacity } = this.Document.currentFrame === undefined ? params.pair.layout :
+            CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.currentFrame || 0);
         const { z, color, zIndex } = params.pair.layout;
         return {
             x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"),
@@ -1395,7 +1396,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                         <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
                     </div>
                     <div key="num" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }} onClick={action(() => this.Document.editing = !this.Document.editing)} >
-                        {NumCast(this.Document.currentTimecode)}
+                        {NumCast(this.Document.currentFrame)}
                     </div>
                     <div key="fwd" className="fwdKeyframe" onClick={this.nextKeyframe}>
                         <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 88fbdd589..682aed8f5 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -69,21 +69,24 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     public static getValues(doc: Doc, time: number) {
+        const timecode = Math.round(time);
         return ({
-            x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= time && x !== undefined) || p === undefined ? x : p, undefined as any as number),
-            y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= time && y !== undefined) || p === undefined ? y : p, undefined as any as number),
-            opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= time || p === undefined ? o : p, undefined as any as number),
+            x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number),
+            y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number),
+            opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number),
         });
     }
 
-    public static setValues(timecode: number, d: Doc, x?: number, y?: number, opacity?: number) {
+    public static setValues(time: number, d: Doc, x?: number, y?: number, opacity?: number) {
+        const timecode = Math.round(time);
         Cast(d["x-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = x as any as number;
-        Cast(d["y-indexed"], listSpec("number"), null)[Math.max(0, timecode - 1)] = y as any as number;
+        Cast(d["y-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = y as any as number;
         Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number;
-        Cast(d["y-indexed"], listSpec("number"), null)[timecode] = y as any as number;
+        Cast(d["y-indexed"], listSpec("number"), [])[timecode] = y as any as number;
         Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number;
     }
-    public static updateKeyframe(docs: Doc[], timecode: number) {
+    public static updateKeyframe(docs: Doc[], time: number) {
+        const timecode = Math.round(time);
         docs.forEach(doc => {
             const xindexed = Cast(doc['x-indexed'], listSpec("number"), null);
             const yindexed = Cast(doc['y-indexed'], listSpec("number"), null);
@@ -112,10 +115,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             doc["x-indexed"] = xlist;
             doc["y-indexed"] = ylist;
             doc["opacity-indexed"] = olist;
-            doc.displayTimecode = ComputedField.MakeFunction("self.context ? (self.context.currentTimecode||0) : 0");
-            doc.x = ComputedField.MakeInterpolated("x", "displayTimecode");
-            doc.y = ComputedField.MakeInterpolated("y", "displayTimecode");
-            doc.opacity = ComputedField.MakeInterpolated("opacity", "displayTimecode");
+            doc.activeFrame = ComputedField.MakeFunction("self.context ? (self.context.currentFrame||0) : 0");
+            doc.x = ComputedField.MakeInterpolated("x", "activeFrame");
+            doc.y = ComputedField.MakeInterpolated("y", "activeFrame");
+            doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame");
         });
     }
 
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index aeb77a894..b76c06e2e 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -60,9 +60,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
         this.updateCurrentPresentation();
         const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null);
         const lastFrame = Cast(presTargetDoc.lastTimecode, "number", null);
-        const curFrame = NumCast(presTargetDoc.currentTimecode);
+        const curFrame = NumCast(presTargetDoc.currentFrame);
         if (lastFrame !== undefined && curFrame < lastFrame) {
-            presTargetDoc.currentTimecode = curFrame + 1;
+            presTargetDoc.currentFrame = curFrame + 1;
         }
         else if (this.childDocs[this.itemIndex + 1] !== undefined) {
             let nextSelected = this.itemIndex + 1;
@@ -200,7 +200,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
             this.rootDoc._itemIndex = index;
             const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null);
             if (presTargetDoc.lastTimecode !== undefined) {
-                presTargetDoc.currentTimecode = 0;
+                presTargetDoc.currentFrame = 0;
             }
 
             if (!this.layoutDoc.presStatus) {
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 364c1d060..31ffde8af 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -102,7 +102,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
         const rootTarget = Cast(this.rootDoc.presentationTargetDoc, Doc, null);
         const docs = DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]);
         if (this.rootDoc.presProgressivize) {
-            rootTarget.currentTimecode = 0;
+            rootTarget.currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true);
             rootTarget.lastTimecode = docs.length - 1;
         }
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 142f7e079..7e11049ab 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -12,7 +12,9 @@ export const documentSchema = createSchema({
     links: listSpec(Doc),       // computed (readonly) list of links associated with this document
 
     // "Location" properties in a very general sense
-    currentTimecode: "number",  // current play back time of a temporal document (video / audio)
+    currentFrame: "number",     // current frame of a frame based collection (e.g., a progressive slide)
+    activeFrame: "number",      // the active frame of a frame based animated document 
+    urrentTimecode: "number",   // current play back time of a temporal document (video / audio)
     displayTimecode: "number",  // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
     inOverlay: "boolean",       // whether the document is rendered in an OverlayView which handles selection/dragging differently
     x: "number",                // x coordinate when in a freeform view 
@@ -77,7 +79,7 @@ export const documentSchema = createSchema({
     isBackground: "boolean",    // whether document is a background element and ignores input events (can only select with marquee)
     lockedPosition: "boolean",  // whether the document can be moved (dragged)
     _lockedTransform: "boolean",// whether a freeformview can pan/zoom
-    
+
     // drag drop properties
     dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
     dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
-- 
cgit v1.2.3-70-g09d2


From 19ababdb6098ac36358c86e43ad63ab3066b4663 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 23 May 2020 23:59:34 -0400
Subject: renamed lastTimecode to lastFrame

---
 src/client/documents/Documents.ts                                     | 1 +
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 2 +-
 src/client/views/nodes/PresBox.tsx                                    | 4 ++--
 src/client/views/presentationview/PresElementBox.tsx                  | 2 +-
 src/fields/documentSchemas.ts                                         | 1 +
 5 files changed, 6 insertions(+), 4 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2be961108..7f5b62f22 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -127,6 +127,7 @@ export interface DocumentOptions {
     currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video)  value is in seconds
     displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
     currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide)
+    lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
     activeFrame?: number; // the active frame of a document in a frame base collection
     borderRounding?: string;
     boxShadow?: string;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 91e257f80..4840bb7e7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -171,7 +171,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
         CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
         this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1);
-        this.Document.lastTimecode = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastTimecode));
+        this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame));
     }
     @undoBatch
     @action
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index b76c06e2e..05306c29f 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -59,7 +59,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
     next = () => {
         this.updateCurrentPresentation();
         const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null);
-        const lastFrame = Cast(presTargetDoc.lastTimecode, "number", null);
+        const lastFrame = Cast(presTargetDoc.lastFrame, "number", null);
         const curFrame = NumCast(presTargetDoc.currentFrame);
         if (lastFrame !== undefined && curFrame < lastFrame) {
             presTargetDoc.currentFrame = curFrame + 1;
@@ -199,7 +199,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
         if (index >= 0 && index < this.childDocs.length) {
             this.rootDoc._itemIndex = index;
             const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null);
-            if (presTargetDoc.lastTimecode !== undefined) {
+            if (presTargetDoc.lastFrame !== undefined) {
                 presTargetDoc.currentFrame = 0;
             }
 
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 31ffde8af..475fef5b2 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -104,7 +104,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
         if (this.rootDoc.presProgressivize) {
             rootTarget.currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true);
-            rootTarget.lastTimecode = docs.length - 1;
+            rootTarget.lastFrame = docs.length - 1;
         }
     }
 
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 7e11049ab..32f1b6e6c 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -13,6 +13,7 @@ export const documentSchema = createSchema({
 
     // "Location" properties in a very general sense
     currentFrame: "number",     // current frame of a frame based collection (e.g., a progressive slide)
+    lastFrame: "number",        // last frame of a frame based collection (e.g., a progressive slide)
     activeFrame: "number",      // the active frame of a frame based animated document 
     urrentTimecode: "number",   // current play back time of a temporal document (video / audio)
     displayTimecode: "number",  // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
-- 
cgit v1.2.3-70-g09d2


From 8fd0f6f8b3ce585e58f388d856360373480ecdde Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 28 May 2020 16:53:49 -0400
Subject: fixed pdf text selection

---
 package-lock.json                                  | 84 +++++++++-------------
 package.json                                       |  2 +-
 .../views/collections/CollectionTreeView.tsx       |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  8 +--
 src/client/views/pdf/PDFViewer.scss                |  4 ++
 src/client/views/pdf/PDFViewer.tsx                 |  4 +-
 6 files changed, 45 insertions(+), 59 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index 8b10d91f3..21e4c4e27 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2800,8 +2800,7 @@
                         },
                         "ansi-regex": {
                             "version": "2.1.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "aproba": {
                             "version": "1.2.0",
@@ -2819,13 +2818,11 @@
                         },
                         "balanced-match": {
                             "version": "1.0.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "brace-expansion": {
                             "version": "1.1.11",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "balanced-match": "^1.0.0",
                                 "concat-map": "0.0.1"
@@ -2838,18 +2835,15 @@
                         },
                         "code-point-at": {
                             "version": "1.1.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "concat-map": {
                             "version": "0.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "console-control-strings": {
                             "version": "1.1.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "core-util-is": {
                             "version": "1.0.2",
@@ -2952,8 +2946,7 @@
                         },
                         "inherits": {
                             "version": "2.0.4",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "ini": {
                             "version": "1.3.5",
@@ -2963,7 +2956,6 @@
                         "is-fullwidth-code-point": {
                             "version": "1.0.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "number-is-nan": "^1.0.0"
                             }
@@ -2976,20 +2968,17 @@
                         "minimatch": {
                             "version": "3.0.4",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "brace-expansion": "^1.1.7"
                             }
                         },
                         "minimist": {
                             "version": "1.2.5",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "minipass": {
                             "version": "2.9.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "safe-buffer": "^5.1.2",
                                 "yallist": "^3.0.0"
@@ -3006,7 +2995,6 @@
                         "mkdirp": {
                             "version": "0.5.3",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "minimist": "^1.2.5"
                             }
@@ -3062,8 +3050,7 @@
                         },
                         "npm-normalize-package-bin": {
                             "version": "1.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "npm-packlist": {
                             "version": "1.4.8",
@@ -3088,8 +3075,7 @@
                         },
                         "number-is-nan": {
                             "version": "1.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "object-assign": {
                             "version": "4.1.1",
@@ -3099,7 +3085,6 @@
                         "once": {
                             "version": "1.4.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "wrappy": "1"
                             }
@@ -3168,8 +3153,7 @@
                         },
                         "safe-buffer": {
                             "version": "5.1.2",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "safer-buffer": {
                             "version": "2.1.2",
@@ -3199,7 +3183,6 @@
                         "string-width": {
                             "version": "1.0.2",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "code-point-at": "^1.0.0",
                                 "is-fullwidth-code-point": "^1.0.0",
@@ -3217,7 +3200,6 @@
                         "strip-ansi": {
                             "version": "3.0.1",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "ansi-regex": "^2.0.0"
                             }
@@ -3256,13 +3238,11 @@
                         },
                         "wrappy": {
                             "version": "1.0.2",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "yallist": {
                             "version": "3.1.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         }
                     }
                 }
@@ -9456,7 +9436,7 @@
                 },
                 "chownr": {
                     "version": "1.1.4",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
                     "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
                 },
                 "ci-info": {
@@ -9762,7 +9742,7 @@
                 },
                 "deep-extend": {
                     "version": "0.6.0",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
                     "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
                 },
                 "defaults": {
@@ -10261,7 +10241,7 @@
                 },
                 "glob": {
                     "version": "7.1.6",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
                     "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
                     "requires": {
                         "fs.realpath": "^1.0.0",
@@ -10349,7 +10329,7 @@
                 },
                 "hosted-git-info": {
                     "version": "2.8.8",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
                     "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
                 },
                 "http-cache-semantics": {
@@ -10485,7 +10465,7 @@
                 },
                 "is-ci": {
                     "version": "1.2.1",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
                     "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
                     "requires": {
                         "ci-info": "^1.5.0"
@@ -10561,7 +10541,7 @@
                 },
                 "is-retry-allowed": {
                     "version": "1.2.0",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz",
                     "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg=="
                 },
                 "is-stream": {
@@ -11070,7 +11050,7 @@
                 },
                 "mkdirp": {
                     "version": "0.5.4",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
                     "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==",
                     "requires": {
                         "minimist": "^1.2.5"
@@ -11078,7 +11058,7 @@
                     "dependencies": {
                         "minimist": {
                             "version": "1.2.5",
-                            "resolved": false,
+                            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
                             "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
                         }
                     }
@@ -11130,7 +11110,7 @@
                 },
                 "node-gyp": {
                     "version": "5.1.0",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz",
                     "integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==",
                     "requires": {
                         "env-paths": "^2.2.0",
@@ -11244,7 +11224,7 @@
                 },
                 "npm-packlist": {
                     "version": "1.4.8",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
                     "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
                     "requires": {
                         "ignore-walk": "^3.0.1",
@@ -11264,7 +11244,7 @@
                 },
                 "npm-profile": {
                     "version": "4.0.4",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/npm-profile/-/npm-profile-4.0.4.tgz",
                     "integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==",
                     "requires": {
                         "aproba": "^1.1.2 || 2",
@@ -11274,7 +11254,7 @@
                 },
                 "npm-registry-fetch": {
                     "version": "4.0.3",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.3.tgz",
                     "integrity": "sha512-WGvUx0lkKFhu9MbiGFuT9nG2NpfQ+4dCJwRwwtK2HK5izJEvwDxMeUyqbuMS7N/OkpVCqDorV6rO5E4V9F8lJw==",
                     "requires": {
                         "JSONStream": "^1.3.4",
@@ -11709,7 +11689,7 @@
                 },
                 "rc": {
                     "version": "1.2.8",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
                     "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
                     "requires": {
                         "deep-extend": "^0.6.0",
@@ -11720,7 +11700,7 @@
                     "dependencies": {
                         "minimist": {
                             "version": "1.2.5",
-                            "resolved": false,
+                            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
                             "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
                         }
                     }
@@ -11779,7 +11759,7 @@
                 },
                 "readable-stream": {
                     "version": "3.6.0",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
                     "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
                     "requires": {
                         "inherits": "^2.0.3",
@@ -11800,7 +11780,7 @@
                 },
                 "registry-auth-token": {
                     "version": "3.4.0",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz",
                     "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==",
                     "requires": {
                         "rc": "^1.1.6",
@@ -11864,7 +11844,7 @@
                 },
                 "rimraf": {
                     "version": "2.7.1",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
                     "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
                     "requires": {
                         "glob": "^7.1.3"
@@ -12163,7 +12143,7 @@
                 },
                 "string_decoder": {
                     "version": "1.3.0",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
                     "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
                     "requires": {
                         "safe-buffer": "~5.2.0"
@@ -12171,7 +12151,7 @@
                     "dependencies": {
                         "safe-buffer": {
                             "version": "5.2.0",
-                            "resolved": false,
+                            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
                             "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
                         }
                     }
@@ -12483,7 +12463,7 @@
                 },
                 "widest-line": {
                     "version": "2.0.1",
-                    "resolved": false,
+                    "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",
                     "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==",
                     "requires": {
                         "string-width": "^2.1.1"
diff --git a/package.json b/package.json
index 8bf1e6b74..ca82d523d 100644
--- a/package.json
+++ b/package.json
@@ -228,4 +228,4 @@
         "xoauth2": "^1.2.0",
         "xregexp": "^4.3.0"
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3e99af724..87fe79e19 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -748,7 +748,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
                 Document.childLayoutTemplate = heroView;
                 Document.childClickedOpenTemplateView = new PrefetchProxy(detailView);
                 Document._viewType = CollectionViewType.Time;
-                Document._forceActive = true;
+                Document.forceActive = true;
                 Document._pivotField = "company";
                 Document.childDropAction = "alias";
             }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0639d74a8..9d10ff158 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -76,6 +76,7 @@ const PanZoomDocument = makeInterface(panZoomSchema, collectionSchema, documentS
 export type collectionFreeformViewProps = {
     forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox)
     viewDefDivClick?: ScriptField;
+    childPointerEvents?: boolean;
 };
 
 @observer
@@ -1124,10 +1125,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     replica={entry[1].replica}
                     dataProvider={this.childDataProvider}
                     sizeProvider={this.childSizeProvider}
-                    pointerEvents={
-                        this.backgroundActive ?
-                            true :
-                            (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? false : undefined}
+                    pointerEvents={this.backgroundActive || this.props.childPointerEvents ?
+                        true :
+                        (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? false : undefined}
                     jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))}
                     //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
                     fitToBox={BoolCast(this.props.freezeChildDimensions)} // bcz: check this
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 8541a3149..affffc44e 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -29,6 +29,7 @@
 
     .page {
         position: relative;
+        border: unset;
     }
     .pdfViewer-text-selected {
         .textLayer{
@@ -57,6 +58,9 @@
         display: inline-block;
         width:100%;
     }
+    .pdfViewer-overlay {
+        pointer-events: none;
+    }
     
     .pdfViewer-annotationLayer {
         position: absolute;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 810ce5aea..217032c1f 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -31,6 +31,7 @@ import Annotation from "./Annotation";
 import PDFMenu from "./PDFMenu";
 import "./PDFViewer.scss";
 import React = require("react");
+import { SnappingManager } from "../../util/SnappingManager";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
@@ -641,7 +642,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
     panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0);
     panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0);
     @computed get overlayLayer() {
-        return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay"
+        return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
             style={{ transform: `scale(${this._zoomed})` }}>
             <CollectionFreeFormView {...this.props}
                 LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath}
@@ -661,6 +662,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
                 ContentScaling={this.contentZoom}
                 bringToFront={emptyFunction}
                 whenActiveChanged={this.whenActiveChanged}
+                childPointerEvents={true}
                 removeDocument={this.removeDocument}
                 moveDocument={this.moveDocument}
                 addDocument={this.addDocument}
-- 
cgit v1.2.3-70-g09d2


From 57c4cde2fab898976deec3fd037a3c9052ee2d30 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 28 May 2020 18:43:14 -0400
Subject: added aimated transitions to presentations of frame animations

---
 .../collectionFreeForm/CollectionFreeFormView.tsx          | 14 +++++++++++---
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx  |  5 +++--
 src/client/views/nodes/PresBox.tsx                         |  5 +++++
 3 files changed, 19 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 9d10ff158..cb1228fed 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1350,8 +1350,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             getContainerTransform={this.getContainerTransform}
             getTransform={this.getTransform}
             isAnnotationOverlay={this.isAnnotationOverlay}>
-            <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} shifted={!this.nativeHeight && !this.isAnnotationOverlay}
-                easing={this.easing} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+            <CollectionFreeFormViewPannableContents
+                centeringShiftX={this.centeringShiftX}
+                centeringShiftY={this.centeringShiftY}
+                shifted={!this.nativeHeight && !this.isAnnotationOverlay}
+                easing={this.easing}
+                transition={Cast(this.layoutDoc.transition, "string", null)}
+                viewDefDivClick={this.props.viewDefDivClick}
+                zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
                 {this.children}
             </CollectionFreeFormViewPannableContents>
             {this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)}
@@ -1445,6 +1451,7 @@ interface CollectionFreeFormViewPannableContentsProps {
     viewDefDivClick?: ScriptField;
     children: () => JSX.Element[];
     shifted: boolean;
+    transition?: string;
 }
 
 @observer
@@ -1459,7 +1466,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
         return <div className={freeformclass}
             style={{
                 width: this.props.shifted ? 0 : undefined, height: this.props.shifted ? 0 : undefined,
-                transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`
+                transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`,
+                transition: this.props.transition
             }}>
             {this.props.children()}
         </div>;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 682aed8f5..697111ac8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -96,12 +96,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             opacityindexed?.length <= timecode + 1 && opacityindexed.push(undefined as any as number);
             doc.transition = "all 1s";
         });
-        setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
+        setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010);
     }
 
     public static gotoKeyframe(docs: Doc[]) {
         docs.forEach(doc => doc.transition = "all 1s");
-        setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
+        setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010);
     }
 
     public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) {
@@ -119,6 +119,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             doc.x = ComputedField.MakeInterpolated("x", "activeFrame");
             doc.y = ComputedField.MakeInterpolated("y", "activeFrame");
             doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame");
+            doc.transition = "inherit";
         });
     }
 
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 05306c29f..56a543f46 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -62,6 +62,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
         const lastFrame = Cast(presTargetDoc.lastFrame, "number", null);
         const curFrame = NumCast(presTargetDoc.currentFrame);
         if (lastFrame !== undefined && curFrame < lastFrame) {
+            //const docs = DocListCast(presTargetDoc[Doc.LayoutFieldKey(presTargetDoc)]);
+            presTargetDoc.transition = "all 1s";
+            setTimeout(() => presTargetDoc.transition = undefined, 1010);
+            // docs.forEach(doc => doc.transition = "all 1s");
+            // setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
             presTargetDoc.currentFrame = curFrame + 1;
         }
         else if (this.childDocs[this.itemIndex + 1] !== undefined) {
-- 
cgit v1.2.3-70-g09d2


From 895e84fe6999a39eb9553db68b378aee5c1864a3 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 29 May 2020 01:29:02 -0400
Subject: moved frame animation into freeform chrome.   fixed marquees of docs
 at non-zero frame.  fixed pile view to use its own _pileLayoutEngine key to
 avoid leaving colleciton in an animated state.  added toggle for snap linew
 views.  cached transforms in CollectionFreeformView for performance.

---
 src/client/views/MainView.tsx                      |   6 +-
 .../views/collections/CollectionPileView.tsx       |   8 +-
 .../views/collections/CollectionViewChromes.scss   |  60 +++++-
 .../views/collections/CollectionViewChromes.tsx    | 201 +++++++++++++--------
 .../collectionFreeForm/CollectionFreeFormView.scss |  26 ---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  68 +++----
 .../collections/collectionFreeForm/MarqueeView.tsx |  12 +-
 src/client/views/nodes/DocumentView.tsx            |   9 +-
 src/client/views/nodes/PresBox.tsx                 |   3 -
 9 files changed, 223 insertions(+), 170 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 958ba0ce3..a0a6adab7 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -534,10 +534,10 @@ export class MainView extends React.Component {
     }
 
     @computed get snapLines() {
-        return <div className="mainView-snapLines">
+        return !Doc.UserDoc().showSnapLines ? (null) : <div className="mainView-snapLines">
             <svg style={{ width: "100%", height: "100%" }}>
-                {/* {SnappingManager.horizSnapLines().map(l => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
-                {SnappingManager.vertSnapLines().map(l => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} */}
+                {SnappingManager.horizSnapLines().map(l => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
+                {SnappingManager.vertSnapLines().map(l => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
             </svg>
         </div>;
     }
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 020a87b2e..fc48e0327 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -23,7 +23,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
     @observable _childClickedScript: Opt<ScriptField>;
     componentDidMount() {
         if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") {
-            this.Document._layoutEngine = "pass";
+            this.Document._pileLayoutEngine = "pass";
         }
         this._originalChrome = StrCast(this.layoutDoc._chromeStatus);
         this.layoutDoc._chromeStatus = "disabled";
@@ -34,7 +34,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
         this.layoutDoc._chromeStatus = this._originalChrome;
     }
 
-    layoutEngine = () => StrCast(this.Document._layoutEngine);
+    layoutEngine = () => StrCast(this.Document._pileLayoutEngine);
 
     @computed get contents() {
         return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" ? undefined : "none" }} >
@@ -53,7 +53,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
             Doc.pileup(this.childDocs);
             this.layoutDoc._panX = 0;
             this.layoutDoc._panY = -10;
-            this.props.Document._layoutEngine = 'pass';
+            this.props.Document._pileLayoutEngine = 'pass';
         } else {
             const defaultSize = 25;
             this.layoutDoc._overflow = 'visible';
@@ -67,7 +67,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
             }
             this.layoutDoc._panX = this.layoutDoc._panY = 0;
             this.layoutDoc._width = this.layoutDoc._height = defaultSize;
-            this.props.Document._layoutEngine = 'starburst';
+            this.props.Document._pileLayoutEngine = 'starburst';
         }
     });
 
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index e4581eb46..03bd9a01a 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -8,10 +8,12 @@
     z-index: 9001;
     transition: top .5s;
     background: lightgrey;
+    transform-origin: top left;
 
     .collectionViewChrome {
         display: flex;
-        padding-bottom: 10px;
+        padding-bottom: 1px;
+        height:32px;
         border-bottom: .5px solid rgb(180, 180, 180);
         overflow: hidden;
 
@@ -21,12 +23,12 @@
             .collectionViewBaseChrome-viewPicker {
                 font-size: 75%;
                 //text-transform: uppercase;
-                letter-spacing: 2px;
+                //letter-spacing: 2px;
                 background: rgb(238, 238, 238);
                 color: grey;
                 outline-color: black;
                 border: none;
-                padding: 12px 10px 11px 10px;
+                //padding: 12px 10px 11px 10px;
             }
 
             .collectionViewBaseChrome-viewPicker:active {
@@ -47,6 +49,7 @@
             .collectionViewBaseChrome-cmdPicker {
                 margin-left: 3px;
                 margin-right: 0px;
+                font-size: 75%;
                 background: rgb(238, 238, 238);
                 border: none;
                 color: grey;
@@ -56,6 +59,7 @@
                 background-color: gray;
                 display: flex;
                 flex-direction: row;
+                height:30px;
                 .commandEntry-drop {
                     color:white;
                     width:25px;
@@ -95,8 +99,8 @@
                     margin: auto;
                     background: gray;
                     color: white;
-                    width: 40px;
-                    height: 40px;
+                    width: 30px;
+                    height: 30px;
                     align-items: center;
                     justify-content: center;
                 }
@@ -195,8 +199,7 @@
             .collectionTreeViewChrome-pivotField-label {
                 vertical-align: center;
                 padding-left: 10px;
-                padding-top: 10px;
-                padding-bottom: 10px;
+                margin:auto;
             }
 
             .collectionStackingViewChrome-pivotField,
@@ -204,14 +207,15 @@
                 color: white;
                 width:100%;
                 min-width: 100px;
-                text-align: center;
+                display: flex;
+                align-items: center;
                 background: rgb(238, 238, 238);
 
                 .editable-view-input,
                 input,
                 .editableView-container-editing-oneLine,
                 .editableView-container-editing {
-                    padding: 12px 10px 11px 10px;
+                    margin:auto;
                     border: 0px;
                     color: grey;
                     text-align: center;
@@ -235,6 +239,44 @@
     }
 }
 
+.collectionFreeFormViewChrome-cont {
+    width: 60px;
+    display: flex;
+    position: relative;
+    align-items: center;
+    .fwdKeyframe, .numKeyframe, .backKeyframe {
+        cursor: pointer;
+        position: absolute;
+        width: 20;
+        height: 30;
+        bottom: 0;
+        background: gray;
+        display: flex;
+        align-items: center;
+        color:white;
+    }
+    .backKeyframe {
+        left:0;
+        svg {
+            display:block;
+            margin:auto;
+        }
+    }
+    .numKeyframe {
+        left:20;
+        display: flex;
+        flex-direction: column;
+        padding: 5px;
+    }
+    .fwdKeyframe {
+        left:40;
+        svg {
+            display:block;
+            margin:auto;
+        }
+    }
+}
+
 .collectionSchemaViewChrome-cont {
     display: flex;
     font-size: 10.5px;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 8c4d53d63..29a3e559a 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -15,6 +15,7 @@ import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
 import { CollectionViewType } from "./CollectionView";
 import { CollectionView } from "./CollectionView";
 import "./CollectionViewChromes.scss";
+import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
 const datepicker = require('js-datepicker');
 
 interface CollectionViewChromeProps {
@@ -44,7 +45,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
         initialize: emptyFunction,
     };
     _narrativeCommand = {
-        params: ["target", "source"], title: "=> click clicked open view",
+        params: ["target", "source"], title: "=> child click view",
         script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])",
         immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]),
         initialize: emptyFunction,
@@ -83,22 +84,16 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     private _viewRef = React.createRef<HTMLInputElement>();
     @observable private _currentKey: string = "";
 
-    componentDidMount = () => {
-        runInAction(() => {
-            // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
-            const chromeStatus = this.props.CollectionView.props.Document._chromeStatus;
-            if (chromeStatus) {
-                if (chromeStatus === "disabled") {
-                    throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
-                }
-                else if (chromeStatus === "collapsed") {
-                    if (this.props.collapse) {
-                        this.props.collapse(true);
-                    }
-                }
-            }
-        });
-    }
+    componentDidMount = action(() => {
+        // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
+        switch (this.props.CollectionView.props.Document._chromeStatus) {
+            case "disabled":
+                throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
+            case "collapsed":
+                this.props.collapse?.(true);
+                break;
+        }
+    })
 
     @undoBatch
     viewChanged = (e: React.ChangeEvent) => {
@@ -197,10 +192,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
         }
     }
 
-    subChrome = () => {
+    @computed get subChrome() {
         const collapsed = this.document._chromeStatus !== "enabled";
         if (collapsed) return null;
         switch (this.props.type) {
+            case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
             case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
             case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
             case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
@@ -236,7 +232,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
             const vtype = this.props.CollectionView.collectionViewType;
             const c = {
                 params: ["target"], title: vtype,
-                script: `this.target._viewType = ${StrCast(this.props.CollectionView.props.Document._viewType)}`,
+                script: `this.target._viewType = '${StrCast(this.props.CollectionView.props.Document._viewType)}'`,
                 immediate: (source: Doc[]) => this.props.CollectionView.props.Document._viewType = Doc.getDocTemplate(source?.[0]),
                 initialize: emptyFunction,
             };
@@ -254,14 +250,61 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
         }, emptyFunction, emptyFunction);
     }
 
+    @computed get templateChrome() {
+        const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
+        return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}>
+            <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
+                <div className="commandEntry-drop">
+                    <FontAwesomeIcon icon="bullseye" size="2x" />
+                </div>
+                <select
+                    className="collectionViewBaseChrome-cmdPicker"
+                    onPointerDown={stopPropagation}
+                    onChange={this.commandChanged}
+                    value={this._currentKey}>
+                    <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} />
+                    {this._buttonizableCommands.map(cmd =>
+                        <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option>
+                    )}
+                </select>
+            </div>
+        </div>;
+    }
+
+    @computed get viewModes() {
+        const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
+        return <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}>
+            <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}>
+                <div className="commandEntry-drop">
+                    <FontAwesomeIcon icon="bullseye" size="2x" />
+                </div>
+                <select
+                    className="collectionViewBaseChrome-viewPicker"
+                    onPointerDown={stopPropagation}
+                    onChange={this.viewChanged}
+                    value={StrCast(this.props.CollectionView.props.Document._viewType)}>
+                    {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : (
+                        <option
+                            key={Utils.GenerateGuid()}
+                            className="collectionViewBaseChrome-viewOption"
+                            onPointerDown={stopPropagation}
+                            value={type}>
+                            {type[0].toUpperCase() + type.substring(1)}
+                        </option>
+                    ))}
+                </select>
+            </div>
+        </div>;
+    }
+
     render() {
         const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
+        const scale = Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale);
         return (
             <div className="collectionViewChrome-cont" style={{
                 top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined,
-                transform: collapsed ? "" : `scale(${Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)})`,
-                transformOrigin: "top left",
-                width: `${this.props.PanelWidth() / Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)}px`
+                transform: collapsed ? "" : `scale(${scale})`,
+                width: `${this.props.PanelWidth() / scale}px`
             }}>
                 <div className="collectionViewChrome" style={{ border: "unset", pointerEvents: collapsed ? "none" : undefined }}>
                     <div className="collectionViewBaseChrome">
@@ -276,58 +319,71 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
                             title="Collapse collection chrome" onClick={this.toggleCollapse}>
                             <FontAwesomeIcon icon="caret-up" size="2x" />
                         </button>
-                        <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}>
-                            <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}>
-                                <div className="commandEntry-drop">
-                                    <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon>
-                                </div>
-                                <select
-                                    className="collectionViewBaseChrome-viewPicker"
-                                    onPointerDown={stopPropagation}
-                                    onChange={this.viewChanged}
-                                    value={StrCast(this.props.CollectionView.props.Document._viewType)}>
-                                    {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : (
-                                        <option
-                                            key={Utils.GenerateGuid()}
-                                            className="collectionViewBaseChrome-viewOption"
-                                            onPointerDown={stopPropagation}
-                                            value={type}>
-                                            {type[0].toUpperCase() + type.substring(1)}
-                                        </option>
-                                    ))}
-                                </select>
-                            </div>
-                        </div>
+                        {this.viewModes}
                         <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}>
                             <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.toggleViewSpecs} >
                                 <FontAwesomeIcon icon="filter" size="2x" />
                             </div>
                         </div>
-                        <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}>
-                            <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
-                                <div className="commandEntry-drop">
-                                    <FontAwesomeIcon icon="bullseye" size="2x" />
-                                </div>
-                                <select
-                                    className="collectionViewBaseChrome-cmdPicker"
-                                    onPointerDown={stopPropagation}
-                                    onChange={this.commandChanged}
-                                    value={this._currentKey}>
-                                    <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""}>{""}</option>
-                                    {this._buttonizableCommands.map(cmd =>
-                                        <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option>
-                                    )}
-                                </select>
-                            </div>
-                        </div>
+                        {this.templateChrome}
                     </div>
-                    {this.subChrome()}
+                    {this.subChrome}
                 </div>
             </div>
         );
     }
 }
 
+@observer
+export class CollectionFreeFormViewChrome extends React.Component<CollectionViewChromeProps> {
+
+    get Document() { return this.props.CollectionView.props.Document; }
+    @computed get dataField() {
+        return this.props.CollectionView.props.Document[Doc.LayoutFieldKey(this.props.CollectionView.props.Document)];
+    }
+    @computed get childDocs() {
+        return DocListCast(this.dataField);
+    }
+    @undoBatch
+    @action
+    nextKeyframe = (): void => {
+        const currentFrame = NumCast(this.Document.currentFrame);
+        if (currentFrame === undefined) {
+            this.Document.currentFrame = 0;
+            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
+        }
+        CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+        this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1);
+        this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame));
+    }
+    @undoBatch
+    @action
+    prevKeyframe = (): void => {
+        const currentFrame = NumCast(this.Document.currentFrame);
+        if (currentFrame === undefined) {
+            this.Document.currentFrame = 0;
+            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
+        }
+        CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+        this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1);
+    }
+    render() {
+        return this.Document.isAnnotationOverlay ? (null) :
+            <div className="collectionFreeFormViewChrome-cont">
+                <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}>
+                    <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
+                </div>
+                <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }}
+                    onClick={action(() => this.Document.editing = !this.Document.editing)} >
+                    {NumCast(this.Document.currentFrame)}
+                </div>
+                <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}>
+                    <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
+                </div>
+            </div>;
+    }
+}
+
 @observer
 export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> {
     @observable private _currentKey: string = "";
@@ -474,19 +530,20 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
 @observer
 export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> {
 
-    get dataExtension() {
-        return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "_ext"] as Doc;
+    get sortAscending() {
+        return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"];
     }
-    @computed private get descending() {
-        return this.dataExtension && Cast(this.dataExtension.sortAscending, "boolean", null);
+    set sortAscending(value) {
+        this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"] = value;
+    }
+    @computed private get ascending() {
+        return Cast(this.sortAscending, "boolean", null);
     }
 
     @action toggleSort = () => {
-        if (this.dataExtension) {
-            if (this.dataExtension.sortAscending) this.dataExtension.sortAscending = undefined;
-            else if (this.dataExtension.sortAscending === undefined) this.dataExtension.sortAscending = false;
-            else this.dataExtension.sortAscending = true;
-        }
+        if (this.sortAscending) this.sortAscending = undefined;
+        else if (this.sortAscending === undefined) this.sortAscending = false;
+        else this.sortAscending = true;
     }
 
     render() {
@@ -496,7 +553,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro
                     <div className="collectionTreeViewChrome-sortLabel">
                         Sort
                         </div>
-                    <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending === undefined ? "90" : this.descending ? "180" : "0"}deg)` }}>
+                    <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.ascending === undefined ? "90" : this.ascending ? "180" : "0"}deg)` }}>
                         <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
                     </div>
                 </button>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 5478a1c4a..d9011c9d3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -41,32 +41,6 @@
     // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions.
     touch-action: none;
 
-    .fwdKeyframe, .numKeyframe, .backKeyframe{
-        cursor: pointer;
-        position: absolute;
-        width: 20;
-        height:20;
-        bottom:0;
-        background:gray;
-    }
-    .backKeyframe {
-        right:45;
-        svg {
-            display:block;
-            margin:auto;
-        }
-    }
-    .numKeyframe {
-        right:25;
-        text-align:center;
-    }
-    .fwdKeyframe {
-        right:5;
-        svg {
-            display:block;
-            margin:auto;
-        }
-    }
 
     .collectionfreeformview-placeholder {
         background: gray;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cb1228fed..c753a703d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,7 +1,7 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
 import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, _allowStateChangesInsideComputed } from "mobx";
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, _allowStateChangesInsideComputed, trace } from "mobx";
 import { observer } from "mobx-react";
 import { computedFn } from "mobx-utils";
 import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../fields/Doc";
@@ -118,12 +118,28 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
         this.Document.scale || 1)
 
-    private centeringShiftX = () => !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0;  // shift so pan position is at center of window for non-overlay collections
-    private centeringShiftY = () => !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 0;// shift so pan position is at center of window for non-overlay collections
-    private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
-    private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1);
-    private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
-    private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
+    @computed get cachedCenteringShiftX(): number {
+        return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0;  // shift so pan position is at center of window for non-overlay collections
+    }
+    @computed get cachedCenteringShiftY(): number {
+        return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 0;// shift so pan position is at center of window for non-overlay collections
+    }
+    @computed get cachedGetLocalTransform(): Transform {
+        return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
+    }
+    @computed get cachedGetContainerTransform(): Transform {
+        return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
+    }
+    @computed get cachedGetTransform(): Transform {
+        return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
+    }
+
+    private centeringShiftX = () => this.cachedCenteringShiftX;
+    private centeringShiftY = () => this.cachedCenteringShiftY;
+    private getTransform = () => this.cachedGetTransform.copy();
+    private getLocalTransform = () => this.cachedGetLocalTransform.copy();
+    private getContainerTransform = () => this.cachedGetContainerTransform.copy();
+    private getTransformOverlay = () => this.getContainerTransform().translate(1, 1);
     private addLiveTextBox = (newBox: Doc) => {
         FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
         this.addDocument(newBox);
@@ -162,30 +178,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         return retVal;
     })
 
-    @undoBatch
-    @action
-    nextKeyframe = (): void => {
-        const currentFrame = this.Document.currentFrame;
-        if (currentFrame === undefined) {
-            this.Document.currentFrame = 0;
-            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
-        }
-        CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
-        this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1);
-        this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame));
-    }
-    @undoBatch
-    @action
-    prevKeyframe = (): void => {
-        const currentFrame = this.Document.currentFrame;
-        if (currentFrame === undefined) {
-            this.Document.currentFrame = 0;
-            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
-        }
-        CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
-        this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1);
-    }
-
     private selectDocuments = (docs: Doc[]) => {
         SelectionManager.DeselectAll();
         docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
@@ -1219,6 +1211,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
 
         optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
+        optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" });
         optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
         optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
         optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
@@ -1397,18 +1390,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ?
                 this.placeholder : this.marqueeView}
             <CollectionFreeFormOverlayView elements={this.elementFunc} />
-            {this.isAnnotationOverlay || !this.props.isSelected() || this.props.Document._viewType === CollectionViewType.Pile ? (null) :
-                <>
-                    <div key="back" className="backKeyframe" onClick={this.prevKeyframe}>
-                        <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
-                    </div>
-                    <div key="num" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }} onClick={action(() => this.Document.editing = !this.Document.editing)} >
-                        {NumCast(this.Document.currentFrame)}
-                    </div>
-                    <div key="fwd" className="fwdKeyframe" onClick={this.nextKeyframe}>
-                        <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
-                    </div>
-                </>}
+
             <div className={"pullpane-indicator"}
                 style={{
                     display: this._pullDirection ? "block" : "none",
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index ed70ac9e8..cdfeeaa6b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -357,10 +357,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         const selected = this.marqueeSelect(false);
         if (e instanceof KeyboardEvent ? e.key === "c" : true) {
             selected.map(action(d => {
-                //this.props.removeDocument(d);
-                d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
-                d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
-                d.displayTimecode = undefined;  // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+                const dx = NumCast(d.x);
+                const dy = NumCast(d.y);
+                delete d.x;
+                delete d.y;
+                delete d.activeFrame;
+                delete d.displayTimecode;  // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+                d.x = dx - bounds.left - bounds.width / 2;
+                d.y = dy - bounds.top - bounds.height / 2;
                 return d;
             }));
             this.props.removeDocument(selected);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 00d19752f..2de7b3460 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -965,9 +965,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return this.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
     }
     childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
-    panelWidth = () => this.props.PanelWidth();
-    panelHeight = () => this.props.PanelHeight();
-    screenToLocalTransform = () => this.props.ScreenToLocalTransform();
     @computed get contents() {
         TraceMobx();
         return (<>
@@ -988,10 +985,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 addDocument={this.props.addDocument}
                 removeDocument={this.props.removeDocument}
                 moveDocument={this.props.moveDocument}
-                ScreenToLocalTransform={this.screenToLocalTransform}
+                ScreenToLocalTransform={this.props.ScreenToLocalTransform}
                 renderDepth={this.props.renderDepth}
-                PanelWidth={this.panelWidth}
-                PanelHeight={this.panelHeight}
+                PanelWidth={this.props.PanelWidth}
+                PanelHeight={this.props.PanelHeight}
                 ignoreAutoHeight={this.props.ignoreAutoHeight}
                 focus={this.props.focus}
                 parentActive={this.props.parentActive}
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 56a543f46..9f1e99c77 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -62,11 +62,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
         const lastFrame = Cast(presTargetDoc.lastFrame, "number", null);
         const curFrame = NumCast(presTargetDoc.currentFrame);
         if (lastFrame !== undefined && curFrame < lastFrame) {
-            //const docs = DocListCast(presTargetDoc[Doc.LayoutFieldKey(presTargetDoc)]);
             presTargetDoc.transition = "all 1s";
             setTimeout(() => presTargetDoc.transition = undefined, 1010);
-            // docs.forEach(doc => doc.transition = "all 1s");
-            // setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
             presTargetDoc.currentFrame = curFrame + 1;
         }
         else if (this.childDocs[this.itemIndex + 1] !== undefined) {
-- 
cgit v1.2.3-70-g09d2


From 88a8ae38f6e9be6a1c51783d68864c6cc47ba8e6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 3 Jun 2020 17:50:41 -0400
Subject: cleaned up internalDrop code.  fixed drop to work with ACLs that
 don't permit drop

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 128 ++++++++++-----------
 1 file changed, 62 insertions(+), 66 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 c753a703d..a8eef6875 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -46,7 +46,6 @@ import React = require("react");
 import { CollectionViewType } from "../CollectionView";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { SnappingManager } from "../../../util/SnappingManager";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -188,79 +187,76 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
     }
 
+    onExternalDrop = (e: React.DragEvent) => {
+        return (pt => super.onExternalDrop(e, { x: pt[0], y: pt[1] }))(this.getTransform().transformPoint(e.pageX, e.pageY));
+    }
+
+    @undoBatch
+    @action
+    internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) {
+        if (!super.onInternalDrop(e, de)) return false;
+        const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y);
+        const z = NumCast(docDragData.droppedDocuments[0].z);
+        const x = (z ? xpo : xp) - docDragData.offset[0];
+        const y = (z ? ypo : yp) - docDragData.offset[1];
+        const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
+        zsorted.forEach((doc, index) => doc.zIndex = index + 1);
+        const dropPos = [NumCast(docDragData.droppedDocuments[0].x), NumCast(docDragData.droppedDocuments[0].y)];
+        for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
+            const d = docDragData.droppedDocuments[i];
+            const layoutDoc = Doc.Layout(d);
+            if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) {
+                const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000));
+                CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.opacity);
+            } else {
+                d.x = x + NumCast(d.x) - dropPos[0];
+                d.y = y + NumCast(d.y) - dropPos[1];
+            }
+            const nd = [NumCast(layoutDoc._nativeWidth), NumCast(layoutDoc._nativeHeight)];
+            layoutDoc._width = NumCast(layoutDoc._width, 300);
+            layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300);
+            d.isBackground === undefined && (d.zIndex = zsorted.length + 1 + i); // bringToFront
+        }
+
+        (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments);
+        return true;
+    }
+
+    @undoBatch
     @action
-    onExternalDrop = (e: React.DragEvent): Promise<void> => {
-        const pt = this.getTransform().transformPoint(e.pageX, e.pageY);
-        return super.onExternalDrop(e, { x: pt[0], y: pt[1] });
+    internalPdfAnnoDrop(e: Event, annoDragData: DragManager.PdfAnnoDragData, xp: number, yp: number) {
+        const dragDoc = annoDragData.dropDocument;
+        const dropPos = [NumCast(dragDoc.x), NumCast(dragDoc.y)];
+        dragDoc.x = xp - annoDragData.offset[0] + (NumCast(dragDoc.x) - dropPos[0]);
+        dragDoc.y = yp - annoDragData.offset[1] + (NumCast(dragDoc.y) - dropPos[1]);
+        annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
+        this.bringToFront(dragDoc);
+        return true;
     }
 
     @undoBatch
+    @action
+    internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) {
+        if (linkDragData.linkSourceDocument === this.props.Document) return false;
+        const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" });
+        this.props.addDocument(source);
+        linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed
+        e.stopPropagation();
+        return true;
+    }
+
     @action
     onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
         // if (this.props.Document.isBackground) return false;
-        const xf = this.getTransform();
-        const xfo = this.getTransformOverlay();
-        const [xp, yp] = xf.transformPoint(de.x, de.y);
-        const [xpo, ypo] = xfo.transformPoint(de.x, de.y);
-        const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
-        if (!this.isAnnotationOverlay && de.complete.linkDragData && de.complete.linkDragData.linkSourceDocument !== this.props.Document) {
-            const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" });
-            this.props.addDocument(source);
-            (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceDocument },
-                "doc annotation")); // TODODO this is where in text links get passed
-            e.stopPropagation();
+        const [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
+        if (this.isAnnotationOverlay !== true && de.complete.linkDragData)
+            return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp);
+        if (de.complete.annoDragData?.dropDocument && super.onInternalDrop(e, de))
+            return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp);
+        if (de.complete.docDragData?.droppedDocuments.length && this.internalDocDrop(e, de, de.complete.docDragData, xp, yp)) {
             return true;
-        }
-        if (super.onInternalDrop(e, de)) {
-            if (de.complete.docDragData) {
-                if (de.complete.docDragData.droppedDocuments.length) {
-                    const firstDoc = de.complete.docDragData.droppedDocuments[0];
-                    const z = NumCast(firstDoc.z);
-                    const x = (z ? xpo : xp) - de.complete.docDragData.offset[0];
-                    const y = (z ? ypo : yp) - de.complete.docDragData.offset[1];
-                    const dropX = NumCast(firstDoc.x);
-                    const dropY = NumCast(firstDoc.y);
-                    const droppedDocs = de.complete.docDragData.droppedDocuments;
-                    runInAction(() => {
-                        zsorted.forEach((doc, index) => doc.zIndex = index + 1);
-                        for (let i = 0; i < droppedDocs.length; i++) {
-                            const d = droppedDocs[i];
-                            const layoutDoc = Doc.Layout(d);
-                            if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) {
-                                const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000));
-                                CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropX, y + vals.y - dropY, vals.opacity);
-                            } else {
-                                d.x = x + NumCast(d.x) - dropX;
-                                d.y = y + NumCast(d.y) - dropY;
-                            }
-                            if (!NumCast(layoutDoc._width)) {
-                                layoutDoc._width = 300;
-                            }
-                            if (!NumCast(layoutDoc._height)) {
-                                const nw = NumCast(layoutDoc._nativeWidth);
-                                const nh = NumCast(layoutDoc._nativeHeight);
-                                layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300;
-                            }
-                            d.isBackground === undefined && (d.zIndex = zsorted.length + 1 + i); // bringToFront
-                        }
-                    });
-
-                    (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments);
-                }
-            }
-            else if (de.complete.annoDragData) {
-                if (de.complete.annoDragData.dropDocument) {
-                    const dragDoc = de.complete.annoDragData.dropDocument;
-                    const x = xp - de.complete.annoDragData.offset[0];
-                    const y = yp - de.complete.annoDragData.offset[1];
-                    const dropX = NumCast(dragDoc.x);
-                    const dropY = NumCast(dragDoc.y);
-                    dragDoc.x = x + NumCast(dragDoc.x) - dropX;
-                    dragDoc.y = y + NumCast(dragDoc.y) - dropY;
-                    de.complete.annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
-                    this.bringToFront(dragDoc);
-                }
-            }
+        } else {
+            UndoManager.Undo();
         }
         return false;
     }
-- 
cgit v1.2.3-70-g09d2


From e872414f24edddb31a82738bff10e5b9519d92be Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 4 Jun 2020 11:13:39 -0400
Subject: fixed content fitting of freeform views in treeview.

---
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx    | 9 ++++-----
 src/client/views/collections/collectionFreeForm/MarqueeView.tsx  | 2 +-
 2 files changed, 5 insertions(+), 6 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 a8eef6875..fc5e61026 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -118,10 +118,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         this.Document.scale || 1)
 
     @computed get cachedCenteringShiftX(): number {
-        return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0;  // shift so pan position is at center of window for non-overlay collections
+        const scaling = this.fitToContent ? 1 : this.contentScaling;
+        return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0;  // shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedCenteringShiftY(): number {
-        return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 0;// shift so pan position is at center of window for non-overlay collections
+        const scaling = this.fitToContent ? 1 : this.contentScaling;
+        return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedGetLocalTransform(): Transform {
         return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
@@ -1342,7 +1344,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             <CollectionFreeFormViewPannableContents
                 centeringShiftX={this.centeringShiftX}
                 centeringShiftY={this.centeringShiftY}
-                shifted={!this.nativeHeight && !this.isAnnotationOverlay}
                 easing={this.easing}
                 transition={Cast(this.layoutDoc.transition, "string", null)}
                 viewDefDivClick={this.props.viewDefDivClick}
@@ -1428,7 +1429,6 @@ interface CollectionFreeFormViewPannableContentsProps {
     easing: () => boolean;
     viewDefDivClick?: ScriptField;
     children: () => JSX.Element[];
-    shifted: boolean;
     transition?: string;
 }
 
@@ -1443,7 +1443,6 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
         const zoom = this.props.zoomScaling();
         return <div className={freeformclass}
             style={{
-                width: this.props.shifted ? 0 : undefined, height: this.props.shifted ? 0 : undefined,
                 transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`,
                 transition: this.props.transition
             }}>
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index cdfeeaa6b..46804c848 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -607,7 +607,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @computed
     get marqueeDiv() {
-        const p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
+        const p = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
         const v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
         /**
          * @RE - The commented out span below
-- 
cgit v1.2.3-70-g09d2


From cdc9c7e293a656573d49c4265b5867e34e8db7c8 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 5 Jun 2020 18:30:56 -0400
Subject: changed all scroll behaviors to be _scrollTop to go to a scroll and
 _scrollY to animate to it.

---
 src/client/util/DocumentManager.ts                 |  2 +-
 src/client/views/animationtimeline/Timeline.tsx    |  2 --
 src/client/views/animationtimeline/Track.tsx       |  5 +++--
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/PDFBox.tsx                  |  2 +-
 src/client/views/nodes/WebBox.tsx                  | 14 +++++++-------
 .../views/nodes/formattedText/FormattedTextBox.tsx |  4 ++--
 .../formattedText/FormattedTextBoxComment.tsx      |  2 +-
 src/client/views/pdf/PDFViewer.tsx                 | 22 +++++++++-------------
 src/fields/documentSchemas.ts                      |  8 ++++----
 11 files changed, 30 insertions(+), 35 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 67f2f244c..78c05f572 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -168,7 +168,7 @@ export class DocumentManager {
                 highlight();
             } else {  // otherwise try to get a view of the context of the target
                 const targetDocContextView = getFirstDocView(targetDocContext);
-                targetDocContext.scrollY = 0;  // this will force PDFs to activate and load their annotations / allow scrolling
+                targetDocContext._scrollY = 0;  // this will force PDFs to activate and load their annotations / allow scrolling
                 if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first..
                     targetDocContext.panTransformType = "Ease";
                     targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index 017623120..43f15a33f 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -11,8 +11,6 @@ import { KeyframeFunc } from "./Keyframe";
 import "./Timeline.scss";
 import { TimelineOverview } from "./TimelineOverview";
 import { Track } from "./Track";
-import { utils } from "mocha";
-import { Util } from "../../../../deploy/assets/pdf.worker";
 import clamp from "../../util/clamp";
 
 /**
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
index fc96c320a..4987006e7 100644
--- a/src/client/views/animationtimeline/Track.tsx
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -31,13 +31,14 @@ export class Track extends React.Component<IProps> {
     @observable private _autoKfReaction: any;
     @observable private _newKeyframe: boolean = false;
     private readonly MAX_TITLE_HEIGHT = 75;
-    private _trackHeight = 0;
+    @observable private _trackHeight = 0;
     private primitiveWhitelist = [
         "x",
         "y",
         "_width",
         "_height",
         "opacity",
+        "_scrollTop"
     ];
     private objectWhitelist = [
         "data"
@@ -51,7 +52,7 @@ export class Track extends React.Component<IProps> {
         if (!regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
         //these two lines are exactly same from timeline.tsx 
         const relativeHeight = window.innerHeight / 20;
-        this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //for responsiveness
+        runInAction(() => this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT); //for responsiveness
         this._timelineVisibleReaction = this.timelineVisibleReaction();
         this._currentBarXReaction = this.currentBarXReaction();
         if (DocListCast(this.props.node.regions).length === 0) this.createRegion(this.time);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 423eb1d90..56c6ce457 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -343,7 +343,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                                 if (focusNode) {
                                     const rect = "getBoundingClientRect" in focusNode ? focusNode.getBoundingClientRect() : focusNode?.parentElement.getBoundingClientRect();
                                     const x = (rect?.x || 0);
-                                    const y = NumCast(srcWeb.scrollTop) + (rect?.y || 0);
+                                    const y = NumCast(srcWeb._scrollTop) + (rect?.y || 0);
                                     const anchor = Docs.Create.FreeformDocument([], { _LODdisable: true, _backgroundColor: "transparent", _width: 25, _height: 25, x, y, annotationOn: srcWeb });
                                     anchor.context = srcWeb;
                                     const key = Doc.LayoutFieldKey(srcWeb);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index fc5e61026..80ee2a65d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -886,7 +886,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             } else {
                 const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height);
                 const offset = annotOn && (contextHgt / 2 * 96 / 72);
-                this.props.Document.scrollY = NumCast(doc.y) - offset;
+                this.props.Document._scrollY = NumCast(doc.y) - offset;
             }
 
             afterFocus && setTimeout(afterFocus, 1000);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 493f23dc4..fae216f17 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -248,7 +248,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
     _pdfjsRequested = false;
     render() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
-        if (this.props.isSelected() || this.props.renderDepth <= 1 || this.props.Document.scrollY !== undefined) this._everActive = true;
+        if (this.props.isSelected() || this.props.renderDepth <= 1 || this.props.Document._scrollY !== undefined) this._everActive = true;
         if (pdfUrl && (this._everActive || this.props.Document._scrollTop || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) {
             if (pdfUrl instanceof PdfField && this._pdf) {
                 return this.renderPdfView;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c4ab3c9e2..6aff22f41 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -58,15 +58,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
             iframe.contentDocument.addEventListener('pointerdown', this.iframedown, false);
             iframe.contentDocument.addEventListener('scroll', this.iframeScrolled, false);
             this.layoutDoc.scrollHeight = iframe.contentDocument.children?.[0].scrollHeight || 1000;
-            iframe.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc.scrollTop);
-            iframe.contentDocument.children[0].scrollLeft = NumCast(this.layoutDoc.scrollLeft);
+            iframe.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc._scrollTop);
+            iframe.contentDocument.children[0].scrollLeft = NumCast(this.layoutDoc._scrollLeft);
         }
         this._reactionDisposer?.();
-        this._reactionDisposer = reaction(() => ({ y: this.layoutDoc.scrollY, x: this.layoutDoc.scrollX }),
+        this._reactionDisposer = reaction(() => ({ y: this.layoutDoc._scrollY, x: this.layoutDoc._scrollX }),
             ({ x, y }) => {
                 if (y !== undefined) {
                     this._outerRef.current!.scrollTop = y;
-                    this.layoutDoc.scrollY = undefined;
+                    this.layoutDoc._scrollY = undefined;
                 }
                 if (x !== undefined) {
                     this._outerRef.current!.scrollLeft = x;
@@ -83,8 +83,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
     iframeScrolled = (e: any) => {
         const scrollTop = e.target?.children?.[0].scrollTop;
         const scrollLeft = e.target?.children?.[0].scrollLeft;
-        this.layoutDoc.scrollTop = this._outerRef.current!.scrollTop = scrollTop;
-        this.layoutDoc.scrollLeft = this._outerRef.current!.scrollLeft = scrollLeft;
+        this.layoutDoc._scrollTop = this._outerRef.current!.scrollTop = scrollTop;
+        this.layoutDoc._scrollLeft = this._outerRef.current!.scrollLeft = scrollLeft;
     }
     async componentDidMount() {
         const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField);
@@ -440,7 +440,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
             {this.urlEditor()}
         </>);
     }
-    scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc.scrollLeft), NumCast(this.layoutDoc.scrollTop));
+    scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc._scrollLeft), NumCast(this.layoutDoc._scrollTop));
     render() {
         return (<div className={`webBox-container`}
             style={{
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fc131cd38..8b98fd083 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -721,7 +721,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             },
             { fireImmediately: true }
         );
-        this._disposers.scroll = reaction(() => NumCast(this.layoutDoc.scrollPos),
+        this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop),
             pos => this._scrollRef.current && this._scrollRef.current.scrollTo({ top: pos }), { fireImmediately: true }
         );
 
@@ -1174,7 +1174,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     }
 
     onscrolled = (ev: React.UIEvent) => {
-        this.layoutDoc.scrollPos = this._scrollRef.current!.scrollTop;
+        this.layoutDoc._scrollTop = this._scrollRef.current!.scrollTop;
     }
     @action
     tryUpdateHeight(limitHeight?: number) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index d47ae63af..59a6045ab 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -183,7 +183,7 @@ export class FormattedTextBoxComment {
                             const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
                             const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor;
                             if (anchor !== target && anchor && target) {
-                                target.scrollY = NumCast(anchor?.y);
+                                target._scrollY = NumCast(anchor?.y);
                             }
                             if (target) {
                                 ReactDOM.render(<ContentFittingDocumentView
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5bad248be..ec5c3f4ff 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -2,7 +2,6 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
-import * as rp from "request-promise";
 import { Dictionary } from "typescript-collections";
 import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
@@ -38,9 +37,7 @@ import { Networking } from "../../Network";
 
 export const pageSchema = createSchema({
     curPage: "number",
-    fitWidth: "boolean",
     rotation: "number",
-    scrollY: "number",
     scrollHeight: "number",
     serachMatch: "boolean"
 });
@@ -95,7 +92,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
     @observable private _showWaiting = true;
     @observable private _showCover = false;
     @observable private _zoomed = 1;
-    @observable private _scrollTop = 0;
 
     private _pdfViewer: any;
     private _retries = 0; // number of times tried to create the PDF viewer 
@@ -143,6 +139,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         this._coverPath = href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" };
         runInAction(() => this._showWaiting = this._showCover = true);
         this.props.startupLive && this.setupPdfJsViewer();
+        this._mainCont.current!.scrollTop = this.layoutDoc._scrollTop || 0;
         this._searchReactionDisposer = reaction(() => this.Document.searchMatch, search => {
             if (search) {
                 this.search(Doc.SearchQuery(), true);
@@ -158,14 +155,12 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
             () => (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(),
             { fireImmediately: true });
         this._reactionDisposer = reaction(
-            () => this.Document.scrollY,
+            () => this.Document._scrollY,
             (scrollY) => {
                 if (scrollY !== undefined) {
-                    if (this._showCover || this._showWaiting) {
-                        this.setupPdfJsViewer();
-                    }
-                    this._mainCont.current && smoothScroll(1000, this._mainCont.current, (this.Document.scrollY || 0));
-                    this.Document.scrollY = undefined;
+                    (this._showCover || this._showWaiting) && this.setupPdfJsViewer();
+                    this._mainCont.current && smoothScroll(1000, this._mainCont.current, (this.Document._scrollY || 0));
+                    setTimeout(() => this.Document._scrollY = undefined, 1000);
                 }
             },
             { fireImmediately: true }
@@ -220,7 +215,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         await this.initialLoad();
 
         this._scrollTopReactionDisposer = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null),
-            (stop) => (stop !== undefined) && this._mainCont.current && smoothScroll(500, this._mainCont.current, stop), { fireImmediately: true });
+            (stop) => (stop !== undefined && this.layoutDoc._scrollY === undefined) && (this._mainCont.current!.scrollTop = stop), { fireImmediately: true });
+
         this._annotationReactionDisposer = reaction(
             () => DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]),
             annotations => annotations?.length && (this._annotations = annotations),
@@ -352,7 +348,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
 
     @action
     onScroll = (e: React.UIEvent<HTMLElement>) => {
-        this._scrollTop = this._mainCont.current!.scrollTop;
+        this.Document._scrollY === undefined && (this.layoutDoc._scrollTop = this._mainCont.current!.scrollTop);
         this._pdfViewer && (this.Document.curPage = this._pdfViewer.currentPageNumber);
     }
 
@@ -604,7 +600,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
     }
 
     scrollXf = () => {
-        return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform();
+        return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this.layoutDoc._scrollTop || 0) : this.props.ScreenToLocalTransform();
     }
     onClick = (e: React.MouseEvent) => {
         this._setPreviewCursor &&
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index e7031cc39..e0a02d93c 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -22,10 +22,10 @@ export const documentSchema = createSchema({
     y: "number",                // y coordinate when in a freeform view 
     z: "number",                // z "coordinate" - non-zero specifies the overlay layer of a freeformview
     zIndex: "number",           // zIndex of a document in a freeform view
-    scrollY: "number",          // "command" to scroll a document to a position on load (the value will be reset to 0 after that )
-    scrollX: "number",          // "command" to scroll a document to a position on load (the value will be reset to 0 after that )
-    scrollTop: "number",        // scroll position of a scrollable document (pdf, text, web)
-    scrollLeft: "number",        // scroll position of a scrollable document (pdf, text, web)
+    _scrollY: "number",          // "command" to scroll a document to a position on load (the value will be reset to 0 after that )
+    _scrollX: "number",          // "command" to scroll a document to a position on load (the value will be reset to 0 after that )
+    _scrollTop: "number",        // scroll position of a scrollable document (pdf, text, web)
+    _scrollLeft: "number",        // scroll position of a scrollable document (pdf, text, web)
 
     // appearance properties on the layout
     _autoHeight: "boolean",     // whether the height of the document should be computed automatically based on its contents
-- 
cgit v1.2.3-70-g09d2


From 632dec95b9fccccef13b50cb41fc598613a9df1e Mon Sep 17 00:00:00 2001
From: yunahi <60233430+yunahi@users.noreply.github.com>
Date: Mon, 8 Jun 2020 10:56:36 +0900
Subject: added ink options menu

---
 package-lock.json                                  |  56 ++++-
 package.json                                       |   2 +
 src/client/documents/Documents.ts                  |   7 +-
 src/client/util/InteractionUtils.tsx               | 139 ++++++++++-
 src/client/views/GestureOverlay.tsx                | 123 ++++++++-
 src/client/views/InkingControl.tsx                 |  46 +++-
 src/client/views/InkingStroke.tsx                  |   3 +-
 src/client/views/MainView.tsx                      |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 .../collectionFreeForm/InkOptionsMenu.scss         |  36 +++
 .../collectionFreeForm/InkOptionsMenu.tsx          | 274 +++++++++++++++++++++
 .../collections/collectionFreeForm/MarqueeView.tsx | 126 +++++++++-
 src/client/views/nodes/ColorBox.tsx                |   6 +-
 src/fields/documentSchemas.ts                      |   1 +
 src/pen-gestures/GestureUtils.ts                   |   5 +-
 src/pen-gestures/ndollar.ts                        |  17 +-
 src/typings/index.d.ts                             |   2 +
 17 files changed, 808 insertions(+), 39 deletions(-)
 create mode 100644 src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss
 create mode 100644 src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx

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

diff --git a/package-lock.json b/package-lock.json
index 842553659..b3fb00ab6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2200,6 +2200,11 @@
                 "callsite": "1.0.0"
             }
         },
+        "bezier-curve": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/bezier-curve/-/bezier-curve-1.0.0.tgz",
+            "integrity": "sha1-o9+v6rEqlMRicw1QeYxSqEBdc3k="
+        },
         "big.js": {
             "version": "5.2.2",
             "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -2827,7 +2832,8 @@
                         },
                         "ansi-regex": {
                             "version": "2.1.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "aproba": {
                             "version": "1.2.0",
@@ -2845,11 +2851,13 @@
                         },
                         "balanced-match": {
                             "version": "1.0.0",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "brace-expansion": {
                             "version": "1.1.11",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "balanced-match": "^1.0.0",
                                 "concat-map": "0.0.1"
@@ -2862,15 +2870,18 @@
                         },
                         "code-point-at": {
                             "version": "1.1.0",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "concat-map": {
                             "version": "0.0.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "console-control-strings": {
                             "version": "1.1.0",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "core-util-is": {
                             "version": "1.0.2",
@@ -2973,7 +2984,8 @@
                         },
                         "inherits": {
                             "version": "2.0.4",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "ini": {
                             "version": "1.3.5",
@@ -2983,6 +2995,7 @@
                         "is-fullwidth-code-point": {
                             "version": "1.0.0",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "number-is-nan": "^1.0.0"
                             }
@@ -2995,17 +3008,20 @@
                         "minimatch": {
                             "version": "3.0.4",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "brace-expansion": "^1.1.7"
                             }
                         },
                         "minimist": {
                             "version": "1.2.5",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "minipass": {
                             "version": "2.9.0",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "safe-buffer": "^5.1.2",
                                 "yallist": "^3.0.0"
@@ -3022,6 +3038,7 @@
                         "mkdirp": {
                             "version": "0.5.3",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "minimist": "^1.2.5"
                             }
@@ -3077,7 +3094,8 @@
                         },
                         "npm-normalize-package-bin": {
                             "version": "1.0.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "npm-packlist": {
                             "version": "1.4.8",
@@ -3102,7 +3120,8 @@
                         },
                         "number-is-nan": {
                             "version": "1.0.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "object-assign": {
                             "version": "4.1.1",
@@ -3112,6 +3131,7 @@
                         "once": {
                             "version": "1.4.0",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "wrappy": "1"
                             }
@@ -3180,7 +3200,8 @@
                         },
                         "safe-buffer": {
                             "version": "5.1.2",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "safer-buffer": {
                             "version": "2.1.2",
@@ -3210,6 +3231,7 @@
                         "string-width": {
                             "version": "1.0.2",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "code-point-at": "^1.0.0",
                                 "is-fullwidth-code-point": "^1.0.0",
@@ -3227,6 +3249,7 @@
                         "strip-ansi": {
                             "version": "3.0.1",
                             "bundled": true,
+                            "optional": true,
                             "requires": {
                                 "ansi-regex": "^2.0.0"
                             }
@@ -3265,11 +3288,13 @@
                         },
                         "wrappy": {
                             "version": "1.0.2",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         },
                         "yallist": {
                             "version": "3.1.1",
-                            "bundled": true
+                            "bundled": true,
+                            "optional": true
                         }
                     }
                 }
@@ -5879,6 +5904,11 @@
                 "resolve-dir": "^1.0.1"
             }
         },
+        "fit-curve": {
+            "version": "0.1.7",
+            "resolved": "https://registry.npmjs.org/fit-curve/-/fit-curve-0.1.7.tgz",
+            "integrity": "sha512-Md3b3ReA/qJwwYvKXeHpOV1fhPqwhJ9/29zc9lfi8ijyg00J0S9C7+5XMZDFhlDAKbwOvVgBxVIG1EHoMSC3vw=="
+        },
         "flat": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
@@ -18044,4 +18074,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index d1d7935af..20614f5eb 100644
--- a/package.json
+++ b/package.json
@@ -124,6 +124,7 @@
         "async": "^2.6.2",
         "babel-runtime": "^6.26.0",
         "bcrypt-nodejs": "0.0.3",
+        "bezier-curve": "^1.0.0",
         "bluebird": "^3.7.2",
         "body-parser": "^1.18.3",
         "bootstrap": "^4.4.1",
@@ -146,6 +147,7 @@
         "express-validator": "^5.3.1",
         "expressjs": "^1.0.1",
         "find-in-files": "^0.5.0",
+        "fit-curve": "^0.1.7",
         "flexlayout-react": "^0.3.3",
         "formidable": "^1.2.1",
         "golden-layout": "^1.5.9",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f7e19eecd..71bf8a516 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -630,12 +630,13 @@ export namespace Docs {
             return doc;
         }
 
-        export function InkDocument(color: string, tool: number, strokeWidth: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
+        export function InkDocument(color: string, tool: number, strokeWidth: string, strokeBezier: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
             const I = new Doc();
             I.type = DocumentType.INK;
             I.layout = InkingStroke.LayoutString("data");
             I.color = color;
             I.strokeWidth = strokeWidth;
+            I.strokeBezier = strokeBezier;
             I.tool = tool;
             I.title = "ink";
             I.x = options.x;
@@ -936,8 +937,8 @@ export namespace Docs {
                 created = Docs.Create.AudioDocument((field).url.href, resolved);
                 layout = AudioBox.LayoutString;
             } else if (field instanceof InkField) {
-                const { selectedColor, selectedWidth, selectedTool } = InkingControl.Instance;
-                created = Docs.Create.InkDocument(selectedColor, selectedTool, selectedWidth, (field).inkData, resolved);
+                const { selectedColor, selectedWidth, selectedTool, selectedBezier } = InkingControl.Instance;
+                created = Docs.Create.InkDocument(selectedColor, selectedTool, selectedWidth, selectedBezier, (field).inkData, resolved);
                 layout = InkingStroke.LayoutString;
             } else if (field instanceof List && field[0] instanceof Doc) {
                 created = Docs.Create.StackingDocument(DocListCast(field), resolved);
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 3a5345c80..ab1ccb25a 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,4 +1,7 @@
 import React = require("react");
+import * as beziercurve from 'bezier-curve';
+import * as fitCurve from 'fit-curve';
+import InkOptionsMenu from "../views/collections/collectionFreeForm/InkOptionsMenu";
 
 export namespace InteractionUtils {
     export const MOUSETYPE = "mouse";
@@ -87,8 +90,45 @@ export namespace InteractionUtils {
         return myTouches;
     }
 
-    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string) {
-        const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string) {
+        var pts = "";
+        var shape = "";
+        if (InkOptionsMenu.Instance._circle) {
+            shape = "circle";
+        } else if (InkOptionsMenu.Instance._rectangle) {
+            shape = "rectangle";
+        } else if (InkOptionsMenu.Instance._triangle) {
+            shape = "triangle";
+        } else if (InkOptionsMenu.Instance._arrow) {
+            shape = "arrow";
+        } else if (InkOptionsMenu.Instance._line) {
+            shape = "line";
+        }
+        if (shape !== "") {
+            //if any of the shape are true
+            const shapePts = makePolygon(shape, points);
+            pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+        }
+        else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) {
+            //pointer is up (first and last points are the same)
+            const newPoints: number[][] = [];
+            const newPts: { X: number; Y: number; }[] = [];
+            //convert to [][] for fitcurve module
+            for (var i = 0; i < points.length - 1; i++) {
+                newPoints.push([points[i].X, points[i].Y]);
+            }
+            const bezierCurves = fitCurve(newPoints, parseInt(bezier));
+            for (var i = 0; i < bezierCurves.length; i++) {
+                for (var t = 0; t < 1; t += 0.01) {
+                    const point = beziercurve(t, bezierCurves[i]);
+                    newPts.push({ X: point[0], Y: point[1] });
+                }
+            }
+            pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+        } else {
+            //in the middle of drawing
+            pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+        }
         return (
             <polyline
                 points={pts}
@@ -103,6 +143,101 @@ export namespace InteractionUtils {
         );
     }
 
+    export function makePolygon(shape: string, points: { X: number, Y: number }[]) {
+        if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
+            //pointer is up (first and last points are the same)
+            if (shape === "arrow" || shape === "line") {
+                //if arrow or line, the two end points should be the starting and the ending point
+                var left = points[0].X;
+                var top = points[0].Y;
+                var right = points[1].X;
+                var bottom = points[1].Y;
+            } else {
+                //otherwise take max and min
+                const xs = points.map(p => p.X);
+                const ys = points.map(p => p.Y);
+                right = Math.max(...xs);
+                left = Math.min(...xs);
+                bottom = Math.max(...ys);
+                top = Math.min(...ys);
+            }
+        } else {
+            //if in the middle of drawing
+            //take first and last points
+            right = points[points.length - 1].X;
+            left = points[0].X;
+            bottom = points[points.length - 1].Y;
+            top = points[0].Y;
+            if (shape !== "arrow" && shape !== "line") {
+                //switch left/right and top/bottom if needed
+                if (left > right) {
+                    const temp = right;
+                    right = left;
+                    left = temp;
+                }
+                if (top > bottom) {
+                    const temp = top;
+                    top = bottom;
+                    bottom = temp;
+                }
+            }
+        }
+        points = [];
+        switch (shape) {
+            case "rectangle":
+                points.push({ X: left, Y: top });
+                points.push({ X: right, Y: top });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: top });
+                return points;
+            case "triangle":
+                points.push({ X: left, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: left, Y: bottom });
+                return points;
+            case "circle":
+                const centerX = (right + left) / 2;
+                const centerY = (bottom + top) / 2;
+                const radius = bottom - centerY;
+                for (var y = top; y < bottom; y++) {
+                    const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+                    points.push({ X: x, Y: y });
+                }
+                for (var y = bottom; y > top; y--) {
+                    const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+                    const newX = centerX - (x - centerX);
+                    points.push({ X: newX, Y: y });
+                }
+                points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top });
+                return points;
+            case "arrow":
+                const x1 = left;
+                const y1 = top;
+                const x2 = right;
+                const y2 = bottom;
+                const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2)));
+                const L2 = L1 / 5;
+                const angle = 0.785398;
+                const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle));
+                const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
+                const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
+                const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
+                points.push({ X: x1, Y: y1 });
+                points.push({ X: x2, Y: y2 });
+                points.push({ X: x3, Y: y3 });
+                points.push({ X: x4, Y: y4 });
+                points.push({ X: x2, Y: y2 });
+                return points;
+            case "line":
+                points.push({ X: left, Y: top });
+                points.push({ X: right, Y: bottom });
+                return points;
+            default:
+                return points;
+        }
+    }
     /**
      * Returns whether or not the pointer event passed in is of the type passed in
      * @param e - pointer event. this event could be from a mouse, a pen, or a finger
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 4352ac52c..5714970c1 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -32,6 +32,7 @@ import { MobileInkOverlayContent } from "../../server/Message";
 import MobileInkOverlay from "../../mobile/MobileInkOverlay";
 import { RadialMenu } from "./nodes/RadialMenu";
 import { SelectionManager } from "../util/SelectionManager";
+import InkOptionsMenu from "./collections/collectionFreeForm/InkOptionsMenu";
 
 
 @observer
@@ -581,7 +582,8 @@ export default class GestureOverlay extends Touchable {
         if (this._points.length > 1) {
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
-
+            //push first points to so interactionUtil knows pointer is up
+            this._points.push({ X: this._points[0].X, Y: this._points[0].Y });
             if (MobileInterface.Instance && MobileInterface.Instance.drawingInk) {
                 const { selectedColor, selectedWidth } = InkingControl.Instance;
                 DocServer.Mobile.dispatchGesturePoints({
@@ -630,6 +632,23 @@ export default class GestureOverlay extends Touchable {
                         break;
                 }
             }
+            //if any of the shape is activated in the InkOptionsMenu
+            else if (InkOptionsMenu.Instance._circle || InkOptionsMenu.Instance._triangle || InkOptionsMenu.Instance._rectangle || InkOptionsMenu.Instance._line || InkOptionsMenu.Instance._arrow) {
+                if (InkOptionsMenu.Instance._circle) {
+                    this.makePolygon("circle", false);
+                } else if (InkOptionsMenu.Instance._triangle) {
+                    this.makePolygon("triangle", false);
+                } else if (InkOptionsMenu.Instance._rectangle) {
+                    this.makePolygon("rectangle", false);
+                } else if (InkOptionsMenu.Instance._line) {
+                    this.makePolygon("line", false);
+                } else if (InkOptionsMenu.Instance._arrow) {
+                    this.makePolygon("arrow", false);
+                }
+                this.dispatchGesture(GestureUtils.Gestures.Stroke);
+                this._points = [];
+                InkOptionsMenu.Instance.allFalse();
+            }
             // if we're not drawing in a toolglass try to recognize as gesture
             else {
                 const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize(new Array(points));
@@ -651,6 +670,15 @@ export default class GestureOverlay extends Touchable {
                         case GestureUtils.Gestures.Line:
                             actionPerformed = this.handleLineGesture();
                             break;
+                        case GestureUtils.Gestures.Triangle:
+                            this.makePolygon("triangle", true);
+                            break;
+                        case GestureUtils.Gestures.Circle:
+                            this.makePolygon("circle", true);
+                            break;
+                        case GestureUtils.Gestures.Rectangle:
+                            this.makePolygon("rectangle", true);
+                            break;
                         case GestureUtils.Gestures.Scribble:
                             console.log("scribble");
                             break;
@@ -671,6 +699,95 @@ export default class GestureOverlay extends Touchable {
         document.removeEventListener("pointerup", this.onPointerUp);
     }
 
+    makePolygon = (shape: string, gesture: boolean) => {
+        const xs = this._points.map(p => p.X);
+        const ys = this._points.map(p => p.Y);
+        var right = Math.max(...xs);
+        var left = Math.min(...xs);
+        var bottom = Math.max(...ys);
+        var top = Math.min(...ys);
+
+        if (!gesture) {
+            //if shape options is activated in inkOptionMenu
+            //take second to last point because _point[length-1] is _points[0]
+            right = this._points[this._points.length - 2].X;
+            left = this._points[0].X;
+            bottom = this._points[this._points.length - 2].Y;
+            top = this._points[0].Y;
+            if (shape !== "arrow" && shape !== "line") {
+                if (left > right) {
+                    const temp = right;
+                    right = left;
+                    left = temp;
+                }
+                if (top > bottom) {
+                    const temp = top;
+                    top = bottom;
+                    bottom = temp;
+                }
+            }
+        }
+        this._points = [];
+        switch (shape) {
+            //must push an extra point in the end so InteractionUtils knows pointer is up.
+            //must be (points[0].X,points[0]-1) 
+            case "rectangle":
+                this._points.push({ X: left, Y: top });
+                this._points.push({ X: right, Y: top });
+                this._points.push({ X: right, Y: bottom });
+                this._points.push({ X: left, Y: bottom });
+                this._points.push({ X: left, Y: top });
+                this._points.push({ X: left, Y: top - 1 });
+                break;
+            case "triangle":
+                this._points.push({ X: left, Y: bottom });
+                this._points.push({ X: right, Y: bottom });
+                this._points.push({ X: (right + left) / 2, Y: top });
+                this._points.push({ X: left, Y: bottom });
+                this._points.push({ X: left, Y: bottom - 1 });
+                break;
+            case "circle":
+                const centerX = (right + left) / 2;
+                const centerY = (bottom + top) / 2;
+                const radius = bottom - centerY;
+                for (var y = top; y < bottom; y++) {
+                    const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+                    this._points.push({ X: x, Y: y });
+                }
+                for (var y = bottom; y > top; y--) {
+                    const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+                    const newX = centerX - (x - centerX);
+                    this._points.push({ X: newX, Y: y });
+                }
+                this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top });
+                this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top - 1 });
+                break;
+            case "line":
+                this._points.push({ X: left, Y: top });
+                this._points.push({ X: right, Y: bottom });
+                this._points.push({ X: right, Y: bottom - 1 });
+                break;
+            case "arrow":
+                const x1 = left;
+                const y1 = top;
+                const x2 = right;
+                const y2 = bottom;
+                const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2)));
+                const L2 = L1 / 5;
+                const angle = 0.785398;
+                const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle));
+                const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
+                const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
+                const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
+                this._points.push({ X: x1, Y: y1 });
+                this._points.push({ X: x2, Y: y2 });
+                this._points.push({ X: x3, Y: y3 });
+                this._points.push({ X: x4, Y: y4 });
+                this._points.push({ X: x2, Y: y2 });
+                this._points.push({ X: x1, Y: y1 - 1 });
+        }
+    }
+
     dispatchGesture = (gesture: "box" | "line" | "startbracket" | "endbracket" | "stroke" | "scribble" | "text", stroke?: InkData, data?: any) => {
         const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y);
         target?.dispatchEvent(
@@ -710,11 +827,11 @@ export default class GestureOverlay extends Touchable {
             [this._strokes.map(l => {
                 const b = this.getBounds(l);
                 return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                    {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth)}
+                    {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier)}
             </svg>]
         ];
     }
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 41ee36d05..83109db1c 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -9,14 +9,15 @@ import { SelectionManager } from "../util/SelectionManager";
 import { undoBatch } from "../util/UndoManager";
 import GestureOverlay from "./GestureOverlay";
 import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
+import InkOptionsMenu from "./collections/collectionFreeForm/InkOptionsMenu";
 
 export class InkingControl {
     @observable static Instance: InkingControl;
     @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; }
     @computed private get _selectedColor(): string { return CurrentUserUtils.ActivePen ? FieldValue(StrCast(CurrentUserUtils.ActivePen.backgroundColor)) ?? "rgb(0, 0, 0)" : "rgb(0, 0, 0)"; }
     @computed private get _selectedWidth(): string { return FieldValue(StrCast(Doc.UserDoc().inkWidth)) ?? "2"; }
+    @computed private get _selectedBezier(): string { return FieldValue(StrCast(Doc.UserDoc().inkBezier)) ?? "2"; }
     @observable public _open: boolean = false;
-
     constructor() {
         InkingControl.Instance = this;
     }
@@ -32,10 +33,21 @@ export class InkingControl {
         return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
     }
 
+    @action
+    inkOptionsMenuChangeColor = (color: string) => {
+        const col: ColorState = {
+            hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
+            rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
+        };
+        this.switchColor(col);
+        InkOptionsMenu.Instance._colorBt = false;
+    }
+
     @undoBatch
     switchColor = action((color: ColorState): void => {
         Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ?
             color.hex + (color.rgb.a ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
+        InkOptionsMenu.Instance._color = StrCast(Doc.UserDoc().backgroundColor);
         CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = color.hex);
 
         if (InkingControl.Instance.selectedTool === InkTool.None) {
@@ -60,6 +72,23 @@ export class InkingControl {
         if (!isNaN(parseInt(width))) {
             Doc.UserDoc().inkWidth = width;
         }
+        InkOptionsMenu.Instance._widthBt = false;
+    }
+
+    @action
+    switchBezier = (bezier: string): void => {
+        if (!isNaN(parseInt(bezier))) {
+            Doc.UserDoc().inkBezier = bezier;
+        }
+    }
+
+    @action
+    inkOptionsMenuChangeBezier = (e: React.PointerEvent): void => {
+        if (InkOptionsMenu.Instance._bezierBt === true) {
+            Doc.UserDoc().inkBezier = "300";
+        } else {
+            Doc.UserDoc().inkBezier = "0";
+        }
     }
 
     @computed
@@ -83,8 +112,21 @@ export class InkingControl {
         return this._selectedWidth;
     }
 
+    @computed
+    get selectedBezier() {
+        return this._selectedBezier;
+    }
 }
-Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
+Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) {
+    InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color);
+    //setup InkOptionsMenu(change jumpto value if necessary.Currenlty hardcoded to 300,300)
+    pen ? InkOptionsMenu.Instance.jumpTo(300, 300) : InkOptionsMenu.Instance.fadeOut(true);
+    InkOptionsMenu.Instance.changeColor = InkingControl.Instance.inkOptionsMenuChangeColor;
+    InkOptionsMenu.Instance.changeBezier = InkingControl.Instance.inkOptionsMenuChangeBezier;
+    InkOptionsMenu.Instance.changeWidth = InkingControl.Instance.switchWidth;
+    InkOptionsMenu.Instance._widthSelected = width;
+    InkOptionsMenu.Instance._color = color;
+});
 Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
 Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
 Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); });
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 8938e8b6c..3dc0a5b20 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -40,7 +40,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const bottom = Math.max(...ys);
         const points = InteractionUtils.CreatePolyline(data, left, top,
             StrCast(this.layoutDoc.color, InkingControl.Instance.selectedColor),
-            StrCast(this.layoutDoc.strokeWidth, InkingControl.Instance.selectedWidth));
+            StrCast(this.layoutDoc.strokeWidth, InkingControl.Instance.selectedWidth),
+            StrCast(this.layoutDoc.strokeBezier, InkingControl.Instance.selectedBezier));
         const width = right - left;
         const height = bottom - top;
         const scaleX = this.props.PanelWidth() / width;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a1d1b0ece..3677746cd 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -33,6 +33,7 @@ import SharingManager from '../util/SharingManager';
 import { Transform } from '../util/Transform';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu';
+import InkOptionsMenu from './collections/collectionFreeForm/InkOptionsMenu';
 import { CollectionLinearView } from './collections/CollectionLinearView';
 import { CollectionView, CollectionViewType } from './collections/CollectionView';
 import { ContextMenu } from './ContextMenu';
@@ -567,6 +568,7 @@ export class MainView extends React.Component {
             <RadialMenu />
             <PDFMenu />
             <MarqueeOptionsMenu />
+            <InkOptionsMenu />
             <RichTextMenu />
             <OverlayView />
             <TimelineMenu />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c753a703d..fb7784b58 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -458,7 +458,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss
new file mode 100644
index 000000000..a7f4d4e53
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss
@@ -0,0 +1,36 @@
+.antimodeMenu-button {
+    .color-preview {
+        width: 100%;
+        height: 100%;
+    }
+
+
+}
+
+.sketch-picker {
+    background: #323232;
+
+    .flexbox-fit {
+        background: #323232;
+    }
+}
+
+.btn-group {
+    display: grid;
+    grid-template-columns: auto auto auto auto;
+    /* Make the buttons appear below each other */
+}
+
+.btn2-group {
+    display: block;
+    background: #323232;
+    grid-template-columns: auto;
+
+    /* Make the buttons appear below each other */
+    .antimodeMenu-button {
+        background: #323232;
+        display: block;
+
+
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
new file mode 100644
index 000000000..44488cbcf
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
@@ -0,0 +1,274 @@
+import React = require("react");
+import AntimodeMenu from "../../AntimodeMenu";
+import { observer } from "mobx-react";
+import { unimplementedFunction } from "../../../../Utils";
+import { observable, action } from "mobx";
+import "./InkOptionsMenu.scss";
+
+
+@observer
+export default class InkOptionsMenu extends AntimodeMenu {
+    static Instance: InkOptionsMenu;
+    public changeColor: (color: string) => void = unimplementedFunction;
+    public changeBezier: (e: React.PointerEvent) => void = unimplementedFunction;
+    public changeWidth: (color: string) => void = unimplementedFunction;
+
+    private _palette: (string)[];
+    private _width: (string)[];
+
+
+    public _circle: boolean;
+    public _triangle: boolean;
+    public _rectangle: boolean;
+    public _arrow: boolean;
+    public _line: boolean;
+    public _widthSelected: string;
+
+    @observable public _circleBt: boolean;
+    @observable public _triangleBt: boolean;
+    @observable public _rectangleBt: boolean;
+    @observable public _arrowBt: boolean;
+    @observable public _lineBt: boolean;
+    @observable public _colorBt: boolean;
+    @observable public _color: string;
+    @observable public _bezierBt: boolean;
+    @observable public _widthBt: boolean;
+
+
+
+    constructor(props: Readonly<{}>) {
+        super(props);
+        InkOptionsMenu.Instance = this;
+        this._canFade = false;
+
+        this._circle = false;
+        this._triangle = false;
+        this._rectangle = false;
+        this._arrow = false;
+        this._line = false;
+        this._circleBt = false;
+        this._triangleBt = false;
+        this._rectangleBt = false;
+        this._arrowBt = false;
+        this._lineBt = false;
+        this._colorBt = false;
+        this._bezierBt = false;
+        this._widthBt = false;
+
+        this._color = "";
+        this._widthSelected = "";
+
+
+        this._palette = [
+            "D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF",
+        ];
+
+        this._width = [
+            "1", "5", "10", "100", "200", "300"
+        ];
+
+    }
+
+
+
+    drag = (e: React.PointerEvent) => {
+        this.dragStart(e);
+    }
+
+
+
+
+
+    @action
+    toggleCircle = (e: React.PointerEvent) => {
+        const curr = this._circle;
+        this.allFalse();
+        curr ? this._circle = false : this._circle = true;
+        this._circleBt = this._circle;
+    }
+    @action
+    toggleTriangle = (e: React.PointerEvent) => {
+        const curr = this._triangle;
+        this.allFalse();
+        curr ? this._triangle = false : this._triangle = true;
+        this._triangleBt = this._triangle;
+    }
+    @action
+    toggleRectangle = (e: React.PointerEvent) => {
+        const curr = this._rectangle;
+        this.allFalse();
+        curr ? this._rectangle = false : this._rectangle = true;
+        this._rectangleBt = this._rectangle;
+    }
+    @action
+    toggleArrow = (e: React.PointerEvent) => {
+        const curr = this._arrow;
+        this.allFalse();
+        curr ? this._arrow = false : this._arrow = true;
+        this._arrowBt = this._arrow;
+    }
+    @action
+    toggleLine = (e: React.PointerEvent) => {
+        const curr = this._line;
+        this.allFalse();
+        curr ? this._line = false : this._line = true;
+        this._lineBt = this._line;
+    }
+
+    @action
+    changeBezierClick = (e: React.PointerEvent) => {
+        const curr = this._bezierBt;
+        this.allFalse();
+        curr ? this._bezierBt = false : this._bezierBt = true;
+        this.changeBezier(e);
+    }
+
+    @action
+    changeWidthClick = (e: React.PointerEvent) => {
+        this._widthBt ? this._widthBt = false : this._widthBt = true;
+    }
+    @action
+    changeColorClick = (e: React.PointerEvent) => {
+        this._colorBt ? this._colorBt = false : this._colorBt = true;
+    }
+
+    allFalse = () => {
+        this._circle = false;
+        this._triangle = false;
+        this._rectangle = false;
+        this._arrow = false;
+        this._line = false;
+        this._circleBt = false;
+        this._triangleBt = false;
+        this._rectangleBt = false;
+        this._arrowBt = false;
+        this._lineBt = false;
+        this._bezierBt = false;
+    }
+
+    render() {
+        var widthPicker;
+        if (this._widthBt) {
+            widthPicker = <div className="btn2-group">
+                <button
+                    className="antimodeMenu-button"
+                    key="width"
+                    onPointerDown={this.changeWidthClick}
+                    style={this._widthBt ? { backgroundColor: "121212" } : {}}>
+                    W
+                </button>
+                {this._width.map(wid => {
+                    return <button
+                        className="antimodeMenu-button"
+                        key={wid}
+                        onPointerDown={() => this.changeWidth(wid)}
+                        style={this._colorBt ? { backgroundColor: "121212" } : {}}>
+                        {wid}
+                    </button>;
+
+                })}
+            </div>;
+        } else {
+            widthPicker = <button
+                className="antimodeMenu-button"
+                key="width"
+                onPointerDown={this.changeWidthClick}
+                style={this._widthBt ? { backgroundColor: "121212" } : {}}>
+                W
+        </button>;
+        }
+
+        var colorPicker;
+        if (this._colorBt) {
+            colorPicker = <div className="btn-group">
+                <button
+                    className="antimodeMenu-button"
+                    key="color"
+                    onPointerDown={this.changeColorClick}
+                    style={this._colorBt ? { backgroundColor: "121212" } : {}}>
+                    <div className="color-preview" style={this._color === "" ? { backgroundColor: "121212" } : { backgroundColor: this._color }}></div>
+                </button>
+                {this._palette.map(color => {
+                    return <button
+                        className="antimodeMenu-button"
+                        key={color}
+                        onPointerDown={() => this.changeColor(color)}
+                        style={this._colorBt ? { backgroundColor: "121212" } : {}}>
+                        <div className="color-preview" style={{ backgroundColor: color }}></div>
+                    </button>;
+                })}
+            </div>;
+        } else {
+            colorPicker = <button
+                className="antimodeMenu-button"
+                title="colorChanger"
+                key="color"
+                onPointerDown={this.changeColorClick}
+                style={this._colorBt ? { backgroundColor: "121212" } : {}}>
+                <div className="color-preview" style={this._color === "" ? { backgroundColor: "121212" } : { backgroundColor: this._color }}></div>
+            </button>;
+        }
+
+
+        const buttons = [
+            <button
+                className="antimodeMenu-button"
+                title="Drag"
+                key="drag"
+                onPointerDown={this.drag}>
+                ✜
+            </button>,
+            <button
+                className="antimodeMenu-button"
+                title="Draw Circle"
+                key="circle"
+                onPointerDown={this.toggleCircle}
+                style={this._circleBt ? { backgroundColor: "121212" } : {}}>
+                O
+            </button>,
+            <button
+                className="antimodeMenu-button"
+                title="Draw Traingle"
+                key="triangle"
+                onPointerDown={this.toggleTriangle}
+                style={this._triangleBt ? { backgroundColor: "121212" } : {}}>
+                ∆
+            </button>,
+            <button
+                className="antimodeMenu-button"
+                title="Draw Rectangle"
+                key="rectangle"
+                onPointerDown={this.toggleRectangle}
+                style={this._rectangleBt ? { backgroundColor: "121212" } : {}}>
+                ロ
+            </button>,
+            <button
+                className="antimodeMenu-button"
+                title="Draw Arrow"
+                key="arrow"
+                onPointerDown={this.toggleArrow}
+                style={this._arrowBt ? { backgroundColor: "121212" } : {}}>
+                ➜
+                </button>,
+            <button
+                className="antimodeMenu-button"
+                title="Draw Line"
+                key="line"
+                onPointerDown={this.toggleLine}
+                style={this._lineBt ? { backgroundColor: "121212" } : {}}>
+                –
+            </button>,
+            <button
+                className="antimodeMenu-button"
+                title="Bezier changer"
+                key="bezier"
+                onPointerDown={this.changeBezierClick}
+                style={this._bezierBt ? { backgroundColor: "121212" } : {}}>
+                B
+            </button>,
+            widthPicker,
+            colorPicker,
+        ];
+        return this.getElement(buttons);
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index cdfeeaa6b..97244ed06 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -42,6 +42,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     @observable _downY: number = 0;
     @observable _visible: boolean = false;
     _commandExecuted = false;
+    @observable _pointsX: number[] = [];
+    @observable _pointsY: number[] = [];
+    @observable _freeHand: boolean = false;
 
     componentDidMount() {
         this.props.setPreviewCursor?.(this.setPreviewCursor);
@@ -57,6 +60,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         if (hideMarquee) {
             this._visible = false;
         }
+        this._pointsX = [];
+        this._pointsY = [];
+        this._freeHand = false;
     }
 
     @undoBatch
@@ -191,6 +197,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     onPointerMove = (e: PointerEvent): void => {
         this._lastX = e.pageX;
         this._lastY = e.pageY;
+        this._pointsX.push(e.clientX);
+        this._pointsY.push(e.clientY);
         if (!e.cancelBubble) {
             if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD ||
                 Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) {
@@ -519,6 +527,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             }
             this.cleanupInteractions(false);
         }
+        if (e.key === "r") {
+            this._commandExecuted = true;
+            e.stopPropagation();
+            e.preventDefault();
+            this.changeFreeHand(true);
+        }
+    }
+
+    @action
+    changeFreeHand = (x: boolean) => {
+        this._freeHand = x;
     }
     // @action
     // marqueeInkSelect(ink: Map<any, any>) {
@@ -559,7 +578,51 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     //         this.ink = new InkField(idata);
     //     }
     // }
+    touchesLine(r1: { left: number, top: number, width: number, height: number }) {
+        for (var i = 0; i < this._pointsX.length; i++) {
+            const topLeft = this.props.getTransform().transformPoint(this._pointsX[i], this._pointsY[i]);
+            if (topLeft[0] > r1.left &&
+                topLeft[0] < r1.left + r1.width &&
+                topLeft[1] > r1.top &&
+                topLeft[1] < r1.top + r1.height) {
+                return true;
+            }
+        }
+        return false;
+    }
 
+    boundingShape(r1: { left: number, top: number, width: number, height: number }) {
+        const trueLeft = this.props.getTransform().transformPoint(Math.min(...this._pointsX), Math.min(...this._pointsY))[0];
+        const trueTop = this.props.getTransform().transformPoint(Math.min(...this._pointsX), Math.min(...this._pointsY))[1];
+        const trueRight = this.props.getTransform().transformPoint(Math.max(...this._pointsX), Math.max(...this._pointsY))[0];
+        const trueBottom = this.props.getTransform().transformPoint(Math.max(...this._pointsX), Math.max(...this._pointsY))[1];
+
+        if (r1.left > trueLeft && r1.top > trueTop && r1.left + r1.width < trueRight && r1.top + r1.height < trueBottom) {
+            var hasTop = false;
+            var hasLeft = false;
+            var hasBottom = false;
+            var hasRight = false;
+            for (var i = 0; i < this._pointsX.length; i++) {
+                const truePoint = this.props.getTransform().transformPoint(this._pointsX[i], this._pointsY[i]);
+                if (!hasLeft && (truePoint[0] > trueLeft && truePoint[0] < r1.left) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height)) {
+                    hasLeft = true;
+                }
+                if (!hasTop && (truePoint[1] > trueTop && truePoint[1] < r1.top) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width)) {
+                    hasTop = true;
+                }
+                if (!hasRight && (truePoint[0] < trueRight && truePoint[0] > r1.left + r1.width) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height)) {
+                    hasRight = true;
+                }
+                if (!hasBottom && (truePoint[1] < trueBottom && truePoint[1] > r1.top + r1.height) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width)) {
+                    hasBottom = true;
+                }
+                if (hasTop && hasLeft && hasBottom && hasRight) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
     marqueeSelect(selectBackgrounds: boolean = true) {
         const selRect = this.Bounds;
         const selection: Doc[] = [];
@@ -569,8 +632,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             const y = NumCast(doc.y);
             const w = NumCast(layoutDoc._width);
             const h = NumCast(layoutDoc._height);
-            if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
-                selection.push(doc);
+            if (this._freeHand === false) {
+                if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
+                    selection.push(doc);
+                }
+            } else {
+                if (this.touchesLine({ left: x, top: y, width: w, height: h }) ||
+                    this.boundingShape({ left: x, top: y, width: w, height: h })) {
+                    selection.push(doc);
+                }
             }
         });
         if (!selection.length && selectBackgrounds) {
@@ -597,8 +667,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 const y = NumCast(doc.y);
                 const w = NumCast(layoutDoc._width);
                 const h = NumCast(layoutDoc._height);
-                if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
-                    selection.push(doc);
+                if (this._freeHand === false) {
+                    if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
+                        selection.push(doc);
+                    }
+                } else {
+                    if (this.touchesLine({ left: x, top: y, width: w, height: h }) ||
+                        this.boundingShape({ left: x, top: y, width: w, height: h })) {
+                        selection.push(doc);
+                    }
                 }
             });
         }
@@ -614,13 +691,40 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
          * This contains the "C for collection, ..." text on marquees.
          * Commented out by syip2 when the marquee menu was added.
          */
-        return <div className="marquee" style={{
-            transform: `translate(${p[0]}px, ${p[1]}px)`,
-            width: `${Math.abs(v[0])}`,
-            height: `${Math.abs(v[1])}`, zIndex: 2000
-        }} >
-            {/* <span className="marquee-legend" /> */}
-        </div>;
+        if (!this._freeHand) {
+            return <div className="marquee" style={{
+                transform: `translate(${p[0]}px, ${p[1]}px)`,
+                width: `${Math.abs(v[0])}`,
+                height: `${Math.abs(v[1])}`, zIndex: 2000
+            }} >
+                {/* <span className="marquee-legend" /> */}
+            </div>;
+
+        } else {
+            //subtracted 250 for offset
+            var str: string = "";
+            for (var i = 0; i < this._pointsX.length; i++) {
+                var x = 0;
+                x = this._pointsX[i] - 250;
+                str += x.toString();
+                str += ",";
+                str += this._pointsY[i].toString();
+                str += (" ");
+            }
+
+            //hardcoded height and width.
+            return <div className="marquee" style={{ zIndex: 2000 }}>
+                <svg height={2000} width={2000}>
+                    <polyline
+                        points={str}
+                        fill="none"
+                        stroke="black"
+                        strokeWidth="1"
+                        strokeDasharray="3"
+                    />
+                </svg>
+            </div>;
+        }
     }
 
     render() {
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 6d53915ea..2ddf2c74a 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -28,8 +28,12 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument
                 color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined,
                     StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} />
             <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}>
-                <div>{InkingControl.Instance.selectedWidth ?? 2}</div>
+                <div> {InkingControl.Instance.selectedWidth ?? 2}</div>
                 <input type="range" value={InkingControl.Instance.selectedWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} />
+                <div> {InkingControl.Instance.selectedBezier ?? 2}</div>
+                <input type="range" value={InkingControl.Instance.selectedBezier ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} />
+                <br />
+                <br />
             </div>
         </div>;
     }
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index e7031cc39..31f589ec0 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -63,6 +63,7 @@ export const documentSchema = createSchema({
     letterSpacing: "string",
     opacity: "number",          // opacity of document
     strokeWidth: "number",
+    strokeBezier: "number",
     textTransform: "string",
     treeViewOpen: "boolean",    //  flag denoting whether the documents sub-tree (contents) is visible or hidden
     treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 3b6170f68..a50cca2b0 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -39,7 +39,10 @@ export namespace GestureUtils {
         EndBracket = "endbracket",
         Stroke = "stroke",
         Scribble = "scribble",
-        Text = "text"
+        Text = "text",
+        Triangle = "triangle",
+        Circle = "circle",
+        Rectangle = "rectangle",
     }
 
     export const GestureRecognizer = new NDollarRecognizer(false);
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index e5740d105..9d42035d1 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -142,7 +142,7 @@ export class Result {
 //
 // NDollarRecognizer constants
 //
-const NumMultistrokes = 4;
+const NumMultistrokes = 7;
 const NumPoints = 96;
 const SquareSize = 250.0;
 const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35)
@@ -190,6 +190,21 @@ export class NDollarRecognizer {
             // new Array(new Point(150, 150), new Point(150, 0), new Point(150, 150), new Point(0, 150))
             new Array(new Point(10, 100), new Point(100, 100), new Point(150, 12), new Point(200, 103), new Point(300, 100))
         ));
+        this.Multistrokes[4] = new Multistroke(GestureUtils.Gestures.Triangle, useBoundedRotationInvariance, new Array(
+            new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100))
+        ));
+        this.Multistrokes[5] = new Multistroke(GestureUtils.Gestures.Circle, useBoundedRotationInvariance, new Array(
+            new Array(new Point(200, 250), new Point(240, 230), new Point(248, 210), new Point(248, 190), new Point(240, 170), new Point(200, 150), new Point(160, 170), new Point(151, 190), new Point(151, 210), new Point(160, 230), new Point(201, 250))
+        ));
+        this.Multistrokes[6] = new Multistroke(GestureUtils.Gestures.Rectangle, useBoundedRotationInvariance, new Array(
+            new Array(
+                new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
+                new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
+                new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
+                new Point(106, 146), //new Point(80, 150), new Point(50, 146),
+                new Point(30, 143),
+                new Point(29, 220))
+        ));
 
         //
         // PREDEFINED STROKES
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index 850c533fc..452882e09 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -5,6 +5,8 @@ declare module 'react-image-lightbox-with-rotate';
 declare module 'cors';
 
 declare module 'webrtc-adapter';
+declare module 'bezier-curve';
+declare module 'fit-curve'
 
 
 declare module '@react-pdf/renderer' {
-- 
cgit v1.2.3-70-g09d2


From 3dea269077151542bc2450bccd749ede87681556 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 8 Jun 2020 14:18:11 -0400
Subject: a bunch of cleanup to fix import order and to organize/restructure
 ink things in the right places.

---
 package-lock.json                                  |  46 +-
 src/client/apis/youtube/YoutubeBox.tsx             |   7 +-
 src/client/cognitive_services/CognitiveServices.ts |   3 +-
 src/client/documents/Documents.ts                  | 495 +++++++++++----------
 src/client/util/CurrentUserUtils.ts                |   8 +-
 src/client/util/DragManager.ts                     |   2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |   4 +-
 src/client/util/Import & Export/ImageUtils.ts      |   2 +-
 src/client/util/InteractionUtils.tsx               |  23 +-
 src/client/views/AntimodeMenu.tsx                  |   1 -
 src/client/views/DocComponent.tsx                  |   8 +-
 src/client/views/GestureOverlay.tsx                |  83 ++--
 src/client/views/InkingControl.tsx                 | 109 +----
 src/client/views/InkingStroke.tsx                  |  45 +-
 src/client/views/PreviewCursor.tsx                 |   4 +-
 src/client/views/Touchable.tsx                     |   2 -
 src/client/views/collections/CollectionMapView.tsx |   2 +-
 src/client/views/collections/CollectionSubView.tsx |  25 +-
 .../views/collections/CollectionTreeView.tsx       |   2 +-
 src/client/views/collections/CollectionView.tsx    |  42 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  17 +-
 .../collectionFreeForm/InkOptionsMenu.tsx          | 283 +++---------
 src/client/views/nodes/ColorBox.tsx                |  50 ++-
 src/client/views/nodes/DocumentView.tsx            |  13 +-
 src/client/views/nodes/ImageBox.tsx                |   2 +-
 src/client/views/nodes/PresBox.tsx                 |   4 +-
 src/client/views/nodes/ScreenshotBox.tsx           |  12 +-
 src/client/views/nodes/VideoBox.tsx                |   7 +-
 src/client/views/nodes/WebBox.tsx                  |   3 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  12 +-
 src/client/views/pdf/PDFViewer.tsx                 |   6 +-
 src/client/views/webcam/DashWebRTCVideo.tsx        |   4 +-
 src/fields/Doc.ts                                  | 322 ++++++--------
 src/pen-gestures/GestureUtils.ts                   |  10 +-
 34 files changed, 701 insertions(+), 957 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index b3fb00ab6..c956fd3e9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2832,8 +2832,7 @@
                         },
                         "ansi-regex": {
                             "version": "2.1.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "aproba": {
                             "version": "1.2.0",
@@ -2851,13 +2850,11 @@
                         },
                         "balanced-match": {
                             "version": "1.0.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "brace-expansion": {
                             "version": "1.1.11",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "balanced-match": "^1.0.0",
                                 "concat-map": "0.0.1"
@@ -2870,18 +2867,15 @@
                         },
                         "code-point-at": {
                             "version": "1.1.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "concat-map": {
                             "version": "0.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "console-control-strings": {
                             "version": "1.1.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "core-util-is": {
                             "version": "1.0.2",
@@ -2984,8 +2978,7 @@
                         },
                         "inherits": {
                             "version": "2.0.4",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "ini": {
                             "version": "1.3.5",
@@ -2995,7 +2988,6 @@
                         "is-fullwidth-code-point": {
                             "version": "1.0.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "number-is-nan": "^1.0.0"
                             }
@@ -3008,20 +3000,17 @@
                         "minimatch": {
                             "version": "3.0.4",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "brace-expansion": "^1.1.7"
                             }
                         },
                         "minimist": {
                             "version": "1.2.5",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "minipass": {
                             "version": "2.9.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "safe-buffer": "^5.1.2",
                                 "yallist": "^3.0.0"
@@ -3038,7 +3027,6 @@
                         "mkdirp": {
                             "version": "0.5.3",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "minimist": "^1.2.5"
                             }
@@ -3094,8 +3082,7 @@
                         },
                         "npm-normalize-package-bin": {
                             "version": "1.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "npm-packlist": {
                             "version": "1.4.8",
@@ -3120,8 +3107,7 @@
                         },
                         "number-is-nan": {
                             "version": "1.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "object-assign": {
                             "version": "4.1.1",
@@ -3131,7 +3117,6 @@
                         "once": {
                             "version": "1.4.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "wrappy": "1"
                             }
@@ -3200,8 +3185,7 @@
                         },
                         "safe-buffer": {
                             "version": "5.1.2",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "safer-buffer": {
                             "version": "2.1.2",
@@ -3231,7 +3215,6 @@
                         "string-width": {
                             "version": "1.0.2",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "code-point-at": "^1.0.0",
                                 "is-fullwidth-code-point": "^1.0.0",
@@ -3249,7 +3232,6 @@
                         "strip-ansi": {
                             "version": "3.0.1",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "ansi-regex": "^2.0.0"
                             }
@@ -3288,13 +3270,11 @@
                         },
                         "wrappy": {
                             "version": "1.0.2",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "yallist": {
                             "version": "3.1.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         }
                     }
                 }
@@ -18074,4 +18054,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index ce7f49e64..a7045ccb7 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -6,7 +6,6 @@ import { Utils } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { Docs } from "../../documents/Documents";
 import { DocumentDecorations } from "../../views/DocumentDecorations";
-import { InkingControl } from "../../views/InkingControl";
 import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
 import "../../views/nodes/WebBox.scss";
 import "./YoutubeBox.scss";
@@ -156,14 +155,14 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
     @action
     processVideoDetails = (videoDetails: any[]) => {
         this.videoDetails = videoDetails;
-        this.props.Document.cachedDetails = Docs.Get.FromJson({ data: videoDetails, title: "detailBackUp" });
+        this.props.Document.cachedDetails = Doc.Get.FromJson({ data: videoDetails, title: "detailBackUp" });
     }
 
     /**
      * The function that stores the search results in the props document.
      */
     backUpSearchResults = (videos: any[]) => {
-        this.props.Document.cachedSearchResults = Docs.Get.FromJson({ data: videos, title: "videosBackUp" });
+        this.props.Document.cachedSearchResults = Doc.Get.FromJson({ data: videos, title: "videosBackUp" });
     }
 
     /**
@@ -350,7 +349,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
 
-        const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
         return (
             <>
                 <div className={classname}  >
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index b816d1617..6b0b3e029 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -1,7 +1,6 @@
 import * as request from "request-promise";
 import { Doc, Field } from "../../fields/Doc";
 import { Cast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
 import { Utils } from "../../Utils";
 import { InkData } from "../../fields/InkField";
 import { UndoManager } from "../util/UndoManager";
@@ -195,7 +194,7 @@ export namespace CognitiveServices {
                 let results = await ExecuteQuery(Service.Handwriting, Manager, inkData);
                 if (results) {
                     results.recognitionUnits && (results = results.recognitionUnits);
-                    target[keys[0]] = Docs.Get.FromJson({ data: results, title: "Ink Analysis" });
+                    target[keys[0]] = Doc.Get.FromJson({ data: results, title: "Ink Analysis" });
                     const recognizedText = results.map((item: any) => item.recognizedText);
                     const recognizedObjects = results.map((item: any) => item.recognizedObject);
                     const individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1);
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 71bf8a516..994f8a147 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -9,11 +9,11 @@ import { ScriptingBox } from "../views/nodes/ScriptingBox";
 import { VideoBox } from "../views/nodes/VideoBox";
 import { WebBox } from "../views/nodes/WebBox";
 import { OmitKeys, JSONUtils, Utils } from "../../Utils";
-import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../fields/Doc";
+import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast, HeightSym, WidthSym } from "../../fields/Doc";
 import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../fields/URLField";
 import { HtmlField } from "../../fields/HtmlField";
 import { List } from "../../fields/List";
-import { Cast, NumCast, StrCast } from "../../fields/Types";
+import { Cast, NumCast, StrCast, FieldValue } from "../../fields/Types";
 import { DocServer } from "../DocServer";
 import { dropActionType } from "../util/DragManager";
 import { DateField } from "../../fields/DateField";
@@ -36,11 +36,9 @@ import { PresElementBox } from "../views/presentationview/PresElementBox";
 import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
 import { QueryBox } from "../views/nodes/QueryBox";
 import { ColorBox } from "../views/nodes/ColorBox";
-import { LinkAnchorBox } from "../views/nodes/LinkAnchorBox";
 import { DocHolderBox } from "../views/nodes/DocHolderBox";
 import { InkingStroke } from "../views/InkingStroke";
 import { InkField } from "../../fields/InkField";
-import { InkingControl } from "../views/InkingControl";
 import { RichTextField } from "../../fields/RichTextField";
 import { extname } from "path";
 import { MessageStore } from "../../server/Message";
@@ -49,6 +47,12 @@ import { ContextMenu } from "../views/ContextMenu";
 import { LinkBox } from "../views/nodes/LinkBox";
 import { ScreenshotBox } from "../views/nodes/ScreenshotBox";
 import { ComparisonBox } from "../views/nodes/ComparisonBox";
+import { Id } from "../../fields/FieldSymbols";
+import { listSpec } from "../../fields/Schema";
+import { ObjectField } from "../../fields/ObjectField";
+import { RefField } from "../../fields/RefField";
+import { runInAction } from "mobx";
+import { UndoManager } from "../util/UndoManager";
 const path = require('path');
 
 export interface DocumentOptions {
@@ -455,7 +459,7 @@ export namespace Docs {
                     const doc = StackingDocument(deviceImages, { title, _LODdisable: true, hero: new ImageField(constructed[0].url) });
                     doc.nameAliases = new List<string>([title.toLowerCase()]);
                     // add the parsed attributes to this main document
-                    Docs.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
+                    Doc.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
                     Doc.AddDocToList(parentProto, "data", doc);
                 } else if (errors) {
                     console.log(errors);
@@ -777,225 +781,6 @@ export namespace Docs {
             return InstanceFromProto(proto, undefined, options);
         }
     }
-
-    export namespace Get {
-
-        const primitives = ["string", "number", "boolean"];
-
-        export interface JsonConversionOpts {
-            data: any;
-            title?: string;
-            appendToExisting?: { targetDoc: Doc, fieldKey?: string };
-            excludeEmptyObjects?: boolean;
-        }
-
-        const defaultKey = "json";
-
-        /**
-         * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
-         * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
-         * 
-         * After building a hierarchy within / below a top-level document, it then returns that top-level parent.
-         * 
-         * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
-         * string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
-         * call that returned a regular string value to be stored as a Field.
-         * 
-         * If we've received something other than a string, since the caller might also pass in the results of a
-         * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
-         * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
-         * 
-         * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
-         * lacking the key value structure, gets stored as a field in a wrapper document.
-         * 
-         * @param data for convenience and flexibility, either a valid JSON string to be parsed,
-         * or the result of any JSON.parse() call.
-         * @param title an optional title to give to the highest parent document in the hierarchy.
-         * If whether this function creates a new document or appendToExisting is specified and that document already has a title,
-         * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title.
-         * @param appendToExisting **if specified**, there are two cases, both of which return the target document:
-         * 
-         * 1) the json to be converted can be represented as a document, in which case the target document will act as the root
-         * of the tree and receive all the conversion results as new fields on itself
-         * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion
-         * results to either the specified key on the target document, or to its "json" key by default.
-         * 
-         * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls)
-         * to act as the root of the tree.
-         * 
-         * One might choose to specify this field if you want to write to a document returned from a Document.Create function call,
-         * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created
-         * from a default call to new Doc.
-         * 
-         * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even
-         * if they contain no data. By default, empty objects and arrays are ignored.
-         */
-        export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> {
-            if (excludeEmptyObjects === undefined) {
-                excludeEmptyObjects = true;
-            }
-            if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) {
-                return undefined;
-            }
-            let resolved: any;
-            try {
-                resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data));
-            } catch (e) {
-                return undefined;
-            }
-            let output: Opt<Doc>;
-            if (typeof resolved === "object" && !(resolved instanceof Array)) {
-                output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc);
-            } else {
-                const result = toField(resolved, excludeEmptyObjects);
-                if (appendToExisting) {
-                    (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result;
-                } else {
-                    (output = new Doc).json = result;
-                }
-            }
-            title && output && (output.title = title);
-            return output;
-        }
-
-        /**
-         * For each value of the object, recursively convert it to its appropriate field value
-         * and store the field at the appropriate key in the document if it is not undefined
-         * @param object the object to convert
-         * @returns the object mapped from JSON to field values, where each mapping 
-         * might involve arbitrary recursion (since toField might itself call convertObject)
-         */
-        const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => {
-            const hasEntries = Object.keys(object).length;
-            if (hasEntries || !excludeEmptyObjects) {
-                const resolved = target ?? new Doc;
-                if (hasEntries) {
-                    let result: Opt<Field>;
-                    Object.keys(object).map(key => {
-                        // if excludeEmptyObjects is true, any qualifying conversions from toField will
-                        // be undefined, and thus the results that would have
-                        // otherwise been empty (List or Doc)s will just not be written
-                        if (result = toField(object[key], excludeEmptyObjects, key)) {
-                            resolved[key] = result;
-                        }
-                    });
-                }
-                title && (resolved.title = title);
-                return resolved;
-            }
-        };
-
-        /**
-         * For each element in the list, recursively convert it to a document or other field 
-         * and push the field to the list if it is not undefined
-         * @param list the list to convert
-         * @returns the list mapped from JSON to field values, where each mapping 
-         * might involve arbitrary recursion (since toField might itself call convertList)
-         */
-        const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
-            const target = new List();
-            let result: Opt<Field>;
-            // if excludeEmptyObjects is true, any qualifying conversions from toField will
-            // be undefined, and thus the results that would have
-            // otherwise been empty (List or Doc)s will just not be written
-            list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result));
-            if (target.length || !excludeEmptyObjects) {
-                return target;
-            }
-        };
-
-        const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => {
-            if (data === null || data === undefined) {
-                return undefined;
-            }
-            if (primitives.includes(typeof data)) {
-                return data;
-            }
-            if (typeof data === "object") {
-                return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined);
-            }
-            throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
-        };
-
-        export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
-            let created: Doc | undefined;
-            let layout: ((fieldKey: string) => string) | undefined;
-            const field = target[fieldKey];
-            const resolved = options || {};
-            if (field instanceof ImageField) {
-                created = Docs.Create.ImageDocument((field).url.href, resolved);
-                layout = ImageBox.LayoutString;
-            } else if (field instanceof Doc) {
-                created = field;
-            } else if (field instanceof VideoField) {
-                created = Docs.Create.VideoDocument((field).url.href, resolved);
-                layout = VideoBox.LayoutString;
-            } else if (field instanceof PdfField) {
-                created = Docs.Create.PdfDocument((field).url.href, resolved);
-                layout = PDFBox.LayoutString;
-            } else if (field instanceof AudioField) {
-                created = Docs.Create.AudioDocument((field).url.href, resolved);
-                layout = AudioBox.LayoutString;
-            } else if (field instanceof InkField) {
-                const { selectedColor, selectedWidth, selectedTool, selectedBezier } = InkingControl.Instance;
-                created = Docs.Create.InkDocument(selectedColor, selectedTool, selectedWidth, selectedBezier, (field).inkData, resolved);
-                layout = InkingStroke.LayoutString;
-            } else if (field instanceof List && field[0] instanceof Doc) {
-                created = Docs.Create.StackingDocument(DocListCast(field), resolved);
-                layout = CollectionView.LayoutString;
-            } else {
-                created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
-                layout = FormattedTextBox.LayoutString;
-            }
-            if (created) {
-                created.layout = layout?.(fieldKey);
-                created.title = fieldKey;
-                proto && created.proto && (created.proto = Doc.GetProto(proto));
-            }
-            return created;
-        }
-
-        export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
-            let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
-            if (type.indexOf("image") !== -1) {
-                ctor = Docs.Create.ImageDocument;
-                if (!options._width) options._width = 300;
-            }
-            if (type.indexOf("video") !== -1) {
-                ctor = Docs.Create.VideoDocument;
-                if (!options._width) options._width = 600;
-                if (!options._height) options._height = options._width * 2 / 3;
-            }
-            if (type.indexOf("audio") !== -1) {
-                ctor = Docs.Create.AudioDocument;
-            }
-            if (type.indexOf("pdf") !== -1) {
-                ctor = Docs.Create.PdfDocument;
-                if (!options._width) options._width = 400;
-                if (!options._height) options._height = options._width * 1200 / 927;
-            }
-            if (type.indexOf("html") !== -1) {
-                if (path.includes(window.location.hostname)) {
-                    const s = path.split('/');
-                    const id = s[s.length - 1];
-                    return DocServer.GetRefField(id).then(field => {
-                        if (field instanceof Doc) {
-                            const alias = Doc.MakeAlias(field);
-                            alias.x = options.x || 0;
-                            alias.y = options.y || 0;
-                            alias._width = options._width || 300;
-                            alias._height = options._height || options._width || 300;
-                            return alias;
-                        }
-                        return undefined;
-                    });
-                }
-                ctor = Docs.Create.WebDocument;
-                options = { ...options, _nativeWidth: 850, _nativeHeight: 962, _width: 500, _height: 566, title: path, };
-            }
-            return ctor ? ctor(path, options) : undefined;
-        }
-    }
 }
 
 export namespace DocUtils {
@@ -1049,6 +834,155 @@ export namespace DocUtils {
         return linkDoc;
     }
 
+
+    export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
+        let created: Doc | undefined;
+        let layout: ((fieldKey: string) => string) | undefined;
+        const field = target[fieldKey];
+        const resolved = options || {};
+        if (field instanceof ImageField) {
+            created = Docs.Create.ImageDocument((field).url.href, resolved);
+            layout = ImageBox.LayoutString;
+        } else if (field instanceof Doc) {
+            created = field;
+        } else if (field instanceof VideoField) {
+            created = Docs.Create.VideoDocument((field).url.href, resolved);
+            layout = VideoBox.LayoutString;
+        } else if (field instanceof PdfField) {
+            created = Docs.Create.PdfDocument((field).url.href, resolved);
+            layout = PDFBox.LayoutString;
+        } else if (field instanceof AudioField) {
+            created = Docs.Create.AudioDocument((field).url.href, resolved);
+            layout = AudioBox.LayoutString;
+        } else if (field instanceof InkField) {
+            created = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, (field).inkData, resolved);
+            layout = InkingStroke.LayoutString;
+        } else if (field instanceof List && field[0] instanceof Doc) {
+            created = Docs.Create.StackingDocument(DocListCast(field), resolved);
+            layout = CollectionView.LayoutString;
+        } else {
+            created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
+            layout = FormattedTextBox.LayoutString;
+        }
+        if (created) {
+            created.layout = layout?.(fieldKey);
+            created.title = fieldKey;
+            proto && created.proto && (created.proto = Doc.GetProto(proto));
+        }
+        return created;
+    }
+
+    export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
+        let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
+        if (type.indexOf("image") !== -1) {
+            ctor = Docs.Create.ImageDocument;
+            if (!options._width) options._width = 300;
+        }
+        if (type.indexOf("video") !== -1) {
+            ctor = Docs.Create.VideoDocument;
+            if (!options._width) options._width = 600;
+            if (!options._height) options._height = options._width * 2 / 3;
+        }
+        if (type.indexOf("audio") !== -1) {
+            ctor = Docs.Create.AudioDocument;
+        }
+        if (type.indexOf("pdf") !== -1) {
+            ctor = Docs.Create.PdfDocument;
+            if (!options._width) options._width = 400;
+            if (!options._height) options._height = options._width * 1200 / 927;
+        }
+        if (type.indexOf("html") !== -1) {
+            if (path.includes(window.location.hostname)) {
+                const s = path.split('/');
+                const id = s[s.length - 1];
+                return DocServer.GetRefField(id).then(field => {
+                    if (field instanceof Doc) {
+                        const alias = Doc.MakeAlias(field);
+                        alias.x = options.x || 0;
+                        alias.y = options.y || 0;
+                        alias._width = options._width || 300;
+                        alias._height = options._height || options._width || 300;
+                        return alias;
+                    }
+                    return undefined;
+                });
+            }
+            ctor = Docs.Create.WebDocument;
+            options = { ...options, _nativeWidth: 850, _nativeHeight: 962, _width: 500, _height: 566, title: path, };
+        }
+        return ctor ? ctor(path, options) : undefined;
+    }
+
+    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
+        if (Doc.IsBaseProto(doc)) return doc;
+        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
+        const copy = new Doc(undefined, true);
+        cloneMap.set(doc[Id], copy);
+        if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
+        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
+        Object.keys(doc).forEach(key => {
+            if (exclude.includes(key)) return;
+            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+            const field = ProxyField.WithoutProxy(() => doc[key]);
+            const copyObjectField = (field: ObjectField) => {
+                const list = Cast(doc[key], listSpec(Doc));
+                if (list !== undefined && !(list instanceof Promise)) {
+                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => DocUtils.makeClone(d as Doc, cloneMap, rtfs)));
+                } else if (doc[key] instanceof Doc) {
+                    copy[key] = key.includes("layout[") ? undefined : DocUtils.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
+                } else {
+                    copy[key] = ObjectField.MakeCopy(field);
+                    if (field instanceof RichTextField) {
+                        if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
+                            rtfs.push({ copy, key, field });
+                        }
+                    }
+                }
+            };
+            if (key === "proto") {
+                if (doc[key] instanceof Doc) {
+                    copy[key] = DocUtils.makeClone(doc[key]!, cloneMap, rtfs);
+                }
+            } else {
+                if (field instanceof RefField) {
+                    copy[key] = field;
+                } else if (cfield instanceof ComputedField) {
+                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
+                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
+                } else if (field instanceof ObjectField) {
+                    copyObjectField(field);
+                } else if (field instanceof Promise) {
+                    debugger; //This shouldn't happend...
+                } else {
+                    copy[key] = field;
+                }
+            }
+        });
+        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
+        copy.cloneOf = doc;
+        cloneMap.set(doc[Id], copy);
+        return copy;
+    }
+    export function MakeClone(doc: Doc): Doc {
+        const cloneMap = new Map<string, Doc>();
+        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
+        const copy = DocUtils.makeClone(doc, cloneMap, rtfMap);
+        rtfMap.map(({ copy, key, field }) => {
+            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
+                const mapped = cloneMap.get(id);
+                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
+            };
+            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
+                const mapped = cloneMap.get(id);
+                return href + (mapped ? mapped[Id] : id);
+            };
+            const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
+            const re = new RegExp(regex, "g");
+            copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+        });
+        return copy;
+    }
+
     export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void {
         ContextMenu.Instance.addItem({
             description: "Add Note ...",
@@ -1083,6 +1017,119 @@ export namespace DocUtils {
             })) as ContextMenuProps[],
             icon: "eye"
         });
+    }// applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
+    export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
+        const batch = UndoManager.StartBatch("makeCustomViewClicked");
+        runInAction(() => {
+            doc.layoutKey = "layout_" + templateSignature;
+            if (doc[doc.layoutKey] === undefined) {
+                createCustomView(doc, creator, templateSignature, docLayoutTemplate);
+            }
+        });
+        batch.end();
+    }
+    export function findTemplate(templateName: string, type: string, signature: string) {
+        let docLayoutTemplate: Opt<Doc>;
+        const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data);
+        const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data);
+        const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data);
+        const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
+        const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc);
+        // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
+        // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
+        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc));
+        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc));
+        return docLayoutTemplate;
+    }
+    export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
+        const templateName = templateSignature.replace(/\(.*\)/, "");
+        docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature);
+
+        const customName = "layout_" + templateSignature;
+        const _width = NumCast(doc._width);
+        const _height = NumCast(doc._height);
+        const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
+
+        let fieldTemplate: Opt<Doc>;
+        if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
+            fieldTemplate = Docs.Create.TextDocument("", options);
+        } else if (doc.data instanceof PdfField) {
+            fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
+        } else if (doc.data instanceof VideoField) {
+            fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
+        } else if (doc.data instanceof AudioField) {
+            fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
+        } else if (doc.data instanceof ImageField) {
+            fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+        }
+        const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
+
+        fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
+        docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
+    }
+    export function makeCustomView(doc: Doc, custom: boolean, layout: string) {
+        Doc.setNativeView(doc);
+        if (custom) {
+            makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined);
+        }
+    }
+    export function iconify(doc: Doc) {
+        const layoutKey = Cast(doc.layoutKey, "string", null);
+        DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined);
+        if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", "");
+    }
+
+    export function pileup(docList: Doc[], x?: number, y?: number) {
+        let w = 0, h = 0;
+        runInAction(() => {
+            docList.forEach(d => {
+                DocUtils.iconify(d);
+                w = Math.max(d[WidthSym](), w);
+                h = Math.max(d[HeightSym](), h);
+            });
+            h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away.  so this is a fallback hack to try to do something reasonable
+            docList.forEach((d, i) => {
+                d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2;
+                d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2;
+                d.displayTimecode = undefined;  // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+            });
+        });
+        if (x !== undefined && y !== undefined) {
+            const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _LODdisable: true });
+            newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55;
+            newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55;
+            newCollection._width = newCollection._height = 110;
+            //newCollection.borderRounding = "40px";
+            newCollection._jitterRotation = 10;
+            newCollection._backgroundColor = "gray";
+            newCollection._overflow = "visible";
+            return newCollection;
+        }
+    }
+
+
+    export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) {
+        let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey);
+        if (!(optionsCollection instanceof Doc)) {
+            optionsCollection = Docs.Create.StackingDocument([], { title: `${enumeratedFieldKey} field set` }, enumeratedFieldKey);
+            Doc.AddDocToList((Doc.UserDoc().fieldTypes as Doc), "data", optionsCollection as Doc);
+        }
+        const options = optionsCollection as Doc;
+        const targetDoc = doc && Doc.GetProto(Cast(doc.rootDocument, Doc, null) || doc);
+        const docFind = `options.data.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`;
+        targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(docFind + `._backgroundColor || "white"`, undefined, { options }));
+        targetDoc && (targetDoc.color = ComputedField.MakeFunction(docFind + `.color || "black"`, undefined, { options }));
+        targetDoc && (targetDoc.borderRounding = ComputedField.MakeFunction(docFind + `.borderRounding`, undefined, { options }));
+        enumerations.map(enumeration => {
+            const found = DocListCast(options.data).find(d => d.title === enumeration.title);
+            if (found) {
+                found._backgroundColor = enumeration._backgroundColor || found._backgroundColor;
+                found._color = enumeration.color || found._color;
+            } else {
+                Doc.AddDocToList(options, "data", Docs.Create.TextDocument(enumeration.title, enumeration));
+            }
+        });
+        return optionsCollection;
     }
 }
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 496099557..d2614a898 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -2,7 +2,7 @@ import { computed, observable, reaction } from "mobx";
 import * as rp from 'request-promise';
 import { Utils } from "../../Utils";
 import { DocServer } from "../DocServer";
-import { Docs, DocumentOptions } from "../documents/Documents";
+import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
 import { UndoManager } from "./UndoManager";
 import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
 import { List } from "../../fields/List";
@@ -221,7 +221,7 @@ export class CurrentUserUtils {
         ];
         if (doc.fieldTypes === undefined) {
             doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" });
-            Doc.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues);
+            DocUtils.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues);
         }
 
         if (doc["template-notes"] === undefined) {
@@ -612,7 +612,7 @@ export class CurrentUserUtils {
     static setupDockedButtons(doc: Doc) {
         if (doc["dockedBtn-pen"] === undefined) {
             doc["dockedBtn-pen"] = CurrentUserUtils.ficon({
-                onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this, this.inkWidth, this.backgroundColor)"),
+                onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)"),
                 author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen,  this)`), activePen: doc
             });
         }
@@ -692,6 +692,8 @@ export class CurrentUserUtils {
         doc.title = Doc.CurrentUserEmail;
         doc.activePen = doc;
         doc.inkColor = StrCast(doc.backgroundColor, "rgb(0, 0, 0)");
+        doc.inkWidth = StrCast(doc.inkWidth, "1");
+        doc.inkBezier = StrCast(doc.inkBezier, "");
         doc.fontSize = NumCast(doc.fontSize, 12);
         doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // 
         doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 2a9c1633a..e7a14f9c5 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -210,7 +210,7 @@ export namespace DragManager {
             e.docDragData && (e.docDragData.droppedDocuments =
                 dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) :
                     dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) :
-                        dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeClone(d) : d)
+                        dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeDelegate(d) : d)
             );
             e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) =>
                 (dragData?.removeDropProperties || []).concat(Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), [])).map(prop => drop[prop] = undefined)
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 1e8f07049..4e5cde558 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -123,10 +123,10 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             }
             const { accessPaths, exifData } = result;
             const path = Utils.prepend(accessPaths.agnostic.client);
-            const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
+            const document = await Doc.Get.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
-                Doc.GetProto(document).exif = error || Docs.Get.FromJson({ data });
+                Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
                 docs.push(document);
             }
         }));
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 072e5f58a..0d12b39b8 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -20,7 +20,7 @@ export namespace ImageUtils {
             nativeHeight,
             exifData: { error, data }
         } = await Networking.PostToServer("/inspectImage", { source });
-        document.exif = error || Docs.Get.FromJson({ data });
+        document.exif = error || Doc.Get.FromJson({ data });
         const proto = Doc.GetProto(document);
         proto["data-nativeWidth"] = nativeWidth;
         proto["data-nativeHeight"] = nativeHeight;
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index ab1ccb25a..3948611f0 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,7 +1,6 @@
 import React = require("react");
 import * as beziercurve from 'bezier-curve';
 import * as fitCurve from 'fit-curve';
-import InkOptionsMenu from "../views/collections/collectionFreeForm/InkOptionsMenu";
 
 export namespace InteractionUtils {
     export const MOUSETYPE = "mouse";
@@ -90,24 +89,12 @@ export namespace InteractionUtils {
         return myTouches;
     }
 
-    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string) {
+    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string, scalex: number, scaley: number, shape: string) {
         var pts = "";
-        var shape = "";
-        if (InkOptionsMenu.Instance._circle) {
-            shape = "circle";
-        } else if (InkOptionsMenu.Instance._rectangle) {
-            shape = "rectangle";
-        } else if (InkOptionsMenu.Instance._triangle) {
-            shape = "triangle";
-        } else if (InkOptionsMenu.Instance._arrow) {
-            shape = "arrow";
-        } else if (InkOptionsMenu.Instance._line) {
-            shape = "line";
-        }
-        if (shape !== "") {
+        if (shape) {
             //if any of the shape are true
             const shapePts = makePolygon(shape, points);
-            pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+            pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
         }
         else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) {
             //pointer is up (first and last points are the same)
@@ -124,10 +111,10 @@ export namespace InteractionUtils {
                     newPts.push({ X: point[0], Y: point[1] });
                 }
             }
-            pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+            pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
         } else {
             //in the middle of drawing
-            pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+            pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
         }
         return (
             <polyline
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index f810361c6..9a6121d20 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -1,5 +1,4 @@
 import React = require("react");
-import { observer } from "mobx-react";
 import { observable, action } from "mobx";
 import "./AntimodeMenu.scss";
 
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 1ba9fcc32..c398b2633 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -2,8 +2,6 @@ import { Doc, Opt, DataSym, DocListCast } from '../../fields/Doc';
 import { Touchable } from './Touchable';
 import { computed, action, observable } from 'mobx';
 import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
-import { listSpec } from '../../fields/Schema';
-import { InkingControl } from './InkingControl';
 import { InkTool } from '../../fields/InkField';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { List } from '../../fields/List';
@@ -59,7 +57,7 @@ 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;
 
-        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
+        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);//  && !Doc.selectedTool;  // bcz: inking state shouldn't affect static tools 
         protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     }
     return Component;
@@ -148,9 +146,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
         }
 
         whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
-        active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+        active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
             (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0 || BoolCast((this.layoutDoc as any).forceActive)) ? true : false)
-        annotationsActive = (outsideReaction?: boolean) => (InkingControl.Instance.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) ||
+        annotationsActive = (outsideReaction?: boolean) => (Doc.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) ||
             (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
     }
     return Component;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 5714970c1..4ea75d7d7 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,38 +1,30 @@
 import React = require("react");
-import { Touchable } from "./Touchable";
+import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import "./GestureOverlay.scss";
-import { computed, observable, action, runInAction, IReactionDisposer, reaction, flow, trace } from "mobx";
+import { Doc } from "../../fields/Doc";
+import { InkData, InkTool } from "../../fields/InkField";
+import { Cast, FieldValue, NumCast } from "../../fields/Types";
+import MobileInkOverlay from "../../mobile/MobileInkOverlay";
+import MobileInterface from "../../mobile/MobileInterface";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
+import { MobileInkOverlayContent } from "../../server/Message";
+import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from "../../Utils";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { DocServer } from "../DocServer";
+import { DocUtils } from "../documents/Documents";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { InteractionUtils } from "../util/InteractionUtils";
-import { InkingControl } from "./InkingControl";
-import { InkTool, InkData } from "../../fields/InkField";
-import { Doc } from "../../fields/Doc";
 import { LinkManager } from "../util/LinkManager";
-import { DocUtils, Docs } from "../documents/Documents";
-import { undoBatch } from "../util/UndoManager";
 import { Scripting } from "../util/Scripting";
-import { FieldValue, Cast, NumCast, BoolCast } from "../../fields/Types";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import HorizontalPalette from "./Palette";
-import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange, returnZero } from "../../Utils";
-import { DocumentView } from "./nodes/DocumentView";
 import { Transform } from "../util/Transform";
-import { DocumentContentsView } from "./nodes/DocumentContentsView";
-import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { DocServer } from "../DocServer";
-import htmlToImage from "html-to-image";
-import { ScriptField } from "../../fields/ScriptField";
-import { listSpec } from "../../fields/Schema";
-import { List } from "../../fields/List";
-import { CollectionViewType } from "./collections/CollectionView";
-import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
-import MobileInterface from "../../mobile/MobileInterface";
-import { MobileInkOverlayContent } from "../../server/Message";
-import MobileInkOverlay from "../../mobile/MobileInkOverlay";
+import "./GestureOverlay.scss";
+import { InkingControl } from "./InkingControl";
+import { DocumentView } from "./nodes/DocumentView";
 import { RadialMenu } from "./nodes/RadialMenu";
-import { SelectionManager } from "../util/SelectionManager";
-import InkOptionsMenu from "./collections/collectionFreeForm/InkOptionsMenu";
+import HorizontalPalette from "./Palette";
+import { Touchable } from "./Touchable";
+import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
+import { InkingStroke } from "./InkingStroke";
 
 
 @observer
@@ -500,7 +492,7 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             e.stopPropagation();
             e.preventDefault();
@@ -514,7 +506,7 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerMove = (e: PointerEvent) => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             e.stopPropagation();
             e.preventDefault();
@@ -585,12 +577,11 @@ export default class GestureOverlay extends Touchable {
             //push first points to so interactionUtil knows pointer is up
             this._points.push({ X: this._points[0].X, Y: this._points[0].Y });
             if (MobileInterface.Instance && MobileInterface.Instance.drawingInk) {
-                const { selectedColor, selectedWidth } = InkingControl.Instance;
                 DocServer.Mobile.dispatchGesturePoints({
                     points: this._points,
                     bounds: B,
-                    color: selectedColor,
-                    width: selectedWidth
+                    color: InkingStroke.InkColor,
+                    width: InkingStroke.InkWidth
                 });
             }
 
@@ -633,21 +624,11 @@ export default class GestureOverlay extends Touchable {
                 }
             }
             //if any of the shape is activated in the InkOptionsMenu
-            else if (InkOptionsMenu.Instance._circle || InkOptionsMenu.Instance._triangle || InkOptionsMenu.Instance._rectangle || InkOptionsMenu.Instance._line || InkOptionsMenu.Instance._arrow) {
-                if (InkOptionsMenu.Instance._circle) {
-                    this.makePolygon("circle", false);
-                } else if (InkOptionsMenu.Instance._triangle) {
-                    this.makePolygon("triangle", false);
-                } else if (InkOptionsMenu.Instance._rectangle) {
-                    this.makePolygon("rectangle", false);
-                } else if (InkOptionsMenu.Instance._line) {
-                    this.makePolygon("line", false);
-                } else if (InkOptionsMenu.Instance._arrow) {
-                    this.makePolygon("arrow", false);
-                }
+            else if (InkingStroke.InkShape) {
+                this.makePolygon(InkingStroke.InkShape, false);
                 this.dispatchGesture(GestureUtils.Gestures.Stroke);
                 this._points = [];
-                InkOptionsMenu.Instance.allFalse();
+                InkingStroke.InkShape = "";
             }
             // if we're not drawing in a toolglass try to recognize as gesture
             else {
@@ -827,11 +808,11 @@ export default class GestureOverlay extends Touchable {
             [this._strokes.map(l => {
                 const b = this.getBounds(l);
                 return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                    {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier)}
+                    {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)}
             </svg>]
         ];
     }
@@ -921,15 +902,15 @@ Scripting.addGlobal(function setToolglass(tool: any) {
 });
 Scripting.addGlobal(function setPen(width: any, color: any) {
     runInAction(() => {
-        GestureOverlay.Instance.SavedColor = InkingControl.Instance.selectedColor;
-        InkingControl.Instance.updateSelectedColor(color);
-        GestureOverlay.Instance.SavedWidth = InkingControl.Instance.selectedWidth;
+        GestureOverlay.Instance.SavedColor = InkingStroke.InkColor;
+        InkingControl.Instance.switchColor(color);
+        GestureOverlay.Instance.SavedWidth = InkingStroke.InkWidth;
         InkingControl.Instance.switchWidth(width);
     });
 });
 Scripting.addGlobal(function resetPen() {
     runInAction(() => {
-        InkingControl.Instance.updateSelectedColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)");
+        InkingControl.Instance.switchColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)");
         InkingControl.Instance.switchWidth(GestureOverlay.Instance.SavedWidth ?? "2");
     });
 });
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 83109db1c..349bc6ffc 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,22 +1,12 @@
-import { action, computed, observable } from "mobx";
-import { ColorState } from 'react-color';
+import { action, observable } from "mobx";
 import { Doc } from "../../fields/Doc";
 import { InkTool } from "../../fields/InkField";
-import { FieldValue, NumCast, StrCast } from "../../fields/Types";
 import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { Scripting } from "../util/Scripting";
-import { SelectionManager } from "../util/SelectionManager";
-import { undoBatch } from "../util/UndoManager";
-import GestureOverlay from "./GestureOverlay";
-import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
-import InkOptionsMenu from "./collections/collectionFreeForm/InkOptionsMenu";
+import { InkingStroke } from "./InkingStroke";
 
 export class InkingControl {
     @observable static Instance: InkingControl;
-    @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; }
-    @computed private get _selectedColor(): string { return CurrentUserUtils.ActivePen ? FieldValue(StrCast(CurrentUserUtils.ActivePen.backgroundColor)) ?? "rgb(0, 0, 0)" : "rgb(0, 0, 0)"; }
-    @computed private get _selectedWidth(): string { return FieldValue(StrCast(Doc.UserDoc().inkWidth)) ?? "2"; }
-    @computed private get _selectedBezier(): string { return FieldValue(StrCast(Doc.UserDoc().inkBezier)) ?? "2"; }
     @observable public _open: boolean = false;
     constructor() {
         InkingControl.Instance = this;
@@ -26,110 +16,27 @@ export class InkingControl {
         // this._selectedTool = tool;
         Doc.UserDoc().inkTool = tool;
     });
-    decimalToHexString(number: number) {
-        if (number < 0) {
-            number = 0xFFFFFFFF + number + 1;
-        }
-        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
-    }
 
-    @action
-    inkOptionsMenuChangeColor = (color: string) => {
-        const col: ColorState = {
-            hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
-            rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
-        };
-        this.switchColor(col);
-        InkOptionsMenu.Instance._colorBt = false;
-    }
-
-    @undoBatch
-    switchColor = action((color: ColorState): void => {
-        Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ?
-            color.hex + (color.rgb.a ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
-        InkOptionsMenu.Instance._color = StrCast(Doc.UserDoc().backgroundColor);
-        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = color.hex);
-
-        if (InkingControl.Instance.selectedTool === InkTool.None) {
-            const selected = SelectionManager.SelectedDocuments();
-            selected.map(view => {
-                const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
-                    view.props.Document.layout instanceof Doc ? view.props.Document.layout :
-                        view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document);
-                if (targetDoc) {
-                    if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) {
-                        Doc.Layout(view.props.Document).color = Doc.UserDoc().bacgroundColor;
-                    } else {
-                        Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific.  'backgroundColor' would apply to all templates, but has no UI at the moment
-                    }
-                }
-            });
-        }
-    });
     @action
     switchWidth = (width: string): void => {
-        // this._selectedWidth = width;
         if (!isNaN(parseInt(width))) {
-            Doc.UserDoc().inkWidth = width;
+            CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkWidth = InkingStroke.InkWidth = width);
         }
-        InkOptionsMenu.Instance._widthBt = false;
     }
 
     @action
     switchBezier = (bezier: string): void => {
-        if (!isNaN(parseInt(bezier))) {
-            Doc.UserDoc().inkBezier = bezier;
-        }
+        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkBezier = InkingStroke.InkBezierApprox = isNaN(parseInt(bezier)) ? "" : bezier);
     }
 
     @action
-    inkOptionsMenuChangeBezier = (e: React.PointerEvent): void => {
-        if (InkOptionsMenu.Instance._bezierBt === true) {
-            Doc.UserDoc().inkBezier = "300";
-        } else {
-            Doc.UserDoc().inkBezier = "0";
-        }
-    }
-
-    @computed
-    get selectedTool() {
-        return this._selectedTool;
-    }
-
-    @computed
-    get selectedColor() {
-        return this._selectedColor;
-    }
-
-    @action
-    updateSelectedColor(value: string) {
-        // this._selectedColor = value;
-        Doc.UserDoc().inkColor = value;
-    }
-
-    @computed
-    get selectedWidth() {
-        return this._selectedWidth;
-    }
-
-    @computed
-    get selectedBezier() {
-        return this._selectedBezier;
+    switchColor(value: string) {
+        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = InkingStroke.InkColor = value);
     }
 }
-Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) {
-    InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color);
-    //setup InkOptionsMenu(change jumpto value if necessary.Currenlty hardcoded to 300,300)
-    pen ? InkOptionsMenu.Instance.jumpTo(300, 300) : InkOptionsMenu.Instance.fadeOut(true);
-    InkOptionsMenu.Instance.changeColor = InkingControl.Instance.inkOptionsMenuChangeColor;
-    InkOptionsMenu.Instance.changeBezier = InkingControl.Instance.inkOptionsMenuChangeBezier;
-    InkOptionsMenu.Instance.changeWidth = InkingControl.Instance.switchWidth;
-    InkOptionsMenu.Instance._widthSelected = width;
-    InkOptionsMenu.Instance._color = color;
-});
-Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
+Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.switchColor(color); });
 Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
 Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); });
 Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
 Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
-Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); });
\ No newline at end of file
+Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.switchColor(color); });
\ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 3dc0a5b20..78d729eee 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,19 +1,19 @@
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
+import { observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { documentSchema } from "../../fields/documentSchemas";
 import { InkData, InkField, InkTool } from "../../fields/InkField";
 import { makeInterface } from "../../fields/Schema";
-import { Cast, StrCast, NumCast } from "../../fields/Types";
+import { Cast, StrCast } from "../../fields/Types";
+import { TraceMobx } from "../../fields/util";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { InteractionUtils } from "../util/InteractionUtils";
+import { ContextMenu } from "./ContextMenu";
 import { ViewBoxBaseComponent } from "./DocComponent";
-import { InkingControl } from "./InkingControl";
 import "./InkingStroke.scss";
 import { FieldView, FieldViewProps } from "./nodes/FieldView";
 import React = require("react");
-import { TraceMobx } from "../../fields/util";
-import { InteractionUtils } from "../util/InteractionUtils";
-import { ContextMenu } from "./ContextMenu";
-import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
-import { library } from "@fortawesome/fontawesome-svg-core";
 
 library.add(faPaintBrush);
 
@@ -23,6 +23,22 @@ const InkDocument = makeInterface(documentSchema);
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
+    @observable public static InkColor: string;
+    @observable public static InkWidth: string;
+    @observable public static InkBezierApprox: string;
+    @observable public static InkShape: string;
+
+    constructor(props: any) {
+        super(props);
+        if (InkingStroke.InkBezierApprox === undefined) {
+            runInAction(() => {
+                InkingStroke.InkBezierApprox = "";
+                InkingStroke.InkWidth = "1";
+                InkingStroke.InkColor = "black";
+                InkingStroke.InkShape = "";
+            });
+        }
+    }
 
     private analyzeStrokes = () => {
         const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
@@ -38,22 +54,19 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const top = Math.min(...ys);
         const right = Math.max(...xs);
         const bottom = Math.max(...ys);
-        const points = InteractionUtils.CreatePolyline(data, left, top,
-            StrCast(this.layoutDoc.color, InkingControl.Instance.selectedColor),
-            StrCast(this.layoutDoc.strokeWidth, InkingControl.Instance.selectedWidth),
-            StrCast(this.layoutDoc.strokeBezier, InkingControl.Instance.selectedBezier));
         const width = right - left;
         const height = bottom - top;
         const scaleX = this.props.PanelWidth() / width;
         const scaleY = this.props.PanelHeight() / height;
+        const points = InteractionUtils.CreatePolyline(data, left, top,
+            StrCast(this.layoutDoc.color, InkingStroke.InkColor || "black"),
+            StrCast(this.layoutDoc.strokeWidth, InkingStroke.InkWidth || "1"),
+            StrCast(this.layoutDoc.strokeBezier, InkingStroke.InkBezierApprox || ""), scaleX, scaleY, "");
         return (
             <svg className="inkingStroke"
                 width={width}
                 height={height}
-                style={{
-                    transform: `scale(${scaleX}, ${scaleY})`,
-                    mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
-                }}
+                style={{ mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset" }}
                 onContextMenu={() => {
                     ContextMenu.Instance.addItem({
                         description: "Analyze Stroke",
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index dd65681d4..33150ab7c 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import "normalize.css";
 import * as React from 'react';
 import "./PreviewCursor.scss";
-import { Docs } from '../documents/Documents';
+import { Docs, DocUtils } from '../documents/Documents';
 import { Doc } from '../../fields/Doc';
 import { Transform } from "../util/Transform";
 import { DocServer } from '../DocServer';
@@ -65,7 +65,7 @@ export class PreviewCursor extends React.Component<{}> {
                         count++;
                         if (doc instanceof Doc) {
                             i === 1 && (first = doc);
-                            const alias = Doc.MakeClone(doc);
+                            const alias = DocUtils.MakeClone(doc);
                             const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx;
                             const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty;
                             alias.x = newPoint[0] + deltaX;
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 10d023d83..5e48d5ffb 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -1,8 +1,6 @@
 import * as React from 'react';
 import { action } from 'mobx';
 import { InteractionUtils } from '../util/InteractionUtils';
-import { SelectionManager } from '../util/SelectionManager';
-import { RadialMenu } from './nodes/RadialMenu';
 
 const HOLD_DURATION = 1000;
 
diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx
index a0b7cd8a8..cfec3a6bc 100644
--- a/src/client/views/collections/CollectionMapView.tsx
+++ b/src/client/views/collections/CollectionMapView.tsx
@@ -42,7 +42,7 @@ const query = async (data: string | google.maps.LatLngLiteral) => {
 };
 
 @observer
-class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) {
+export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) {
 
     private _cancelAddrReq = new Map<string, boolean>();
     private _cancelLocReq = new Map<string, boolean>();
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 423eb1d90..b60419258 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -6,27 +6,19 @@ import { Id } from "../../../fields/FieldSymbols";
 import { List } from "../../../fields/List";
 import { listSpec } from "../../../fields/Schema";
 import { ScriptField } from "../../../fields/ScriptField";
+import { WebField } from "../../../fields/URLField";
 import { Cast, ScriptCast, NumCast } from "../../../fields/Types";
 import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { Upload } from "../../../server/SharedMediaTypes";
 import { Utils } from "../../../Utils";
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { DocServer } from "../../DocServer";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
 import { Networking } from "../../Network";
-import { DragManager, dropActionType } from "../../util/DragManager";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { InteractionUtils } from "../../util/InteractionUtils";
 import { undoBatch, UndoManager } from "../../util/UndoManager";
 import { DocComponent } from "../DocComponent";
 import { FieldViewProps } from "../nodes/FieldView";
-import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
-import { CollectionView } from "./CollectionView";
 import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
-import { WebField } from "../../../fields/URLField";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc | Doc[]) => boolean;
@@ -405,7 +397,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                     const stringContents = await new Promise<string>(resolve => item.getAsString(resolve));
                     const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"];
                     if (type) {
-                        const doc = await Docs.Get.DocumentFromType(type, stringContents, options);
+                        const doc = await DocUtils.DocumentFromType(type, stringContents, options);
                         doc && generatedDocuments.push(doc);
                     }
                 }
@@ -435,7 +427,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                 }
                 const full = { ...options, _width: 400, title: name };
                 const pathname = Utils.prepend(result.accessPaths.agnostic.client);
-                const doc = await Docs.Get.DocumentFromType(type, pathname, full);
+                const doc = await DocUtils.DocumentFromType(type, pathname, full);
                 if (!doc) {
                     continue;
                 }
@@ -450,9 +442,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                 generatedDocuments.push(doc);
             }
             if (generatedDocuments.length) {
-                const set = generatedDocuments.length > 1 && generatedDocuments.map(d => Doc.iconify(d));
+                const set = generatedDocuments.length > 1 && generatedDocuments.map(d => DocUtils.iconify(d));
                 if (set) {
-                    addDocument(Doc.pileup(generatedDocuments, options.x!, options.y!)!);
+                    addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!);
                 } else {
                     generatedDocuments.forEach(addDocument);
                 }
@@ -469,3 +461,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
     return CollectionSubView;
 }
 
+import { DragManager, dropActionType } from "../../util/DragManager";
+import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
+import { CurrentUserUtils } from "../../util/CurrentUserUtils";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
+import { CollectionView } from "./CollectionView";
+import { SelectionManager } from "../../util/SelectionManager";
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index b2e1c0f73..e891c4274 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -757,7 +757,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
         const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
         onClicks.push({
-            description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
+            description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
         });
         !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
     }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index fecba32c5..873f61331 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -14,36 +14,16 @@ import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Ty
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { Utils, setupMoveUpEvents, returnFalse, returnZero, emptyPath, emptyFunction, returnOne } from '../../../Utils';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { ContextMenu } from "../ContextMenu";
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { ScriptBox } from '../ScriptBox';
 import { Touchable } from '../Touchable';
-import { CollectionCarouselView } from './CollectionCarouselView';
-import { CollectionDockingView } from "./CollectionDockingView";
-import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
-import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
-import { CollectionLinearView } from './CollectionLinearView';
-import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
-import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
-import { CollectionSchemaView } from "./CollectionSchemaView";
-import { CollectionStackingView } from './CollectionStackingView';
-import { CollectionStaffView } from './CollectionStaffView';
-import { SubCollectionViewProps } from './CollectionSubView';
-import { CollectionTimeView } from './CollectionTimeView';
-import { CollectionTreeView } from "./CollectionTreeView";
-import './CollectionView.scss';
-import { CollectionViewBaseChrome } from './CollectionViewChromes';
-import { CurrentUserUtils } from '../../util/CurrentUserUtils';
 import { Id } from '../../../fields/FieldSymbols';
 import { listSpec } from '../../../fields/Schema';
-import { Docs } from '../../documents/Documents';
 import { ScriptField, ComputedField } from '../../../fields/ScriptField';
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { ObjectField } from '../../../fields/ObjectField';
-import CollectionMapView from './CollectionMapView';
-import { CollectionPileView } from './CollectionPileView';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -519,4 +499,24 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
             {this.facetWidth() < 10 ? (null) : this.filterView}
         </div>);
     }
-}
\ No newline at end of file
+}
+import { SubCollectionViewProps } from './CollectionSubView';
+import { CollectionCarouselView } from './CollectionCarouselView';
+import { CollectionDockingView } from "./CollectionDockingView";
+import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionLinearView } from './CollectionLinearView';
+import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
+import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
+import { CollectionSchemaView } from "./CollectionSchemaView";
+import { CollectionStackingView } from './CollectionStackingView';
+import { CollectionStaffView } from './CollectionStaffView';
+import { CollectionTimeView } from './CollectionTimeView';
+import { CollectionTreeView } from "./CollectionTreeView";
+import { CollectionMapView } from './CollectionMapView';
+import { CollectionPileView } from './CollectionPileView';
+import './CollectionView.scss';
+import { CollectionViewBaseChrome } from './CollectionViewChromes';
+import { CurrentUserUtils } from '../../util/CurrentUserUtils';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index fb7784b58..576c0c560 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -29,7 +29,6 @@ import { undoBatch, UndoManager } from "../../../util/UndoManager";
 import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
-import { InkingControl } from "../../InkingControl";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
@@ -46,7 +45,7 @@ import React = require("react");
 import { CollectionViewType } from "../CollectionView";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { SnappingManager } from "../../../util/SnappingManager";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { InkingStroke } from "../../InkingStroke";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -391,7 +390,7 @@ 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) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
             return;
         }
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
@@ -408,7 +407,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             document.addEventListener("pointermove", this.onPointerMove);
             document.addEventListener("pointerup", this.onPointerUp);
             // if not using a pen and in no ink mode
-            if (InkingControl.Instance.selectedTool === InkTool.None) {
+            if (Doc.selectedTool === InkTool.None) {
                 this._downX = this._lastX = e.pageX;
                 this._downY = this._lastY = e.pageY;
             }
@@ -432,13 +431,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     this.addMoveListeners();
                     this.removeEndListeners();
                     this.addEndListeners();
-                    // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) {
+                    // if (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen) {
                     //     e.stopPropagation();
                     //     e.preventDefault();
                     //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
                     //     this._points.push({ X: point[0], Y: point[1] });
                     // }
-                    if (InkingControl.Instance.selectedTool === InkTool.None) {
+                    if (Doc.selectedTool === InkTool.None) {
                         this._lastX = pt.pageX;
                         this._lastY = pt.pageY;
                         e.preventDefault();
@@ -458,7 +457,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+                const inkDoc = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
@@ -619,7 +618,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             return;
         }
         if (!e.cancelBubble) {
-            const selectedTool = InkingControl.Instance.selectedTool;
+            const selectedTool = Doc.selectedTool;
             if (selectedTool === InkTool.None) {
                 if (this._hitCluster && this.tryDragCluster(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
@@ -641,7 +640,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
             const pt = myTouches[0];
             if (pt) {
-                if (InkingControl.Instance.selectedTool === InkTool.None) {
+                if (Doc.selectedTool === InkTool.None) {
                     if (this._hitCluster && this.tryDragCluster(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();
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
index 44488cbcf..b35c9682e 100644
--- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
@@ -1,269 +1,117 @@
 import React = require("react");
 import AntimodeMenu from "../../AntimodeMenu";
 import { observer } from "mobx-react";
-import { unimplementedFunction } from "../../../../Utils";
 import { observable, action } from "mobx";
 import "./InkOptionsMenu.scss";
-
+import { InkingStroke } from "../../InkingStroke";
+import { Scripting } from "../../../util/Scripting";
+import { InkTool } from "../../../../fields/InkField";
+import { InkingControl } from "../../InkingControl";
+import { StrCast } from "../../../../fields/Types";
+import { ColorState } from "react-color";
+import { ColorBox } from "../../nodes/ColorBox";
 
 @observer
 export default class InkOptionsMenu extends AntimodeMenu {
     static Instance: InkOptionsMenu;
-    public changeColor: (color: string) => void = unimplementedFunction;
-    public changeBezier: (e: React.PointerEvent) => void = unimplementedFunction;
-    public changeWidth: (color: string) => void = unimplementedFunction;
-
-    private _palette: (string)[];
-    private _width: (string)[];
-
-
-    public _circle: boolean;
-    public _triangle: boolean;
-    public _rectangle: boolean;
-    public _arrow: boolean;
-    public _line: boolean;
-    public _widthSelected: string;
-
-    @observable public _circleBt: boolean;
-    @observable public _triangleBt: boolean;
-    @observable public _rectangleBt: boolean;
-    @observable public _arrowBt: boolean;
-    @observable public _lineBt: boolean;
-    @observable public _colorBt: boolean;
-    @observable public _color: string;
-    @observable public _bezierBt: boolean;
-    @observable public _widthBt: boolean;
 
+    private _palette = ["D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF"];
+    private _width = ["1", "5", "10", "100", "200", "300"];
+    private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"];
+    private _icons = ["O", "∆", "ロ", "➜", "-"];
 
+    @observable _colorBtn = false;
+    @observable _widthBtn = false;
 
     constructor(props: Readonly<{}>) {
         super(props);
         InkOptionsMenu.Instance = this;
-        this._canFade = false;
-
-        this._circle = false;
-        this._triangle = false;
-        this._rectangle = false;
-        this._arrow = false;
-        this._line = false;
-        this._circleBt = false;
-        this._triangleBt = false;
-        this._rectangleBt = false;
-        this._arrowBt = false;
-        this._lineBt = false;
-        this._colorBt = false;
-        this._bezierBt = false;
-        this._widthBt = false;
-
-        this._color = "";
-        this._widthSelected = "";
-
-
-        this._palette = [
-            "D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF",
-        ];
-
-        this._width = [
-            "1", "5", "10", "100", "200", "300"
-        ];
-
-    }
-
-
-
-    drag = (e: React.PointerEvent) => {
-        this.dragStart(e);
+        this._canFade = false; // don't let the inking menu fade away
     }
 
-
-
-
-
-    @action
-    toggleCircle = (e: React.PointerEvent) => {
-        const curr = this._circle;
-        this.allFalse();
-        curr ? this._circle = false : this._circle = true;
-        this._circleBt = this._circle;
-    }
-    @action
-    toggleTriangle = (e: React.PointerEvent) => {
-        const curr = this._triangle;
-        this.allFalse();
-        curr ? this._triangle = false : this._triangle = true;
-        this._triangleBt = this._triangle;
-    }
-    @action
-    toggleRectangle = (e: React.PointerEvent) => {
-        const curr = this._rectangle;
-        this.allFalse();
-        curr ? this._rectangle = false : this._rectangle = true;
-        this._rectangleBt = this._rectangle;
-    }
-    @action
-    toggleArrow = (e: React.PointerEvent) => {
-        const curr = this._arrow;
-        this.allFalse();
-        curr ? this._arrow = false : this._arrow = true;
-        this._arrowBt = this._arrow;
-    }
     @action
-    toggleLine = (e: React.PointerEvent) => {
-        const curr = this._line;
-        this.allFalse();
-        curr ? this._line = false : this._line = true;
-        this._lineBt = this._line;
+    changeColor = (color: string) => {
+        const col: ColorState = {
+            hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
+            rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
+        };
+        ColorBox.switchColor(col);
     }
 
     @action
-    changeBezierClick = (e: React.PointerEvent) => {
-        const curr = this._bezierBt;
-        this.allFalse();
-        curr ? this._bezierBt = false : this._bezierBt = true;
-        this.changeBezier(e);
-    }
-
-    @action
-    changeWidthClick = (e: React.PointerEvent) => {
-        this._widthBt ? this._widthBt = false : this._widthBt = true;
-    }
-    @action
-    changeColorClick = (e: React.PointerEvent) => {
-        this._colorBt ? this._colorBt = false : this._colorBt = true;
-    }
-
-    allFalse = () => {
-        this._circle = false;
-        this._triangle = false;
-        this._rectangle = false;
-        this._arrow = false;
-        this._line = false;
-        this._circleBt = false;
-        this._triangleBt = false;
-        this._rectangleBt = false;
-        this._arrowBt = false;
-        this._lineBt = false;
-        this._bezierBt = false;
+    changeBezier = (e: React.PointerEvent): void => {
+        InkingControl.Instance.switchBezier(!InkingStroke.InkBezierApprox ? "300" : "");
     }
 
     render() {
-        var widthPicker;
-        if (this._widthBt) {
+        var widthPicker = <button
+            className="antimodeMenu-button"
+            key="width"
+            onPointerDown={action(e => this._widthBtn = !this._widthBtn)}
+            style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
+            W
+        </button>;
+        if (this._widthBtn) {
             widthPicker = <div className="btn2-group">
-                <button
-                    className="antimodeMenu-button"
-                    key="width"
-                    onPointerDown={this.changeWidthClick}
-                    style={this._widthBt ? { backgroundColor: "121212" } : {}}>
-                    W
-                </button>
+                {widthPicker}
                 {this._width.map(wid => {
                     return <button
                         className="antimodeMenu-button"
                         key={wid}
-                        onPointerDown={() => this.changeWidth(wid)}
-                        style={this._colorBt ? { backgroundColor: "121212" } : {}}>
+                        onPointerDown={action(() => { InkingControl.Instance.switchWidth(wid); this._widthBtn = false; })}
+                        style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
                         {wid}
                     </button>;
-
                 })}
             </div>;
-        } else {
-            widthPicker = <button
-                className="antimodeMenu-button"
-                key="width"
-                onPointerDown={this.changeWidthClick}
-                style={this._widthBt ? { backgroundColor: "121212" } : {}}>
-                W
-        </button>;
         }
 
-        var colorPicker;
-        if (this._colorBt) {
+        var colorPicker = <button
+            className="antimodeMenu-button"
+            key="color"
+            title="colorChanger"
+            onPointerDown={action(e => this._colorBtn = !this._colorBtn)}
+            style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
+            <div className="color-preview" style={{ backgroundColor: InkingStroke.InkColor ?? "121212" }}></div>
+        </button>;
+        if (this._colorBtn) {
             colorPicker = <div className="btn-group">
-                <button
-                    className="antimodeMenu-button"
-                    key="color"
-                    onPointerDown={this.changeColorClick}
-                    style={this._colorBt ? { backgroundColor: "121212" } : {}}>
-                    <div className="color-preview" style={this._color === "" ? { backgroundColor: "121212" } : { backgroundColor: this._color }}></div>
-                </button>
+                {colorPicker}
                 {this._palette.map(color => {
                     return <button
                         className="antimodeMenu-button"
                         key={color}
-                        onPointerDown={() => this.changeColor(color)}
-                        style={this._colorBt ? { backgroundColor: "121212" } : {}}>
+                        onPointerDown={action(() => { this.changeColor(color); this._colorBtn = false; })}
+                        style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
                         <div className="color-preview" style={{ backgroundColor: color }}></div>
                     </button>;
                 })}
             </div>;
-        } else {
-            colorPicker = <button
-                className="antimodeMenu-button"
-                title="colorChanger"
-                key="color"
-                onPointerDown={this.changeColorClick}
-                style={this._colorBt ? { backgroundColor: "121212" } : {}}>
-                <div className="color-preview" style={this._color === "" ? { backgroundColor: "121212" } : { backgroundColor: this._color }}></div>
-            </button>;
         }
 
-
         const buttons = [
-            <button
-                className="antimodeMenu-button"
+            <button className="antimodeMenu-button"
                 title="Drag"
                 key="drag"
-                onPointerDown={this.drag}>
+                onPointerDown={e => this.dragStart(e)}>
                 ✜
             </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Circle"
-                key="circle"
-                onPointerDown={this.toggleCircle}
-                style={this._circleBt ? { backgroundColor: "121212" } : {}}>
-                O
-            </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Traingle"
-                key="triangle"
-                onPointerDown={this.toggleTriangle}
-                style={this._triangleBt ? { backgroundColor: "121212" } : {}}>
-                ∆
-            </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Rectangle"
-                key="rectangle"
-                onPointerDown={this.toggleRectangle}
-                style={this._rectangleBt ? { backgroundColor: "121212" } : {}}>
-                ロ
-            </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Arrow"
-                key="arrow"
-                onPointerDown={this.toggleArrow}
-                style={this._arrowBt ? { backgroundColor: "121212" } : {}}>
-                ➜
-                </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Line"
-                key="line"
-                onPointerDown={this.toggleLine}
-                style={this._lineBt ? { backgroundColor: "121212" } : {}}>
-                –
-            </button>,
+            <>
+                {this._buttons.map((btn, i) => <button
+                    className="antimodeMenu-button"
+                    title={`Draw ${btn}`}
+                    key={btn}
+                    onPointerDown={action(e => InkingStroke.InkShape = btn)}
+                    style={btn === InkingStroke.InkShape ? { backgroundColor: "121212" } : {}}>
+                    {this._icons[i]}
+                </button>)},
+            </>,
             <button
                 className="antimodeMenu-button"
                 title="Bezier changer"
                 key="bezier"
-                onPointerDown={this.changeBezierClick}
-                style={this._bezierBt ? { backgroundColor: "121212" } : {}}>
+                onPointerDown={e => this.changeBezier(e)}
+                style={InkingStroke.InkBezierApprox ? { backgroundColor: "121212" } : {}}>
                 B
             </button>,
             widthPicker,
@@ -271,4 +119,15 @@ export default class InkOptionsMenu extends AntimodeMenu {
         ];
         return this.getElement(buttons);
     }
-}
\ No newline at end of file
+}
+Scripting.addGlobal(function activatePen(pen: any) {
+    InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None);
+    if (pen) {
+        InkingControl.Instance.switchWidth(StrCast(pen.inkWidth, "1"));
+        InkingControl.Instance.switchColor(StrCast(pen.inkColor, "black"));
+        InkingControl.Instance.switchBezier(StrCast(pen.inkBezier, ""));
+        InkOptionsMenu.Instance.jumpTo(300, 300);
+    } else {
+        InkOptionsMenu.Instance.fadeOut(true);
+    }
+});
\ No newline at end of file
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 2ddf2c74a..762c57ae9 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -1,6 +1,6 @@
 import React = require("react");
 import { observer } from "mobx-react";
-import { SketchPicker } from 'react-color';
+import { SketchPicker, ColorState } from 'react-color';
 import { documentSchema } from "../../../fields/documentSchemas";
 import { makeInterface } from "../../../fields/Schema";
 import { StrCast } from "../../../fields/Types";
@@ -10,6 +10,12 @@ import { ViewBoxBaseComponent } from "../DocComponent";
 import { InkingControl } from "../InkingControl";
 import "./ColorBox.scss";
 import { FieldView, FieldViewProps } from './FieldView';
+import { InkingStroke } from "../InkingStroke";
+import { Doc } from "../../../fields/Doc";
+import { InkTool } from "../../../fields/InkField";
+import { undoBatch } from "../../util/UndoManager";
+import { action } from "mobx";
+import { FormattedTextBox } from "./formattedText/FormattedTextBox";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
 const ColorDocument = makeInterface(documentSchema);
@@ -18,20 +24,52 @@ const ColorDocument = makeInterface(documentSchema);
 export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument>(ColorDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
 
+    static decimalToHexString(number: number) {
+        if (number < 0) {
+            number = 0xFFFFFFFF + number + 1;
+        }
+        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
+    }
+
+    @undoBatch
+    @action
+    static switchColor(color: ColorState) {
+        Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ?
+            color.hex + (color.rgb.a ? ColorBox.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
+        InkingStroke.InkColor = StrCast(Doc.UserDoc().backgroundColor);
+        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = color.hex);
+
+        if (Doc.selectedTool === InkTool.None) {
+            const selected = SelectionManager.SelectedDocuments();
+            selected.map(view => {
+                const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
+                    view.props.Document.layout instanceof Doc ? view.props.Document.layout :
+                        view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document);
+                if (targetDoc) {
+                    if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) {
+                        Doc.Layout(view.props.Document).color = Doc.UserDoc().bacgroundColor;
+                    } else {
+                        Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific.  'backgroundColor' would apply to all templates, but has no UI at the moment
+                    }
+                }
+            });
+        }
+    }
+
     render() {
         const selDoc = SelectionManager.SelectedDocuments()?.[0]?.rootDoc;
         return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
             onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
             style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
 
-            <SketchPicker onChange={InkingControl.Instance.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
+            <SketchPicker onChange={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
                 color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined,
                     StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} />
             <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}>
-                <div> {InkingControl.Instance.selectedWidth ?? 2}</div>
-                <input type="range" value={InkingControl.Instance.selectedWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} />
-                <div> {InkingControl.Instance.selectedBezier ?? 2}</div>
-                <input type="range" value={InkingControl.Instance.selectedBezier ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} />
+                <div> {InkingStroke.InkWidth ?? 2}</div>
+                <input type="range" value={InkingStroke.InkWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} />
+                <div> {InkingStroke.InkBezierApprox ?? 2}</div>
+                <input type="range" value={InkingStroke.InkBezierApprox ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} />
                 <br />
                 <br />
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e245e045c..196e61a31 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -37,7 +37,6 @@ import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from "../DocComponent";
 import { EditableView } from '../EditableView';
-import { InkingControl } from '../InkingControl';
 import { KeyphraseQueryView } from '../KeyphraseQueryView';
 import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
@@ -330,7 +329,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 } else func();
             } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself
                 const alias = Doc.MakeAlias(this.props.Document);
-                Doc.makeCustomViewClicked(alias, undefined, "onClick");
+                DocUtils.makeCustomViewClicked(alias, undefined, "onClick");
                 this.props.addDocTab(alias, "onRight");
                 // UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick");
                 //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click");
@@ -513,7 +512,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         // console.log(e.button)
         // console.log(e.nativeEvent)
         // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document)
-        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
             if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
                 e.stopPropagation();
                 // TODO: check here for panning/inking
@@ -544,7 +543,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     onPointerMove = (e: PointerEvent): void => {
 
         if ((e as any).formattedHandled) { e.stopPropagation(); return; }
-        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) return;
+        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) return;
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
         }
@@ -728,7 +727,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" });
         onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link on Right", event: this.toggleFollowOnRight, icon: "concierge-bell" });
         onClicks.push({ description: this.Document.isLinkButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" });
-        onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
+        onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
         !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
 
 
@@ -1091,14 +1090,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get ignorePointerEvents() {
         return this.props.pointerEvents === false ||
             (this.Document.isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) ||
-            (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
+            (this.Document.type === DocumentType.INK && Doc.selectedTool !== InkTool.None);
     }
     @undoBatch
     @action
     setCustomView = (custom: boolean, layout: string): void => {
         Doc.setNativeView(this.props.Document);
         if (custom) {
-            Doc.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
+            DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
         }
     }
     @observable _animateScalingTo = 0;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 6913dfbc7..cabf30c13 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -192,7 +192,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
 
     extractFaces = () => {
         const converter = (results: any) => {
-            return results.map((face: CognitiveServices.Image.Face) => Docs.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!);
+            return results.map((face: CognitiveServices.Image.Face) => Doc.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!);
         };
         this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-faces"], this.url, Service.Face, converter);
     }
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 9f1e99c77..81669dc2a 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -11,7 +11,6 @@ import { DocumentManager } from "../../util/DocumentManager";
 import { undoBatch } from "../../util/UndoManager";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionView, CollectionViewType } from "../collections/CollectionView";
-import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./PresBox.scss";
 import { ViewBoxBaseComponent } from "../DocComponent";
@@ -20,6 +19,7 @@ import { Docs } from "../../documents/Documents";
 import { PrefetchProxy } from "../../../fields/Proxy";
 import { ScriptField } from "../../../fields/ScriptField";
 import { Scripting } from "../../util/Scripting";
+import { InkingStroke } from "../InkingStroke";
 
 type PresBoxSchema = makeInterface<[typeof documentSchema]>;
 const PresBoxDocument = makeInterface(documentSchema);
@@ -293,7 +293,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
     selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex));
     getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
     panelHeight = () => this.props.PanelHeight() - 20;
-    active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
+    active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
         (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
 
     render() {
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 29e3c008a..1184f32f1 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -5,21 +5,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from 'request-promise';
+import { Doc } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
-import { makeInterface, listSpec } from "../../../fields/Schema";
+import { listSpec, makeInterface } from "../../../fields/Schema";
 import { Cast, NumCast } from "../../../fields/Types";
 import { VideoField } from "../../../fields/URLField";
-import { emptyFunction, returnFalse, returnOne, Utils, returnZero } from "../../../Utils";
-import { Docs, DocUtils } from "../../documents/Documents";
+import { emptyFunction, returnFalse, returnOne, returnZero, Utils } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxBaseComponent } from "../DocComponent";
-import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ScreenshotBox.scss";
-import { Doc, WidthSym, HeightSym } from "../../../fields/Doc";
-import { OverlayView } from "../OverlayView";
 const path = require('path');
 
 type ScreenshotDocument = makeInterface<[typeof documentSchema]>;
@@ -134,7 +132,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh
     }
 
     @computed get content() {
-        const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + interactive;
         return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
             style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 9d02239fc..8c8b7a622 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -15,7 +15,6 @@ import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
-import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
 import { documentSchema } from "../../../fields/documentSchemas";
@@ -229,7 +228,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
 
     @computed get content() {
         const field = Cast(this.dataDoc[this.fieldKey], VideoField);
-        const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
             <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
@@ -273,8 +272,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
             this._reactionDisposer && this._reactionDisposer();
             this._youtubeReactionDisposer && this._youtubeReactionDisposer();
             this._reactionDisposer = reaction(() => this.layoutDoc.currentTimecode, () => !this._playing && this.Seek((this.layoutDoc.currentTimecode || 0)));
-            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
-                const interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
+            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, Doc.selectedTool], () => {
+                const interactive = Doc.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
                 iframe.style.pointerEvents = interactive ? "all" : "none";
             }, { fireImmediately: true });
         };
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c4ab3c9e2..dcc87068b 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -15,7 +15,6 @@ import { DragManager } from "../../util/DragManager";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
-import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./WebBox.scss";
 import React = require("react");
@@ -425,7 +424,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         const frozen = !this.props.isSelected() || decInteracting;
 
         return (<>
-            <div className={"webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")}
+            <div className={"webBox-cont" + (this.props.isSelected() && Doc.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")}
                 onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
                 {view}
             </div>;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fc131cd38..c0a8bec40 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -52,14 +52,12 @@ import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from "../../DocComponent";
 import { DocumentButtonBar } from '../../DocumentButtonBar';
-import { InkingControl } from "../../InkingControl";
 import { AudioBox } from '../AudioBox';
 import { FieldView, FieldViewProps } from "../FieldView";
 import "./FormattedTextBox.scss";
 import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
 import React = require("react");
-import { ScriptField } from '../../../../fields/ScriptField';
-import GoogleAuthenticationManager from '../../../apis/GoogleAuthenticationManager';
+import { InkingStroke } from '../../InkingStroke';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -479,11 +477,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             changeItems.push({
                 description: StrCast(note.title), event: undoBatch(() => {
                     Doc.setNativeView(this.rootDoc);
-                    Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
+                    DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
                 }), icon: "eye"
             });
         });
-        changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
+        changeItems.push({ description: "FreeForm", event: undoBatch(() => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
         !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" });
     }
 
@@ -1213,7 +1211,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         TraceMobx();
         const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1);
         const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
-        const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
+        const interactive = Doc.selectedTool || this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
             this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props);
         } else if (FormattedTextBoxComment.textBox === this) {
@@ -1268,7 +1266,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     </div>
                     {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
                         <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> :
-                        <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
+                        <div className={"formattedTextBox-sidebar" + (Doc.selectedTool !== InkTool.None ? "-inking" : "")}
                             style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                             <CollectionFreeFormView {...this.props}
                                 PanelHeight={this.props.PanelHeight}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5bad248be..c18f69e1d 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -2,7 +2,6 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
-import * as rp from "request-promise";
 import { Dictionary } from "typescript-collections";
 import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
@@ -26,7 +25,6 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec
 import { CollectionView } from "../collections/CollectionView";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
-import { InkingControl } from "../InkingControl";
 import Annotation from "./Annotation";
 import PDFMenu from "./PDFMenu";
 import "./PDFViewer.scss";
@@ -583,7 +581,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
         Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]);
         clipDoc.rootDocument = targetDoc;
-        Doc.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined);
+        DocUtils.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined);
         targetDoc.layoutKey = "layout";
         // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
         // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy]();
@@ -652,7 +650,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
     panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0);
     panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0);
     @computed get overlayLayer() {
-        return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
+        return <div className={`pdfViewer-overlay${Doc.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
             style={{ transform: `scale(${this._zoomed})` }}>
             <CollectionFreeFormView {...this.props}
                 LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath}
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
index 2ea011316..0b1e90b1d 100644
--- a/src/client/views/webcam/DashWebRTCVideo.tsx
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -4,10 +4,8 @@ import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeForm
 import { FieldViewProps, FieldView } from "../nodes/FieldView";
 import { observable, action } from "mobx";
 import { DocumentDecorations } from "../DocumentDecorations";
-import { InkingControl } from "../InkingControl";
 import "../../views/nodes/WebBox.scss";
 import "./DashWebRTCVideo.scss";
-import adapter from 'webrtc-adapter';
 import { initialize, hangup, refreshVideos } from "./WebCamLogic";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
@@ -73,7 +71,7 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV
             </div >;
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
-        const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
 
 
         return (
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6891bf652..d635836bb 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -6,20 +6,18 @@ import { DocumentType } from "../client/documents/DocumentTypes";
 import { Scripting, scriptingGlobal } from "../client/util/Scripting";
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
 import { UndoManager } from "../client/util/UndoManager";
-import { intersectRect, Utils } from "../Utils";
-import { HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update, Copy } from "./FieldSymbols";
+import { intersectRect } from "../Utils";
+import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
+import { InkTool } from "./InkField";
 import { List } from "./List";
 import { ObjectField } from "./ObjectField";
 import { PrefetchProxy, ProxyField } from "./Proxy";
 import { FieldId, RefField } from "./RefField";
 import { RichTextField } from "./RichTextField";
 import { listSpec } from "./Schema";
-import { ComputedField, ScriptField } from "./ScriptField";
-import { Cast, FieldValue, NumCast, StrCast, ToConstructor, ScriptCast } from "./Types";
+import { ComputedField } from "./ScriptField";
+import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
-import { Docs, DocumentOptions } from "../client/documents/Documents";
-import { PdfField, VideoField, AudioField, ImageField } from "./URLField";
-import { LinkManager } from "../client/util/LinkManager";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -134,6 +132,7 @@ function fetchProto(doc: Doc) {
 @scriptingGlobal
 @Deserializable("Doc", fetchProto).withFields(["id"])
 export class Doc extends RefField {
+    @computed public static get selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; }
     constructor(id?: FieldId, forceSave?: boolean) {
         super(id);
         const doc = new Proxy<this>(this, {
@@ -618,76 +617,6 @@ export namespace Doc {
         return copy;
     }
 
-    export function MakeClone(doc: Doc): Doc {
-        const cloneMap = new Map<string, Doc>();
-        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
-        const copy = Doc.makeClone(doc, cloneMap, rtfMap);
-        rtfMap.map(({ copy, key, field }) => {
-            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
-            };
-            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return href + (mapped ? mapped[Id] : id);
-            };
-            const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
-            const re = new RegExp(regex, "g");
-            copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
-        });
-        return copy;
-    }
-
-    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
-        if (Doc.IsBaseProto(doc)) return doc;
-        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
-        const copy = new Doc(undefined, true);
-        cloneMap.set(doc[Id], copy);
-        if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
-        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
-        Object.keys(doc).forEach(key => {
-            if (exclude.includes(key)) return;
-            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-            const field = ProxyField.WithoutProxy(() => doc[key]);
-            const copyObjectField = (field: ObjectField) => {
-                const list = Cast(doc[key], listSpec(Doc));
-                if (list !== undefined && !(list instanceof Promise)) {
-                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs)));
-                } else if (doc[key] instanceof Doc) {
-                    copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
-                } else {
-                    copy[key] = ObjectField.MakeCopy(field);
-                    if (field instanceof RichTextField) {
-                        if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
-                            rtfs.push({ copy, key, field });
-                        }
-                    }
-                }
-            };
-            if (key === "proto") {
-                if (doc[key] instanceof Doc) {
-                    copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs);
-                }
-            } else {
-                if (field instanceof RefField) {
-                    copy[key] = field;
-                } else if (cfield instanceof ComputedField) {
-                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
-                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
-                } else if (field instanceof ObjectField) {
-                    copyObjectField(field);
-                } else if (field instanceof Promise) {
-                    debugger; //This shouldn't happend...
-                } else {
-                    copy[key] = field;
-                }
-            }
-        });
-        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
-        copy.cloneOf = doc;
-        cloneMap.set(doc[Id], copy);
-        return copy;
-    }
 
     export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc;
     export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>;
@@ -998,120 +927,147 @@ export namespace Doc {
         return false;
     }
 
-    // applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
-    export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
-        const batch = UndoManager.StartBatch("makeCustomViewClicked");
-        runInAction(() => {
-            doc.layoutKey = "layout_" + templateSignature;
-            if (doc[doc.layoutKey] === undefined) {
-                createCustomView(doc, creator, templateSignature, docLayoutTemplate);
-            }
-        });
-        batch.end();
-    }
-    export function findTemplate(templateName: string, type: string, signature: string) {
-        let docLayoutTemplate: Opt<Doc>;
-        const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data);
-        const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data);
-        const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data);
-        const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
-        const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc);
-        // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
-        // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
-        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc));
-        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc));
-        return docLayoutTemplate;
-    }
-    export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
-        const templateName = templateSignature.replace(/\(.*\)/, "");
-        docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature);
-
-        const customName = "layout_" + templateSignature;
-        const _width = NumCast(doc._width);
-        const _height = NumCast(doc._height);
-        const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
-
-        let fieldTemplate: Opt<Doc>;
-        if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
-            fieldTemplate = Docs.Create.TextDocument("", options);
-        } else if (doc.data instanceof PdfField) {
-            fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
-        } else if (doc.data instanceof VideoField) {
-            fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
-        } else if (doc.data instanceof AudioField) {
-            fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
-        } else if (doc.data instanceof ImageField) {
-            fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
-        }
-        const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
 
-        fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
-        docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
-    }
-    export function makeCustomView(doc: Doc, custom: boolean, layout: string) {
-        Doc.setNativeView(doc);
-        if (custom) {
-            makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined);
+    export namespace Get {
+
+        const primitives = ["string", "number", "boolean"];
+
+        export interface JsonConversionOpts {
+            data: any;
+            title?: string;
+            appendToExisting?: { targetDoc: Doc, fieldKey?: string };
+            excludeEmptyObjects?: boolean;
         }
-    }
-    export function iconify(doc: Doc) {
-        const layoutKey = Cast(doc.layoutKey, "string", null);
-        Doc.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined);
-        if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", "");
-    }
 
-    export function pileup(docList: Doc[], x?: number, y?: number) {
-        let w = 0, h = 0;
-        runInAction(() => {
-            docList.forEach(d => {
-                Doc.iconify(d);
-                w = Math.max(d[WidthSym](), w);
-                h = Math.max(d[HeightSym](), h);
-            });
-            h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away.  so this is a fallback hack to try to do something reasonable
-            docList.forEach((d, i) => {
-                d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2;
-                d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2;
-                d.displayTimecode = undefined;  // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
-            });
-        });
-        if (x !== undefined && y !== undefined) {
-            const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _LODdisable: true });
-            newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55;
-            newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55;
-            newCollection._width = newCollection._height = 110;
-            //newCollection.borderRounding = "40px";
-            newCollection._jitterRotation = 10;
-            newCollection._backgroundColor = "gray";
-            newCollection._overflow = "visible";
-            return newCollection;
+        const defaultKey = "json";
+
+        /**
+         * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
+         * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
+         * 
+         * After building a hierarchy within / below a top-level document, it then returns that top-level parent.
+         * 
+         * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
+         * string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
+         * call that returned a regular string value to be stored as a Field.
+         * 
+         * If we've received something other than a string, since the caller might also pass in the results of a
+         * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
+         * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
+         * 
+         * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
+         * lacking the key value structure, gets stored as a field in a wrapper document.
+         * 
+         * @param data for convenience and flexibility, either a valid JSON string to be parsed,
+         * or the result of any JSON.parse() call.
+         * @param title an optional title to give to the highest parent document in the hierarchy.
+         * If whether this function creates a new document or appendToExisting is specified and that document already has a title,
+         * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title.
+         * @param appendToExisting **if specified**, there are two cases, both of which return the target document:
+         * 
+         * 1) the json to be converted can be represented as a document, in which case the target document will act as the root
+         * of the tree and receive all the conversion results as new fields on itself
+         * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion
+         * results to either the specified key on the target document, or to its "json" key by default.
+         * 
+         * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls)
+         * to act as the root of the tree.
+         * 
+         * One might choose to specify this field if you want to write to a document returned from a Document.Create function call,
+         * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created
+         * from a default call to new Doc.
+         * 
+         * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even
+         * if they contain no data. By default, empty objects and arrays are ignored.
+         */
+        export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> {
+            if (excludeEmptyObjects === undefined) {
+                excludeEmptyObjects = true;
+            }
+            if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) {
+                return undefined;
+            }
+            let resolved: any;
+            try {
+                resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data));
+            } catch (e) {
+                return undefined;
+            }
+            let output: Opt<Doc>;
+            if (typeof resolved === "object" && !(resolved instanceof Array)) {
+                output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc);
+            } else {
+                const result = toField(resolved, excludeEmptyObjects);
+                if (appendToExisting) {
+                    (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result;
+                } else {
+                    (output = new Doc).json = result;
+                }
+            }
+            title && output && (output.title = title);
+            return output;
         }
-    }
 
+        /**
+         * For each value of the object, recursively convert it to its appropriate field value
+         * and store the field at the appropriate key in the document if it is not undefined
+         * @param object the object to convert
+         * @returns the object mapped from JSON to field values, where each mapping 
+         * might involve arbitrary recursion (since toField might itself call convertObject)
+         */
+        const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => {
+            const hasEntries = Object.keys(object).length;
+            if (hasEntries || !excludeEmptyObjects) {
+                const resolved = target ?? new Doc;
+                if (hasEntries) {
+                    let result: Opt<Field>;
+                    Object.keys(object).map(key => {
+                        // if excludeEmptyObjects is true, any qualifying conversions from toField will
+                        // be undefined, and thus the results that would have
+                        // otherwise been empty (List or Doc)s will just not be written
+                        if (result = toField(object[key], excludeEmptyObjects, key)) {
+                            resolved[key] = result;
+                        }
+                    });
+                }
+                title && (resolved.title = title);
+                return resolved;
+            }
+        };
 
-    export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) {
-        let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey);
-        if (!(optionsCollection instanceof Doc)) {
-            optionsCollection = Docs.Create.StackingDocument([], { title: `${enumeratedFieldKey} field set` }, enumeratedFieldKey);
-            Doc.AddDocToList((Doc.UserDoc().fieldTypes as Doc), "data", optionsCollection as Doc);
-        }
-        const options = optionsCollection as Doc;
-        const targetDoc = doc && Doc.GetProto(Cast(doc.rootDocument, Doc, null) || doc);
-        const docFind = `options.data.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`;
-        targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(docFind + `._backgroundColor || "white"`, undefined, { options }));
-        targetDoc && (targetDoc.color = ComputedField.MakeFunction(docFind + `.color || "black"`, undefined, { options }));
-        targetDoc && (targetDoc.borderRounding = ComputedField.MakeFunction(docFind + `.borderRounding`, undefined, { options }));
-        enumerations.map(enumeration => {
-            const found = DocListCast(options.data).find(d => d.title === enumeration.title);
-            if (found) {
-                found._backgroundColor = enumeration._backgroundColor || found._backgroundColor;
-                found._color = enumeration.color || found._color;
-            } else {
-                Doc.AddDocToList(options, "data", Docs.Create.TextDocument(enumeration.title, enumeration));
+        /**
+         * For each element in the list, recursively convert it to a document or other field 
+         * and push the field to the list if it is not undefined
+         * @param list the list to convert
+         * @returns the list mapped from JSON to field values, where each mapping 
+         * might involve arbitrary recursion (since toField might itself call convertList)
+         */
+        const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
+            const target = new List();
+            let result: Opt<Field>;
+            // if excludeEmptyObjects is true, any qualifying conversions from toField will
+            // be undefined, and thus the results that would have
+            // otherwise been empty (List or Doc)s will just not be written
+            list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result));
+            if (target.length || !excludeEmptyObjects) {
+                return target;
             }
-        });
-        return optionsCollection;
+        };
+
+        const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => {
+            if (data === null || data === undefined) {
+                return undefined;
+            }
+            if (primitives.includes(typeof data)) {
+                return data;
+            }
+            if (typeof data === "object") {
+                return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined);
+            }
+            throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
+        };
     }
+
 }
 
 Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index a50cca2b0..20e14a68d 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -1,12 +1,6 @@
-import { NDollarRecognizer } from "./ndollar";
-import { Type } from "typescript";
-import { InkField, PointData } from "../fields/InkField";
-import { Docs } from "../client/documents/Documents";
-import { Doc, WidthSym, HeightSym } from "../fields/Doc";
-import { NumCast } from "../fields/Types";
-import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView";
 import { Rect } from "react-measure";
-import { Scripting } from "../client/util/Scripting";
+import { PointData } from "../fields/InkField";
+import { NDollarRecognizer } from "./ndollar";
 
 export namespace GestureUtils {
     export class GestureEvent {
-- 
cgit v1.2.3-70-g09d2


From 4cc9c6ae9a2735266994c62c01818de4bdc25ac6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 8 Jun 2020 20:45:38 -0400
Subject: final cleanup of ink related stuff so that things are more structured
 and we avoid input cycles

---
 src/Utils.ts                                       |  13 ++
 .../apis/google_docs/GooglePhotosClientUtils.ts    |   4 +-
 src/client/apis/youtube/YoutubeBox.tsx             |   2 +-
 src/client/documents/Documents.ts                  |  81 +------------
 src/client/util/CurrentUserUtils.ts                |  59 +++++-----
 src/client/util/LinkManager.ts                     |  10 +-
 src/client/views/DocComponent.tsx                  |   6 +-
 src/client/views/GestureOverlay.tsx                |  35 +++---
 src/client/views/GlobalKeyHandler.ts               |  37 +++---
 src/client/views/InkingControl.scss                | 131 ---------------------
 src/client/views/InkingControl.tsx                 |  42 -------
 src/client/views/InkingStroke.tsx                  |  47 ++++----
 src/client/views/PreviewCursor.tsx                 |   4 +-
 src/client/views/collections/CollectionView.tsx    |  33 ++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  16 +--
 .../collectionFreeForm/InkOptionsMenu.tsx          |  95 ++++++++-------
 src/client/views/nodes/ColorBox.tsx                |  43 +++----
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 src/client/views/nodes/PresBox.tsx                 |   2 +-
 src/client/views/nodes/ScreenshotBox.tsx           |   2 +-
 src/client/views/nodes/VideoBox.tsx                |   6 +-
 src/client/views/nodes/WebBox.tsx                  |   2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 src/client/views/pdf/PDFViewer.tsx                 |   2 +-
 src/client/views/webcam/DashWebRTCVideo.tsx        |   4 +-
 src/fields/Doc.ts                                  |  79 ++++++++++++-
 src/fields/InkField.ts                             |  10 +-
 src/mobile/MobileInterface.tsx                     |   7 +-
 28 files changed, 312 insertions(+), 470 deletions(-)
 delete mode 100644 src/client/views/InkingControl.scss
 delete mode 100644 src/client/views/InkingControl.tsx

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

diff --git a/src/Utils.ts b/src/Utils.ts
index ef5002bec..e527634fd 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,6 +2,7 @@ import v4 = require('uuid/v4');
 import v5 = require("uuid/v5");
 import { Socket, Room } from 'socket.io';
 import { Message } from './server/Message';
+import { ColorState } from 'react-color';
 
 export namespace Utils {
     export let DRAG_THRESHOLD = 4;
@@ -75,6 +76,18 @@ export namespace Utils {
         document.body.removeChild(textArea);
     }
 
+    export function decimalToHexString(number: number) {
+        if (number < 0) {
+            number = 0xFFFFFFFF + number + 1;
+        }
+        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
+    }
+
+    export function colorString(color: ColorState) {
+        return color.hex.startsWith("#") ?
+            color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
+    }
+
     export function fromRGBAstr(rgba: string) {
         const rm = rgba.match(/rgb[a]?\(([ 0-9]+)/);
         const r = rm ? Number(rm[1]) : 0;
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index fef71ffeb..a604c7de1 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -8,7 +8,7 @@ import { Cast, StrCast } from "../../../fields/Types";
 import { ImageField } from "../../../fields/URLField";
 import { MediaItem, NewMediaItemResult } from "../../../server/apis/google/SharedTypes";
 import { Utils } from "../../../Utils";
-import { Docs, DocumentOptions } from "../../documents/Documents";
+import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
 import { Networking } from "../../Network";
 import { FormattedTextBox } from "../../views/nodes/formattedText/FormattedTextBox";
 import GoogleAuthenticationManager from "../GoogleAuthenticationManager";
@@ -332,7 +332,7 @@ export namespace GooglePhotos {
                 const url = data.url.href;
                 const target = Doc.MakeAlias(source);
                 const description = parseDescription(target, descriptionKey);
-                await Doc.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
+                await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
                 media.push({ url, description });
             }
             if (media.length) {
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index a7045ccb7..748d571c0 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -349,7 +349,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
 
-        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.GetSelectedTool() && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
         return (
             <>
                 <div className={classname}  >
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 994f8a147..9c807eff2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -37,7 +37,7 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
 import { QueryBox } from "../views/nodes/QueryBox";
 import { ColorBox } from "../views/nodes/ColorBox";
 import { DocHolderBox } from "../views/nodes/DocHolderBox";
-import { InkingStroke } from "../views/InkingStroke";
+import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../views/InkingStroke";
 import { InkField } from "../../fields/InkField";
 import { RichTextField } from "../../fields/RichTextField";
 import { extname } from "path";
@@ -47,10 +47,6 @@ import { ContextMenu } from "../views/ContextMenu";
 import { LinkBox } from "../views/nodes/LinkBox";
 import { ScreenshotBox } from "../views/nodes/ScreenshotBox";
 import { ComparisonBox } from "../views/nodes/ComparisonBox";
-import { Id } from "../../fields/FieldSymbols";
-import { listSpec } from "../../fields/Schema";
-import { ObjectField } from "../../fields/ObjectField";
-import { RefField } from "../../fields/RefField";
 import { runInAction } from "mobx";
 import { UndoManager } from "../util/UndoManager";
 const path = require('path');
@@ -149,7 +145,7 @@ export interface DocumentOptions {
     dbDoc?: Doc;
     linkRelationship?: string; // type of relatinoship a link represents
     ischecked?: ScriptField; // returns whether a font icon box is checked
-    activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
+    activeInkPen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
     onClick?: ScriptField;
     onDoubleClick?: ScriptField;
     onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked
@@ -855,7 +851,7 @@ export namespace DocUtils {
             created = Docs.Create.AudioDocument((field).url.href, resolved);
             layout = AudioBox.LayoutString;
         } else if (field instanceof InkField) {
-            created = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, (field).inkData, resolved);
+            created = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), (field).inkData, resolved);
             layout = InkingStroke.LayoutString;
         } else if (field instanceof List && field[0] instanceof Doc) {
             created = Docs.Create.StackingDocument(DocListCast(field), resolved);
@@ -913,76 +909,6 @@ export namespace DocUtils {
         return ctor ? ctor(path, options) : undefined;
     }
 
-    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
-        if (Doc.IsBaseProto(doc)) return doc;
-        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
-        const copy = new Doc(undefined, true);
-        cloneMap.set(doc[Id], copy);
-        if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
-        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
-        Object.keys(doc).forEach(key => {
-            if (exclude.includes(key)) return;
-            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-            const field = ProxyField.WithoutProxy(() => doc[key]);
-            const copyObjectField = (field: ObjectField) => {
-                const list = Cast(doc[key], listSpec(Doc));
-                if (list !== undefined && !(list instanceof Promise)) {
-                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => DocUtils.makeClone(d as Doc, cloneMap, rtfs)));
-                } else if (doc[key] instanceof Doc) {
-                    copy[key] = key.includes("layout[") ? undefined : DocUtils.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
-                } else {
-                    copy[key] = ObjectField.MakeCopy(field);
-                    if (field instanceof RichTextField) {
-                        if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
-                            rtfs.push({ copy, key, field });
-                        }
-                    }
-                }
-            };
-            if (key === "proto") {
-                if (doc[key] instanceof Doc) {
-                    copy[key] = DocUtils.makeClone(doc[key]!, cloneMap, rtfs);
-                }
-            } else {
-                if (field instanceof RefField) {
-                    copy[key] = field;
-                } else if (cfield instanceof ComputedField) {
-                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
-                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
-                } else if (field instanceof ObjectField) {
-                    copyObjectField(field);
-                } else if (field instanceof Promise) {
-                    debugger; //This shouldn't happend...
-                } else {
-                    copy[key] = field;
-                }
-            }
-        });
-        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
-        copy.cloneOf = doc;
-        cloneMap.set(doc[Id], copy);
-        return copy;
-    }
-    export function MakeClone(doc: Doc): Doc {
-        const cloneMap = new Map<string, Doc>();
-        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
-        const copy = DocUtils.makeClone(doc, cloneMap, rtfMap);
-        rtfMap.map(({ copy, key, field }) => {
-            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
-            };
-            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return href + (mapped ? mapped[Id] : id);
-            };
-            const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
-            const re = new RegExp(regex, "g");
-            copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
-        });
-        return copy;
-    }
-
     export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void {
         ContextMenu.Instance.addItem({
             description: "Add Note ...",
@@ -1107,7 +1033,6 @@ export namespace DocUtils {
         }
     }
 
-
     export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) {
         let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey);
         if (!(optionsCollection instanceof Doc)) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d2614a898..b0cea9947 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -11,7 +11,6 @@ import { ScriptField, ComputedField } from "../../fields/ScriptField";
 import { Cast, PromiseValue, StrCast, NumCast } from "../../fields/Types";
 import { nullAudio } from "../../fields/URLField";
 import { DragManager } from "./DragManager";
-import { InkingControl } from "../views/InkingControl";
 import { Scripting } from "./Scripting";
 import { CollectionViewType } from "../views/collections/CollectionView";
 import { makeTemplate } from "./DropConverter";
@@ -32,7 +31,6 @@ export class CurrentUserUtils {
     public static get MainDocId() { return this.mainDocId; }
     public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
     @computed public static get UserDocument() { return Doc.UserDoc(); }
-    @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).inkPen as Doc; }
 
     @observable public static GuestTarget: Doc | undefined;
     @observable public static GuestWorkspace: Doc | undefined;
@@ -301,7 +299,7 @@ export class CurrentUserUtils {
 
     static creatorBtnDescriptors(doc: Doc): {
         title: string, label: string, icon: string, drag?: string, ignoreClick?: boolean,
-        click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc
+        click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc
     }[] {
         if (doc.emptyPresentation === undefined) {
             doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(),
@@ -334,11 +332,11 @@ export class CurrentUserUtils {
             { title: "Drag an import folder", label: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
             { title: "Drag a mobile view", label: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
             { title: "Drag an instance of the device collection", label: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' },
-            // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
-            // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
+            // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen,  this)`, activeInkPen: doc },
+            // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc },
+            // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc },
             { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
             { title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
             { title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
@@ -358,7 +356,7 @@ export class CurrentUserUtils {
             }
         }
         const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title));
-        const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activePen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
+        const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
             _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
             icon,
             title,
@@ -368,7 +366,7 @@ export class CurrentUserUtils {
             onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined,
             onClick: click ? ScriptField.MakeScript(click) : undefined,
             ischecked: ischecked ? ComputedField.MakeFunction(ischecked) : undefined,
-            activePen,
+            activeInkPen,
             backgroundColor,
             removeDropProperties: new List<string>(["dropAction"]),
             dragFactory,
@@ -387,31 +385,31 @@ export class CurrentUserUtils {
     }
 
     static setupMobileButtons(doc: Doc, buttons?: string[]) {
-        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
             { title: "record", icon: "microphone", ignoreClick: true, click: "FILL" },
-            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
-            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
-            // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "red", activePen: doc },
+            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen,  this)`, activeInkPen: doc },
+            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc },
+            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc },
+            // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "red", activeInkPen: doc },
             { title: "upload", icon: "upload", click: 'switchMobileView(setupMobileUploadDoc, renderMobileUpload, onSwitchMobileUpload);', backgroundColor: "orange" },
             // { title: "upload", icon: "upload", click: 'uploadImageMobile();', backgroundColor: "cyan" },
         ];
         return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
             _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
             onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
-            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activeInkPen: data.activeInkPen,
             backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
         }));
     }
 
     static setupThumbButtons(doc: Doc) {
-        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
-            { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+            { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen,  this)`, activeInkPen: doc },
+            { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
         ];
         return docProtoData.map(data => Docs.Create.FontIconDocument({
             _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon,
@@ -419,7 +417,7 @@ export class CurrentUserUtils {
             onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
             clipboard: data.clipboard,
             onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
-            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, pointerHack: true,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activeInkPen: data.activeInkPen, pointerHack: true,
             backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
         }));
     }
@@ -612,8 +610,8 @@ export class CurrentUserUtils {
     static setupDockedButtons(doc: Doc) {
         if (doc["dockedBtn-pen"] === undefined) {
             doc["dockedBtn-pen"] = CurrentUserUtils.ficon({
-                onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)"),
-                author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen,  this)`), activePen: doc
+                onClick: ScriptField.MakeScript("activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)"),
+                author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activeInkPen,  this)`), activeInkPen: doc
             });
         }
         if (doc["dockedBtn-undo"] === undefined) {
@@ -688,12 +686,11 @@ export class CurrentUserUtils {
     }
 
     static async updateUserDocument(doc: Doc) {
-        new InkingControl();
         doc.title = Doc.CurrentUserEmail;
-        doc.activePen = doc;
-        doc.inkColor = StrCast(doc.backgroundColor, "rgb(0, 0, 0)");
-        doc.inkWidth = StrCast(doc.inkWidth, "1");
-        doc.inkBezier = StrCast(doc.inkBezier, "");
+        doc.activeInkPen = doc;
+        doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)");
+        doc.activeInkWidth = StrCast(doc.activeInkWidth, "1");
+        doc.activeInkBezier = StrCast(doc.activeInkBezier, "");
         doc.fontSize = NumCast(doc.fontSize, 12);
         doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // 
         doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // 
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 8e6ccf098..95528e25a 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -2,10 +2,8 @@ import { Doc, DocListCast } from "../../fields/Doc";
 import { List } from "../../fields/List";
 import { listSpec } from "../../fields/Schema";
 import { Cast, StrCast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
 import { Scripting } from "./Scripting";
 
-
 /* 
  * link doc: 
  * - anchor1: doc
@@ -34,16 +32,12 @@ export class LinkManager {
     // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
     // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type 
     public get LinkManagerDoc(): Doc | undefined {
-        return Docs.Prototypes.MainLinkDocument();
+        return Doc.UserDoc().globalLinkDatabase as Doc;
     }
 
     public getAllLinks(): Doc[] {
         const ldoc = LinkManager.Instance.LinkManagerDoc;
-        if (ldoc) {
-            const docs = DocListCast(ldoc.data);
-            return docs;
-        }
-        return [];
+        return ldoc ? DocListCast(ldoc.data) : [];
     }
 
     public addLink(linkDoc: Doc): boolean {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index c398b2633..3af570f1e 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -57,7 +57,7 @@ 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;
 
-        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);//  && !Doc.selectedTool;  // bcz: inking state shouldn't affect static tools 
+        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);//  && !Doc.SelectedTool();  // bcz: inking state shouldn't affect static tools 
         protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     }
     return Component;
@@ -146,9 +146,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
         }
 
         whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
-        active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+        active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.props.Document.isBackground) &&
             (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0 || BoolCast((this.layoutDoc as any).forceActive)) ? true : false)
-        annotationsActive = (outsideReaction?: boolean) => (Doc.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) ||
+        annotationsActive = (outsideReaction?: boolean) => (Doc.GetSelectedTool() !== InkTool.None || (this.props.Document.isBackground && this.props.active()) ||
             (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
     }
     return Component;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 4ea75d7d7..3384b5fce 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -18,19 +18,18 @@ import { LinkManager } from "../util/LinkManager";
 import { Scripting } from "../util/Scripting";
 import { Transform } from "../util/Transform";
 import "./GestureOverlay.scss";
-import { InkingControl } from "./InkingControl";
+import { ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke";
 import { DocumentView } from "./nodes/DocumentView";
 import { RadialMenu } from "./nodes/RadialMenu";
 import HorizontalPalette from "./Palette";
 import { Touchable } from "./Touchable";
 import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
-import { InkingStroke } from "./InkingStroke";
-
 
 @observer
 export default class GestureOverlay extends Touchable {
     static Instance: GestureOverlay;
 
+    @observable public InkShape: string = "";
     @observable public SavedColor?: string;
     @observable public SavedWidth?: string;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
@@ -492,7 +491,7 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             e.stopPropagation();
             e.preventDefault();
@@ -506,7 +505,7 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerMove = (e: PointerEvent) => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             e.stopPropagation();
             e.preventDefault();
@@ -580,8 +579,8 @@ export default class GestureOverlay extends Touchable {
                 DocServer.Mobile.dispatchGesturePoints({
                     points: this._points,
                     bounds: B,
-                    color: InkingStroke.InkColor,
-                    width: InkingStroke.InkWidth
+                    color: ActiveInkColor(),
+                    width: ActiveInkWidth()
                 });
             }
 
@@ -624,11 +623,11 @@ export default class GestureOverlay extends Touchable {
                 }
             }
             //if any of the shape is activated in the InkOptionsMenu
-            else if (InkingStroke.InkShape) {
-                this.makePolygon(InkingStroke.InkShape, false);
+            else if (this.InkShape) {
+                this.makePolygon(this.InkShape, false);
                 this.dispatchGesture(GestureUtils.Gestures.Stroke);
                 this._points = [];
-                InkingStroke.InkShape = "";
+                this.InkShape = "";
             }
             // if we're not drawing in a toolglass try to recognize as gesture
             else {
@@ -808,11 +807,11 @@ export default class GestureOverlay extends Touchable {
             [this._strokes.map(l => {
                 const b = this.getBounds(l);
                 return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                    {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)}
+                    {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)}
             </svg>]
         ];
     }
@@ -902,16 +901,16 @@ Scripting.addGlobal(function setToolglass(tool: any) {
 });
 Scripting.addGlobal(function setPen(width: any, color: any) {
     runInAction(() => {
-        GestureOverlay.Instance.SavedColor = InkingStroke.InkColor;
-        InkingControl.Instance.switchColor(color);
-        GestureOverlay.Instance.SavedWidth = InkingStroke.InkWidth;
-        InkingControl.Instance.switchWidth(width);
+        GestureOverlay.Instance.SavedColor = ActiveInkColor();
+        SetActiveInkColor(color);
+        GestureOverlay.Instance.SavedWidth = ActiveInkWidth();
+        SetActiveInkWidth(width);
     });
 });
 Scripting.addGlobal(function resetPen() {
     runInAction(() => {
-        InkingControl.Instance.switchColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)");
-        InkingControl.Instance.switchWidth(GestureOverlay.Instance.SavedWidth ?? "2");
+        SetActiveInkColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)");
+        SetActiveInkWidth(GestureOverlay.Instance.SavedWidth ?? "2");
     });
 });
 Scripting.addGlobal(function createText(text: any, x: any, y: any) {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 255142771..2d5af5386 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,26 +1,25 @@
-import { UndoManager, undoBatch } from "../util/UndoManager";
-import { SelectionManager } from "../util/SelectionManager";
-import { CollectionDockingView } from "./collections/CollectionDockingView";
-import { MainView } from "./MainView";
-import { DragManager } from "../util/DragManager";
-import { action, runInAction } from "mobx";
+import { action } from "mobx";
+import { DateField } from "../../fields/DateField";
 import { Doc, DocListCast } from "../../fields/Doc";
-import { DictationManager } from "../util/DictationManager";
-import SharingManager from "../util/SharingManager";
-import { Cast, PromiseValue, NumCast } from "../../fields/Types";
-import { ScriptField } from "../../fields/ScriptField";
-import { InkingControl } from "./InkingControl";
+import { Id } from "../../fields/FieldSymbols";
 import { InkTool } from "../../fields/InkField";
-import { DocumentView } from "./nodes/DocumentView";
+import { List } from "../../fields/List";
+import { ScriptField } from "../../fields/ScriptField";
+import { Cast, PromiseValue } from "../../fields/Types";
 import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
-import { CollectionFreeFormView } from "./collections/collectionFreeForm/CollectionFreeFormView";
+import { DocServer } from "../DocServer";
+import { DocumentType } from "../documents/DocumentTypes";
+import { DictationManager } from "../util/DictationManager";
+import { DragManager } from "../util/DragManager";
+import { SelectionManager } from "../util/SelectionManager";
+import SharingManager from "../util/SharingManager";
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { CollectionDockingView } from "./collections/CollectionDockingView";
 import { MarqueeView } from "./collections/collectionFreeForm/MarqueeView";
-import { Id } from "../../fields/FieldSymbols";
 import { DocumentDecorations } from "./DocumentDecorations";
-import { DocumentType } from "../documents/DocumentTypes";
-import { DocServer } from "../DocServer";
-import { List } from "../../fields/List";
-import { DateField } from "../../fields/DateField";
+import { InkingStroke } from "./InkingStroke";
+import { MainView } from "./MainView";
+import { DocumentView } from "./nodes/DocumentView";
 
 const modifiers = ["control", "meta", "shift", "alt"];
 type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -78,7 +77,7 @@ export default class KeyManager {
                 break;
             case "escape":
                 const main = MainView.Instance;
-                InkingControl.Instance.switchTool(InkTool.None);
+                Doc.SetSelectedTool(InkTool.None);
                 if (main.isPointerDown) {
                     DragManager.AbortDrag();
                 } else {
diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss
deleted file mode 100644
index 465e14d07..000000000
--- a/src/client/views/InkingControl.scss
+++ /dev/null
@@ -1,131 +0,0 @@
-@import "globalCssVariables";
-.inking-control {
-    bottom: 20px;
-    margin: 0;
-    padding: 0;
-    display: flex;
-    label,
-    input,
-    option {
-        font-size: 12px;
-    }
-    input[type="range"] {
-        -webkit-appearance: none;
-        background-color: transparent;
-        vertical-align: middle;
-        margin-top: 8px;
-        &:focus {
-            outline: none;
-        }
-        &::-webkit-slider-runnable-track {
-            width: 100%;
-            height: 3px;
-            border-radius: 1.5px;
-            cursor: pointer;
-            background: $intermediate-color;
-        }
-        &::-webkit-slider-thumb {
-            height: 12px;
-            width: 12px;
-            border: 1px solid $intermediate-color;
-            border-radius: 6px;
-            background: $light-color;
-            cursor: pointer;
-            -webkit-appearance: none;
-            margin-top: -4px;
-        }
-        &::-moz-range-track {
-            width: 100%;
-            height: 3px;
-            border-radius: 1.5px;
-            cursor: pointer;
-            background: $light-color;
-        }
-        &::-moz-range-thumb {
-            height: 12px;
-            width: 12px;
-            border: 1px solid $intermediate-color;
-            border-radius: 6px;
-            background: $light-color;
-            cursor: pointer;
-            -webkit-appearance: none;
-            margin-top: -4px;
-        }
-    }
-    input[type="text"] {
-        border: none;
-        padding: 0 0px;
-        background: transparent;
-        color: $dark-color;
-        font-size: 12px;
-        margin-top: 4px;
-    }
-    .ink-panel {
-        height: 24px;
-        vertical-align: middle;
-        line-height: 28px;
-        padding: 0 10px;
-        color: $intermediate-color;
-        &:first {
-            margin-top: 0;
-        }
-    }
-    .ink-tools {
-        display: flex;
-        background-color: transparent;
-        border-radius: 0;
-        padding: 0;
-        button {
-            height: 36px;
-            padding: 0px;
-            padding-bottom: 3px;
-            margin-left: 10px;
-            background-color: transparent;
-            color: $intermediate-color;
-        }
-        button:hover {
-            transform: scale(1.15);
-        }
-    }
-    .ink-size {
-        display: flex;
-        justify-content: space-between;
-        input[type="text"] {
-            width: 42px;
-        }
-        >* {
-            margin-right: 6px;
-            &:last-child {
-                margin-right: 0;
-            }
-        }
-    }
-    .ink-color {
-        display: flex;
-        position: relative;
-        padding-right: 0;
-        label {
-            margin-right: 6px;
-        }
-        .ink-color-display {
-            border-radius: 11px;
-            width: 22px;
-            height: 22px;
-            cursor: pointer;
-            text-align: center; //   span {
-            //     color: $light-color;
-            //     font-size: 8px;
-            //     user-select: none;
-            //   }
-        }
-        .ink-color-picker {
-            background-color: $light-color;
-            border-radius: 5px;
-            padding: 12px;
-            position: absolute;
-            bottom: 36px;
-            left: -3px;
-            box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
deleted file mode 100644
index 349bc6ffc..000000000
--- a/src/client/views/InkingControl.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { action, observable } from "mobx";
-import { Doc } from "../../fields/Doc";
-import { InkTool } from "../../fields/InkField";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import { Scripting } from "../util/Scripting";
-import { InkingStroke } from "./InkingStroke";
-
-export class InkingControl {
-    @observable static Instance: InkingControl;
-    @observable public _open: boolean = false;
-    constructor() {
-        InkingControl.Instance = this;
-    }
-
-    switchTool = action((tool: InkTool): void => {
-        // this._selectedTool = tool;
-        Doc.UserDoc().inkTool = tool;
-    });
-
-    @action
-    switchWidth = (width: string): void => {
-        if (!isNaN(parseInt(width))) {
-            CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkWidth = InkingStroke.InkWidth = width);
-        }
-    }
-
-    @action
-    switchBezier = (bezier: string): void => {
-        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkBezier = InkingStroke.InkBezierApprox = isNaN(parseInt(bezier)) ? "" : bezier);
-    }
-
-    @action
-    switchColor(value: string) {
-        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = InkingStroke.InkColor = value);
-    }
-}
-Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.switchColor(color); });
-Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
-Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); });
-Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
-Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
-Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.switchColor(color); });
\ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 78d729eee..b545ede54 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,6 +1,6 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
-import { observable, runInAction } from "mobx";
+import { observable, runInAction, action } from "mobx";
 import { observer } from "mobx-react";
 import { documentSchema } from "../../fields/documentSchemas";
 import { InkData, InkField, InkTool } from "../../fields/InkField";
@@ -14,6 +14,8 @@ import { ViewBoxBaseComponent } from "./DocComponent";
 import "./InkingStroke.scss";
 import { FieldView, FieldViewProps } from "./nodes/FieldView";
 import React = require("react");
+import { Scripting } from "../util/Scripting";
+import { Doc } from "../../fields/Doc";
 
 library.add(faPaintBrush);
 
@@ -23,22 +25,6 @@ const InkDocument = makeInterface(documentSchema);
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
-    @observable public static InkColor: string;
-    @observable public static InkWidth: string;
-    @observable public static InkBezierApprox: string;
-    @observable public static InkShape: string;
-
-    constructor(props: any) {
-        super(props);
-        if (InkingStroke.InkBezierApprox === undefined) {
-            runInAction(() => {
-                InkingStroke.InkBezierApprox = "";
-                InkingStroke.InkWidth = "1";
-                InkingStroke.InkColor = "black";
-                InkingStroke.InkShape = "";
-            });
-        }
-    }
 
     private analyzeStrokes = () => {
         const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
@@ -59,9 +45,9 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const scaleX = this.props.PanelWidth() / width;
         const scaleY = this.props.PanelHeight() / height;
         const points = InteractionUtils.CreatePolyline(data, left, top,
-            StrCast(this.layoutDoc.color, InkingStroke.InkColor || "black"),
-            StrCast(this.layoutDoc.strokeWidth, InkingStroke.InkWidth || "1"),
-            StrCast(this.layoutDoc.strokeBezier, InkingStroke.InkBezierApprox || ""), scaleX, scaleY, "");
+            StrCast(this.layoutDoc.color, ActiveInkColor()),
+            StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth()),
+            StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "");
         return (
             <svg className="inkingStroke"
                 width={width}
@@ -79,4 +65,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             </svg>
         );
     }
-}
\ No newline at end of file
+}
+
+
+export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); }
+export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); }
+export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); }
+export function ActiveInkPen(): Doc { return Cast(Doc.UserDoc().activeInkPen, Doc, null); }
+export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
+export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); }
+export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
+Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) {
+    Doc.SetSelectedTool(pen ? InkTool.Highlighter : InkTool.None);
+    SetActiveInkWidth(width);
+    SetActiveInkColor(color);
+});
+Scripting.addGlobal(function activateEraser(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Eraser : InkTool.None); });
+Scripting.addGlobal(function activateStamp(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Stamp : InkTool.None); });
+Scripting.addGlobal(function deactivateInk() { return Doc.SetSelectedTool(InkTool.None); });
+Scripting.addGlobal(function setInkWidth(width: any) { return Doc.SetSelectedTool(width); });
+Scripting.addGlobal(function setInkColor(color: any) { return Doc.SetSelectedTool(color); });
\ No newline at end of file
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 33150ab7c..dd65681d4 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import "normalize.css";
 import * as React from 'react';
 import "./PreviewCursor.scss";
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs } from '../documents/Documents';
 import { Doc } from '../../fields/Doc';
 import { Transform } from "../util/Transform";
 import { DocServer } from '../DocServer';
@@ -65,7 +65,7 @@ export class PreviewCursor extends React.Component<{}> {
                         count++;
                         if (doc instanceof Doc) {
                             i === 1 && (first = doc);
-                            const alias = DocUtils.MakeClone(doc);
+                            const alias = Doc.MakeClone(doc);
                             const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx;
                             const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty;
                             alias.x = newPoint[0] + deltaX;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 873f61331..8a73565c8 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,34 +1,37 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEye, faEdit } from '@fortawesome/free-regular-svg-icons';
+import { faEdit, faEye } from '@fortawesome/free-regular-svg-icons';
+import { faColumns, faCopy, faEllipsisV, faFingerprint, faGlobeAmericas, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree, faGlobeAmericas } from '@fortawesome/free-solid-svg-icons';
-import { action, observable, computed } from 'mobx';
+import { action, computed, observable } from 'mobx';
 import { observer } from "mobx-react";
 import * as React from 'react';
 import Lightbox from 'react-image-lightbox-with-rotate';
 import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
 import { DateField } from '../../../fields/DateField';
-import { DataSym, Doc, DocListCast, Field, Opt, AclSym, AclAddonly, AclReadonly } from '../../../fields/Doc';
+import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
-import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Types';
+import { ObjectField } from '../../../fields/ObjectField';
+import { listSpec } from '../../../fields/Schema';
+import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { Utils, setupMoveUpEvents, returnFalse, returnZero, emptyPath, emptyFunction, returnOne } from '../../../Utils';
+import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
+import { InteractionUtils } from '../../util/InteractionUtils';
 import { ContextMenu } from "../ContextMenu";
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { ScriptBox } from '../ScriptBox';
 import { Touchable } from '../Touchable';
-import { Id } from '../../../fields/FieldSymbols';
-import { listSpec } from '../../../fields/Schema';
-import { ScriptField, ComputedField } from '../../../fields/ScriptField';
-import { InteractionUtils } from '../../util/InteractionUtils';
-import { ObjectField } from '../../../fields/ObjectField';
+import './CollectionView.scss';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
 export const COLLECTION_BORDER_WIDTH = 2;
 const path = require('path');
+
 library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faGlobeAmericas, faEllipsisV, faImage, faEye as any, faCopy);
 
 export enum CollectionViewType {
@@ -500,6 +503,12 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
         </div>);
     }
 }
+
+
+
+// to avoid an import cycle that will cause runtime errors, 
+// we import all of the bindings needed within methods after the
+// class has been defined.
 import { SubCollectionViewProps } from './CollectionSubView';
 import { CollectionCarouselView } from './CollectionCarouselView';
 import { CollectionDockingView } from "./CollectionDockingView";
@@ -515,8 +524,6 @@ import { CollectionTimeView } from './CollectionTimeView';
 import { CollectionTreeView } from "./CollectionTreeView";
 import { CollectionMapView } from './CollectionMapView';
 import { CollectionPileView } from './CollectionPileView';
-import './CollectionView.scss';
 import { CollectionViewBaseChrome } from './CollectionViewChromes';
 import { CurrentUserUtils } from '../../util/CurrentUserUtils';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 576c0c560..9c0e5e917 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -45,7 +45,7 @@ import React = require("react");
 import { CollectionViewType } from "../CollectionView";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { SnappingManager } from "../../../util/SnappingManager";
-import { InkingStroke } from "../../InkingStroke";
+import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../../InkingStroke";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -390,7 +390,7 @@ 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) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
+        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
             return;
         }
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
@@ -407,7 +407,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             document.addEventListener("pointermove", this.onPointerMove);
             document.addEventListener("pointerup", this.onPointerUp);
             // if not using a pen and in no ink mode
-            if (Doc.selectedTool === InkTool.None) {
+            if (Doc.GetSelectedTool() === InkTool.None) {
                 this._downX = this._lastX = e.pageX;
                 this._downY = this._lastY = e.pageY;
             }
@@ -431,13 +431,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     this.addMoveListeners();
                     this.removeEndListeners();
                     this.addEndListeners();
-                    // if (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen) {
+                    // if (Doc.SelectedTool() === InkTool.Highlighter || Doc.SelectedTool() === InkTool.Pen) {
                     //     e.stopPropagation();
                     //     e.preventDefault();
                     //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
                     //     this._points.push({ X: point[0], Y: point[1] });
                     // }
-                    if (Doc.selectedTool === InkTool.None) {
+                    if (Doc.GetSelectedTool() === InkTool.None) {
                         this._lastX = pt.pageX;
                         this._lastY = pt.pageY;
                         e.preventDefault();
@@ -457,7 +457,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+                const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
@@ -618,7 +618,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             return;
         }
         if (!e.cancelBubble) {
-            const selectedTool = Doc.selectedTool;
+            const selectedTool = Doc.GetSelectedTool();
             if (selectedTool === InkTool.None) {
                 if (this._hitCluster && this.tryDragCluster(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
@@ -640,7 +640,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
             const pt = myTouches[0];
             if (pt) {
-                if (Doc.selectedTool === InkTool.None) {
+                if (Doc.GetSelectedTool() === InkTool.None) {
                     if (this._hitCluster && this.tryDragCluster(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();
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
index b35c9682e..5a27f74e5 100644
--- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
@@ -1,15 +1,15 @@
 import React = require("react");
 import AntimodeMenu from "../../AntimodeMenu";
 import { observer } from "mobx-react";
-import { observable, action } from "mobx";
+import { observable, action, computed } from "mobx";
 import "./InkOptionsMenu.scss";
-import { InkingStroke } from "../../InkingStroke";
+import { ActiveInkColor, ActiveInkBezierApprox, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox } from "../../InkingStroke";
 import { Scripting } from "../../../util/Scripting";
 import { InkTool } from "../../../../fields/InkField";
-import { InkingControl } from "../../InkingControl";
-import { StrCast } from "../../../../fields/Types";
 import { ColorState } from "react-color";
-import { ColorBox } from "../../nodes/ColorBox";
+import { Utils } from "../../../../Utils";
+import GestureOverlay from "../../GestureOverlay";
+import { Doc } from "../../../../fields/Doc";
 
 @observer
 export default class InkOptionsMenu extends AntimodeMenu {
@@ -35,15 +35,15 @@ export default class InkOptionsMenu extends AntimodeMenu {
             hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
             rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
         };
-        ColorBox.switchColor(col);
+        SetActiveInkColor(Utils.colorString(col));
     }
 
     @action
     changeBezier = (e: React.PointerEvent): void => {
-        InkingControl.Instance.switchBezier(!InkingStroke.InkBezierApprox ? "300" : "");
+        SetActiveBezierApprox(!ActiveInkBezierApprox() ? "300" : "");
     }
 
-    render() {
+    @computed get widthPicker() {
         var widthPicker = <button
             className="antimodeMenu-button"
             key="width"
@@ -52,30 +52,33 @@ export default class InkOptionsMenu extends AntimodeMenu {
             W
         </button>;
         if (this._widthBtn) {
-            widthPicker = <div className="btn2-group">
+            widthPicker = <div className="btn2-group" key="width">
                 {widthPicker}
                 {this._width.map(wid => {
                     return <button
                         className="antimodeMenu-button"
                         key={wid}
-                        onPointerDown={action(() => { InkingControl.Instance.switchWidth(wid); this._widthBtn = false; })}
+                        onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; })}
                         style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
                         {wid}
                     </button>;
                 })}
             </div>;
         }
+        return widthPicker;
+    }
 
+    @computed get colorPicker() {
         var colorPicker = <button
             className="antimodeMenu-button"
             key="color"
             title="colorChanger"
             onPointerDown={action(e => this._colorBtn = !this._colorBtn)}
             style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
-            <div className="color-preview" style={{ backgroundColor: InkingStroke.InkColor ?? "121212" }}></div>
+            <div className="color-preview" style={{ backgroundColor: ActiveInkColor() ?? "121212" }}></div>
         </button>;
         if (this._colorBtn) {
-            colorPicker = <div className="btn-group">
+            colorPicker = <div className="btn-group" key="color">
                 {colorPicker}
                 {this._palette.map(color => {
                     return <button
@@ -88,46 +91,50 @@ export default class InkOptionsMenu extends AntimodeMenu {
                 })}
             </div>;
         }
+        return colorPicker;
+    }
 
-        const buttons = [
-            <button className="antimodeMenu-button"
-                title="Drag"
-                key="drag"
-                onPointerDown={e => this.dragStart(e)}>
-                ✜
-            </button>,
-            <>
-                {this._buttons.map((btn, i) => <button
-                    className="antimodeMenu-button"
-                    title={`Draw ${btn}`}
-                    key={btn}
-                    onPointerDown={action(e => InkingStroke.InkShape = btn)}
-                    style={btn === InkingStroke.InkShape ? { backgroundColor: "121212" } : {}}>
-                    {this._icons[i]}
-                </button>)},
-            </>,
-            <button
+    @computed get shapeButtons() {
+        return <>
+            {this._buttons.map((btn, i) => <button
                 className="antimodeMenu-button"
-                title="Bezier changer"
-                key="bezier"
-                onPointerDown={e => this.changeBezier(e)}
-                style={InkingStroke.InkBezierApprox ? { backgroundColor: "121212" } : {}}>
-                B
-            </button>,
-            widthPicker,
-            colorPicker,
+                title={`Draw ${btn}`}
+                key={btn}
+                onPointerDown={action(e => GestureOverlay.Instance.InkShape = btn)}
+                style={{ backgroundColor: btn === GestureOverlay.Instance.InkShape ? "121212" : "" }}>
+                {this._icons[i]}
+            </button>)},
+        </>;
+    }
+
+    @computed get bezierButton() {
+        return <button
+            className="antimodeMenu-button"
+            title="Bezier changer"
+            key="bezier"
+            onPointerDown={e => this.changeBezier(e)}
+            style={ { backgroundColor:ActiveInkBezierApprox() ? "121212":"" } }>
+            B
+        </button>;
+    }
+
+    render() {
+        const buttons = [
+            <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}>  ✜  </button>,
+            this.shapeButtons,
+            this.bezierButton,
+            this.widthPicker,
+            this.colorPicker,
         ];
         return this.getElement(buttons);
     }
 }
-Scripting.addGlobal(function activatePen(pen: any) {
-    InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None);
-    if (pen) {
-        InkingControl.Instance.switchWidth(StrCast(pen.inkWidth, "1"));
-        InkingControl.Instance.switchColor(StrCast(pen.inkColor, "black"));
-        InkingControl.Instance.switchBezier(StrCast(pen.inkBezier, ""));
+Scripting.addGlobal(function activatePen(penBtn: any) {
+    if (penBtn) {
+        Doc.SetSelectedTool(InkTool.Pen);
         InkOptionsMenu.Instance.jumpTo(300, 300);
     } else {
+        Doc.SetSelectedTool(InkTool.None);
         InkOptionsMenu.Instance.fadeOut(true);
     }
 });
\ No newline at end of file
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 762c57ae9..0d6258cf3 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -1,20 +1,19 @@
 import React = require("react");
+import { action } from "mobx";
 import { observer } from "mobx-react";
-import { SketchPicker, ColorState } from 'react-color';
+import { ColorState, SketchPicker } from 'react-color';
+import { Doc } from "../../../fields/Doc";
+import { Utils } from "../../../Utils";
 import { documentSchema } from "../../../fields/documentSchemas";
+import { InkTool } from "../../../fields/InkField";
 import { makeInterface } from "../../../fields/Schema";
 import { StrCast } from "../../../fields/Types";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
 import { ViewBoxBaseComponent } from "../DocComponent";
-import { InkingControl } from "../InkingControl";
+import { ActiveInkPen, ActiveInkWidth, ActiveInkBezierApprox, SetActiveInkColor, SetActiveInkWidth, SetActiveBezierApprox } from "../InkingStroke";
 import "./ColorBox.scss";
 import { FieldView, FieldViewProps } from './FieldView';
-import { InkingStroke } from "../InkingStroke";
-import { Doc } from "../../../fields/Doc";
-import { InkTool } from "../../../fields/InkField";
-import { undoBatch } from "../../util/UndoManager";
-import { action } from "mobx";
 import { FormattedTextBox } from "./formattedText/FormattedTextBox";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
@@ -24,22 +23,13 @@ const ColorDocument = makeInterface(documentSchema);
 export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument>(ColorDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
 
-    static decimalToHexString(number: number) {
-        if (number < 0) {
-            number = 0xFFFFFFFF + number + 1;
-        }
-        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
-    }
-
     @undoBatch
     @action
     static switchColor(color: ColorState) {
-        Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ?
-            color.hex + (color.rgb.a ? ColorBox.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
-        InkingStroke.InkColor = StrCast(Doc.UserDoc().backgroundColor);
-        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = color.hex);
+        Doc.UserDoc().backgroundColor = Utils.colorString(color);
+        SetActiveInkColor(color.hex);
 
-        if (Doc.selectedTool === InkTool.None) {
+        if (Doc.GetSelectedTool() === InkTool.None) {
             const selected = SelectionManager.SelectedDocuments();
             selected.map(view => {
                 const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
@@ -56,6 +46,9 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument
         }
     }
 
+    constructor(props: any) {
+        super(props);
+    }
     render() {
         const selDoc = SelectionManager.SelectedDocuments()?.[0]?.rootDoc;
         return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
@@ -63,13 +56,13 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument
             style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
 
             <SketchPicker onChange={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
-                color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined,
+                color={StrCast(ActiveInkPen()?.backgroundColor,
                     StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} />
             <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}>
-                <div> {InkingStroke.InkWidth ?? 2}</div>
-                <input type="range" value={InkingStroke.InkWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} />
-                <div> {InkingStroke.InkBezierApprox ?? 2}</div>
-                <input type="range" value={InkingStroke.InkBezierApprox ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} />
+                <div> {ActiveInkWidth() ?? 2}</div>
+                <input type="range" value={ActiveInkWidth() ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveInkWidth(e.target.value)} />
+                <div> {ActiveInkBezierApprox() ?? 2}</div>
+                <input type="range" value={ActiveInkBezierApprox() ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveBezierApprox(e.target.value)} />
                 <br />
                 <br />
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 196e61a31..98be1adc0 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -512,7 +512,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         // console.log(e.button)
         // console.log(e.nativeEvent)
         // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document)
-        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
+        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
             if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
                 e.stopPropagation();
                 // TODO: check here for panning/inking
@@ -543,7 +543,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     onPointerMove = (e: PointerEvent): void => {
 
         if ((e as any).formattedHandled) { e.stopPropagation(); return; }
-        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) return;
+        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) return;
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
         }
@@ -1090,7 +1090,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get ignorePointerEvents() {
         return this.props.pointerEvents === false ||
             (this.Document.isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) ||
-            (this.Document.type === DocumentType.INK && Doc.selectedTool !== InkTool.None);
+            (this.Document.type === DocumentType.INK && Doc.GetSelectedTool() !== InkTool.None);
     }
     @undoBatch
     @action
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 81669dc2a..dbc879920 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -293,7 +293,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
     selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex));
     getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
     panelHeight = () => this.props.PanelHeight() - 20;
-    active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
+    active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground) &&
         (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
 
     render() {
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 1184f32f1..d75b864cf 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -132,7 +132,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh
     }
 
     @computed get content() {
-        const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.GetSelectedTool() || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + interactive;
         return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
             style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 8c8b7a622..1e0a859d3 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -228,7 +228,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
 
     @computed get content() {
         const field = Cast(this.dataDoc[this.fieldKey], VideoField);
-        const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.GetSelectedTool() || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
             <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
@@ -272,8 +272,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
             this._reactionDisposer && this._reactionDisposer();
             this._youtubeReactionDisposer && this._youtubeReactionDisposer();
             this._reactionDisposer = reaction(() => this.layoutDoc.currentTimecode, () => !this._playing && this.Seek((this.layoutDoc.currentTimecode || 0)));
-            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, Doc.selectedTool], () => {
-                const interactive = Doc.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
+            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, Doc.GetSelectedTool()], () => {
+                const interactive = Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
                 iframe.style.pointerEvents = interactive ? "all" : "none";
             }, { fireImmediately: true });
         };
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index dcc87068b..8fdde61e0 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -424,7 +424,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         const frozen = !this.props.isSelected() || decInteracting;
 
         return (<>
-            <div className={"webBox-cont" + (this.props.isSelected() && Doc.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")}
+            <div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !decInteracting ? "-interactive" : "")}
                 onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
                 {view}
             </div>;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c0a8bec40..2c1d93222 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1211,7 +1211,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         TraceMobx();
         const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1);
         const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
-        const interactive = Doc.selectedTool || this.layoutDoc.isBackground;
+        const interactive = Doc.GetSelectedTool() || this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
             this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props);
         } else if (FormattedTextBoxComment.textBox === this) {
@@ -1266,7 +1266,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     </div>
                     {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
                         <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> :
-                        <div className={"formattedTextBox-sidebar" + (Doc.selectedTool !== InkTool.None ? "-inking" : "")}
+                        <div className={"formattedTextBox-sidebar" + (Doc.GetSelectedTool() !== InkTool.None ? "-inking" : "")}
                             style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                             <CollectionFreeFormView {...this.props}
                                 PanelHeight={this.props.PanelHeight}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c18f69e1d..8cf5026c8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -650,7 +650,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
     panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0);
     panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0);
     @computed get overlayLayer() {
-        return <div className={`pdfViewer-overlay${Doc.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
+        return <div className={`pdfViewer-overlay${Doc.GetSelectedTool() !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
             style={{ transform: `scale(${this._zoomed})` }}>
             <CollectionFreeFormView {...this.props}
                 LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath}
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
index 0b1e90b1d..83de9cc15 100644
--- a/src/client/views/webcam/DashWebRTCVideo.tsx
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -10,6 +10,7 @@ import { initialize, hangup, refreshVideos } from "./WebCamLogic";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
 import { faSync, faPhoneSlash } from "@fortawesome/free-solid-svg-icons";
+import { Doc } from "../../../fields/Doc";
 
 library.add(faSync);
 library.add(faPhoneSlash);
@@ -71,8 +72,7 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV
             </div >;
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
-        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
-
+        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.GetSelectedTool() && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
 
         return (
             <>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index d635836bb..981025483 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -6,7 +6,7 @@ import { DocumentType } from "../client/documents/DocumentTypes";
 import { Scripting, scriptingGlobal } from "../client/util/Scripting";
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
 import { UndoManager } from "../client/util/UndoManager";
-import { intersectRect } from "../Utils";
+import { intersectRect, Utils } from "../Utils";
 import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
 import { InkTool } from "./InkField";
 import { List } from "./List";
@@ -18,6 +18,7 @@ import { listSpec } from "./Schema";
 import { ComputedField } from "./ScriptField";
 import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
+import { LinkManager } from "../client/util/LinkManager";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -132,7 +133,6 @@ function fetchProto(doc: Doc) {
 @scriptingGlobal
 @Deserializable("Doc", fetchProto).withFields(["id"])
 export class Doc extends RefField {
-    @computed public static get selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; }
     constructor(id?: FieldId, forceSave?: boolean) {
         super(id);
         const doc = new Proxy<this>(this, {
@@ -486,6 +486,78 @@ export namespace Doc {
         return alias;
     }
 
+
+
+    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
+        if (Doc.IsBaseProto(doc)) return doc;
+        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
+        const copy = new Doc(undefined, true);
+        cloneMap.set(doc[Id], copy);
+        if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
+        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
+        Object.keys(doc).forEach(key => {
+            if (exclude.includes(key)) return;
+            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+            const field = ProxyField.WithoutProxy(() => doc[key]);
+            const copyObjectField = (field: ObjectField) => {
+                const list = Cast(doc[key], listSpec(Doc));
+                if (list !== undefined && !(list instanceof Promise)) {
+                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs)));
+                } else if (doc[key] instanceof Doc) {
+                    copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
+                } else {
+                    copy[key] = ObjectField.MakeCopy(field);
+                    if (field instanceof RichTextField) {
+                        if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
+                            rtfs.push({ copy, key, field });
+                        }
+                    }
+                }
+            };
+            if (key === "proto") {
+                if (doc[key] instanceof Doc) {
+                    copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs);
+                }
+            } else {
+                if (field instanceof RefField) {
+                    copy[key] = field;
+                } else if (cfield instanceof ComputedField) {
+                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
+                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
+                } else if (field instanceof ObjectField) {
+                    copyObjectField(field);
+                } else if (field instanceof Promise) {
+                    debugger; //This shouldn't happend...
+                } else {
+                    copy[key] = field;
+                }
+            }
+        });
+        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
+        copy.cloneOf = doc;
+        cloneMap.set(doc[Id], copy);
+        return copy;
+    }
+    export function MakeClone(doc: Doc): Doc {
+        const cloneMap = new Map<string, Doc>();
+        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
+        const copy = Doc.makeClone(doc, cloneMap, rtfMap);
+        rtfMap.map(({ copy, key, field }) => {
+            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
+                const mapped = cloneMap.get(id);
+                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
+            };
+            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
+                const mapped = cloneMap.get(id);
+                return href + (mapped ? mapped[Id] : id);
+            };
+            const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
+            const re = new RegExp(regex, "g");
+            copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+        });
+        return copy;
+    }
+
     //
     // Determines whether the layout needs to be expanded (as a template).
     // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
@@ -743,6 +815,9 @@ export namespace Doc {
     export function SearchQuery(): string { return manager._searchQuery; }
     export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
     export function UserDoc(): Doc { return manager._user_doc; }
+
+    export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; }
+    export function GetSelectedTool(): InkTool { return (FieldValue(StrCast(Doc.UserDoc().activeInkTool)) ?? InkTool.None) as InkTool; }
     export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
     export function IsBrushed(doc: Doc) {
         return computedFn(function IsBrushed(doc: Doc) {
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index bb93de5ac..51a5768bf 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -4,11 +4,11 @@ import { ObjectField } from "./ObjectField";
 import { Copy, ToScriptString, ToString } from "./FieldSymbols";
 
 export enum InkTool {
-    None,
-    Pen,
-    Highlighter,
-    Eraser,
-    Stamp
+    None = "none",
+    Pen = "pen",
+    Highlighter = "highlighter",
+    Eraser = "eraser",
+    Stamp = "stamp"
 }
 
 export interface PointData {
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 6c2e797d6..da14ffc88 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -12,7 +12,6 @@ import { Scripting } from '../client/util/Scripting';
 import { Transform } from '../client/util/Transform';
 import { DocumentDecorations } from '../client/views/DocumentDecorations';
 import GestureOverlay from '../client/views/GestureOverlay';
-import { InkingControl } from '../client/views/InkingControl';
 import { DocumentView } from '../client/views/nodes/DocumentView';
 import { RadialMenu } from '../client/views/nodes/RadialMenu';
 import { PreviewCursor } from '../client/views/PreviewCursor';
@@ -26,6 +25,7 @@ import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
 import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from '../Utils';
 import "./MobileInterface.scss";
 import { CollectionView } from '../client/views/collections/CollectionView';
+import { InkingStroke } from '../client/views/InkingStroke';
 
 library.add(faLongArrowAltLeft);
 
@@ -68,7 +68,7 @@ export default class MobileInterface extends React.Component {
     }
 
     onSwitchInking = () => {
-        InkingControl.Instance.switchTool(InkTool.Pen);
+        Doc.SetSelectedTool(InkTool.Pen);
         MobileInterface.Instance.drawingInk = true;
 
         DocServer.Mobile.dispatchOverlayTrigger({
@@ -134,7 +134,7 @@ export default class MobileInterface extends React.Component {
 
     onBack = (e: React.MouseEvent) => {
         this.switchCurrentView((userDoc: Doc) => this.mainDoc);
-        InkingControl.Instance.switchTool(InkTool.None); // TODO: switch to previous tool
+        Doc.SetSelectedTool(InkTool.None); // TODO: switch to previous tool
 
         DocServer.Mobile.dispatchOverlayTrigger({
             enableOverlay: false,
@@ -187,6 +187,7 @@ export default class MobileInterface extends React.Component {
                         Document={this.mainContainer}
                         DataDoc={undefined}
                         LibraryPath={emptyPath}
+                        filterAddDocument={returnTrue}
                         fieldKey={""}
                         dropAction={"alias"}
                         bringToFront={emptyFunction}
-- 
cgit v1.2.3-70-g09d2

