From baf156f58856004279223b2e1f858c5ff7e88686 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 4 Jun 2024 13:54:56 -0400
Subject: small fixes

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 6 ++++--
 1 file changed, 4 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 b6e1fca77..c6db8290d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -504,12 +504,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     case InkTool.SegmentEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         this._eraserPts.length = 0;
-                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
+                        const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
+                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction, hit !== -1, false);
                         break;
                     case InkTool.RadiusEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         this._eraserPts.length = 0;
-                        setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction);
+                        // const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
+                        // setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction, hit !== -1, false);
                         break;
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
-- 
cgit v1.2.3-70-g09d2


From 2f5757ffaebaec9d459404fec266295abeebd2b0 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 6 Jun 2024 11:31:52 -0400
Subject: created input box for gpt draw

---
 src/client/apis/gpt/GPT.ts                         |   1 +
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/views/MainView.tsx                      |   2 +
 src/client/views/SmartDraw.tsx                     |   0
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  44 ++++--
 .../collectionFreeForm/SmartDrawHandler.tsx        | 154 +++++++++++++++++++++
 src/fields/InkField.ts                             |   1 +
 7 files changed, 195 insertions(+), 9 deletions(-)
 delete mode 100644 src/client/views/SmartDraw.tsx
 create mode 100644 src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 3a5e49731..5afb345a0 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -12,6 +12,7 @@ enum GPTCallType {
     DESCRIBE = 'describe',
     MERMAID = 'mermaid',
     DATA = 'data',
+    DRAW = 'draw',
 }
 
 type GPTCallOpts = {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 4e379219f..3250f10a8 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -745,7 +745,7 @@ pie title Minerals in my tap water
             { title: "Labels", toolTip: "Lab els",                          btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
             { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberSliderButton,               toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
             { title: "Ink",    toolTip: "Stroke color",                     btnType: ButtonType.ColorButton,  icon: "pen",        toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
-            { title: "Smart Draw", toolTip: "Draw with GPT",                btnType: ButtonType.ToggleButton, icon: "user-pen",   toolType: "smartDraw",                      scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
+            { title: "Smart Draw", toolTip: "Draw with GPT",                btnType: ButtonType.ToggleButton, icon: "user-pen",   toolType: "smartdraw",                      scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
         ];
     }
 
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8430db883..f7e1617fc 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -55,6 +55,7 @@ import { TabDocView } from './collections/TabDocView';
 import './collections/TreeView.scss';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { ImageLabelHandler } from './collections/collectionFreeForm/ImageLabelHandler';
+import { SmartDrawHandler } from './collections/collectionFreeForm/SmartDrawHandler';
 import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
 import { CollectionLinearView } from './collections/collectionLinear';
 import { LinkMenu } from './linking/LinkMenu';
@@ -1090,6 +1091,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 <TaskCompletionBox />
                 <ContextMenu />
                 <ImageLabelHandler />
+                <SmartDrawHandler />
                 <AnchorMenu />
                 <MapAnchorMenu />
                 <DirectionsAnchorMenu />
diff --git a/src/client/views/SmartDraw.tsx b/src/client/views/SmartDraw.tsx
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c6db8290d..194c99c3d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -54,6 +54,8 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
+import { SmartDrawHandler } from './SmartDrawHandler';
+import { ImageLabelHandler } from './ImageLabelHandler';
 
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -496,30 +498,33 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             if (!this.Document.isGroup) {
                 // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
                 // prettier-ignore
+                const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                 switch (Doc.ActiveTool) {
-                    case InkTool.Highlighter: break;
-                    case InkTool.Write: break;
-                    case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
+                    case InkTool.Highlighter:
+                        break;
+                    case InkTool.Write:
+                        break;
+                    case InkTool.Pen:
+                        break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
                     case InkTool.StrokeEraser:
                     case InkTool.SegmentEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         this._eraserPts.length = 0;
-                        const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction, hit !== -1, false);
                         break;
                     case InkTool.RadiusEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         this._eraserPts.length = 0;
-                        // const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
-                        // setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction, hit !== -1, false);
+                        setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction, hit !== -1, false);
                         break;
+                    case InkTool.SmartDraw:
+                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.createDrawing, hit !== -1, false);
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
-                            const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                             setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
                         }
                         break;
-                    default: 
+                    default:
                 }
             }
         }
@@ -1225,6 +1230,29 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return tVals;
     };
 
+    @action
+    createDrawing = (e: PointerEvent, doubleTap?: boolean) => {
+        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY);
+        if (SmartDrawHandler.Instance.coords) {
+            // const coords: InkData = SmartDrawHandler.Instance.coords;
+            // const inkField = new InkField(coords);
+            // const points = coords.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
+            // const bounds = InkField.getBounds(points);
+            // const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+            // const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+            // return Docs.Create.InkDocument(
+            //     points,
+            //     { title: 'stroke',
+            //     x: B.x - inkWidth / 2,
+            //     y: B.y - inkWidth / 2,
+            //     _width: B.width + inkWidth,
+            //     _height: B.height + inkWidth,
+            //     stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+            //     inkWidth
+            // );
+        }
+    };
+
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
         if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
new file mode 100644
index 000000000..fc88b5cc6
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
@@ -0,0 +1,154 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import React from 'react';
+import { SettingsManager } from '../../../util/SettingsManager';
+import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { Button, IconButton } from 'browndash-components';
+import ReactLoading from 'react-loading';
+import { AiOutlineSend } from 'react-icons/ai';
+import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
+import './ImageLabelHandler.scss';
+import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
+import { InkingStroke } from '../../InkingStroke';
+import { InkData } from '../../../../fields/InkField';
+
+@observer
+export class SmartDrawHandler extends ObservableReactComponent<{}> {
+    static Instance: SmartDrawHandler;
+
+    @observable private _display: boolean = false;
+    @observable private _pageX: number = 0;
+    @observable private _pageY: number = 0;
+    @observable private _yRelativeToTop: boolean = true;
+    @observable private _isLoading: boolean = false;
+    @observable private _userInput: string = '';
+    @observable public coords: InkData | undefined = undefined;
+
+    constructor(props: any) {
+        super(props);
+        makeObservable(this);
+        SmartDrawHandler.Instance = this;
+    }
+
+    @action
+    setIsLoading = (isLoading: boolean) => {
+        this._isLoading = isLoading;
+    };
+
+    @action
+    setUserInput = (input: string) => {
+        this._userInput = input;
+    };
+
+    @action
+    displaySmartDrawHandler = (x: number, y: number) => {
+        this._pageX = x;
+        this._pageY = y;
+        this._display = true;
+    };
+
+    @action
+    hideLabelhandler = () => {
+        this._display = false;
+    };
+
+    @action
+    drawWithGPT = async (startPoint: {X: number, Y: number}, input: string) => {
+        this.setIsLoading(true);
+        try {
+            const res = await gptAPICall(input, GPTCallType.DRAW);
+            if (!res) {
+                console.error('GPT call failed');
+                return;
+            }
+            console.log("GPT response:", res);
+            // controlPts: {X: number, Y: number}[] = []
+            // code to extract list of coords from the string
+            // this.coords = controlPts.map(pt: {X: number, Y: number } => {pt.X + startPoint.X, pt.Y + startPoint.Y});
+           
+
+        } catch (err) {
+            console.error('GPT call failed');
+        }
+
+        this.setIsLoading(false);
+        this.setUserInput('');
+    };
+
+    render() {
+        if (this._display) {
+            return (
+                <div
+                    id="label-handler"
+                    className="contextMenu-cont"
+                    style={{
+                        display: this._display ? '' : 'none',
+                        left: this._pageX,
+                        ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+                        background: SettingsManager.userBackgroundColor,
+                        color: SettingsManager.userColor,
+                    }}>
+                    <div>
+                        <IconButton tooltip={'Cancel'} onPointerDown={this.hideLabelhandler} icon={<FontAwesomeIcon icon="xmark" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} />
+                        <input
+                            aria-label="label-input"
+                            id="new-label"
+                            type="text"
+                            style={{ color: 'black' }}
+                            value={this._userInput}
+                            onChange={e => {
+                                this.setUserInput(e.target.value);
+                            }}
+                            placeholder="Enter item to draw"
+                        />
+                        <Button
+                            style={{ alignSelf: 'flex-end' }}
+                            text="Send"
+                            icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            color={MarqueeOptionsMenu.Instance.userColor}
+                            onClick={e => {
+                                this.drawWithGPT({X: e.clientX, Y: e.clientY}, this._userInput);
+                            }}
+                        />
+                        {/* <IconButton
+                            tooltip={'Generate Drawing'}
+                            onPointerDown={() => {
+                                const input = document.getElementById('new-label') as HTMLInputElement;
+                                const newLabel = input.value;
+                                // this.addLabel(newLabel);
+                                // this._currentLabel = '';
+                                input.value = '';
+                            }}
+                            icon={<FontAwesomeIcon icon="plus" />}
+                            color={MarqueeOptionsMenu.Instance.userColor}
+                            style={{ width: '19px' }}
+                        />
+                        <IconButton tooltip={'Group Images'} icon={<FontAwesomeIcon icon="object-group" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> */}
+                    </div>
+                    <div>
+                        {/* {this._labelGroups.map(group => {
+                            return (
+                                <div>
+                                    <p>{group}</p>
+                                    <IconButton
+                                        tooltip={'Remove Label'}
+                                        onPointerDown={() => {
+                                            this.removeLabel(group);
+                                        }}
+                                        icon={'x'}
+                                        color={MarqueeOptionsMenu.Instance.userColor}
+                                        style={{ width: '19px' }}
+                                    />
+                                </div>
+                            );
+                        })} */}
+                    </div>
+                </div>
+            );
+        } else {
+            return <></>;
+        }
+    }
+}
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 32abf0076..123d32301 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -17,6 +17,7 @@ export enum InkTool {
     Stamp = 'stamp',
     Write = 'write',
     PresentationPin = 'presentationpin',
+    SmartDraw = 'smartdraw',
 }
 
 export type Segment = Array<Bezier>;
-- 
cgit v1.2.3-70-g09d2


From 2277349fc4d5460e94a7a6b705b56488c0efb184 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 6 Jun 2024 13:14:41 -0400
Subject: working on creating ink strokes from pts

---
 src/client/apis/gpt/GPT.ts                         |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 37 ++++++++++++----------
 .../collectionFreeForm/SmartDrawHandler.tsx        | 27 ++++++++++++----
 3 files changed, 42 insertions(+), 24 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 5afb345a0..454ea8116 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -58,7 +58,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 256,
         temp: 0.5,
-        prompt: 'Given an item to draw, generate a list of Bezier control points that will represent the item. Answer only with the list of coordinates and no additional text',
+        prompt: 'Given an item to draw, generate Bezier control points that will represent the item. Answer only with a list of lists of coordinates, where each list of coordinates is one Bezier ink stroke. Do not include any text, description, or comments.',
     }
 };
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 194c99c3d..d22b3569e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1233,23 +1233,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     createDrawing = (e: PointerEvent, doubleTap?: boolean) => {
         SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY);
-        if (SmartDrawHandler.Instance.coords) {
-            // const coords: InkData = SmartDrawHandler.Instance.coords;
-            // const inkField = new InkField(coords);
-            // const points = coords.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
-            // const bounds = InkField.getBounds(points);
-            // const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
-            // const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
-            // return Docs.Create.InkDocument(
-            //     points,
-            //     { title: 'stroke',
-            //     x: B.x - inkWidth / 2,
-            //     y: B.y - inkWidth / 2,
-            //     _width: B.width + inkWidth,
-            //     _height: B.height + inkWidth,
-            //     stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-            //     inkWidth
-            // );
+        if (SmartDrawHandler.Instance.strokes.length > 0) {
+            const strokeList: InkData[] = SmartDrawHandler.Instance.strokes;
+            strokeList.forEach(coords => {
+                // const stroke = new InkField(coords);
+                // const points = coords.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.X, Y: p.Y }) ?? { X: 0, Y: 0 }), [] as PointData[]);
+                const bounds = InkField.getBounds(coords);
+                const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+                const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+                const inkDoc = Docs.Create.InkDocument(
+                    coords,
+                    { title: 'stroke',
+                    x: B.x - inkWidth / 2,
+                    y: B.y - inkWidth / 2,
+                    _width: B.width + inkWidth,
+                    _height: B.height + inkWidth,
+                    stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                    inkWidth
+                );
+                this.addDocument(inkDoc);
+            });
         }
     };
 
diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
index fc88b5cc6..7e66a62d4 100644
--- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
+++ b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
@@ -23,7 +23,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _yRelativeToTop: boolean = true;
     @observable private _isLoading: boolean = false;
     @observable private _userInput: string = '';
-    @observable public coords: InkData | undefined = undefined;
+    @observable public strokes: InkData[] = [];
 
     constructor(props: any) {
         super(props);
@@ -55,6 +55,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
 
     @action
     drawWithGPT = async (startPoint: {X: number, Y: number}, input: string) => {
+        console.log("start point is", startPoint);
         this.setIsLoading(true);
         try {
             const res = await gptAPICall(input, GPTCallType.DRAW);
@@ -63,13 +64,27 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log("GPT response:", res);
-            // controlPts: {X: number, Y: number}[] = []
-            // code to extract list of coords from the string
-            // this.coords = controlPts.map(pt: {X: number, Y: number } => {pt.X + startPoint.X, pt.Y + startPoint.Y});
-           
+            try {
+                // const controlPts: [number, number][][] = JSON.parse(res) as [number, number][][];
+                // console.log("Control Points", controlPts);
+                // const transformedPts: { X: number; Y: number }[][] = [];
+                // controlPts.forEach(stroke => {
+                //     stroke.map(pt => {
+                //         pt.X += startPoint.X, pt.Y += startPoint.Y;
+                //     });
+                //     transformedPts.push(stroke);
+                // });
+
+                const controlPts: { X: number; Y: number }[][] = JSON.parse(res).map((stroke: [number, number][]) => 
+                    stroke.map(([X, Y]) => ({ X: X + startPoint.X, Y: Y + startPoint.Y })));
+                console.log("transformed points", controlPts);
+                this.strokes = controlPts;
+            } catch (err) {
+                console.error('Incompatible GPT output type');
+            }
 
         } catch (err) {
-            console.error('GPT call failed');
+            console.error('GPT call failed', err);
         }
 
         this.setIsLoading(false);
-- 
cgit v1.2.3-70-g09d2


From 33761fc2227458acf36a5cc4b1f08eaae6e58695 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 11 Jun 2024 11:15:25 -0400
Subject: some changes

---
 src/client/apis/gpt/GPT.ts                         |   2 +-
 src/client/util/CurrentUserUtils.ts                |   1 +
 src/client/util/bezierFit.ts                       |   6 +
 src/client/views/MainView.tsx                      |   1 +
 src/client/views/MarqueeAnnotator.tsx              | 147 +++++++++++++++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 170 +++++++++++++--------
 .../collectionFreeForm/SmartDrawHandler.tsx        |  31 ++--
 src/client/views/pdf/AnchorMenu.tsx                |  14 ++
 src/client/views/pdf/PDFViewer.tsx                 |   3 +
 9 files changed, 293 insertions(+), 82 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 454ea8116..e02488607 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -58,7 +58,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 256,
         temp: 0.5,
-        prompt: 'Given an item to draw, generate Bezier control points that will represent the item. Answer only with a list of lists of coordinates, where each list of coordinates is one Bezier ink stroke. Do not include any text, description, or comments.',
+        prompt: 'Given an item to draw, generate Bezier control points that will represent the item. Answer only with a list of lists of coordinates, where each list of coordinates is one Bezier ink stroke. Remember that Bezier curves will smooth out along control points, so try to keep as much in one stroke as possible. However, if there is an edge or corner be sure to split into a new stroke. Make sure you generate control handle points as well as the actual anchor points. Do not include any text, description, or comments. ONLY USE INTEGERS, NOT DECIMALS.',
     }
 };
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 3250f10a8..1eb2d9cc1 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -730,6 +730,7 @@ pie title Minerals in my tap water
     static inkTools():Button[] {
         return [
             { title: "Pen",    toolTip: "Pen (Ctrl+P)",                     btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                            scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
+            { title: "Highlight", toolTip: "Highlight (Ctrl+H)",        btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter",                    scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",             btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
             { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.MultiToggleButton,                                                            scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {toolType:"activeEraserTool()"},
               subMenu: [
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index d6f3f2340..bb3b6b1eb 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -557,6 +557,12 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     const negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y);
     FitCubic(d, splitPoint2D, last, negThatCenter, tHat2, error, result);
 }
+/**
+ * Convert polyline coordinates to a (multi) segment bezier curve
+ * @param d - polyline coordinates
+ * @param error - how much error to allow in fitting (measured in pixels)
+ * @returns 
+ */
 export function FitCurve(d: Point[], error: number) {
     const tHat1 = ComputeLeftTangent(d, 0); //  Unit tangent vectors at endpoints
     const tHat2 = ComputeRightTangent(d, d.length - 1);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f7e1617fc..a1cb44106 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -402,6 +402,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faPortrait,
                 fa.faRedoAlt,
                 fa.faStamp,
+                fa.faTape,
                 fa.faStickyNote,
                 fa.faArrowsAltV,
                 fa.faTimesCircle,
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index c18ac6738..db48e095d 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -28,6 +28,7 @@ export interface MarqueeAnnotatorProps {
     marqueeContainer: HTMLDivElement;
     docView: () => DocumentView;
     savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>;
+    savedTapes: () => ObservableMap<number, HTMLDivElement[]>;
     selectionText: () => string;
     annotationLayer: HTMLDivElement;
     addDocument: (doc: Doc) => boolean;
@@ -73,7 +74,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
                 onClick: isLinkButton ? FollowLinkScript() : undefined,
                 backgroundColor: color,
                 annotationOn: this.props.Document,
-                title: 'Annotation on ' + this.props.Document.title,
+                title: 'Annotation on ' + this.props.Document.title,a
             });
             marqueeAnno.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
             marqueeAnno.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
@@ -127,6 +128,140 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         savedAnnoMap.clear();
         return textRegionAnno;
     };
+
+    // @undoBatch
+    // makeTapeDocument = (color: string, isLinkButton?: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
+    //     const savedTapeMap = savedTapes?.values() && Array.from(savedTapes?.values()).length ? savedTapes : this.props.savedTapes();
+    //     if (savedTapeMap.size === 0) return undefined;
+    //     const tapes = Array.from(savedTapeMap.values())[0];
+    //     const doc = this.props.Document;
+    //     const scale = (this.props.annotationLayerScaling?.() || 1) * NumCast(doc._freeform_scale, 1);
+    //     if (tapes.length && (tapes[0] as any).marqueeing) {
+    //         const anno = tapes[0];
+    //         const containerOffset = this.props.containerOffset?.() || [0, 0];
+    //         const tape = Docs.Create.FreeformDocument([], {
+    //             onClick: isLinkButton ? FollowLinkScript() : undefined,
+    //             backgroundColor: color,
+    //             annotationOn: this.props.Document,
+    //             title: 'Tape on ' + this.props.Document.title,
+    //         });
+    //         tape.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
+    //         tape.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
+    //         tape._height = parseInt(anno.style.height || '0') / scale;
+    //         tape._width = parseInt(anno.style.width || '0') / scale;
+    //         anno.remove();
+    //         savedTapeMap.clear();
+    //         return tape;
+    //     }
+
+    // const textRegionAnno = Docs.Create.ConfigDocument({
+    //     annotationOn: this.props.Document,
+    //     text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too.
+    //     text_html: this.props.selectionText() as any,
+    //     backgroundColor: 'transparent',
+    //     presentation_duration: 2100,
+    //     presentation_transition: 500,
+    //     presentation_zoomText: true,
+    //     title: '>' + this.props.Document.title,
+    // });
+    // const textRegionAnnoProto = textRegionAnno[DocData];
+    // let minX = Number.MAX_VALUE;
+    // let maxX = -Number.MAX_VALUE;
+    // let minY = Number.MAX_VALUE;
+    // let maxY = -Number.MIN_VALUE;
+    // const annoRects: string[] = [];
+    // savedAnnoMap.forEach((value: HTMLDivElement[]) =>
+    //     value.forEach(anno => {
+    //         const x = parseInt(anno.style.left ?? '0');
+    //         const y = parseInt(anno.style.top ?? '0');
+    //         const height = parseInt(anno.style.height ?? '0');
+    //         const width = parseInt(anno.style.width ?? '0');
+    //         annoRects.push(`${x}:${y}:${width}:${height}`);
+    //         anno.remove();
+    //         minY = Math.min(NumCast(y), minY);
+    //         minX = Math.min(NumCast(x), minX);
+    //         maxY = Math.max(NumCast(y) + NumCast(height), maxY);
+    //         maxX = Math.max(NumCast(x) + NumCast(width), maxX);
+    //     })
+    // );
+
+    // textRegionAnnoProto.y = Math.max(minY, 0);
+    // textRegionAnnoProto.x = Math.max(minX, 0);
+    // textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0);
+    // textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0);
+    // textRegionAnnoProto.backgroundColor = color;
+    // // mainAnnoDocProto.text = this._selectionText;
+    // textRegionAnnoProto.text_inlineAnnotations = new List<string>(annoRects);
+    // textRegionAnnoProto.opacity = 0;
+    // textRegionAnnoProto.layout_unrendered = true;
+    // savedAnnoMap.clear();
+    // return textRegionAnno;
+    // };
+
+    @undoBatch
+    makeTapeDocument = (color: string, isLinkButton?: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
+        // const savedAnnoMap = savedTapes?.values() && Array.from(savedTapes?.values()).length ? savedTapes : this.props.savedTapes();
+        // if (savedAnnoMap.size === 0) return undefined;
+        // const savedAnnos = Array.from(savedAnnoMap.values())[0];
+        const doc = this.props.Document;
+        const scale = (this.props.annotationLayerScaling?.() || 1) * NumCast(doc._freeform_scale, 1);
+        const marqueeAnno = Docs.Create.FreeformDocument([], {
+            onClick: isLinkButton ? FollowLinkScript() : undefined,
+            backgroundColor: color,
+            annotationOn: this.props.Document,
+            title: 'Annotation on ' + this.props.Document.title,
+        });
+        marqueeAnno.x = NumCast(doc.freeform_panX_min) / scale;
+        marqueeAnno.y = NumCast(doc.freeform_panY_min) / scale;
+        marqueeAnno._height = parseInt('100') / scale;
+        marqueeAnno._width = parseInt('100') / scale;
+            return marqueeAnno;
+        // }
+
+        // const textRegionAnno = Docs.Create.ConfigDocument({
+        //     annotationOn: this.props.Document,
+        //     text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too.
+        //     text_html: this.props.selectionText() as any,
+        //     backgroundColor: 'transparent',
+        //     presentation_duration: 2100,
+        //     presentation_transition: 500,
+        //     presentation_zoomText: true,
+        //     title: '>' + this.props.Document.title,
+        // });
+        // const textRegionAnnoProto = textRegionAnno[DocData];
+        // let minX = Number.MAX_VALUE;
+        // let maxX = -Number.MAX_VALUE;
+        // let minY = Number.MAX_VALUE;
+        // let maxY = -Number.MIN_VALUE;
+        // const annoRects: string[] = [];
+        // savedAnnoMap.forEach((value: HTMLDivElement[]) =>
+        //     value.forEach(anno => {
+        //         const x = parseInt(anno.style.left ?? '0');
+        //         const y = parseInt(anno.style.top ?? '0');
+        //         const height = parseInt(anno.style.height ?? '0');
+        //         const width = parseInt(anno.style.width ?? '0');
+        //         annoRects.push(`${x}:${y}:${width}:${height}`);
+        //         anno.remove();
+        //         minY = Math.min(NumCast(y), minY);
+        //         minX = Math.min(NumCast(x), minX);
+        //         maxY = Math.max(NumCast(y) + NumCast(height), maxY);
+        //         maxX = Math.max(NumCast(x) + NumCast(width), maxX);
+        //     })
+        // );
+
+        // textRegionAnnoProto.y = Math.max(minY, 0);
+        // textRegionAnnoProto.x = Math.max(minX, 0);
+        // textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0);
+        // textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0);
+        // textRegionAnnoProto.backgroundColor = color;
+        // // mainAnnoDocProto.text = this._selectionText;
+        // textRegionAnnoProto.text_inlineAnnotations = new List<string>(annoRects);
+        // textRegionAnnoProto.opacity = 0;
+        // textRegionAnnoProto.layout_unrendered = true;
+        // savedAnnoMap.clear();
+        // return textRegionAnno;
+    };
+
     @action
     highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
         // creates annotation documents for current highlights
@@ -136,6 +271,15 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         return annotationDoc as Doc;
     };
 
+    @action
+    tape = (color: string, isLinkButton: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
+        // creates annotation documents for current highlights
+        const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]);
+        const tape = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeTapeDocument(color, isLinkButton, savedTapes);
+        addAsAnnotation && tape && this.props.addDocument(tape);
+        return tape as Doc;
+    };
+
     public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, HTMLDivElement[]>, annotationLayer: HTMLDivElement, div: HTMLDivElement, page: number) => {
         div.style.backgroundColor = '#ACCEF7';
         div.style.opacity = '0.5';
@@ -182,6 +326,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         AnchorMenu.Instance.OnClick = undoable(() => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation');
         AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = (color: string) => this.highlight(color, false, undefined, true);
+        AnchorMenu.Instance.Tape = (color: string) => this.tape(color, false, undefined, true);
         AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]> /* , addAsAnnotation?: boolean */) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
         AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d22b3569e..e66dbd796 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -144,7 +144,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             : this._props.childPointerEvents?.() ??
                   (this._props.viewDefDivClick || //
                   (this.layoutEngine === computePassLayout.name && !this._props.isSelected()) ||
-                  this.isContentActive() === false
+                  this.isContentActive() === false ||
+                  Doc.ActiveTool === InkTool.RadiusEraser ||
+                  Doc.ActiveTool === InkTool.SegmentEraser ||
+                  Doc.ActiveTool === InkTool.StrokeEraser
                       ? 'none'
                       : this._props.pointerEvents?.());
     }
@@ -501,27 +504,24 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                 switch (Doc.ActiveTool) {
                     case InkTool.Highlighter:
-                        break;
                     case InkTool.Write:
-                        break;
                     case InkTool.Pen:
                         break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
                     case InkTool.StrokeEraser:
                     case InkTool.SegmentEraser:
-                        this._batch = UndoManager.StartBatch('collectionErase');
-                        this._eraserPts.length = 0;
-                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction, hit !== -1, false);
-                        break;
                     case InkTool.RadiusEraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         this._eraserPts.length = 0;
-                        setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction, hit !== -1, false);
+                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1);
+                        e.stopPropagation();
                         break;
                     case InkTool.SmartDraw:
-                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.createDrawing, hit !== -1, false);
+                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.createDrawing, hit !== -1);
+                        e.stopPropagation();
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
-                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1);
+                            e.stopPropagation();
                         }
                         break;
                     default:
@@ -608,50 +608,83 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     _eraserLock = 0;
     _eraserPts: number[][] = []; // keep track of the last few eraserPts to make the eraser circle 'stretch'
 
-    /**
-     * Erases strokes by intersecting them with an invisible "eraser stroke".
-     *  By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
-     * and deletes the original stroke.
-     */
-    @action
-    onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
+    erase = (e: PointerEvent, delta: number[]) => {
         const currPoint = { X: e.clientX, Y: e.clientY };
         this._eraserPts.push([currPoint.X, currPoint.Y]);
         this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
-        // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future
-        this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
-            if (!this._deleteList.includes(intersect.inkView)) {
-                this._deleteList.push(intersect.inkView);
-                SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
-                SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
-                // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
-                if (Doc.ActiveTool !== InkTool.StrokeEraser) {
-                    // this._eraserLock++;
-                    const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
-                    const newStrokes = segments?.map(segment => {
-                        const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
-                        const bounds = InkField.getBounds(points);
-                        const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
-                        const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
-                        return Docs.Create.InkDocument(
-                            points,
-                            { title: 'stroke',
+        if (Doc.ActiveTool === InkTool.RadiusEraser) {
+            const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
+
+            strokeMap.forEach((intersects, stroke) => {
+                if (!this._deleteList.includes(stroke)) {
+                    this._deleteList.push(stroke);
+                    SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
+                    SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
+                    const segments = this.radiusErase(stroke, intersects.sort());
+                    segments?.forEach(segment =>
+                        this.forceStrokeGesture(
+                            e,
+                            Gestures.Stroke,
+                            segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
+                        )
+                    );
+                }
+                stroke.layoutDoc.opacity = 0;
+                stroke.layoutDoc.dontIntersect = true;
+            });
+        } else {
+            this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
+                if (!this._deleteList.includes(intersect.inkView)) {
+                    this._deleteList.push(intersect.inkView);
+                    SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
+                    SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
+                    // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
+                    if (Doc.ActiveTool !== InkTool.StrokeEraser) {
+                        // this._eraserLock++;
+                        const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
+                        const newStrokes = segments?.map(segment => {
+                            const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
+                            const bounds = InkField.getBounds(points);
+                            const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+                            const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+                            return Docs.Create.InkDocument(
+                                points,
+                                { title: 'stroke',
                             x: B.x - inkWidth / 2,
                             y: B.y - inkWidth / 2,
                             _width: B.width + inkWidth,
                             _height: B.height + inkWidth,
                             stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                            inkWidth
-                        );
-                    });
-                    newStrokes && this.addDocument?.(newStrokes);
-                    // setTimeout(() => this._eraserLock--);
+                                inkWidth
+                            );
+                        });
+                        newStrokes && this.addDocument?.(newStrokes);
+                        // setTimeout(() => this._eraserLock--);
+                    }
+                    // Lower ink opacity to give the user a visual indicator of deletion.
+                    intersect.inkView.layoutDoc.opacity = 0;
+                    intersect.inkView.layoutDoc.dontIntersect = true;
                 }
-                // Lower ink opacity to give the user a visual indicator of deletion.
-                intersect.inkView.layoutDoc.opacity = 0;
-                intersect.inkView.layoutDoc.dontIntersect = true;
-            }
-        });
+            });
+        }
+        return false;
+    };
+
+    /**
+     * Erases strokes by intersecting them with an invisible "eraser stroke".
+     *  By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
+     * and deletes the original stroke.
+     */
+    @action
+    onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
+        this.erase(e, delta);
+        // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future
+        return false;
+    };
+
+    @action
+    onEraserClick = (e: PointerEvent, doubleTap?: boolean) => {
+        this.erase(e, [0, 0]);
         return false;
     };
 
@@ -720,7 +753,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // increase radius slightly based on the erased stroke's width, added to make eraser look more realistic
         const radius = ActiveEraserWidth() + 5 + inkStrokeWidth * 0.1; // add 5 to prevent eraser from being too small
         const c = 0.551915024494; // circle tangent length to side ratio
-        const movement = { x: endInkCoordsIn.X - startInkCoordsIn.X, y: endInkCoordsIn.Y - startInkCoordsIn.Y };
+        const movement = { x: Math.max(endInkCoordsIn.X - startInkCoordsIn.X, 1), y: Math.max(endInkCoordsIn.Y - startInkCoordsIn.Y, 1) };
         const moveLen = Math.sqrt(movement.x ** 2 + movement.y ** 2);
         const direction = { x: (movement.x / moveLen) * radius, y: (movement.y / moveLen) * radius };
         const normal    = { x: -direction.y, y: direction.x }; // prettier-ignore
@@ -1232,28 +1265,29 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     createDrawing = (e: PointerEvent, doubleTap?: boolean) => {
-        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY);
-        if (SmartDrawHandler.Instance.strokes.length > 0) {
-            const strokeList: InkData[] = SmartDrawHandler.Instance.strokes;
-            strokeList.forEach(coords => {
-                // const stroke = new InkField(coords);
-                // const points = coords.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.X, Y: p.Y }) ?? { X: 0, Y: 0 }), [] as PointData[]);
-                const bounds = InkField.getBounds(coords);
-                const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
-                const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
-                const inkDoc = Docs.Create.InkDocument(
-                    coords,
-                    { title: 'stroke',
+        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createInkStroke);
+    };
+
+    @action
+    createInkStroke = (strokeList: InkData[]) => {
+        strokeList.forEach(coords => {
+            // const stroke = new InkField(coords);
+            // const points = coords.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.X, Y: p.Y }) ?? { X: 0, Y: 0 }), [] as PointData[]);
+            const bounds = InkField.getBounds(coords);
+            const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+            const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+            const inkDoc = Docs.Create.InkDocument(
+                coords,
+                { title: 'stroke',
                     x: B.x - inkWidth / 2,
                     y: B.y - inkWidth / 2,
                     _width: B.width + inkWidth,
                     _height: B.height + inkWidth,
                     stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                    inkWidth
-                );
-                this.addDocument(inkDoc);
-            });
-        }
+                inkWidth
+            );
+            this.addDocument(inkDoc);
+        });
     };
 
     @action
@@ -1849,8 +1883,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     onCursorMove = (e: React.PointerEvent) => {
-        this._eraserX = e.clientX;
-        this._eraserY = e.clientY;
+        const locPt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY);
+        this._eraserX = locPt[0];
+        this._eraserY = locPt[1];
+        // Doc.ActiveTool === InkTool.RadiusEraser ? this._childPointerEvents = 'none' : this._childPointerEvents = 'all'
         //  super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     };
 
@@ -2161,8 +2197,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         onPointerMove={this.onCursorMove}
                         style={{
                             position: 'fixed',
-                            left: this._eraserX - 60,
-                            top: this._eraserY - 100,
+                            left: this._eraserX,
+                            top: this._eraserY,
                             width: (ActiveEraserWidth() + 5) * 2,
                             height: (ActiveEraserWidth() + 5) * 2,
                             borderRadius: '50%',
diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
index 7e66a62d4..fc8f7a429 100644
--- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
+++ b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
@@ -10,7 +10,6 @@ import { AiOutlineSend } from 'react-icons/ai';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './ImageLabelHandler.scss';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
-import { InkingStroke } from '../../InkingStroke';
 import { InkData } from '../../../../fields/InkField';
 
 @observer
@@ -23,7 +22,8 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _yRelativeToTop: boolean = true;
     @observable private _isLoading: boolean = false;
     @observable private _userInput: string = '';
-    @observable public strokes: InkData[] = [];
+    // @observable public strokes: InkData[] = [];
+    private _addToDocFunc: (strokeList: InkData[]) => void = () => {};
 
     constructor(props: any) {
         super(props);
@@ -42,10 +42,11 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    displaySmartDrawHandler = (x: number, y: number) => {
+    displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeList: InkData[]) => void) => {
         this._pageX = x;
         this._pageY = y;
         this._display = true;
+        this._addToDocFunc = addToDoc;
     };
 
     @action
@@ -54,8 +55,11 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    drawWithGPT = async (startPoint: {X: number, Y: number}, input: string) => {
-        console.log("start point is", startPoint);
+    waitForCoords = async () => {};
+
+    @action
+    drawWithGPT = async (startPoint: { X: number; Y: number }, input: string) => {
+        console.log('start point is', startPoint);
         this.setIsLoading(true);
         try {
             const res = await gptAPICall(input, GPTCallType.DRAW);
@@ -63,8 +67,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 console.error('GPT call failed');
                 return;
             }
-            console.log("GPT response:", res);
-            try {
+            console.log('GPT response:', res);
                 // const controlPts: [number, number][][] = JSON.parse(res) as [number, number][][];
                 // console.log("Control Points", controlPts);
                 // const transformedPts: { X: number; Y: number }[][] = [];
@@ -74,15 +77,17 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 //     });
                 //     transformedPts.push(stroke);
                 // });
+            const simplifiedRes: string = res.replace(/[^\d\[\],]/g, '');
+            console.log(simplifiedRes)
+            try {
+                const controlPts: { X: number; Y: number }[][] = JSON.parse(simplifiedRes).map((stroke: [number, number][]) => stroke.map(([X, Y]) => ({ X: X + startPoint.X, Y: Y + startPoint.Y })));
+                console.log('transformed points', controlPts);
 
-                const controlPts: { X: number; Y: number }[][] = JSON.parse(res).map((stroke: [number, number][]) => 
-                    stroke.map(([X, Y]) => ({ X: X + startPoint.X, Y: Y + startPoint.Y })));
-                console.log("transformed points", controlPts);
-                this.strokes = controlPts;
+                // this.strokes = controlPts;
+                this._addToDocFunc(controlPts);
             } catch (err) {
                 console.error('Incompatible GPT output type');
             }
-
         } catch (err) {
             console.error('GPT call failed', err);
         }
@@ -124,7 +129,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             iconPlacement="right"
                             color={MarqueeOptionsMenu.Instance.userColor}
                             onClick={e => {
-                                this.drawWithGPT({X: e.clientX, Y: e.clientY}, this._userInput);
+                                this.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._userInput);
                             }}
                         />
                         {/* <IconButton
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 2f6824466..df990b0c0 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -51,6 +51,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public Highlight: (color: string) => Opt<Doc> = (/* color: string */) => undefined;
+    public Tape: (color: string) => Opt<Doc> = (/* color: string */) => undefined;
     public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = emptyFunction;
     public Delete: () => void = unimplementedFunction;
     public PinToPres: () => void = unimplementedFunction;
@@ -172,6 +173,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         AnchorMenu.Instance.fadeOut(true);
     };
 
+    @action
+    tapeClicked = () => {
+        this.Tape(this.highlightColor);
+        // AnchorMenu.Instance.fadeOut(true);
+    };
+
     @computed get highlighter() {
         return (
             <Group>
@@ -182,6 +189,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                     colorPicker={this.highlightColor}
                     color={SettingsManager.userColor}
                 />
+                <IconButton
+                    tooltip="Click to Add Tape" //
+                    icon={<FontAwesomeIcon icon="tape" />}
+                    onClick={this.tapeClicked}
+                    colorPicker={this.highlightColor}
+                    color={SettingsManager.userColor}
+                />
                 <ColorPicker selectedColor={this.highlightColor} setFinalColor={this.changeHighlightColor} setSelectedColor={this.changeHighlightColor} size={Size.XSMALL} />
             </Group>
         );
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 6c1617c38..9ca05965b 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -67,6 +67,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
 
     @observable _pageSizes: { width: number; height: number }[] = [];
     @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+    @observable _savedTapes = new ObservableMap<number, HTMLDivElement[]>();
     @observable _textSelecting = true;
     @observable _showWaiting = true;
     @observable Index: number = -1;
@@ -583,6 +584,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         return <div className={'pdfViewerDash-text' + (this._props.pointerEvents?.() !== 'none' && this._textSelecting && this._props.isContentActive() ? '-selected' : '')} ref={this._viewer} />;
     }
     savedAnnotations = () => this._savedAnnotations;
+    savedTapes = () => this._savedTapes;
     addDocumentWrapper = (doc: Doc | Doc[]) => this._props.addDocument!(doc);
     render() {
         TraceMobx();
@@ -616,6 +618,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
                             docView={this._props.pdfBox.DocumentView!}
                             finishMarquee={this.finishMarquee}
                             savedAnnotations={this.savedAnnotations}
+                            savedTapes={this.savedTapes}
                             selectionText={this.selectionText}
                             annotationLayer={this._annotationLayer.current}
                             marqueeContainer={this._mainCont.current}
-- 
cgit v1.2.3-70-g09d2


From b6ae411cfa04f6736d91749e6c99beb8179b3a30 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Fri, 14 Jun 2024 14:23:56 -0400
Subject: looking for weird error

---
 src/client/apis/gpt/GPT.ts                         |  6 +-
 src/client/util/CurrentUserUtils.ts                |  2 +-
 src/client/util/bezierFit.ts                       | 26 +++++-
 src/client/views/MainView.tsx                      |  4 +-
 src/client/views/MarqueeAnnotator.tsx              |  5 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 26 +++---
 .../collectionFreeForm/SmartDrawHandler.tsx        | 95 +++++++++-------------
 7 files changed, 86 insertions(+), 78 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index e02488607..0c993680e 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -56,10 +56,10 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
     },
     draw: {
         model: 'gpt-4o',
-        maxTokens: 256,
+        maxTokens: 1024,
         temp: 0.5,
-        prompt: 'Given an item to draw, generate Bezier control points that will represent the item. Answer only with a list of lists of coordinates, where each list of coordinates is one Bezier ink stroke. Remember that Bezier curves will smooth out along control points, so try to keep as much in one stroke as possible. However, if there is an edge or corner be sure to split into a new stroke. Make sure you generate control handle points as well as the actual anchor points. Do not include any text, description, or comments. ONLY USE INTEGERS, NOT DECIMALS.',
-    }
+        prompt: 'I would like you to generate me vector art with Bezier curves. Given a prompt, generate a sequence of cubic Bezier coordinates in the range of 0 to 200 (unless specified larger/smaller) that creates a line drawing of the object. Respond only with the coordinates',
+    },
 };
 
 let lastCall = '';
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 1eb2d9cc1..b6a7cacba 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -730,7 +730,7 @@ pie title Minerals in my tap water
     static inkTools():Button[] {
         return [
             { title: "Pen",    toolTip: "Pen (Ctrl+P)",                     btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                            scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
-            { title: "Highlight", toolTip: "Highlight (Ctrl+H)",        btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter",                    scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
+            { title: "Highlight", toolTip: "Highlight (Ctrl+H)",            btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter",                    scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",             btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
             { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.MultiToggleButton,                                                            scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {toolType:"activeEraserTool()"},
               subMenu: [
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index bb3b6b1eb..fbc2bb7cd 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -561,7 +561,7 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
  * Convert polyline coordinates to a (multi) segment bezier curve
  * @param d - polyline coordinates
  * @param error - how much error to allow in fitting (measured in pixels)
- * @returns 
+ * @returns
  */
 export function FitCurve(d: Point[], error: number) {
     const tHat1 = ComputeLeftTangent(d, 0); //  Unit tangent vectors at endpoints
@@ -592,6 +592,30 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) {
     return { finalCtrls, error };
 }
 
+// alpha determines how far away the tangents are, or the "tightness" of the bezier
+export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
+    const firstEnd = coordinates.length ? [coordinates[0], coordinates[0]] : [];
+    const lastEnd = coordinates.length ? [coordinates.lastElement(), coordinates.lastElement()] : [];
+    const points: Point[] = coordinates.slice(1, coordinates.length - 1).flatMap((pt, index, inkData) => {
+        const prevPt: Point = index === 0 ? firstEnd[0] : inkData[index - 1];
+        const nextPt: Point = index === inkData.length - 1 ? lastEnd[0] : inkData[index + 1];
+        if (prevPt.X === nextPt.X) {
+            const verticalDist = nextPt.Y - prevPt.Y;
+            return [{ X: pt.X, Y: pt.Y - alpha * verticalDist }, pt, pt, { X: pt.X, Y: pt.Y + alpha * verticalDist }];
+        } else if (prevPt.Y === nextPt.Y) {
+            const horizDist = nextPt.X - prevPt.X;
+            return [{ X: pt.X - alpha * horizDist, Y: pt.Y }, pt, pt, { X: pt.X + alpha * horizDist, Y: pt.Y }];
+        }
+        // tangent vectors between the adjacent points
+        const tanX = nextPt.X - prevPt.X;
+        const tanY = nextPt.Y - prevPt.Y;
+        const ctrlPt1: Point = { X: pt.X - alpha * tanX, Y: pt.Y - alpha * tanY };
+        const ctrlPt2: Point = { X: pt.X + alpha * tanX, Y: pt.Y + alpha * tanY };
+        return [ctrlPt1, pt, pt, ctrlPt2];
+    });
+    return [...firstEnd, ...points, ...lastEnd];
+}
+
 /*
 static double                GetTValueFromSValue  (const BezierRep &parent, double t, double endT, bool left, double influenceDistance, double &excess) {
 double dist = 0;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a1cb44106..44e00396e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -55,7 +55,6 @@ import { TabDocView } from './collections/TabDocView';
 import './collections/TreeView.scss';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { ImageLabelHandler } from './collections/collectionFreeForm/ImageLabelHandler';
-import { SmartDrawHandler } from './collections/collectionFreeForm/SmartDrawHandler';
 import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
 import { CollectionLinearView } from './collections/collectionLinear';
 import { LinkMenu } from './linking/LinkMenu';
@@ -318,6 +317,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faCompass,
                 fa.faSnowflake,
                 fa.faStar,
+                fa.faSplotch,
                 fa.faMicrophone,
                 fa.faCircleHalfStroke,
                 fa.faKeyboard,
@@ -402,7 +402,6 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faPortrait,
                 fa.faRedoAlt,
                 fa.faStamp,
-                fa.faTape,
                 fa.faStickyNote,
                 fa.faArrowsAltV,
                 fa.faTimesCircle,
@@ -1092,7 +1091,6 @@ export class MainView extends ObservableReactComponent<{}> {
                 <TaskCompletionBox />
                 <ContextMenu />
                 <ImageLabelHandler />
-                <SmartDrawHandler />
                 <AnchorMenu />
                 <MapAnchorMenu />
                 <DirectionsAnchorMenu />
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index db48e095d..f06f3efe0 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -74,7 +74,8 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
                 onClick: isLinkButton ? FollowLinkScript() : undefined,
                 backgroundColor: color,
                 annotationOn: this.props.Document,
-                title: 'Annotation on ' + this.props.Document.title,a
+                title: 'Annotation on ' + this.props.Document.title,
+                a,
             });
             marqueeAnno.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
             marqueeAnno.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
@@ -215,7 +216,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         marqueeAnno.y = NumCast(doc.freeform_panY_min) / scale;
         marqueeAnno._height = parseInt('100') / scale;
         marqueeAnno._width = parseInt('100') / scale;
-            return marqueeAnno;
+        return marqueeAnno;
         // }
 
         // const textRegionAnno = Docs.Create.ConfigDocument({
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e66dbd796..a27ac2a0c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,12 +1,13 @@
 /* eslint-disable react/jsx-props-no-spreading */
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 /* eslint-disable jsx-a11y/no-static-element-interactions */
-import { Bezier } from 'bezier-js';
+import { Bezier, Point } from 'bezier-js';
 import { Colors } from 'browndash-components';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
+import { TbAlpha } from 'react-icons/tb';
 import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
 import { ActiveEraserWidth, ActiveInkWidth, Doc, DocListCast, Field, FieldType, Opt, SetActiveInkColor, SetActiveInkWidth } from '../../../../fields/Doc';
@@ -26,6 +27,7 @@ import { aggregateBounds, clamp, emptyFunction, intersectRect, Utils } from '../
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocUtils } from '../../../documents/DocUtils';
+import { FitCurve, GenerateControlPoints } from '../../../util/bezierFit';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
 import { CompileScript } from '../../../util/Scripting';
@@ -55,7 +57,6 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import { SmartDrawHandler } from './SmartDrawHandler';
-import { ImageLabelHandler } from './ImageLabelHandler';
 
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -144,10 +145,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             : this._props.childPointerEvents?.() ??
                   (this._props.viewDefDivClick || //
                   (this.layoutEngine === computePassLayout.name && !this._props.isSelected()) ||
-                  this.isContentActive() === false ||
-                  Doc.ActiveTool === InkTool.RadiusEraser ||
-                  Doc.ActiveTool === InkTool.SegmentEraser ||
-                  Doc.ActiveTool === InkTool.StrokeEraser
+                  this.isContentActive() === false
                       ? 'none'
                       : this._props.pointerEvents?.());
     }
@@ -685,6 +683,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     onEraserClick = (e: PointerEvent, doubleTap?: boolean) => {
         this.erase(e, [0, 0]);
+        e.stopPropagation();
         return false;
     };
 
@@ -1265,19 +1264,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     createDrawing = (e: PointerEvent, doubleTap?: boolean) => {
-        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createInkStroke);
+        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createInkStrokes);
     };
 
     @action
-    createInkStroke = (strokeList: InkData[]) => {
-        strokeList.forEach(coords => {
-            // const stroke = new InkField(coords);
-            // const points = coords.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.X, Y: p.Y }) ?? { X: 0, Y: 0 }), [] as PointData[]);
-            const bounds = InkField.getBounds(coords);
+    createInkStrokes = (strokeList: InkData[], alpha?: number) => {
+        console.log(strokeList.length);
+        strokeList.forEach(inkData => {
+            // const points: InkData = FitCurve(inkData, 20) as InkData;
+            const allPts = GenerateControlPoints(inkData, alpha);
+            const bounds = InkField.getBounds(allPts);
             const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
             const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
             const inkDoc = Docs.Create.InkDocument(
-                coords,
+                allPts,
                 { title: 'stroke',
                     x: B.x - inkWidth / 2,
                     y: B.y - inkWidth / 2,
diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
index fc8f7a429..4c2e78e31 100644
--- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
+++ b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
@@ -11,6 +11,7 @@ import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './ImageLabelHandler.scss';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
 import { InkData } from '../../../../fields/InkField';
+import { ButtonType } from '../../nodes/FontIconBox/FontIconBox';
 
 @observer
 export class SmartDrawHandler extends ObservableReactComponent<{}> {
@@ -22,8 +23,11 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _yRelativeToTop: boolean = true;
     @observable private _isLoading: boolean = false;
     @observable private _userInput: string = '';
+    @observable private _drawingTypeToolTip = 'Create Geometric Drawing';
+    @observable private _drawingTypeIcon: 'star' | 'splotch' = 'star';
+    @observable private _alpha: number | undefined = undefined; // number between 0 and 1 that determines how rounded a drawing will be
     // @observable public strokes: InkData[] = [];
-    private _addToDocFunc: (strokeList: InkData[]) => void = () => {};
+    private _addToDocFunc: (strokeList: InkData[], alpha?: number) => void = () => {};
 
     constructor(props: any) {
         super(props);
@@ -42,7 +46,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeList: InkData[]) => void) => {
+    displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeList: InkData[], alpha?: number) => void) => {
         this._pageX = x;
         this._pageY = y;
         this._display = true;
@@ -50,13 +54,10 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    hideLabelhandler = () => {
+    hideSmartDrawHandler = () => {
         this._display = false;
     };
 
-    @action
-    waitForCoords = async () => {};
-
     @action
     drawWithGPT = async (startPoint: { X: number; Y: number }, input: string) => {
         console.log('start point is', startPoint);
@@ -68,25 +69,18 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log('GPT response:', res);
-                // const controlPts: [number, number][][] = JSON.parse(res) as [number, number][][];
-                // console.log("Control Points", controlPts);
-                // const transformedPts: { X: number; Y: number }[][] = [];
-                // controlPts.forEach(stroke => {
-                //     stroke.map(pt => {
-                //         pt.X += startPoint.X, pt.Y += startPoint.Y;
-                //     });
-                //     transformedPts.push(stroke);
-                // });
             const simplifiedRes: string = res.replace(/[^\d\[\],]/g, '');
-            console.log(simplifiedRes)
             try {
-                const controlPts: { X: number; Y: number }[][] = JSON.parse(simplifiedRes).map((stroke: [number, number][]) => stroke.map(([X, Y]) => ({ X: X + startPoint.X, Y: Y + startPoint.Y })));
-                console.log('transformed points', controlPts);
-
-                // this.strokes = controlPts;
-                this._addToDocFunc(controlPts);
+                const parsedPts = JSON.parse(simplifiedRes);
+                if (parsedPts[0][0][0]) {
+                    const controlPts = (parsedPts as [number, number][][]).map((stroke: [number, number][]) => stroke.map(([X, Y]) => ({ X: X + startPoint.X - 100, Y: Y + startPoint.Y - 100 })));
+                    this._addToDocFunc(controlPts, this._alpha);
+                } else {
+                    const controlPts = (parsedPts as [number, number][]).map(([X, Y]) => ({ X: X + startPoint.X - 100, Y: Y + startPoint.Y - 100 }));
+                    this._addToDocFunc([controlPts], this._alpha);
+                }
             } catch (err) {
-                console.error('Incompatible GPT output type');
+                console.error('Error likely from bad GPT output type');
             }
         } catch (err) {
             console.error('GPT call failed', err);
@@ -94,6 +88,19 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
 
         this.setIsLoading(false);
         this.setUserInput('');
+        this.hideSmartDrawHandler();
+    };
+
+    changeDrawingType = () => {
+        if (this._drawingTypeIcon === 'star') {
+            this._drawingTypeIcon = 'splotch';
+            this._drawingTypeToolTip = 'Create Rounded Drawing';
+            this._alpha = 0.2;
+        } else {
+            this._drawingTypeIcon = 'star';
+            this._drawingTypeToolTip = 'Create Geometric Drawing';
+            this._alpha = 0;
+        }
     };
 
     render() {
@@ -110,7 +117,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                         color: SettingsManager.userColor,
                     }}>
                     <div>
-                        <IconButton tooltip={'Cancel'} onPointerDown={this.hideLabelhandler} icon={<FontAwesomeIcon icon="xmark" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} />
+                        <IconButton tooltip={'Cancel'} onClick={this.hideSmartDrawHandler} icon={<FontAwesomeIcon icon="xmark" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} />
                         <input
                             aria-label="label-input"
                             id="new-label"
@@ -122,6 +129,16 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             }}
                             placeholder="Enter item to draw"
                         />
+                        <IconButton tooltip={this._drawingTypeToolTip} icon={<FontAwesomeIcon icon={this._drawingTypeIcon} />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '14px' }} onClick={this.changeDrawingType} />
+                        {/* <IconButton
+                            tooltip="Create Geometric Drawing"
+                            icon={<FontAwesomeIcon icon="star" />}
+                            color={MarqueeOptionsMenu.Instance.userColor}
+                            style={{ width: '14px' }}
+                            onClick={() => {
+                                this._alpha = 0;
+                            }}
+                        /> */}
                         <Button
                             style={{ alignSelf: 'flex-end' }}
                             text="Send"
@@ -132,38 +149,6 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                                 this.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._userInput);
                             }}
                         />
-                        {/* <IconButton
-                            tooltip={'Generate Drawing'}
-                            onPointerDown={() => {
-                                const input = document.getElementById('new-label') as HTMLInputElement;
-                                const newLabel = input.value;
-                                // this.addLabel(newLabel);
-                                // this._currentLabel = '';
-                                input.value = '';
-                            }}
-                            icon={<FontAwesomeIcon icon="plus" />}
-                            color={MarqueeOptionsMenu.Instance.userColor}
-                            style={{ width: '19px' }}
-                        />
-                        <IconButton tooltip={'Group Images'} icon={<FontAwesomeIcon icon="object-group" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> */}
-                    </div>
-                    <div>
-                        {/* {this._labelGroups.map(group => {
-                            return (
-                                <div>
-                                    <p>{group}</p>
-                                    <IconButton
-                                        tooltip={'Remove Label'}
-                                        onPointerDown={() => {
-                                            this.removeLabel(group);
-                                        }}
-                                        icon={'x'}
-                                        color={MarqueeOptionsMenu.Instance.userColor}
-                                        style={{ width: '19px' }}
-                                    />
-                                </div>
-                            );
-                        })} */}
                     </div>
                 </div>
             );
-- 
cgit v1.2.3-70-g09d2


From bd64bbd29a38ae4979b2165d1fa9b9c76c2600d5 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 18 Jun 2024 14:10:40 -0400
Subject: svg to bezier conversion

---
 package-lock.json                                  |  73 +++++++++++++
 package.json                                       |   1 +
 src/client/apis/gpt/GPT.ts                         |   3 +-
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/util/bezierFit.ts                       | 116 +++++++++++++++++++++
 src/client/views/GestureOverlay.tsx                |  28 ++---
 src/client/views/MainView.tsx                      |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   8 +-
 .../collectionFreeForm/SmartDrawHandler.tsx        |  51 ++++++---
 9 files changed, 252 insertions(+), 32 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index b143dc5c1..30b2c9124 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -219,6 +219,7 @@
                 "stream-browserify": "^3.0.0",
                 "styled-components": "^6.1.1",
                 "supercluster": "^8.0.1",
+                "svgson": "^5.3.1",
                 "textarea-caret": "^3.1.0",
                 "tough-cookie": "^4.1.3",
                 "tslint": "^6.1.3",
@@ -16641,6 +16642,29 @@
             "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
             "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
         },
+        "node_modules/deep-rename-keys": {
+            "version": "0.2.1",
+            "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz",
+            "integrity": "sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==",
+            "dependencies": {
+                "kind-of": "^3.0.2",
+                "rename-keys": "^1.1.2"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/deep-rename-keys/node_modules/kind-of": {
+            "version": "3.2.2",
+            "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+            "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+            "dependencies": {
+                "is-buffer": "^1.1.5"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/deepmerge": {
             "version": "4.3.1",
             "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -22370,6 +22394,11 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/is-buffer": {
+            "version": "1.1.6",
+            "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+            "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+        },
         "node_modules/is-callable": {
             "version": "1.2.7",
             "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -36944,6 +36973,14 @@
                 "@types/unist": "*"
             }
         },
+        "node_modules/rename-keys": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz",
+            "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==",
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
         "node_modules/renderkid": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
@@ -38917,6 +38954,15 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/svgson": {
+            "version": "5.3.1",
+            "resolved": "https://registry.npmjs.org/svgson/-/svgson-5.3.1.tgz",
+            "integrity": "sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==",
+            "dependencies": {
+                "deep-rename-keys": "^0.2.1",
+                "xml-reader": "2.4.3"
+            }
+        },
         "node_modules/symbol-tree": {
             "version": "3.2.4",
             "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -41766,6 +41812,19 @@
                 "xtend": "^4.0.0"
             }
         },
+        "node_modules/xml-lexer": {
+            "version": "0.2.2",
+            "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz",
+            "integrity": "sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==",
+            "dependencies": {
+                "eventemitter3": "^2.0.0"
+            }
+        },
+        "node_modules/xml-lexer/node_modules/eventemitter3": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
+            "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
+        },
         "node_modules/xml-name-validator": {
             "version": "5.0.0",
             "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
@@ -41780,6 +41839,20 @@
             "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
             "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="
         },
+        "node_modules/xml-reader": {
+            "version": "2.4.3",
+            "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz",
+            "integrity": "sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==",
+            "dependencies": {
+                "eventemitter3": "^2.0.0",
+                "xml-lexer": "^0.2.2"
+            }
+        },
+        "node_modules/xml-reader/node_modules/eventemitter3": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
+            "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
+        },
         "node_modules/xml2js": {
             "version": "0.5.0",
             "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
diff --git a/package.json b/package.json
index 52f627b63..0b1717721 100644
--- a/package.json
+++ b/package.json
@@ -304,6 +304,7 @@
         "stream-browserify": "^3.0.0",
         "styled-components": "^6.1.1",
         "supercluster": "^8.0.1",
+        "svgson": "^5.3.1",
         "textarea-caret": "^3.1.0",
         "tough-cookie": "^4.1.3",
         "tslint": "^6.1.3",
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 0c993680e..06c562562 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -58,7 +58,8 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 1024,
         temp: 0.5,
-        prompt: 'I would like you to generate me vector art with Bezier curves. Given a prompt, generate a sequence of cubic Bezier coordinates in the range of 0 to 200 (unless specified larger/smaller) that creates a line drawing of the object. Respond only with the coordinates',
+        prompt: 'Given an item, generate a detailed line drawing representation of it. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.',
+        // prompt: 'I would like you to generate me vector art with Bezier curves. Given a prompt, generate a sequence of cubic Bezier coordinates in the range of 0 to 200 (unless specified larger/smaller) that creates a line drawing of the object. Format your response like this: M (100,30) C (75,10) (25,10) (50,50) C (25,75) (10,125) (50,150) C (25,75) (10,125) (50,150) and give no additional text. If a disconnected stroke is required, repeat that pattern with a new M marker',
     },
 };
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b6a7cacba..141695d86 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -743,7 +743,7 @@ pie title Minerals in my tap water
             { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType: Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType: Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                             btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
-            { title: "Labels", toolTip: "Lab els",                          btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
+            { title: "Labels", toolTip: "Labels",                           btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
             { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberSliderButton,               toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
             { title: "Ink",    toolTip: "Stroke color",                     btnType: ButtonType.ColorButton,  icon: "pen",        toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
             { title: "Smart Draw", toolTip: "Draw with GPT",                btnType: ButtonType.ToggleButton, icon: "user-pen",   toolType: "smartdraw",                      scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index fbc2bb7cd..f5696afaf 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -2,8 +2,18 @@
 /* eslint-disable prefer-destructuring */
 /* eslint-disable no-param-reassign */
 /* eslint-disable camelcase */
+import e from 'cors';
 import { Point } from '../../pen-gestures/ndollar';
 
+export enum SVGType {
+    Rect = 'rect',
+    Path = 'path',
+    Circle = 'circle',
+    Ellipse = 'ellipse',
+    Line = 'line',
+    Polygon = 'polygon',
+}
+
 class SmartRect {
     minx: number = 0;
     miny: number = 0;
@@ -616,6 +626,112 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
     return [...firstEnd, ...points, ...lastEnd];
 }
 
+export function SVGToBezier(name: SVGType, attributes: any): Point[] {
+    console.log('in svg to bezier', name, attributes);
+    switch (name) {
+        case 'line':
+            const x1 = parseInt(attributes.x1);
+            const x2 = parseInt(attributes.x2);
+            const y1 = parseInt(attributes.y1);
+            const y2 = parseInt(attributes.y2);
+            return [
+                { X: x1, Y: y1 },
+                { X: x1, Y: y1 },
+                { X: x2, Y: y2 },
+                { X: x2, Y: y2 },
+            ];
+        case 'circle':
+        case 'ellipse':
+            const c = 0.551915024494;
+            const centerX = parseInt(attributes.cx);
+            const centerY = parseInt(attributes.cy);
+            const radiusX = parseInt(attributes.rx) || parseInt(attributes.r);
+            const radiusY = parseInt(attributes.ry) || parseInt(attributes.r);
+            return [
+                { X: centerX, Y: centerY + radiusY },
+                { X: centerX + c * radiusX, Y: centerY + radiusY },
+                { X: centerX + radiusX, Y: centerY + c * radiusY },
+                { X: centerX + radiusX, Y: centerY },
+                { X: centerX + radiusX, Y: centerY },
+                { X: centerX + radiusX, Y: centerY - c * radiusY },
+                { X: centerX + c * radiusX, Y: centerY - radiusY },
+                { X: centerX, Y: centerY - radiusY },
+                { X: centerX, Y: centerY - radiusY },
+                { X: centerX - c * radiusX, Y: centerY - radiusY },
+                { X: centerX - radiusX, Y: centerY - c * radiusY },
+                { X: centerX - radiusX, Y: centerY },
+                { X: centerX - radiusX, Y: centerY },
+                { X: centerX - radiusX, Y: centerY + c * radiusY },
+                { X: centerX - c * radiusX, Y: centerY + radiusY },
+                { X: centerX, Y: centerY + radiusY },
+            ];
+        case 'rect':
+            const x = parseInt(attributes.x);
+            const y = parseInt(attributes.y);
+            const width = parseInt(attributes.width);
+            const height = parseInt(attributes.height);
+            return [
+                { X: x, Y: y },
+                { X: x, Y: y },
+                { X: x + width, Y: y },
+                { X: x + width, Y: y },
+                { X: x + width, Y: y },
+                { X: x + width, Y: y },
+                { X: x + width, Y: y + height },
+                { X: x + width, Y: y + height },
+                { X: x + width, Y: y + height },
+                { X: x + width, Y: y + height },
+                { X: x, Y: y + height },
+                { X: x, Y: y + height },
+                { X: x, Y: y + height },
+                { X: x, Y: y + height },
+                { X: x, Y: y },
+                { X: x, Y: y },
+            ];
+        case 'path':
+            const coordList: Point[] = [];
+            const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/);
+            coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+            const matches: RegExpMatchArray[] = Array.from(
+                attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
+            );
+            let lastPt: Point;
+            matches.forEach(match => {
+                if (match[0].startsWith('Q')) {
+                    coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
+                    coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
+                    coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
+                    coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
+                    lastPt = { X: parseInt(match[3]), Y: parseInt(match[4]) };
+                } else if (match[0].startsWith('C')) {
+                    coordList.push({ X: parseInt(match[5]), Y: parseInt(match[6]) });
+                    coordList.push({ X: parseInt(match[7]), Y: parseInt(match[8]) });
+                    coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
+                    coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
+                    lastPt = { X: parseInt(match[9]), Y: parseInt(match[10]) };
+                } else {
+                    coordList.push(lastPt || { X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+                    coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
+                    coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
+                    coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
+                    lastPt = { X: parseInt(match[11]), Y: parseInt(match[12]) };
+                }
+            });
+            const hasZ = attributes.d.match(/Z/);
+            if (hasZ) {
+                coordList.push(lastPt);
+                coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+                coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+            } else {
+                coordList.pop();
+            }
+            return coordList;
+        case 'polygon':
+        default:
+            return [];
+    }
+}
+
 /*
 static double                GetTValueFromSValue  (const BezierRep &parent, double t, double endT, bool left, double influenceDistance, double &excess) {
 double dist = 0;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 2f26bdaef..dc6edf81d 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -242,15 +242,15 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         this._points.length = 0;
         switch (shape) {
             case Gestures.Rectangle:
-                this._points.push({ X: left, Y: top });
-                this._points.push({ X: left, Y: top });
-                this._points.push({ X: right, Y: top });
-                this._points.push({ X: right, Y: top });
+                this._points.push({ X: left, Y: top }); // curr pt
+                this._points.push({ X: left, Y: top }); // curr first ctrl pt
+                this._points.push({ X: right, Y: top }); // next ctrl pt
+                this._points.push({ X: right, Y: top }); // next pt
 
-                this._points.push({ X: right, Y: top });
-                this._points.push({ X: right, Y: top });
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
+                this._points.push({ X: right, Y: top }); // next pt
+                this._points.push({ X: right, Y: top }); // next first ctrl pt
+                this._points.push({ X: right, Y: bottom }); // next next ctrl pt
+                this._points.push({ X: right, Y: bottom }); // next next pt
 
                 this._points.push({ X: right, Y: bottom });
                 this._points.push({ X: right, Y: bottom });
@@ -293,13 +293,13 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                     const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
 
                     // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
-                    this._points.push({ X: centerX, Y: centerY + radius });
-                    this._points.push({ X: centerX + c * radius, Y: centerY + radius });
-                    this._points.push({ X: centerX + radius, Y: centerY + c * radius });
-                    this._points.push({ X: centerX + radius, Y: centerY });
+                    this._points.push({ X: centerX, Y: centerY + radius }); // curr pt
+                    this._points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
+                    this._points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
+                    this._points.push({ X: centerX + radius, Y: centerY }); // next pt
 
-                    this._points.push({ X: centerX + radius, Y: centerY });
-                    this._points.push({ X: centerX + radius, Y: centerY - c * radius });
+                    this._points.push({ X: centerX + radius, Y: centerY }); // next pt
+                    this._points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
                     this._points.push({ X: centerX + c * radius, Y: centerY - radius });
                     this._points.push({ X: centerX, Y: centerY - radius });
 
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 44e00396e..cbc337860 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -76,6 +76,7 @@ import { PresBox } from './nodes/trails';
 import { AnchorMenu } from './pdf/AnchorMenu';
 import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
 import { TopBar } from './topbar/TopBar';
+import { SmartDrawHandler } from './collections/collectionFreeForm/SmartDrawHandler';
 
 const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 const _global = (window /* browser */ || global) /* node */ as any;
@@ -1091,6 +1092,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 <TaskCompletionBox />
                 <ContextMenu />
                 <ImageLabelHandler />
+                <SmartDrawHandler />
                 <AnchorMenu />
                 <MapAnchorMenu />
                 <DirectionsAnchorMenu />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a27ac2a0c..93b63ac4c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1267,17 +1267,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createInkStrokes);
     };
 
-    @action
+    @undoBatch
     createInkStrokes = (strokeList: InkData[], alpha?: number) => {
         console.log(strokeList.length);
         strokeList.forEach(inkData => {
             // const points: InkData = FitCurve(inkData, 20) as InkData;
-            const allPts = GenerateControlPoints(inkData, alpha);
-            const bounds = InkField.getBounds(allPts);
+            // const allPts = GenerateControlPoints(inkData, alpha);
+            const bounds = InkField.getBounds(inkData);
             const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
             const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
             const inkDoc = Docs.Create.InkDocument(
-                allPts,
+                inkData,
                 { title: 'stroke',
                     x: B.x - inkWidth / 2,
                     y: B.y - inkWidth / 2,
diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
index 4c2e78e31..956a8d7e9 100644
--- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
+++ b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
@@ -12,6 +12,8 @@ import './ImageLabelHandler.scss';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
 import { InkData } from '../../../../fields/InkField';
 import { ButtonType } from '../../nodes/FontIconBox/FontIconBox';
+import { SVGToBezier } from '../../../util/bezierFit';
+const { parse, stringify } = require('svgson');
 
 @observer
 export class SmartDrawHandler extends ObservableReactComponent<{}> {
@@ -69,19 +71,44 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log('GPT response:', res);
-            const simplifiedRes: string = res.replace(/[^\d\[\],]/g, '');
-            try {
-                const parsedPts = JSON.parse(simplifiedRes);
-                if (parsedPts[0][0][0]) {
-                    const controlPts = (parsedPts as [number, number][][]).map((stroke: [number, number][]) => stroke.map(([X, Y]) => ({ X: X + startPoint.X - 100, Y: Y + startPoint.Y - 100 })));
-                    this._addToDocFunc(controlPts, this._alpha);
-                } else {
-                    const controlPts = (parsedPts as [number, number][]).map(([X, Y]) => ({ X: X + startPoint.X - 100, Y: Y + startPoint.Y - 100 }));
-                    this._addToDocFunc([controlPts], this._alpha);
-                }
-            } catch (err) {
-                console.error('Error likely from bad GPT output type');
+            const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+            console.log('svg', svg);
+            if (svg) {
+                const svgObject = await parse(svg[0]);
+                console.log('svg object', svgObject);
+                const svgStrokes: any = svgObject.children;
+                const beziers: InkData[] = [];
+                svgStrokes.forEach((stroke: any) => {
+                    const convertedBezier: InkData = SVGToBezier(stroke.name, stroke.attributes);
+                    beziers.push(
+                        convertedBezier.map(point => {
+                            return { X: point.X + startPoint.X, Y: point.Y + startPoint.Y };
+                        })
+                    );
+                });
+                this._addToDocFunc(beziers);
             }
+
+            // const strokes = res.trim().split(/\s*(?=\s*M)/); // prettier-ignore
+            // const parsedSegments: InkData[] = [];
+            // console.log('strokes', strokes);
+            // strokes.forEach(stroke => {
+            //     stroke = stroke.replace(/C\s*\((\d+,\d+)\)\s*\((\d+,\d+)\)\s*\((\d+,\d+)\)/g, (c, p1, p2, p3) => {
+            //         return `C (${p1}) (${p2}) (${p3}) (${p3})`;
+            //     });
+            //     const coordStrings = stroke.match(/(\d+,\d+)/g);
+            //     const coords: InkData = [];
+            //     if (coordStrings) {
+            //         coordStrings.forEach(coord => {
+            //             const xy = coord.split(',');
+            //             coords.push({ X: parseInt(xy[0]), Y: parseInt(xy[1]) });
+            //         });
+            //         coords.pop();
+            //         parsedSegments.push(coords);
+            //     }
+            //     console.log('coords', coords);
+            // });
+            // this._addToDocFunc(parsedSegments);
         } catch (err) {
             console.error('GPT call failed', err);
         }
-- 
cgit v1.2.3-70-g09d2


From 3d14f06ad1297a6adc851945804211efb9dff7ff Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 20 Jun 2024 16:42:44 -0400
Subject: user customization added

---
 src/client/apis/gpt/GPT.ts                         |   2 +-
 src/client/views/MainView.tsx                      |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  14 +-
 .../collectionFreeForm/ImageLabelHandler.scss      |  14 ++
 .../collectionFreeForm/SmartDrawHandler.tsx        | 261 +++++++++++++++------
 5 files changed, 209 insertions(+), 83 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 06c562562..b5f4c7fe9 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -58,7 +58,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 1024,
         temp: 0.5,
-        prompt: 'Given an item, generate a detailed line drawing representation of it. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.',
+        prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.',
         // prompt: 'I would like you to generate me vector art with Bezier curves. Given a prompt, generate a sequence of cubic Bezier coordinates in the range of 0 to 200 (unless specified larger/smaller) that creates a line drawing of the object. Format your response like this: M (100,30) C (75,10) (25,10) (50,50) C (25,75) (10,125) (50,150) C (25,75) (10,125) (50,150) and give no additional text. If a disconnected stroke is required, repeat that pattern with a new M marker',
     },
 };
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index cbc337860..67b875ecb 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -442,6 +442,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faEyeDropper,
                 fa.faPaintRoller,
                 fa.faBars,
+                fa.faBarsStaggered,
                 fa.faBrush,
                 fa.faShapes,
                 fa.faEllipsisH,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 93b63ac4c..b8257ff31 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1268,23 +1268,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @undoBatch
-    createInkStrokes = (strokeList: InkData[], alpha?: number) => {
-        console.log(strokeList.length);
-        strokeList.forEach(inkData => {
+    createInkStrokes = (strokeData: [InkData, string, string][]) => {
+        strokeData.forEach((stroke: [InkData, string, string]) => {
             // const points: InkData = FitCurve(inkData, 20) as InkData;
             // const allPts = GenerateControlPoints(inkData, alpha);
-            const bounds = InkField.getBounds(inkData);
+            const bounds = InkField.getBounds(stroke[0]);
             const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
             const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
             const inkDoc = Docs.Create.InkDocument(
-                inkData,
+                stroke[0],
                 { title: 'stroke',
                     x: B.x - inkWidth / 2,
                     y: B.y - inkWidth / 2,
                     _width: B.width + inkWidth,
                     _height: B.height + inkWidth,
                     stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                inkWidth
+                inkWidth,
+                stroke[1],
+                undefined,
+                stroke[2] === 'none' ? undefined : stroke[2]
             );
             this.addDocument(inkDoc);
         });
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss
index e7413bf8e..9b8727e1a 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss
@@ -42,3 +42,17 @@
         }
     }
 }
+
+.complexity-slider {
+    width: 50%; /* Full-width */
+    height: 25px; /* Specified height */
+    background: #d3d3d3; /* Grey background */
+    outline: none; /* Remove outline */
+    opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
+    -webkit-transition: 0.2s; /* 0.2 seconds transition on hover */
+    transition: opacity 0.2s;
+
+    :hover {
+        opacity: 1; /* Fully shown on mouse-over */
+    }
+}
diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
index 956a8d7e9..edb814172 100644
--- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
+++ b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
@@ -4,16 +4,20 @@ import { observer } from 'mobx-react';
 import React from 'react';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
-import { Button, IconButton } from 'browndash-components';
+import { Button, IconButton, Size } from 'browndash-components';
 import ReactLoading from 'react-loading';
 import { AiOutlineSend } from 'react-icons/ai';
-import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './ImageLabelHandler.scss';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
 import { InkData } from '../../../../fields/InkField';
-import { ButtonType } from '../../nodes/FontIconBox/FontIconBox';
 import { SVGToBezier } from '../../../util/bezierFit';
-const { parse, stringify } = require('svgson');
+const { parse } = require('svgson');
+import { Slider, Switch } from '@mui/material';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { Flex } from '@adobe/react-spectrum';
+import { Row } from 'react-aria-components';
+import { UndoManager } from '../../../util/UndoManager';
+import e from 'cors';
 
 @observer
 export class SmartDrawHandler extends ObservableReactComponent<{}> {
@@ -25,11 +29,15 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _yRelativeToTop: boolean = true;
     @observable private _isLoading: boolean = false;
     @observable private _userInput: string = '';
-    @observable private _drawingTypeToolTip = 'Create Geometric Drawing';
-    @observable private _drawingTypeIcon: 'star' | 'splotch' = 'star';
-    @observable private _alpha: number | undefined = undefined; // number between 0 and 1 that determines how rounded a drawing will be
-    // @observable public strokes: InkData[] = [];
-    private _addToDocFunc: (strokeList: InkData[], alpha?: number) => void = () => {};
+    @observable private _showOptions: boolean = false;
+    @observable private _menuIcon: string = 'caret-right';
+    @observable private _complexity: number = 5;
+    @observable private _size: number = 300;
+    @observable private _autoColor: boolean = true;
+    @observable private _showRegenerate: boolean = false;
+    private _addToDocFunc: (strokeList: [InkData, string, string][]) => void = () => {};
+    private _lastX: number = 0;
+    private _lastY: number = 0;
 
     constructor(props: any) {
         super(props);
@@ -37,97 +45,82 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         SmartDrawHandler.Instance = this;
     }
 
-    @action
-    setIsLoading = (isLoading: boolean) => {
-        this._isLoading = isLoading;
-    };
-
     @action
     setUserInput = (input: string) => {
         this._userInput = input;
     };
 
     @action
-    displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeList: InkData[], alpha?: number) => void) => {
+    displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeData: [InkData, string, string][]) => void) => {
         this._pageX = x;
         this._pageY = y;
         this._display = true;
         this._addToDocFunc = addToDoc;
     };
 
-    @action
     hideSmartDrawHandler = () => {
+        this._showRegenerate = false;
         this._display = false;
+        this._isLoading = false;
+        this._showOptions = false;
+        this._menuIcon = 'caret-right';
+    };
+
+    hideRegenerate = () => {
+        this._showRegenerate = false;
+        this._userInput = '';
+        this._complexity = 5;
+        this._size = 300;
+        this._autoColor = true;
+        this._isLoading = false;
+    };
+
+    toggleMenu = () => {
+        this._showOptions = !this._showOptions;
+        this._menuIcon === 'caret-right' ? (this._menuIcon = 'caret-down') : (this._menuIcon = 'caret-right');
     };
 
     @action
-    drawWithGPT = async (startPoint: { X: number; Y: number }, input: string) => {
-        console.log('start point is', startPoint);
-        this.setIsLoading(true);
+    drawWithGPT = async (e: React.MouseEvent<Element, MouseEvent>, startPoint: { X: number; Y: number }, input: string, regenerate: boolean = false) => {
+        if (this._userInput === '') return;
+        e.stopPropagation();
+        this._lastX = startPoint.X;
+        this._lastY = startPoint.Y;
+        this._isLoading = true;
+        this._showOptions = false;
         try {
-            const res = await gptAPICall(input, GPTCallType.DRAW);
+            const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW);
             if (!res) {
                 console.error('GPT call failed');
                 return;
             }
-            console.log('GPT response:', res);
             const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
-            console.log('svg', svg);
             if (svg) {
                 const svgObject = await parse(svg[0]);
-                console.log('svg object', svgObject);
                 const svgStrokes: any = svgObject.children;
-                const beziers: InkData[] = [];
-                svgStrokes.forEach((stroke: any) => {
-                    const convertedBezier: InkData = SVGToBezier(stroke.name, stroke.attributes);
-                    beziers.push(
+                const strokeData: [InkData, string, string][] = [];
+                svgStrokes.forEach((child: any) => {
+                    const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
+                    strokeData.push([
                         convertedBezier.map(point => {
-                            return { X: point.X + startPoint.X, Y: point.Y + startPoint.Y };
-                        })
-                    );
+                            return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
+                        }),
+                        this._autoColor ? child.attributes.stroke : undefined,
+                        this._autoColor ? child.attributes.fill : undefined,
+                    ]);
                 });
-                this._addToDocFunc(beziers);
+                if (regenerate) UndoManager.Undo();
+                this._addToDocFunc(strokeData);
             }
-
-            // const strokes = res.trim().split(/\s*(?=\s*M)/); // prettier-ignore
-            // const parsedSegments: InkData[] = [];
-            // console.log('strokes', strokes);
-            // strokes.forEach(stroke => {
-            //     stroke = stroke.replace(/C\s*\((\d+,\d+)\)\s*\((\d+,\d+)\)\s*\((\d+,\d+)\)/g, (c, p1, p2, p3) => {
-            //         return `C (${p1}) (${p2}) (${p3}) (${p3})`;
-            //     });
-            //     const coordStrings = stroke.match(/(\d+,\d+)/g);
-            //     const coords: InkData = [];
-            //     if (coordStrings) {
-            //         coordStrings.forEach(coord => {
-            //             const xy = coord.split(',');
-            //             coords.push({ X: parseInt(xy[0]), Y: parseInt(xy[1]) });
-            //         });
-            //         coords.pop();
-            //         parsedSegments.push(coords);
-            //     }
-            //     console.log('coords', coords);
-            // });
-            // this._addToDocFunc(parsedSegments);
         } catch (err) {
             console.error('GPT call failed', err);
         }
-
-        this.setIsLoading(false);
-        this.setUserInput('');
         this.hideSmartDrawHandler();
+        this._showRegenerate = true;
     };
 
-    changeDrawingType = () => {
-        if (this._drawingTypeIcon === 'star') {
-            this._drawingTypeIcon = 'splotch';
-            this._drawingTypeToolTip = 'Create Rounded Drawing';
-            this._alpha = 0.2;
-        } else {
-            this._drawingTypeIcon = 'star';
-            this._drawingTypeToolTip = 'Create Geometric Drawing';
-            this._alpha = 0;
-        }
+    regenerate = (e: React.MouseEvent<Element, MouseEvent>) => {
+        this.drawWithGPT(e, { X: this._lastX, Y: this._lastY }, `Regenerate the item "${this._userInput}"`, true);
     };
 
     render() {
@@ -144,7 +137,16 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                         color: SettingsManager.userColor,
                     }}>
                     <div>
-                        <IconButton tooltip={'Cancel'} onClick={this.hideSmartDrawHandler} icon={<FontAwesomeIcon icon="xmark" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} />
+                        <IconButton
+                            tooltip={'Cancel'}
+                            onClick={() => {
+                                this.hideSmartDrawHandler();
+                                this.hideRegenerate();
+                            }}
+                            icon={<FontAwesomeIcon icon="xmark" />}
+                            color={SettingsManager.userColor}
+                            style={{ width: '19px' }}
+                        />
                         <input
                             aria-label="label-input"
                             id="new-label"
@@ -156,24 +158,131 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             }}
                             placeholder="Enter item to draw"
                         />
-                        <IconButton tooltip={this._drawingTypeToolTip} icon={<FontAwesomeIcon icon={this._drawingTypeIcon} />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '14px' }} onClick={this.changeDrawingType} />
-                        {/* <IconButton
-                            tooltip="Create Geometric Drawing"
-                            icon={<FontAwesomeIcon icon="star" />}
-                            color={MarqueeOptionsMenu.Instance.userColor}
+                        <IconButton
+                            tooltip="Advanced Options"
+                            icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />}
+                            color={SettingsManager.userColor}
                             style={{ width: '14px' }}
                             onClick={() => {
-                                this._alpha = 0;
+                                this._showOptions = !this._showOptions;
                             }}
-                        /> */}
+                        />
                         <Button
                             style={{ alignSelf: 'flex-end' }}
                             text="Send"
-                            icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
+                            icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
-                            color={MarqueeOptionsMenu.Instance.userColor}
+                            color={SettingsManager.userColor}
+                            onClick={e => {
+                                this.drawWithGPT(e, { X: e.clientX, Y: e.clientY }, this._userInput);
+                            }}
+                        />
+                    </div>
+                    {this._showOptions && (
+                        <>
+                            <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
+                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}>
+                                    Auto color
+                                    <Switch
+                                        sx={{
+                                            '& .MuiSwitch-switchBase.Mui-checked': {
+                                                color: SettingsManager.userColor,
+                                            },
+                                            '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
+                                                backgroundColor: SettingsManager.userVariantColor,
+                                            },
+                                        }}
+                                        defaultChecked={true}
+                                        size="small"
+                                        onChange={() => (this._autoColor = !this._autoColor)}
+                                    />
+                                </div>
+                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}>
+                                    Complexity
+                                    <Slider
+                                        sx={{
+                                            '& .MuiSlider-thumb': {
+                                                color: SettingsManager.userColor,
+                                                '&.Mui-focusVisible, &:hover, &.Mui-active': {
+                                                    boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
+                                                },
+                                            },
+                                            '& .MuiSlider-track': {
+                                                color: SettingsManager.userVariantColor,
+                                            },
+                                            '& .MuiSlider-rail': {
+                                                color: SettingsManager.userColor,
+                                            },
+                                        }}
+                                        style={{ width: '80%' }}
+                                        min={1}
+                                        max={10}
+                                        step={1}
+                                        size="small"
+                                        value={this._complexity}
+                                        onChange={(e, val) => {
+                                            this._complexity = val as number;
+                                        }}
+                                        valueLabelDisplay="auto"
+                                    />
+                                </div>
+                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}>
+                                    Size (in pixels)
+                                    <Slider
+                                        sx={{
+                                            '& .MuiSlider-thumb': {
+                                                color: SettingsManager.userColor,
+                                                '&.Mui-focusVisible, &:hover, &.Mui-active': {
+                                                    boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
+                                                },
+                                            },
+                                            '& .MuiSlider-track': {
+                                                color: SettingsManager.userVariantColor,
+                                            },
+                                            '& .MuiSlider-rail': {
+                                                color: SettingsManager.userColor,
+                                            },
+                                        }}
+                                        style={{ width: '80%' }}
+                                        min={50}
+                                        max={700}
+                                        step={10}
+                                        size="small"
+                                        value={this._size}
+                                        onChange={(e, val) => {
+                                            this._size = val as number;
+                                        }}
+                                        valueLabelDisplay="auto"
+                                    />
+                                </div>
+                            </div>
+                        </>
+                    )}
+                </div>
+            );
+        } else if (this._showRegenerate) {
+            return (
+                <div
+                    id="smartdraw-options-menu"
+                    className="contextMenu-cont"
+                    style={{
+                        left: this._pageX,
+                        ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+                        background: SettingsManager.userBackgroundColor,
+                        color: SettingsManager.userColor,
+                    }}>
+                    <div
+                        style={{
+                            display: 'flex',
+                            flexDirection: 'row',
+                        }}>
+                        <IconButton tooltip="Cancel" onClick={this.hideRegenerate} icon={<FontAwesomeIcon icon="xmark" />} color={SettingsManager.userColor} style={{ width: '19px' }} />
+                        <IconButton
+                            tooltip="Regenerate"
+                            icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
+                            color={SettingsManager.userColor}
                             onClick={e => {
-                                this.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._userInput);
+                                this.regenerate(e);
                             }}
                         />
                     </div>
-- 
cgit v1.2.3-70-g09d2


From 33f416349c00bdec405455340befb0ca85b2fee4 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Mon, 1 Jul 2024 13:54:07 -0400
Subject: weird error?

---
 src/client/apis/gpt/GPT.ts                         |    7 +-
 src/client/views/LightboxView.scss                 |   24 +-
 src/client/views/LightboxView.tsx                  |    7 +
 src/client/views/MainView.tsx                      | 2964 +++++++++++++-------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  107 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |    2 +
 .../collectionFreeForm/SmartDrawHandler.tsx        |  295 --
 src/client/views/nodes/PDFBox.scss                 |   16 +-
 src/client/views/nodes/PDFBox.tsx                  |   37 +-
 src/client/views/smartdraw/DrawingPalette.scss     |   11 +
 src/client/views/smartdraw/DrawingPalette.tsx      |   89 +
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  407 +++
 src/fields/Doc.ts                                  |    1 +
 13 files changed, 2563 insertions(+), 1404 deletions(-)
 delete mode 100644 src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
 create mode 100644 src/client/views/smartdraw/DrawingPalette.scss
 create mode 100644 src/client/views/smartdraw/DrawingPalette.tsx
 create mode 100644 src/client/views/smartdraw/SmartDrawHandler.tsx

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index b5f4c7fe9..a7bd05a21 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -58,8 +58,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 1024,
         temp: 0.5,
-        prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.',
-        // prompt: 'I would like you to generate me vector art with Bezier curves. Given a prompt, generate a sequence of cubic Bezier coordinates in the range of 0 to 200 (unless specified larger/smaller) that creates a line drawing of the object. Format your response like this: M (100,30) C (75,10) (25,10) (50,50) C (25,75) (10,125) (50,150) C (25,75) (10,125) (50,150) and give no additional text. If a disconnected stroke is required, repeat that pattern with a new M marker',
+        prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. Make sure every element has the stroke field filled out. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.',
     },
 };
 
@@ -71,10 +70,10 @@ let lastResp = '';
  * @param inputText Text to process
  * @returns AI Output
  */
-const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: any) => {
+const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: any, dontCache?: boolean) => {
     const inputText = [GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ].includes(callType) ? inputTextIn + '.' : inputTextIn;
     const opts: GPTCallOpts = callTypeMap[callType];
-    if (lastCall === inputText) return lastResp;
+    if (lastCall === inputText && dontCache !== true) return lastResp;
     try {
         lastCall = inputText;
 
diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss
index 6da5c0338..3e65843df 100644
--- a/src/client/views/LightboxView.scss
+++ b/src/client/views/LightboxView.scss
@@ -1,7 +1,7 @@
 .lightboxView-navBtn {
     margin: auto;
     position: absolute;
-    right: 10;
+    right: 19;
     top: 10;
     background: transparent;
     border-radius: 8;
@@ -16,7 +16,7 @@
 .lightboxView-tabBtn {
     margin: auto;
     position: absolute;
-    right: 45;
+    right: 54;
     top: 10;
     background: transparent;
     border-radius: 8;
@@ -28,10 +28,26 @@
         opacity: 1;
     }
 }
+.lightboxView-paletteBtn {
+    margin: auto;
+    position: absolute;
+    right: 89;
+    top: 10;
+    background: transparent;
+    border-radius: 8;
+    opacity: 0.7;
+    width: 25;
+    flex-direction: column;
+    display: flex;
+    &:hover {
+        opacity: 1;
+    }
+}
+
 .lightboxView-penBtn {
     margin: auto;
     position: absolute;
-    right: 80;
+    right: 124;
     top: 10;
     background: transparent;
     border-radius: 8;
@@ -46,7 +62,7 @@
 .lightboxView-exploreBtn {
     margin: auto;
     position: absolute;
-    right: 115;
+    right: 159;
     top: 10;
     background: transparent;
     border-radius: 8;
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 7198c7f05..e93e4949b 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -23,6 +23,7 @@ import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { OverlayView } from './OverlayView';
+import { DrawingPalette } from './smartdraw/DrawingPalette';
 
 interface LightboxViewProps {
     PanelWidth: number;
@@ -59,6 +60,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     @observable private _doc: Opt<Doc> = undefined;
     @observable private _docTarget: Opt<Doc> = undefined;
     @observable private _docView: Opt<DocumentView> = undefined;
+    @observable private _showPalette: boolean = false;
 
     @computed get leftBorder() { return Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]); } // prettier-ignore
     @computed get topBorder()  { return Math.min(this._props.PanelHeight / 4, this._props.maxBorder[1]); } // prettier-ignore
@@ -202,6 +204,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     toggleFitWidth = () => {
         this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth);
     };
+    togglePalette = () => {
+        this._showPalette = !this._showPalette;
+    };
     togglePen = () => {
         Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
     };
@@ -319,8 +324,10 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                 <LightboxTourBtn lightboxDoc={this.lightboxDoc} navBtn={this.renderNavBtn} future={this.future} stepInto={this.stepInto} />
                 {toggleBtn('lightboxView-navBtn', 'toggle reading view', this._doc?._layout_fitWidth, 'book-open', 'book', this.toggleFitWidth)}
                 {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-download', '', this.downloadDoc)}
+                {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)}
                 {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)}
                 {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
+                {this._showPalette && <DrawingPalette />}
             </div>
         );
     }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 67b875ecb..f88eb3bca 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,1129 +1,1991 @@
-/* eslint-disable node/no-unpublished-import */
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons';
-import * as far from '@fortawesome/free-regular-svg-icons';
-import * as fa from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx';
+/* eslint-disable camelcase */
+/* eslint-disable jsx-a11y/control-has-associated-label */
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable react/no-array-index-key */
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable no-return-assign */
+import ArrowLeftIcon from '@mui/icons-material/ArrowLeft';
+import ArrowRightIcon from '@mui/icons-material/ArrowRight';
+import PauseIcon from '@mui/icons-material/Pause';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
+import ReplayIcon from '@mui/icons-material/Replay';
+import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material';
+import Typography from '@mui/material/Typography';
+import { IReactionDisposer, action, computed, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-// eslint-disable-next-line import/no-relative-packages
-import '../../../node_modules/browndash-components/dist/styles/global.min.css';
-import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
-import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast, GetDocFromUrl, Opt } from '../../fields/Doc';
-import { DocData } from '../../fields/DocSymbols';
-import { Id } from '../../fields/FieldSymbols';
-import { DocCast, StrCast, toList } from '../../fields/Types';
-import { DocServer } from '../DocServer';
-import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
-import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { Docs } from '../documents/Documents';
-import { CalendarManager } from '../util/CalendarManager';
-import { CaptureManager } from '../util/CaptureManager';
-import { DocumentManager } from '../util/DocumentManager';
-import { DragManager } from '../util/DragManager';
-import { dropActionType } from '../util/DropActionTypes';
-import { GroupManager } from '../util/GroupManager';
-import { HistoryUtil } from '../util/History';
-import { Hypothesis } from '../util/HypothesisUtils';
-import { UPDATE_SERVER_CACHE } from '../util/LinkManager';
-import { RTFMarkup } from '../util/RTFMarkup';
-import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { ServerStats } from '../util/ServerStats';
-import { SettingsManager } from '../util/SettingsManager';
-import { SharingManager } from '../util/SharingManager';
-import { SnappingManager } from '../util/SnappingManager';
-import { Transform } from '../util/Transform';
-import { ReportManager } from '../util/reportManager/ReportManager';
-import { ComponentDecorations } from './ComponentDecorations';
-import { ContextMenu } from './ContextMenu';
-import { DashboardView } from './DashboardView';
-import { DictationOverlay } from './DictationOverlay';
-import { DocumentDecorations } from './DocumentDecorations';
-import { GestureOverlay } from './GestureOverlay';
-import { LightboxView } from './LightboxView';
-import './MainView.scss';
-import { ObservableReactComponent } from './ObservableReactComponent';
-import { PreviewCursor } from './PreviewCursor';
-import { PropertiesView } from './PropertiesView';
-import { DashboardStyleProvider, DefaultStyleProvider, returnEmptyDocViewList } from './StyleProvider';
-import { TimelineMenu } from './animationtimeline/TimelineMenu';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import { CollectionMenu } from './collections/CollectionMenu';
-import { TabDocView } from './collections/TabDocView';
-import './collections/TreeView.scss';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
-import { ImageLabelHandler } from './collections/collectionFreeForm/ImageLabelHandler';
-import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
-import { CollectionLinearView } from './collections/collectionLinear';
-import { LinkMenu } from './linking/LinkMenu';
-import { AudioBox } from './nodes/AudioBox';
-import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
-import { DocButtonState } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
-import { ImageEditorData as ImageEditor } from './nodes/ImageBox';
-import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
-import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
-import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu';
-import { MapAnchorMenu } from './nodes/MapBox/MapAnchorMenu';
-import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
-import { TaskCompletionBox } from './nodes/TaskCompletedBox';
-import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
-import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
-import GenerativeFill from './nodes/generativeFill/GenerativeFill';
-import { PresBox } from './nodes/trails';
-import { AnchorMenu } from './pdf/AnchorMenu';
-import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
-import { TopBar } from './topbar/TopBar';
-import { SmartDrawHandler } from './collections/collectionFreeForm/SmartDrawHandler';
+import { NumListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from '../FieldView';
+import './PhysicsSimulationBox.scss';
+import InputField from './PhysicsSimulationInputField';
+import questions from './PhysicsSimulationQuestions.json';
+import tutorials from './PhysicsSimulationTutorial.json';
+import Wall from './PhysicsSimulationWall';
+import Weight from './PhysicsSimulationWeight';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
 
-const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
-const _global = (window /* browser */ || global) /* node */ as any;
+interface IWallProps {
+    length: number;
+    xPos: number;
+    yPos: number;
+    angleInDegrees: number;
+}
+interface IForce {
+    description: string;
+    magnitude: number;
+    directionInDegrees: number;
+}
+interface VectorTemplate {
+    top: number;
+    left: number;
+    width: number;
+    height: number;
+    x1: number;
+    y1: number;
+    x2: number;
+    y2: number;
+    weightX: number;
+    weightY: number;
+}
+interface QuestionTemplate {
+    questionSetup: string[];
+    variablesForQuestionSetup: string[];
+    question: string;
+    answerParts: string[];
+    answerSolutionDescriptions: string[];
+    goal: string;
+    hints: { description: string; content: string }[];
+}
+
+interface TutorialTemplate {
+    question: string;
+    steps: {
+        description: string;
+        content: string;
+        forces: {
+            description: string;
+            magnitude: number;
+            directionInDegrees: number;
+            component: boolean;
+        }[];
+        showMagnitude: boolean;
+    }[];
+}
 
 @observer
-export class MainView extends ObservableReactComponent<{}> {
-    // eslint-disable-next-line no-use-before-define
-    public static Instance: MainView;
-    public static Live: boolean = false;
-    private _docBtnRef = React.createRef<HTMLDivElement>();
+export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(PhysicsSimulationBox, fieldKey);
+    }
+
+    _widthDisposer: IReactionDisposer | undefined;
+    @observable _simReset = 0;
+
+    // semi-Constants
+    xMin = 0;
+    yMin = 0;
+    xMax = this._props.PanelWidth() * 0.6;
+    yMax = this._props.PanelHeight();
+    color = `rgba(0,0,0,0.5)`;
+    radius = 50;
+    wallPositions: IWallProps[] = [];
+
+    @computed get circularMotionRadius() {
+        return (NumCast(this.dataDoc.circularMotionRadius, 150) * this._props.PanelWidth()) / 1000;
+    }
+    @computed get gravity() {
+        return NumCast(this.dataDoc.simulation_gravity, -9.81);
+    }
+    @computed get simulationType() {
+        return StrCast(this.dataDoc.simulation_type, 'Inclined Plane');
+    }
+    @computed get simulationMode() {
+        return StrCast(this.dataDoc.simulation_mode, 'Freeform');
+    }
+    // Used for spring simulation
+    @computed get springConstant() {
+        return NumCast(this.dataDoc.spring_constant, 0.5);
+    }
+    @computed get springLengthRest() {
+        return NumCast(this.dataDoc.spring_lengthRest, 200);
+    }
+    @computed get springLengthStart() {
+        return NumCast(this.dataDoc.spring_lengthStart, 200);
+    }
 
-    @observable private _windowWidth: number = 0;
-    @observable private _windowHeight: number = 0;
-    @observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row)
-    @observable private _dashUIHeight: number = 0; // height of entire main dashboard region including top menu buttons
-    @observable private _panelContent: string = 'none';
-    @observable private _sidebarContent: any = Doc.MyLeftSidebarPanel;
-    @observable private _leftMenuFlyoutWidth: number = 0;
-    @computed get _hideUI() {
-        return this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking;
+    @computed get pendulumAngle() {
+        return NumCast(this.dataDoc.pendulum_angle);
+    }
+    @computed get pendulumAngleStart() {
+        return NumCast(this.dataDoc.pendulum_angleStart);
+    }
+    @computed get pendulumLength() {
+        return NumCast(this.dataDoc.pendulum_length);
+    }
+    @computed get pendulumLengthStart() {
+        return NumCast(this.dataDoc.pendulum_lengthStart);
     }
 
-    @computed private get dashboardTabHeight() {
-        return this._hideUI ? 0 : 27;
-    } //  27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
-    @computed private get topOfDashUI() {
-        return this._hideUI || DocumentView.LightboxDoc() ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
+    // Used for wedge simulation
+    @computed get wedgeAngle() {
+        return NumCast(this.dataDoc.wedge_angle, 26);
     }
-    @computed private get topOfHeaderBarDoc() {
-        return this.topOfDashUI;
+    @computed get wedgeHeight() {
+        return NumCast(this.dataDoc.wedge_height, Math.tan((26 * Math.PI) / 180) * this.xMax * 0.5);
     }
-    @computed private get topOfSidebarDoc() {
-        return this.topOfDashUI + this.topMenuHeight();
+    @computed get wedgeWidth() {
+        return NumCast(this.dataDoc.wedge_width, this.xMax * 0.5);
     }
-    @computed private get topOfMainDoc() {
-        return this.topOfDashUI + this.topMenuHeight() + this.headerBarDocHeight();
+    @computed get mass1() {
+        return NumCast(this.dataDoc.mass1, 1);
     }
-    @computed private get topOfMainDocContent() {
-        return this.topOfMainDoc + this.dashboardTabHeight;
+    @computed get mass2() {
+        return NumCast(this.dataDoc.mass2, 1);
     }
-    @computed private get leftScreenOffsetOfMainDocView() {
-        return this.leftMenuWidth() - 2;
+
+    @computed get mass1Radius() {
+        return NumCast(this.dataDoc.mass1_radius, 30);
     }
-    @computed private get userDoc() {
-        return Doc.UserDoc();
+    @computed get mass1PosXStart() {
+        return NumCast(this.dataDoc.mass1_positionXstart);
     }
-    @observable mainDoc: Opt<Doc> = undefined;
-    @computed private get mainContainer() {
-        if (window.location.pathname.startsWith('/doc/') && ClientUtils.CurrentUserEmail() === 'guest') {
-            DocServer.GetRefField(window.location.pathname.substring('/doc/'.length)).then(main =>
-                runInAction(() => {
-                    this.mainDoc = main as Doc;
-                })
-            );
-            return this.mainDoc;
-        }
-        return this.userDoc ? Doc.ActiveDashboard : Doc.GuestDashboard;
+    @computed get mass1PosYStart() {
+        return NumCast(this.dataDoc.mass1_positionYstart);
     }
-    @computed private get headerBarDoc() {
-        return Doc.MyHeaderBar;
+    @computed get mass1VelXStart() {
+        return NumCast(this.dataDoc.mass1_velocityXstart);
     }
-    @computed public get mainFreeform(): Opt<Doc> {
-        return (docs => (docs?.length > 1 ? docs[1] : undefined))(DocListCast(this.mainContainer!.data));
+    @computed get mass1VelYStart() {
+        return NumCast(this.dataDoc.mass1_velocityYstart);
     }
-    @observable public headerBarHeight: number = 0;
-    headerBarHeightFunc = () => this.headerBarHeight;
 
-    @action
-    toggleTopBar = () => {
-        if (this.headerBarHeight > 0) {
-            this.headerBarHeight = 0;
-        } else {
-            this.headerBarHeight = 60;
-        }
-    };
-    headerBarDocWidth = () => this.mainDocViewWidth();
-    headerBarDocHeight = () => (this._hideUI ? 0 : this.headerBarHeight ?? 0);
-    topMenuHeight = () => (this._hideUI ? 0 : 35);
-    topMenuWidth = returnZero; // value is ignored ...
-    leftMenuWidth = () => (this._hideUI ? 0 : Number(LEFT_MENU_WIDTH.replace('px', '')));
-    leftMenuHeight = () => this._dashUIHeight;
-    leftMenuFlyoutWidth = () => this._leftMenuFlyoutWidth;
-    leftMenuFlyoutHeight = () => this._dashUIHeight;
-    propertiesWidth = () => Math.max(0, Math.min(this._dashUIWidth - 50, SnappingManager.PropertiesWidth || 0));
-    propertiesHeight = () => this._dashUIHeight;
-    mainDocViewWidth = () => this._dashUIWidth - this.propertiesWidth() - this.leftMenuWidth() - this.leftMenuFlyoutWidth();
-    mainDocViewHeight = () => this._dashUIHeight - this.headerBarDocHeight();
+    @computed get mass2PosXStart() {
+        return NumCast(this.dataDoc.mass2_positionXstart);
+    }
+    @computed get mass2PosYStart() {
+        return NumCast(this.dataDoc.mass2_positionYstart);
+    }
+    @computed get mass2VelXStart() {
+        return NumCast(this.dataDoc.mass2_velocityXstart);
+    }
+    @computed get mass2VelYStart() {
+        return NumCast(this.dataDoc.mass2_velocityYstart);
+    }
+
+    @computed get selectedQuestion() {
+        return this.dataDoc.selectedQuestion ? (JSON.parse(StrCast(this.dataDoc.selectedQuestion)) as QuestionTemplate) : questions.inclinePlane[0];
+    }
+    @computed get tutorial() {
+        return this.dataDoc.tutorial ? (JSON.parse(StrCast(this.dataDoc.tutorial)) as TutorialTemplate) : tutorials.inclinePlane;
+    }
+    @computed get selectedSolutions() {
+        return NumListCast(this.dataDoc.selectedSolutions);
+    }
+    @computed get questionPartOne() {
+        return StrCast(this.dataDoc.questionPartOne);
+    }
+    @computed get questionPartTwo() {
+        return StrCast(this.dataDoc.questionPartTwo);
+    }
+
+    componentWillUnmount() {
+        this._widthDisposer?.();
+    }
 
     componentDidMount() {
-        // Utils.TraceConsoleLog();
-        reaction(
-            // when a multi-selection occurs, remove focus from all active elements to allow keyboad input to go only to global key manager to act upon selection
-            () => DocumentView.Selected().slice(),
-            views => views.length > 1 && (document.activeElement as any)?.blur !== undefined && (document.activeElement as any)!.blur()
-        );
-        reaction(
-            () => Doc.MyDockedBtns.linearView_IsOpen,
-            open => SnappingManager.SetPrintToConsole(!!open)
-        );
-        const scriptTag = document.createElement('script');
-        scriptTag.setAttribute('type', 'text/javascript');
-        scriptTag.setAttribute('src', 'https://www.bing.com/api/maps/mapcontrol?callback=makeMap');
-        scriptTag.async = true;
-        scriptTag.defer = true;
-        document.body.appendChild(scriptTag);
-        document.getElementById('root')?.addEventListener('scroll', () =>
-            (ele => {
-                ele.scrollLeft = ele.scrollTop = 0;
-            })(document.getElementById('root')!)
-        );
-        const ele = document.getElementById('loader');
-        const prog = document.getElementById('dash-progress');
-        if (ele && prog) {
-            // remove from DOM
-            setTimeout(() => {
-                prog.style.transition = '1s';
-                prog.style.width = '100%';
-            }, 0);
-            setTimeout(() => {
-                ele.outerHTML = '';
-            }, 1000);
-        }
-        this._sidebarContent.proto = undefined;
-        if (!MainView.Live) {
-            DocServer.setLivePlaygroundFields([
-                'dataTransition',
-                'viewTransition',
-                'treeView_Open',
-                'treeView_ExpandedView',
-                'carousel_index',
-                'itemIndex', // for changing slides in presentations
-                'layout_sidebarWidthPercent',
-                'layout_currentTimecode',
-                'layout_timelineHeightPercent',
-                'layout_hideMinimap',
-                'layout_showSidebar',
-                'layout_scrollTop',
-                'layout_fitWidth',
-                'layout_curPage',
-                'presStatus',
-                'freeform_panX',
-                'freeform_panY',
-                'freeform_scale',
-                'overlayX',
-                'overlayY',
-                'text_scrollHeight',
-                'text_height',
-                'hidden',
-                // 'type_collection',
-                'chromeHidden',
-                'currentFrame',
-            ]); // can play with these fields on someone else's
-        }
+        // Setup and update simulation
+        this._widthDisposer = reaction(() => [this._props.PanelWidth(), this._props.PanelHeight()], this.setupSimulation, { fireImmediately: true });
 
-        const tag = document.createElement('script');
-        tag.src = 'https://www.youtube.com/iframe_api';
-        const firstScriptTag = document.getElementsByTagName('script')[0];
-        firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
-        document.addEventListener('dash', (e: any) => {
-            // event used by chrome plugin to tell Dash which document to focus on
-            const id = GetDocFromUrl(e.detail);
-            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentView.showDocument(doc, { willPan: false }) : null));
-        });
-        document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
-        this.initEventListeners();
+        // Create walls
+        this.wallPositions = [
+            { length: 100, xPos: 0, yPos: 0, angleInDegrees: 0 },
+            { length: 100, xPos: 0, yPos: 100, angleInDegrees: 0 },
+            { length: 100, xPos: 0, yPos: 0, angleInDegrees: 90 },
+            { length: 100, xPos: (this.xMax / this._props.PanelWidth()) * 100, yPos: 0, angleInDegrees: 90 },
+        ];
     }
 
-    componentWillUnMount() {
-        // window.removeEventListener('keyup', KeyManager.Instance.unhandle);
-        // window.removeEventListener('keydown', KeyManager.Instance.handle);
-        // window.removeEventListener('pointerdown', this.globalPointerDown, true);
-        // window.removeEventListener('pointermove', this.globalPointerMove, true);
-        // window.removeEventListener('pointerup', this.globalPointerClick, true);
-        // window.removeEventListener('paste', KeyManager.Instance.paste as any);
-        // document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener);
+    componentDidUpdate(prevProps: Readonly<FieldViewProps>) {
+        super.componentDidUpdate(prevProps);
+        if (this.xMax !== this._props.PanelWidth() * 0.6 || this.yMax !== this._props.PanelHeight()) {
+            this.xMax = this._props.PanelWidth() * 0.6;
+            this.yMax = this._props.PanelHeight();
+            this.setupSimulation();
+        }
     }
 
-    constructor(props: any) {
-        super(props);
-        makeObservable(this);
-        DocumentViewInternal.addDocTabFunc = MainView.addDocTabFunc_impl;
-        MainView.Instance = this;
-        DashboardView._urlState = HistoryUtil.parseUrl(window.location) || ({} as any);
+    gravityForce = (mass: number): IForce => ({
+        description: 'Gravity',
+        magnitude: mass * Math.abs(this.gravity),
+        directionInDegrees: 270,
+    });
 
-        // causes errors to be generated when modifying an observable outside of an action
-        configure({ enforceActions: 'observed' });
+    @action
+    setupSimulation = () => {
+        const { simulationType } = this;
+        const mode = this.simulationMode;
+        this.dataDoc.simulation_paused = true;
+        if (simulationType !== 'Circular Motion') {
+            this.dataDoc.mass1_velocityXstart = 0;
+            this.dataDoc.mass1_velocityYstart = 0;
+            this.dataDoc.mass1_velocityX = 0;
+            this.dataDoc.mass1_velocityY = 0;
+        }
+        if (mode === 'Freeform') {
+            this.dataDoc.simulation_showForceMagnitudes = true;
+            // prettier-ignore
+            switch (simulationType) {
+                case 'One Weight': 
+                    this.dataDoc.simulation_showComponentForces = false;
+                    this.dataDoc.mass1_positionYstart = this.yMin + this.mass1Radius;
+                    this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+                    this.dataDoc.mass1_positionY = this.getDisplayYPos(this.yMin + this.mass1Radius);
+                    this.dataDoc.mass1_positionX = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+                    this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
+                    this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
+                    break;
+                case 'Inclined Plane':  this.setupInclinedPlane(); break;
+                case 'Pendulum':        this.setupPendulum(); break;
+                case 'Spring':          this.setupSpring(); break;
+                case 'Circular Motion': this.setupCircular(20); break;
+                case 'Pulley':          this.setupPulley(); break;
+                case 'Suspension':      this.setupSuspension();break;
+                default:
+            }
+            this._simReset++;
+        } else if (mode === 'Review') {
+            this.dataDoc.simulation_showComponentForces = false;
+            this.dataDoc.simulation_showForceMagnitudes = true;
+            this.dataDoc.simulation_showAcceleration = false;
+            this.dataDoc.simulation_showVelocity = false;
+            this.dataDoc.simulation_showForces = true;
+            this.generateNewQuestion();
+            // prettier-ignore
+            switch (simulationType) {
+                case 'One Weight' : break;// TODO - one weight review problems
+                case 'Spring':  this.setupSpring(); break; // TODO - spring review problems
+                case 'Inclined Plane': this.dataDoc.mass1_forcesUpdated =  this.dataDoc.mass1_forcesStart = ''; break;
+                case 'Pendulum':  this.setupPendulum(); break;  // TODO - pendulum review problems
+                case 'Circular Motion':  this.setupCircular(0); break;  // TODO - circular motion review problems
+                case 'Pulley':  this.setupPulley(); break; // TODO - pulley tutorial review problems
+                case 'Suspension': this.setupSuspension(); break;  // TODO - suspension tutorial review problems
+                default:
+            }
+        } else if (mode === 'Tutorial') {
+            this.dataDoc.simulation_showComponentForces = false;
+            this.dataDoc.tutorial_stepNumber = 0;
+            this.dataDoc.simulation_showAcceleration = false;
+            if (this.simulationType !== 'Circular Motion') {
+                this.dataDoc.mass1_velocityX = 0;
+                this.dataDoc.mass1_velocityY = 0;
+                this.dataDoc.simulation_showVelocity = false;
+            } else {
+                this.dataDoc.mass1_velocityX = 20;
+                this.dataDoc.mass1_velocityY = 0;
+                this.dataDoc.simulation_showVelocity = true;
+            }
 
-        if (window.location.pathname !== '/home') {
-            const pathname = window.location.pathname.substr(1).split('/');
-            if (pathname.length > 1 && pathname[0] === 'doc') {
-                DocServer.GetRefField(pathname[1]).then(
-                    action(field => {
-                        if (field instanceof Doc && field._type_collection !== CollectionViewType.Docking) {
-                            Doc.GuestTarget = field;
-                        }
-                    })
-                );
+            switch (this.simulationType) {
+                case 'One Weight':
+                    this.dataDoc.simulation_showForces = true;
+                    this.dataDoc.mass1_positionYstart = this.yMax - 100;
+                    this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+                    this.dataDoc.tutorial = JSON.stringify(tutorials.freeWeight);
+                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.freeWeight.steps[0].forces);
+                    this.dataDoc.simulation_showForceMagnitudes = tutorials.freeWeight.steps[0].showMagnitude;
+                    break;
+                case 'Spring':
+                    this.dataDoc.simulation_showForces = true;
+                    this.setupSpring();
+                    this.dataDoc.mass1_positionYstart = this.yMin + 200 + 19.62;
+                    this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+                    this.dataDoc.tutorial = JSON.stringify(tutorials.spring);
+                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.spring.steps[0].forces);
+                    this.dataDoc.simulation_showForceMagnitudes = tutorials.spring.steps[0].showMagnitude;
+                    break;
+                case 'Pendulum':
+                    this.setupPendulum();
+                    this.dataDoc.tutorial = JSON.stringify(tutorials.pendulum);
+                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.pendulum.steps[0].forces);
+                    this.dataDoc.simulation_showForceMagnitudes = tutorials.pendulum.steps[0].showMagnitude;
+                    break;
+                case 'Inclined Plane':
+                    this.dataDoc.wedge_angle = 26;
+                    this.setupInclinedPlane();
+                    this.dataDoc.simulation_showForces = true;
+                    this.dataDoc.tutorial = JSON.stringify(tutorials.inclinePlane);
+                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.inclinePlane.steps[0].forces);
+                    this.dataDoc.simulation_showForceMagnitudes = tutorials.inclinePlane.steps[0].showMagnitude;
+                    break;
+                case 'Circular Motion':
+                    this.dataDoc.simulation_showForces = true;
+                    this.setupCircular(40);
+                    this.dataDoc.tutorial = JSON.stringify(tutorials.circular);
+                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.circular.steps[0].forces);
+                    this.dataDoc.simulation_showForceMagnitudes = tutorials.circular.steps[0].showMagnitude;
+                    break;
+                case 'Pulley':
+                    this.dataDoc.simulation_showForces = true;
+                    this.setupPulley();
+                    this.dataDoc.tutorial = JSON.stringify(tutorials.pulley);
+                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.pulley.steps[0].forces);
+                    this.dataDoc.simulation_showForceMagnitudes = tutorials.pulley.steps[0].showMagnitude;
+                    break;
+                case 'Suspension':
+                    this.dataDoc.simulation_showForces = true;
+                    this.setupSuspension();
+                    this.dataDoc.tutorial = JSON.stringify(tutorials.suspension);
+                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.suspension.steps[0].forces);
+                    this.dataDoc.simulation_showForceMagnitudes = tutorials.suspension.steps[0].showMagnitude;
+                    break;
+                default:
             }
+            this._simReset++;
         }
+    };
 
-        library.add(
-            ...[
-                fa.faExclamationCircle,
-                fa.faEdit,
-                fa.faArrowDownShortWide,
-                fa.faTrash,
-                fa.faTrashAlt,
-                fa.faShare,
-                fa.faTaxi,
-                fa.faDownload,
-                fa.faPallet,
-                fa.faExpandArrowsAlt,
-                fa.faAmbulance,
-                fa.faLayerGroup,
-                fa.faExternalLinkAlt,
-                fa.faCalendar,
-                fa.faSquare,
-                far.faSquare as any,
-                fa.faConciergeBell,
-                fa.faWindowRestore,
-                fa.faFolder,
-                fa.faFolderOpen,
-                fa.faFolderPlus,
-                fa.faFolderClosed,
-                fa.faBook,
-                fa.faMapPin,
-                fa.faMapMarker,
-                fa.faFingerprint,
-                fa.faCrosshairs,
-                fa.faDesktop,
-                fa.faUnlock,
-                fa.faLock,
-                fa.faLaptopCode,
-                fa.faMale,
-                fa.faCopy,
-                fa.faHome,
-                fa.faHandPointLeft,
-                fa.faHandPointRight,
-                fa.faCompass,
-                fa.faSnowflake,
-                fa.faStar,
-                fa.faSplotch,
-                fa.faMicrophone,
-                fa.faCircleHalfStroke,
-                fa.faKeyboard,
-                fa.faQuestion,
-                fa.faTasks,
-                fa.faPalette,
-                fa.faAngleLeft,
-                fa.faAngleRight,
-                fa.faBell,
-                fa.faCamera,
-                fa.faExpand,
-                fa.faCaretDown,
-                fa.faCaretLeft,
-                fa.faCaretRight,
-                fa.faCaretSquareDown,
-                fa.faCaretSquareRight,
-                fa.faArrowsAltH,
-                fa.faPlus,
-                fa.faMinus,
-                fa.faTerminal,
-                fa.faToggleOn,
-                fa.faFile,
-                fa.faLocationArrow,
-                fa.faSearch,
-                fa.faFileDownload,
-                fa.faFileUpload,
-                fa.faStop,
-                fa.faCalculator,
-                fa.faWindowMaximize,
-                fa.faIdCard,
-                fa.faAddressCard,
-                fa.faQuestionCircle,
-                fa.faArrowLeft,
-                fa.faArrowRight,
-                fa.faArrowDown,
-                fa.faArrowUp,
-                fa.faBolt,
-                fa.faBullseye,
-                fa.faTurnUp,
-                fa.faTurnDown,
-                fa.faCaretUp,
-                fa.faCat,
-                fa.faCheck,
-                fa.faChevronRight,
-                fa.faChevronLeft,
-                fa.faChevronDown,
-                fa.faChevronUp,
-                fa.faClone,
-                fa.faCloudUploadAlt,
-                fa.faCommentAlt,
-                fa.faCommentDots,
-                fa.faCompressArrowsAlt,
-                fa.faCut,
-                fa.faEllipsisV,
-                fa.faEraser,
-                fa.faDeleteLeft,
-                fa.faXmarksLines,
-                fa.faCircleXmark,
-                fa.faXmark,
-                fa.faExclamation,
-                fa.faFileAlt,
-                fa.faFileAudio,
-                fa.faFileVideo,
-                fa.faFilePdf,
-                fa.faFilm,
-                fa.faFilter,
-                fa.faFont,
-                fa.faGlobeAmericas,
-                fa.faGlobeAsia,
-                fa.faHighlighter,
-                fa.faLongArrowAltRight,
-                fa.faMousePointer,
-                fa.faMusic,
-                fa.faObjectGroup,
-                fa.faArrowsLeftRight,
-                fa.faPause,
-                fa.faPen,
-                fa.faUserPen,
-                fa.faPenNib,
-                fa.faPhone,
-                fa.faPlay,
-                fa.faPortrait,
-                fa.faRedoAlt,
-                fa.faStamp,
-                fa.faStickyNote,
-                fa.faArrowsAltV,
-                fa.faTimesCircle,
-                fa.faThumbtack,
-                fa.faTree,
-                fa.faTv,
-                fa.faUndoAlt,
-                fa.faVideoSlash,
-                fa.faVideo,
-                fa.faAsterisk,
-                fa.faBrain,
-                fa.faImage,
-                fa.faPaintBrush,
-                fa.faTimes,
-                fa.faFlag,
-                fa.faScroll,
-                fa.faEye,
-                fa.faArrowsAlt,
-                fa.faQuoteLeft,
-                fa.faSortAmountDown,
-                fa.faAlignLeft,
-                fa.faAlignCenter,
-                fa.faAlignRight,
-                fa.faHeading,
-                fa.faRulerCombined,
-                fa.faFillDrip,
-                fa.faLink,
-                fa.faUnlink,
-                fa.faBold,
-                fa.faItalic,
-                fa.faClipboard,
-                fa.faUnderline,
-                fa.faStrikethrough,
-                fa.faSuperscript,
-                fa.faSubscript,
-                fa.faIndent,
-                fa.faEyeDropper,
-                fa.faPaintRoller,
-                fa.faBars,
-                fa.faBarsStaggered,
-                fa.faBrush,
-                fa.faShapes,
-                fa.faEllipsisH,
-                fa.faHandPaper,
-                fa.faMap,
-                fa.faUser,
-                faHireAHelper as any,
-                fa.faTrashRestore,
-                fa.faUsers,
-                fa.faWrench,
-                fa.faCog,
-                fa.faMap,
-                fa.faBellSlash,
-                fa.faExpandAlt,
-                fa.faArchive,
-                fa.faBezierCurve,
-                fa.faCircle,
-                far.faCircle as any,
-                fa.faLongArrowAltRight,
-                fa.faPenFancy,
-                fa.faAngleDoubleRight,
-                fa.faAngleDoubleDown,
-                fa.faAngleDoubleLeft,
-                fa.faAngleDoubleUp,
-                faBuffer as any,
-                fa.faExpand,
-                fa.faUndo,
-                fa.faSlidersH,
-                fa.faAngleUp,
-                fa.faAngleDown,
-                fa.faPlayCircle,
-                fa.faClock,
-                fa.faRoute,
-                fa.faRocket,
-                fa.faExchangeAlt,
-                fa.faHashtag,
-                fa.faAlignJustify,
-                fa.faCheckSquare,
-                fa.faListUl,
-                fa.faWindowMinimize,
-                fa.faWindowRestore,
-                fa.faTextWidth,
-                fa.faTextHeight,
-                fa.faClosedCaptioning,
-                fa.faInfoCircle,
-                fa.faTag,
-                fa.faSyncAlt,
-                fa.faPhotoVideo,
-                fa.faArrowAltCircleDown,
-                fa.faArrowAltCircleUp,
-                fa.faArrowAltCircleLeft,
-                fa.faArrowAltCircleRight,
-                fa.faStopCircle,
-                fa.faCheckCircle,
-                fa.faGripVertical,
-                fa.faSortUp,
-                fa.faSortDown,
-                fa.faTable,
-                fa.faTableCells,
-                fa.faTableColumns,
-                fa.faTh,
-                fa.faThList,
-                fa.faProjectDiagram,
-                fa.faSignature,
-                fa.faColumns,
-                fa.faChevronCircleUp,
-                fa.faUpload,
-                fa.faBorderAll,
-                fa.faBraille,
-                fa.faPersonChalkboard,
-                fa.faChalkboard,
-                fa.faPencilAlt,
-                fa.faEyeSlash,
-                fa.faSmile,
-                fa.faIndent,
-                fa.faOutdent,
-                fa.faChartBar,
-                fa.faBan,
-                fa.faPhoneSlash,
-                fa.faGripLines,
-                fa.faSave,
-                fa.faBook,
-                fa.faBookmark,
-                fa.faList,
-                fa.faListOl,
-                fa.faLightbulb,
-                fa.faBookOpen,
-                fa.faMapMarkerAlt,
-                fa.faSearchPlus,
-                fa.faSolarPanel,
-                fa.faVolumeUp,
-                fa.faVolumeDown,
-                fa.faSquareRootAlt,
-                fa.faVolumeMute,
-                fa.faUserCircle,
-                fa.faHeart,
-                fa.faHeartBroken,
-                fa.faHighlighter,
-                fa.faRemoveFormat,
-                fa.faHandPointUp,
-                fa.faXRay,
-                fa.faZ,
-                fa.faArrowsUpToLine,
-                fa.faArrowsDownToLine,
-                fa.faPalette,
-                fa.faHourglassHalf,
-                fa.faRobot,
-                fa.faSatellite,
-                fa.faStar,
-            ]
-        );
-    }
+    // Helper function to go between display and real values
+    getDisplayYPos = (yPos: number) => this.yMax - yPos - 2 * this.mass1Radius + 5;
+    getYPosFromDisplay = (yDisplay: number) => this.yMax - yDisplay - 2 * this.mass1Radius + 5;
 
-    private longPressTimer: NodeJS.Timeout | undefined;
-    globalPointerClick = action(() => {
-        this.longPressTimer && clearTimeout(this.longPressTimer);
-        DocumentView.LongPress = false;
-    });
-    globalPointerMove = action((e: PointerEvent) => {
-        if (e.movementX > 3 || e.movementY > 3) this.longPressTimer && clearTimeout(this.longPressTimer);
-    });
-    globalPointerDown = action((e: PointerEvent) => {
-        DocumentView.LongPress = false;
-        this.longPressTimer = setTimeout(
-            action(() => {
-                DocumentView.LongPress = true;
-            }),
-            1000
-        );
-        DocumentManager.removeOverlayViews();
-        Doc.linkFollowUnhighlight();
-        AudioBox.Enabled = true;
-        const targets = document.elementsFromPoint(e.x, e.y);
-        if (targets.length) {
-            let targClass = targets[0].className.toString();
-            for (let i = 0; i < targets.length - 1; i++) {
-                if (typeof targets[i].className === 'object') targClass = targets[i + 1].className.toString();
-                else break;
-            }
-            !targClass.includes('contextMenu') && ContextMenu.Instance.closeMenu();
-            !['timeline-menu-desc', 'timeline-menu-item', 'timeline-menu-input'].includes(targClass) && TimelineMenu.Instance.closeMenu();
+    // Update forces when coefficient of static friction changes in freeform mode
+    updateForcesWithFriction = (coefficient: number, width = this.wedgeWidth, height = this.wedgeHeight) => {
+        const normalForce: IForce = {
+            description: 'Normal Force',
+            magnitude: Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1,
+            directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
+        };
+        const frictionForce: IForce = {
+            description: 'Static Friction Force',
+            magnitude: coefficient * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1,
+            directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
+        };
+        // reduce magnitude or friction force if necessary such that block cannot slide up plane
+        let yForce = -Math.abs(this.gravity) * this.mass1;
+        yForce += normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
+        yForce += frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+        if (yForce > 0) {
+            frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + Math.abs(this.gravity) * this.mass1) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
         }
-    });
 
-    initEventListeners = () => {
-        window.addEventListener('beforeunload', UPDATE_SERVER_CACHE);
-        window.addEventListener('drop', e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page
-        window.addEventListener('dragover', e => e.preventDefault(), false);
-        document.addEventListener('pointerdown', this.globalPointerDown, true);
-        document.addEventListener('pointermove', this.globalPointerMove, true);
-        document.addEventListener('pointerup', this.globalPointerClick, true);
-        document.addEventListener(
-            'click',
-            (e: MouseEvent) => {
-                if (!e.cancelBubble) {
-                    const pathstr = (e as any)?.path?.map((p: any) => p.classList?.toString()).join();
-                    if (pathstr?.includes('libraryFlyout')) {
-                        DocumentView.DeselectAll();
-                    }
-                }
-            },
-            false
-        );
-        document.oncontextmenu = () => false;
+        const normalForceComponent: IForce = {
+            description: 'Normal Force',
+            magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)),
+            directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
+        };
+        const gravityParallel: IForce = {
+            description: 'Gravity Parallel Component',
+            magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin(Math.PI / 2 - Math.atan(height / width)),
+            directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI + 180,
+        };
+        const gravityPerpendicular: IForce = {
+            description: 'Gravity Perpendicular Component',
+            magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(Math.PI / 2 - Math.atan(height / width)),
+            directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI,
+        };
+        const gravityForce = this.gravityForce(this.mass1);
+        if (coefficient !== 0) {
+            this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce, frictionForce]);
+            this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce, frictionForce]);
+            this.dataDoc.mass1_componentForces = JSON.stringify([frictionForce, normalForceComponent, gravityParallel, gravityPerpendicular]);
+        } else {
+            this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce]);
+            this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce]);
+            this.dataDoc.mass1_componentForces = JSON.stringify([normalForceComponent, gravityParallel, gravityPerpendicular]);
+        }
     };
 
-    @action
-    createNewPresentation = () => {
-        const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
-        CollectionDockingView.AddSplit(pres, OpenWhereMod.right);
-        Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', pres); // Doc.MyTrails should be created in createDashboard
-        Doc.ActivePresentation = pres;
+    // Change wedge height and width and weight position to match new wedge angle
+    changeWedgeBasedOnNewAngle = (angle: number) => {
+        const radAng = (angle * Math.PI) / 180;
+        this.dataDoc.wedge_width = this.xMax * 0.5;
+        this.dataDoc.wedge_height = Math.tan(radAng) * this.dataDoc.wedge_width;
+
+        // update weight position based on updated wedge width/height
+        const yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius;
+        const xPos = this.xMax * 0.25 + this.mass1Radius * Math.sin(radAng) - this.mass1Radius;
+
+        this.dataDoc.mass1_positionXstart = xPos;
+        this.dataDoc.mass1_positionYstart = yPos;
+        if (this.simulationMode === 'Freeform') {
+            this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction), this.dataDoc.wedge_width, Math.tan(radAng) * this.dataDoc.wedge_width);
+        }
     };
 
-    @action
-    openPresentation = (pres: Doc) => {
-        if (pres.type === DocumentType.PRES) {
-            CollectionDockingView.AddSplit(pres, OpenWhereMod.right, undefined, PresBox.PanelName);
-            Doc.MyTrails && (Doc.ActivePresentation = pres);
-            Doc.AddDocToList(Doc.MyTrails, 'data', pres);
-            this.closeFlyout();
+    // In review mode, update forces when coefficient of static friction changed
+    updateReviewForcesBasedOnCoefficient = (coefficient: number) => {
+        let theta = this.wedgeAngle;
+        const index = this.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45');
+        if (index >= 0) {
+            theta = NumListCast(this.dataDoc.questionVariables)[index];
+        }
+        if (isNaN(theta)) {
+            return;
+        }
+        this.dataDoc.review_GravityMagnitude = Math.abs(this.gravity);
+        this.dataDoc.review_GravityAngle = 270;
+        this.dataDoc.review_NormalMagnitude = Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180);
+        this.dataDoc.review_NormalAngle = 90 - theta;
+        let yForce = -Math.abs(this.gravity);
+        yForce += Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((90 - theta) * Math.PI) / 180);
+        yForce += coefficient * Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((180 - theta) * Math.PI) / 180);
+        let friction = coefficient * Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180);
+        if (yForce > 0) {
+            friction = (-(Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180)) * Math.sin(((90 - theta) * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin(((180 - theta) * Math.PI) / 180);
         }
+        this.dataDoc.review_StaticMagnitude = friction;
+        this.dataDoc.review_StaticAngle = 180 - theta;
     };
 
-    @action
-    createNewFolder = async () => {
-        const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true });
-        Doc.AddDocToList(Doc.MyFilesystem, 'data', folder);
+    // In review mode, update forces when wedge angle changed
+    updateReviewForcesBasedOnAngle = (angle: number) => {
+        this.dataDoc.review_GravityMagnitude = Math.abs(this.gravity);
+        this.dataDoc.review_GravityAngle = 270;
+        this.dataDoc.review_NormalMagnitude = Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180);
+        this.dataDoc.review_NormalAngle = 90 - angle;
+        let yForce = -Math.abs(this.gravity);
+        yForce += Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180) * Math.sin(((90 - angle) * Math.PI) / 180);
+        yForce += NumCast(this.dataDoc.review_Coefficient) * Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180) * Math.sin(((180 - angle) * Math.PI) / 180);
+        let friction = NumCast(this.dataDoc.review_Coefficient) * Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180);
+        if (yForce > 0) {
+            friction = (-(Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180)) * Math.sin(((90 - angle) * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin(((180 - angle) * Math.PI) / 180);
+        }
+        this.dataDoc.review_StaticMagnitude = friction;
+        this.dataDoc.review_StaticAngle = 180 - angle;
     };
 
-    waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
-    headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1);
-    mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1);
-    addHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true);
-    removeHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true);
-    @computed get headerBarDocView() {
-        return (
-            <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}>
-                <DocumentView
-                    key="headerBarDoc"
-                    Document={this.headerBarDoc}
-                    addDocTab={DocumentViewInternal.addDocTabFunc}
-                    pinToPres={emptyFunction}
-                    containerViewPath={returnEmptyDoclist}
-                    styleProvider={DefaultStyleProvider}
-                    addDocument={this.addHeaderDoc}
-                    removeDocument={this.removeHeaderDoc}
-                    fitContentsToBox={returnTrue}
-                    isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events)
-                    isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive
-                    ScreenToLocalTransform={this.headerBarScreenXf}
-                    childHideResizeHandles
-                    childDragAction={dropActionType.move}
-                    dontRegisterView
-                    hideResizeHandles
-                    PanelWidth={this.headerBarDocWidth}
-                    PanelHeight={this.headerBarDocHeight}
-                    renderDepth={0}
-                    focus={emptyFunction}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    childFilters={returnEmptyFilter}
-                    childFiltersByRanges={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                />
-            </div>
-        );
-    }
-    @computed get mainDocView() {
-        const headerBar = this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView;
-        return (
-            <>
-                {headerBar}
-                <DocumentView
-                    key="main"
-                    Document={this.mainContainer!}
-                    addDocument={undefined}
-                    addDocTab={DocumentViewInternal.addDocTabFunc}
-                    pinToPres={emptyFunction}
-                    containerViewPath={returnEmptyDoclist}
-                    styleProvider={this._hideUI ? DefaultStyleProvider : undefined}
-                    isContentActive={returnTrue}
-                    removeDocument={undefined}
-                    ScreenToLocalTransform={this._hideUI ? this.mainScreenToLocalXf : Transform.Identity}
-                    PanelWidth={this.mainDocViewWidth}
-                    PanelHeight={this.mainDocViewHeight}
-                    focus={emptyFunction}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    childFilters={returnEmptyFilter}
-                    childFiltersByRanges={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                    suppressSetHeight
-                    renderDepth={this._hideUI ? 0 : -1}
-                />
-            </>
-        );
-    }
+    // Solve for the correct answers to the generated problem
+    getAnswersToQuestion = (question: QuestionTemplate, questionVars: number[]) => {
+        const solutions: number[] = [];
 
-    @computed get dockingContent() {
-        return (
-            <GestureOverlay isActive={!DocumentView.LightboxDoc()}>
-                <div
-                    key="docking"
-                    className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? '-flyout' : ''}`}
-                    onDrop={e => {
-                        e.stopPropagation();
-                        e.preventDefault();
-                    }}
-                    style={{
-                        width: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
-                        minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
-                        transform: DocumentView.LightboxDoc() ? 'scale(0.0001)' : undefined,
-                    }}>
-                    {!this.mainContainer ? null : this.mainDocView}
-                </div>
-            </GestureOverlay>
-        );
-    }
+        let theta = this.wedgeAngle;
+        let index = question.variablesForQuestionSetup.indexOf('theta - max 45');
+        if (index >= 0) {
+            theta = questionVars[index];
+        }
+        let muS: number = NumCast(this.dataDoc.coefficientOfStaticFriction);
+        index = question.variablesForQuestionSetup.indexOf('coefficient of static friction');
+        if (index >= 0) {
+            muS = questionVars[index];
+        }
 
-    @action
-    onPropertiesPointerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(
-            this,
-            e,
-            action(() => {
-                SnappingManager.SetPropertiesWidth(Math.max(0, this._dashUIWidth - e.clientX));
-                return !SnappingManager.PropertiesWidth;
-            }),
-            action(() => {
-                SnappingManager.PropertiesWidth < 5 && SnappingManager.SetPropertiesWidth(0);
-            }),
-            action(() => {
-                SnappingManager.SetPropertiesWidth(this.propertiesWidth() < 15 ? Math.min(this._dashUIWidth - 50, 250) : 0);
-            }),
-            false
-        );
+        for (let i = 0; i < question.answerSolutionDescriptions.length; i++) {
+            const description = question.answerSolutionDescriptions[i];
+            if (!isNaN(NumCast(description))) {
+                solutions.push(NumCast(description));
+            } else if (description === 'solve normal force angle from wedge angle') {
+                solutions.push(90 - theta);
+            } else if (description === 'solve normal force magnitude from wedge angle') {
+                solutions.push(Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI));
+            } else if (description === 'solve static force magnitude from wedge angle given equilibrium') {
+                const normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
+                const normalForceAngle = 90 - theta;
+                const frictionForceAngle = 180 - theta;
+                const frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+                solutions.push(frictionForceMagnitude);
+            } else if (description === 'solve static force angle from wedge angle given equilibrium') {
+                solutions.push(180 - theta);
+            } else if (description === 'solve minimum static coefficient from wedge angle given equilibrium') {
+                const normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
+                const normalForceAngle = 90 - theta;
+                const frictionForceAngle = 180 - theta;
+                const frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+                const frictionCoefficient = frictionForceMagnitude / normalForceMagnitude;
+                solutions.push(frictionCoefficient);
+            } else if (description === 'solve maximum wedge angle from coefficient of static friction given equilibrium') {
+                solutions.push((Math.atan(muS) * 180) / Math.PI);
+            }
+        }
+        this.dataDoc.selectedSolutions = new List<number>(solutions);
+        return solutions;
     };
 
-    @action
-    onFlyoutPointerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(
-            this,
-            e,
-            action(ev => {
-                this._leftMenuFlyoutWidth = Math.max(ev.clientX - 58, 0);
-                return false;
-            }),
-            () => this._leftMenuFlyoutWidth < 5 && this.closeFlyout(),
-            this.closeFlyout
-        );
+    // In review mode, check if input answers match correct answers and optionally generate alert
+    checkAnswers = (showAlert: boolean = true) => {
+        let error: boolean = false;
+        const epsilon: number = 0.01;
+        if (this.selectedQuestion) {
+            for (let i = 0; i < this.selectedQuestion.answerParts.length; i++) {
+                if (this.selectedQuestion.answerParts[i] === 'force of gravity') {
+                    if (Math.abs(NumCast(this.dataDoc.review_GravityMagnitude) - this.selectedSolutions[i]) > epsilon) {
+                        error = true;
+                    }
+                } else if (this.selectedQuestion.answerParts[i] === 'angle of gravity') {
+                    if (Math.abs(NumCast(this.dataDoc.review_GravityAngle) - this.selectedSolutions[i]) > epsilon) {
+                        error = true;
+                    }
+                } else if (this.selectedQuestion.answerParts[i] === 'normal force') {
+                    if (Math.abs(NumCast(this.dataDoc.review_NormalMagnitude) - this.selectedSolutions[i]) > epsilon) {
+                        error = true;
+                    }
+                } else if (this.selectedQuestion.answerParts[i] === 'angle of normal force') {
+                    if (Math.abs(NumCast(this.dataDoc.review_NormalAngle) - this.selectedSolutions[i]) > epsilon) {
+                        error = true;
+                    }
+                } else if (this.selectedQuestion.answerParts[i] === 'force of static friction') {
+                    if (Math.abs(NumCast(this.dataDoc.review_StaticMagnitude) - this.selectedSolutions[i]) > epsilon) {
+                        error = true;
+                    }
+                } else if (this.selectedQuestion.answerParts[i] === 'angle of static friction') {
+                    if (Math.abs(NumCast(this.dataDoc.review_StaticAngle) - this.selectedSolutions[i]) > epsilon) {
+                        error = true;
+                    }
+                } else if (this.selectedQuestion.answerParts[i] === 'coefficient of static friction') {
+                    if (Math.abs(NumCast(this.dataDoc.coefficientOfStaticFriction) - this.selectedSolutions[i]) > epsilon) {
+                        error = true;
+                    }
+                } else if (this.selectedQuestion.answerParts[i] === 'wedge angle') {
+                    if (Math.abs(this.wedgeAngle - this.selectedSolutions[i]) > epsilon) {
+                        error = true;
+                    }
+                }
+            }
+        }
+        if (showAlert) {
+            this.dataDoc.simulation_paused = false;
+            setTimeout(() => (this.dataDoc.simulation_paused = true), 3000);
+        }
+        if (this.selectedQuestion.goal === 'noMovement') {
+            this.dataDoc.noMovement = !error;
+        }
     };
 
-    sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
-    mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
-    static addDocTabFunc_impl = (docs: Doc | Doc[], location: OpenWhere): boolean => {
-        const doc = toList(docs).lastElement();
-        const whereFields = location.split(':');
-        const keyValue = whereFields.includes(OpenWhereMod.keyvalue);
-        const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
-        const panelName = whereFields.length > 1 ? whereFields.lastElement() : '';
-        if (doc.dockingConfig && !keyValue) return DashboardView.openDashboard(doc);
-        switch (whereFields[0]) {
-            case OpenWhere.lightbox:   return LightboxView.Instance.AddDocTab(doc, location);
-            case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
-            case OpenWhere.toggle:     return CollectionDockingView.ToggleSplit(doc, whereMods, undefined, TabDocView.DontSelectOnActivate); // bcz: hack!  mark the toggle so that it won't be selected on activation- this is needed so that the backlinks menu can toggle views of targets on and off without selecting them
-            case OpenWhere.replace:    return CollectionDockingView.ReplaceTab(doc, whereMods, undefined, panelName);
-            case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, undefined, undefined, keyValue);
-        } // prettier-ignore
+    // Reset all review values to default
+    resetReviewValuesToDefault = () => {
+        this.dataDoc.review_GravityMagnitude = 0;
+        this.dataDoc.review_GravityAngle = 0;
+        this.dataDoc.review_NormalMagnitude = 0;
+        this.dataDoc.review_NormalAngle = 0;
+        this.dataDoc.review_StaticMagnitude = 0;
+        this.dataDoc.review_StaticAngle = 0;
+        this.dataDoc.coefficientOfKineticFriction = 0;
+        this.dataDoc.simulation_paused = true;
     };
 
-    @computed get flyout() {
-        return !this._leftMenuFlyoutWidth ? (
-            <div key="flyout" className="mainView-libraryFlyout-out">
-                {this.docButtons}
-            </div>
-        ) : (
-            <div key="libFlyout" className="mainView-libraryFlyout" style={{ minWidth: this._leftMenuFlyoutWidth, width: this._leftMenuFlyoutWidth }}>
-                <div className="mainView-contentArea">
-                    <DocumentView
-                        Document={this._sidebarContent.proto || this._sidebarContent}
-                        addDocument={undefined}
-                        addDocTab={DocumentViewInternal.addDocTabFunc}
-                        pinToPres={DocumentView.PinDoc}
-                        containerViewPath={returnEmptyDoclist}
-                        styleProvider={this._sidebarContent.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem || this._sidebarContent.proto === Doc.MyTrails ? DashboardStyleProvider : DefaultStyleProvider}
-                        removeDocument={returnFalse}
-                        ScreenToLocalTransform={this.mainContainerXf}
-                        PanelWidth={this.leftMenuFlyoutWidth}
-                        PanelHeight={this.leftMenuFlyoutHeight}
-                        renderDepth={0}
-                        isContentActive={returnTrue}
-                        focus={emptyFunction}
-                        whenChildContentsActiveChanged={emptyFunction}
-                        childFilters={returnEmptyFilter}
-                        childFiltersByRanges={returnEmptyFilter}
-                        searchFilterDocs={returnEmptyDoclist}
-                    />
-                </div>
-                {this.docButtons}
-            </div>
-        );
-    }
+    // In review mode, reset problem variables and generate a new question
+    generateNewQuestion = () => {
+        this.resetReviewValuesToDefault();
 
-    @computed get leftMenuPanel() {
-        return (
-            <div key="menu" className="mainView-leftMenuPanel" style={{ background: SnappingManager.userBackgroundColor, display: DocumentView.LightboxDoc() ? 'none' : undefined }}>
-                <DocumentView
-                    Document={Doc.MyLeftSidebarMenu}
-                    addDocument={undefined}
-                    addDocTab={DocumentViewInternal.addDocTabFunc}
-                    pinToPres={emptyFunction}
-                    removeDocument={returnFalse}
-                    ScreenToLocalTransform={this.sidebarScreenToLocal}
-                    PanelWidth={this.leftMenuWidth}
-                    PanelHeight={this.leftMenuHeight}
-                    renderDepth={0}
-                    containerViewPath={returnEmptyDoclist}
-                    focus={emptyFunction}
-                    styleProvider={DefaultStyleProvider}
-                    isContentActive={returnTrue}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    childFilters={returnEmptyFilter}
-                    childFiltersByRanges={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                />
-            </div>
-        );
-    }
+        const vars: number[] = [];
+        let question: QuestionTemplate = questions.inclinePlane[0];
 
-    @action
-    selectMenu = (button: Doc) => {
-        const title = StrCast(button[DocData].title);
-        const willOpen = !this._leftMenuFlyoutWidth || this._panelContent !== title;
-        this.closeFlyout();
-        if (willOpen) {
-            switch ((this._panelContent = title)) {
-                case 'Settings':
-                    SettingsManager.Instance.openMgr();
-                    break;
-                case 'Help':
-                    break;
-                default:
-                    this.expandFlyout(button);
+        if (this.simulationType === 'Inclined Plane') {
+            this.dataDoc.questionNumber = (NumCast(this.dataDoc.questionNumber) + 1) % questions.inclinePlane.length;
+            question = questions.inclinePlane[NumCast(this.dataDoc.questionNumber)];
+
+            let coefficient = 0;
+            let wedge_angle = 0;
+
+            for (let i = 0; i < question.variablesForQuestionSetup.length; i++) {
+                if (question.variablesForQuestionSetup[i] === 'theta - max 45') {
+                    const randValue = Math.floor(Math.random() * 44 + 1);
+                    vars.push(randValue);
+                    wedge_angle = randValue;
+                } else if (question.variablesForQuestionSetup[i] === 'coefficient of static friction') {
+                    const randValue = Math.round(Math.random() * 1000) / 1000;
+                    vars.push(randValue);
+                    coefficient = randValue;
+                }
             }
+            this.dataDoc.wedge_angle = wedge_angle;
+            this.changeWedgeBasedOnNewAngle(wedge_angle);
+            this.dataDoc.coefficientOfStaticFriction = coefficient;
+            this.dataDoc.review_Coefficient = coefficient;
         }
-        return true;
+        let q = '';
+        for (let i = 0; i < question.questionSetup.length; i++) {
+            q += question.questionSetup[i];
+            if (i !== question.questionSetup.length - 1) {
+                q += vars[i];
+                if (question.variablesForQuestionSetup[i].includes('theta')) {
+                    q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)';
+                }
+            }
+        }
+        this.dataDoc.questionVariables = new List<number>(vars);
+        this.dataDoc.selectedQuestion = JSON.stringify(question);
+        this.dataDoc.questionPartOne = q;
+        this.dataDoc.questionPartTwo = question.question;
+        this.dataDoc.answers = new List<number>(this.getAnswersToQuestion(question, vars));
+        // this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset);
     };
 
-    @computed get mainInnerContent() {
-        const leftMenuFlyoutWidth = this._leftMenuFlyoutWidth + this.leftMenuWidth();
-        const width = this.propertiesWidth() + leftMenuFlyoutWidth;
-        return (
-            <>
-                {this._hideUI ? null : this.leftMenuPanel}
-                <div key="inner" className="mainView-innerContent">
-                    {this.flyout}
-                    <div
-                        className="mainView-libraryHandle"
-                        style={{ background: SnappingManager.userBackgroundColor, left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }}
-                        onPointerDown={this.onFlyoutPointerDown}>
-                        <FontAwesomeIcon icon="chevron-left" color={SnappingManager.userColor} style={{ opacity: '50%' }} size="sm" />
-                    </div>
-                    <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}>
-                        {this.dockingContent}
+    // Default setup for uniform circular motion simulation
+    @action
+    setupCircular = (value: number) => {
+        this.dataDoc.simulation_showComponentForces = false;
+        this.dataDoc.mass1_velocityYstart = 0;
+        this.dataDoc.mass1_velocityXstart = value;
+        const xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+        const yPos = (this.yMax + this.yMin) / 2 + this.circularMotionRadius - this.mass1Radius;
+        this.dataDoc.mass1_positionYstart = yPos;
+        this.dataDoc.mass1_positionXstart = xPos;
+        const tensionForce: IForce = {
+            description: 'Centripetal Force',
+            magnitude: (this.dataDoc.mass1_velocityXstart ** 2 * this.mass1) / this.circularMotionRadius,
+            directionInDegrees: 90,
+        };
+        this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce]);
+        this.dataDoc.mass1_forcesStart = JSON.stringify([tensionForce]);
+        this._simReset++;
+    };
 
-                        {this._hideUI ? null : (
-                            <div
-                                className={`mainView-propertiesDragger${this.propertiesWidth() < 10 ? '-minified' : ''}`}
-                                key="props"
-                                onPointerDown={this.onPropertiesPointerDown}
-                                style={{ background: SnappingManager.userVariantColor, right: this.propertiesWidth() - 1 }}>
-                                <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={SnappingManager.userColor} size="sm" />
-                            </div>
-                        )}
-                        <div className="properties-container" style={{ width: this.propertiesWidth(), color: SnappingManager.userColor }}>
-                            <div style={{ display: this.propertiesWidth() < 10 ? 'none' : undefined }}>
-                                <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </>
-        );
-    }
+    setupInclinedPlane = () => {
+        this.changeWedgeBasedOnNewAngle(this.wedgeAngle);
+        this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
+        this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction));
+    };
 
-    @computed get mainDashboardArea() {
-        return !this.userDoc ? null : (
-            <div
-                className="mainView-dashboardArea"
-                ref={r => {
-                    r &&
-                        new _global.ResizeObserver(
-                            action(() => {
-                                this._dashUIWidth = r.getBoundingClientRect().width;
-                                this._dashUIHeight = r.getBoundingClientRect().height;
-                            })
-                        ).observe(r);
-                }}
-                style={{
-                    color: 'black',
-                    height: `calc(100% - ${this.topOfDashUI + this.topMenuHeight()}px)`,
-                    width: '100%',
-                }}>
-                {this.mainInnerContent}
-            </div>
-        );
-    }
+    // Default setup for pendulum simulation
+    setupPendulum = () => {
+        const length = (300 * this._props.PanelWidth()) / 1000;
+        const angle = 30;
+        const x = length * Math.cos(((90 - angle) * Math.PI) / 180);
+        const y = length * Math.sin(((90 - angle) * Math.PI) / 180);
+        const xPos = this.xMax / 2 - x - this.mass1Radius;
+        const yPos = y - this.mass1Radius - 5;
+        this.dataDoc.mass1_positionXstart = xPos;
+        this.dataDoc.mass1_positionYstart = yPos;
+        const forceOfTension: IForce = {
+            description: 'Tension',
+            magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin((60 * Math.PI) / 180),
+            directionInDegrees: 90 - angle,
+        };
+        const gravityParallel: IForce = {
+            description: 'Gravity Parallel Component',
+            magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin(((90 - angle) * Math.PI) / 180),
+            directionInDegrees: -angle - 90,
+        };
+        const gravityPerpendicular: IForce = {
+            description: 'Gravity Perpendicular Component',
+            magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(((90 - angle) * Math.PI) / 180),
+            directionInDegrees: -angle,
+        };
 
-    expandFlyout = action((button: Doc) => {
-        // bcz: What's going on here!? --- may be fixed now, so commenting out ...
-        // Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab.  All of the div below the CollectionFreeFormView's main div
-        // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be.
-        // interactively adjusting the flyout fixes the problem.  So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);)
-        this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250;
-        // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
+        this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]);
+        this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
+        this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
+        this.dataDoc.pendulum_angle = this.dataDoc.pendulum_angleStart = 30;
+        this.dataDoc.pendulum_length = this.dataDoc.pendulum_lengthStart = 300;
+    };
 
-        this._sidebarContent.proto = DocCast(button.target);
-        SnappingManager.SetLastPressedBtn(button[Id]);
-    });
+    // Default setup for spring simulation
+    @action
+    setupSpring = () => {
+        this.dataDoc.simulation_showComponentForces = false;
+        this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
+        this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
+        this.dataDoc.mass1_positionXstart = this.xMax / 2 - this.mass1Radius;
+        this.dataDoc.mass1_positionYstart = 200;
+        this.dataDoc.spring_constant = 0.5;
+        this.dataDoc.spring_lengthRest = 200;
+        this.dataDoc.spring_lengthStart = 200;
+        this._simReset++;
+    };
 
-    closeFlyout = action(() => {
-        SnappingManager.SetLastPressedBtn('');
-        this._panelContent = 'none';
-        this._sidebarContent.proto = undefined;
-        this._leftMenuFlyoutWidth = 0;
-    });
+    // Default setup for suspension simulation
+    @action
+    setupSuspension = () => {
+        const xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+        const yPos = this.yMin + 200;
+        this.dataDoc.mass1_positionYstart = yPos;
+        this.dataDoc.mass1_positionXstart = xPos;
+        this.dataDoc.mass1_positionY = this.getDisplayYPos(yPos);
+        this.dataDoc.mass1_positionX = xPos;
+        const tensionMag = (this.mass1 * Math.abs(this.gravity)) / (2 * Math.sin(Math.PI / 4));
+        const tensionForce1: IForce = {
+            description: 'Tension',
+            magnitude: tensionMag,
+            directionInDegrees: 45,
+        };
+        const tensionForce2: IForce = {
+            description: 'Tension',
+            magnitude: tensionMag,
+            directionInDegrees: 135,
+        };
+        const gravity = this.gravityForce(this.mass1);
+        this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
+        this.dataDoc.mass1_forcesStart = JSON.stringify([tensionForce1, tensionForce2, gravity]);
+        this._simReset++;
+    };
 
-    remButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true);
-    moveButtonDoc = (docs: Doc | Doc[], targetCol: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(docs) && addDocument(docs);
-    addButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true);
+    // Default setup for pulley simulation
+    @action
+    setupPulley = () => {
+        this.dataDoc.simulation_showComponentForces = false;
+        this.dataDoc.mass1_positionYstart = (this.yMax + this.yMin) / 2;
+        this.dataDoc.mass1_positionXstart = (this.xMin + this.xMax) / 2 - 2 * this.mass1Radius - 5;
+        this.dataDoc.mass1_positionY = this.getDisplayYPos((this.yMax + this.yMin) / 2);
+        this.dataDoc.mass1_positionX = (this.xMin + this.xMax) / 2 - 2 * this.mass1Radius - 5;
+        const a = (-1 * ((this.mass1 - this.mass2) * Math.abs(this.gravity))) / (this.mass1 + this.mass2);
+        const gravityForce1 = this.gravityForce(this.mass1);
+        const tensionForce1: IForce = {
+            description: 'Tension',
+            magnitude: this.mass1 * a + this.mass1 * Math.abs(this.gravity),
+            directionInDegrees: 90,
+        };
+        this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce1, tensionForce1]);
+        this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce1, tensionForce1]);
 
-    buttonBarXf = () => {
-        if (!this._docBtnRef.current) return Transform.Identity();
-        const { scale, translateX, translateY } = ClientUtils.GetScreenTransform(this._docBtnRef.current);
-        return new Transform(-translateX, -translateY, 1 / scale);
+        const gravityForce2 = this.gravityForce(this.mass2);
+        const tensionForce2: IForce = {
+            description: 'Tension',
+            magnitude: -this.mass2 * a + this.mass2 * Math.abs(this.gravity),
+            directionInDegrees: 90,
+        };
+        this.dataDoc.mass2_positionYstart = (this.yMax + this.yMin) / 2;
+        this.dataDoc.mass2_positionXstart = (this.xMin + this.xMax) / 2 + 5;
+        this.dataDoc.mass2_positionY = this.getDisplayYPos((this.yMax + this.yMin) / 2);
+        this.dataDoc.mass2_positionX = (this.xMin + this.xMax) / 2 + 5;
+        this.dataDoc.mass2_forcesUpdated = JSON.stringify([gravityForce2, tensionForce2]);
+        this.dataDoc.mass2_forcesStart = JSON.stringify([gravityForce2, tensionForce2]);
+        this._simReset++;
     };
 
-    @computed get docButtons() {
-        return !Doc.MyDockedBtns ? null : (
-            <div className="mainView-docButtons" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }} ref={this._docBtnRef}>
-                <CollectionLinearView
-                    Document={Doc.MyDockedBtns}
-                    docViewPath={returnEmptyDocViewList}
-                    fieldKey="data"
-                    dropAction={dropActionType.embed}
-                    styleProvider={DefaultStyleProvider}
-                    select={emptyFunction}
-                    isAnyChildContentActive={returnFalse}
-                    isContentActive={emptyFunction}
-                    isSelected={returnFalse}
-                    moveDocument={this.moveButtonDoc}
-                    addDocument={this.addButtonDoc}
-                    addDocTab={DocumentViewInternal.addDocTabFunc}
-                    pinToPres={emptyFunction}
-                    removeDocument={this.remButtonDoc}
-                    ScreenToLocalTransform={this.buttonBarXf}
-                    PanelWidth={this.leftMenuFlyoutWidth}
-                    PanelHeight={this.leftMenuFlyoutHeight}
-                    renderDepth={0}
-                    focus={emptyFunction}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    childFilters={returnEmptyFilter}
-                    childFiltersByRanges={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                />
-                {['watching', 'recording'].includes(StrCast(this.userDoc?.presentationMode)) ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : null}
-            </div>
-        );
-    }
-    @computed get snapLines() {
-        const dragged = DragManager.docsBeingDragged.lastElement() ?? DocumentView.SelectedDocs().lastElement();
-        const dragPar = dragged ? CollectionFreeFormView.from(DocumentView.getViews(dragged).lastElement()) : undefined;
-        return !dragPar?.layoutDoc.freeform_snapLines ? null : (
-            <div className="mainView-snapLines">
-                <svg style={{ width: '100%', height: '100%' }}>
-                    {[
-                        ...SnappingManager.HorizSnapLines.map((l, i) => (
-                            // eslint-disable-next-line react/no-array-index-key
-                            <line key={'horiz' + i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
-                        )),
-                        ...SnappingManager.VertSnapLines.map((l, i) => (
-                            // eslint-disable-next-line react/no-array-index-key
-                            <line key={'vert' + i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
-                        )),
-                    ]}
-                </svg>
-            </div>
-        );
-    }
-
-    @computed get inkResources() {
-        return (
-            <svg width={0} height={0}>
-                <defs>
-                    <filter id="inkSelectionHalo">
-                        <feColorMatrix
-                            type="matrix"
-                            result="color"
-                            values="1 0 0 0 0
-                0 0 0 0 0
-                0 0 0 0 0
-                0 0 0 1 0"
-                        />
-                        <feGaussianBlur in="color" stdDeviation="4" result="blur" />
-                        <feOffset in="blur" dx="0" dy="0" result="offset" />
-                        <feMerge>
-                            <feMergeNode in="bg" />
-                            <feMergeNode in="offset" />
-                            <feMergeNode in="SourceGraphic" />
-                        </feMerge>
-                    </filter>
-                </defs>
-            </svg>
-        );
+    public static parseJSON(json: string) {
+        return !json ? [] : (JSON.parse(json) as IForce[]);
     }
 
-    togglePropertiesFlyout = () => {
-        if (MainView.Instance.propertiesWidth() > 0) {
-            SnappingManager.SetPropertiesWidth(0);
-        } else {
-            SnappingManager.SetPropertiesWidth(300);
-        }
+    // Handle force change in review mode
+    updateReviewModeValues = () => {
+        const forceOfGravityReview: IForce = {
+            description: 'Gravity',
+            magnitude: NumCast(this.dataDoc.review_GravityMagnitude),
+            directionInDegrees: NumCast(this.dataDoc.review_GravityAngle),
+        };
+        const normalForceReview: IForce = {
+            description: 'Normal Force',
+            magnitude: NumCast(this.dataDoc.review_NormalMagnitude),
+            directionInDegrees: NumCast(this.dataDoc.review_NormalAngle),
+        };
+        const staticFrictionForceReview: IForce = {
+            description: 'Static Friction Force',
+            magnitude: NumCast(this.dataDoc.review_StaticMagnitude),
+            directionInDegrees: NumCast(this.dataDoc.review_StaticAngle),
+        };
+        this.dataDoc.mass1_forcesStart = JSON.stringify([forceOfGravityReview, normalForceReview, staticFrictionForceReview]);
+        this.dataDoc.mass1_forcesUpdated = JSON.stringify([forceOfGravityReview, normalForceReview, staticFrictionForceReview]);
     };
 
-    lightboxMaxBorder = [200, 50];
+    pause = () => (this.dataDoc.simulation_paused = true);
+    componentForces1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_componentForces));
+    setComponentForces1 = (forces: IForce[]) => (this.dataDoc.mass1_componentForces = JSON.stringify(forces));
+    componentForces2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_componentForces));
+    setComponentForces2 = (forces: IForce[]) => (this.dataDoc.mass2_componentForces = JSON.stringify(forces));
+    startForces1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_forcesStart));
+    startForces2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_forcesStart));
+    forcesUpdated1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_forcesUpdated));
+    setForcesUpdated1 = (forces: IForce[]) => (this.dataDoc.mass1_forcesUpdated = JSON.stringify(forces));
+    forcesUpdated2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_forcesUpdated));
+    setForcesUpdated2 = (forces: IForce[]) => (this.dataDoc.mass2_forcesUpdated = JSON.stringify(forces));
+    setPosition1 = (xPos: number | undefined, yPos: number | undefined) => {
+        yPos !== undefined && (this.dataDoc.mass1_positionY = Math.round(yPos * 100) / 100);
+        xPos !== undefined && (this.dataDoc.mass1_positionX = Math.round(xPos * 100) / 100);
+    };
+    setPosition2 = (xPos: number | undefined, yPos: number | undefined) => {
+        yPos !== undefined && (this.dataDoc.mass2_positionY = Math.round(yPos * 100) / 100);
+        xPos !== undefined && (this.dataDoc.mass2_positionX = Math.round(xPos * 100) / 100);
+    };
+    setVelocity1 = (xVel: number | undefined, yVel: number | undefined) => {
+        yVel !== undefined && (this.dataDoc.mass1_velocityY = (-1 * Math.round(yVel * 100)) / 100);
+        xVel !== undefined && (this.dataDoc.mass1_velocityX = Math.round(xVel * 100) / 100);
+    };
+    setVelocity2 = (xVel: number | undefined, yVel: number | undefined) => {
+        yVel !== undefined && (this.dataDoc.mass2_velocityY = (-1 * Math.round(yVel * 100)) / 100);
+        xVel !== undefined && (this.dataDoc.mass2_velocityX = Math.round(xVel * 100) / 100);
+    };
+    setAcceleration1 = (xAccel: number, yAccel: number) => {
+        this.dataDoc.mass1_accelerationY = yAccel;
+        this.dataDoc.mass1_accelerationX = xAccel;
+    };
+    setAcceleration2 = (xAccel: number, yAccel: number) => {
+        this.dataDoc.mass2_accelerationY = yAccel;
+        this.dataDoc.mass2_accelerationX = xAccel;
+    };
+    setPendulumAngle = (angle: number | undefined, length: number | undefined) => {
+        angle !== undefined && (this.dataDoc.pendulum_angle = angle);
+        length !== undefined && (this.dataDoc.pendulum_length = length);
+    };
+    setSpringLength = (length: number) => {
+        this.dataDoc.spring_lengthStart = length;
+    };
+    resetRequest = () => this._simReset;
     render() {
+        const commonWeightProps = {
+            pause: this.pause,
+            paused: BoolCast(this.dataDoc.simulation_paused),
+            panelWidth: this._props.PanelWidth,
+            panelHeight: this._props.PanelHeight,
+            resetRequest: this.resetRequest,
+            xMax: this.xMax,
+            xMin: this.xMin,
+            yMax: this.yMax,
+            yMin: this.yMin,
+            wallPositions: this.wallPositions,
+            gravity: Math.abs(this.gravity),
+            timestepSize: 0.05,
+            showComponentForces: BoolCast(this.dataDoc.simulation_showComponentForces),
+            coefficientOfKineticFriction: NumCast(this.dataDoc.coefficientOfKineticFriction),
+            elasticCollisions: BoolCast(this.dataDoc.elasticCollisions),
+            simulationMode: this.simulationMode,
+            noMovement: BoolCast(this.dataDoc.noMovement),
+            circularMotionRadius: this.circularMotionRadius,
+            wedgeHeight: this.wedgeHeight,
+            wedgeWidth: this.wedgeWidth,
+            springConstant: this.springConstant,
+            springStartLength: this.springLengthStart,
+            springRestLength: this.springLengthRest,
+            setSpringLength: this.setSpringLength,
+            setPendulumAngle: this.setPendulumAngle,
+            pendulumAngle: this.pendulumAngle,
+            pendulumLength: this.pendulumLength,
+            startPendulumAngle: this.pendulumAngleStart,
+            startPendulumLength: this.pendulumLengthStart,
+            radius: this.mass1Radius,
+            simulationSpeed: NumCast(this.dataDoc.simulation_speed, 2),
+            showAcceleration: BoolCast(this.dataDoc.simulation_showAcceleration),
+            showForceMagnitudes: BoolCast(this.dataDoc.simulation_showForceMagnitudes),
+            showForces: BoolCast(this.dataDoc.simulation_showForces),
+            showVelocity: BoolCast(this.dataDoc.simulation_showVelocity),
+            simulationType: this.simulationType,
+        };
         return (
-            <div
-                className="mainView-container"
-                style={{
-                    color: SnappingManager.userColor,
-                    background: SnappingManager.userBackgroundColor,
-                }}
-                onScroll={() =>
-                    (ele => {
-                        ele.scrollTop = ele.scrollLeft = 0;
-                    })(document.getElementById('root')!)
-                }
-                ref={r => {
-                    r &&
-                        new _global.ResizeObserver(
-                            action(() => {
-                                this._windowWidth = r.getBoundingClientRect().width;
-                                this._windowHeight = r.getBoundingClientRect().height;
-                            })
-                        ).observe(r);
-                }}>
-                {this.inkResources}
-                <DictationOverlay />
-                <SharingManager />
-                <CalendarManager />
-                <ServerStats />
-                <RTFMarkup />
-                <SettingsManager />
-                <ReportManager />
-                <CaptureManager />
-                <GroupManager />
-                <GoogleAuthenticationManager />
-                <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfSidebarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} />
-                <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} />
-                {this._hideUI ? null : <TopBar />}
-                <LinkDescriptionPopup />
-                {DocButtonState.Instance.LinkEditorDocView ? (
-                    <LinkMenu
-                        clearLinkEditor={action(() => {
-                            DocButtonState.Instance.LinkEditorDocView = undefined;
-                        })}
-                        docView={DocButtonState.Instance.LinkEditorDocView}
-                    />
-                ) : null}
-                {LinkInfo.Instance?.LinkInfo ? (
-                    // eslint-disable-next-line react/jsx-props-no-spreading
-                    <LinkDocPreview {...LinkInfo.Instance.LinkInfo} />
-                ) : null}
-                {((page: string) => {
-                    // prettier-ignore
-                    switch (page) {
-                        case 'home': return <DashboardView />;
-                        case 'dashboard':
-                        default: return (<>
-                                        <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || DocumentView.LightboxDoc() ? 'none' : undefined, zIndex: 2001 }}>
-                                            <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} togglePropertiesFlyout={this.togglePropertiesFlyout} toggleTopBar={this.toggleTopBar} topBarHeight={this.headerBarHeightFunc}/>
-                                        </div>
-                                        {this.mainDashboardArea}
-                                    </> );
-                    }
-                })(Doc.ActivePage)}
-                <PreviewCursor />
-                <TaskCompletionBox />
-                <ContextMenu />
-                <ImageLabelHandler />
-                <SmartDrawHandler />
-                <AnchorMenu />
-                <MapAnchorMenu />
-                <DirectionsAnchorMenu />
-                <DashFieldViewMenu />
-                <MarqueeOptionsMenu />
-                <TimelineMenu />
-                <RichTextMenu />
-                {this.snapLines}
-                <LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
-                <GPTPopup key="gptpopup" />
-                <SchemaCSVPopUp key="schemacsvpopup" />
-                <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
+            <div className="physicsSimApp">
+                <div className="mechanicsSimulationContainer">
+                    <div className="mechanicsSimulationContentContainer">
+                        <div className="mechanicsSimulationButtonsAndElements">
+                            <div className="mechanicsSimulationButtons">
+                                {!this.dataDoc.simulation_paused && (
+                                    <div
+                                        style={{
+                                            position: 'fixed',
+                                            left: 0.1 * this._props.PanelWidth() + 'px',
+                                            top: 0.95 * this._props.PanelHeight() + 'px',
+                                            width: 0.5 * this._props.PanelWidth() + 'px',
+                                        }}>
+                                        <LinearProgress />
+                                    </div>
+                                )}
+                            </div>
+                            <div
+                                className="mechanicsSimulationElements"
+                                style={{
+                                    //
+                                    width: '60%',
+                                    height: '100%',
+                                    position: 'absolute',
+                                    background: 'yellow',
+                                }}>
+                                <Weight
+                                    {...commonWeightProps}
+                                    color="red"
+                                    componentForces={this.componentForces1}
+                                    setComponentForces={this.setComponentForces1}
+                                    displayXVelocity={NumCast(this.dataDoc.mass1_velocityX)}
+                                    displayYVelocity={NumCast(this.dataDoc.mass1_velocityY)}
+                                    mass={this.mass1}
+                                    startForces={this.startForces1}
+                                    startPosX={this.mass1PosXStart}
+                                    startPosY={this.mass1PosYStart}
+                                    startVelX={this.mass1VelXStart}
+                                    startVelY={this.mass1VelYStart}
+                                    updateMassPosX={NumCast(this.dataDoc.mass1_xChange)}
+                                    updateMassPosY={NumCast(this.dataDoc.mass1_yChange)}
+                                    forcesUpdated={this.forcesUpdated1}
+                                    setForcesUpdated={this.setForcesUpdated1}
+                                    setPosition={this.setPosition1}
+                                    setVelocity={this.setVelocity1}
+                                    setAcceleration={this.setAcceleration1}
+                                />
+                                {this.simulationType === 'Pulley' && (
+                                    <Weight
+                                        {...commonWeightProps}
+                                        color="green"
+                                        componentForces={this.componentForces2}
+                                        setComponentForces={this.setComponentForces2}
+                                        displayXVelocity={NumCast(this.dataDoc.mass2_velocityX)}
+                                        displayYVelocity={NumCast(this.dataDoc.mass2_velocityY)}
+                                        mass={this.mass2}
+                                        startForces={this.startForces2}
+                                        startPosX={this.mass2PosXStart}
+                                        startPosY={this.mass2PosYStart}
+                                        startVelX={this.mass2VelXStart}
+                                        startVelY={this.mass2VelYStart}
+                                        updateMassPosX={NumCast(this.dataDoc.mass2_xChange)}
+                                        updateMassPosY={NumCast(this.dataDoc.mass2_yChange)}
+                                        forcesUpdated={this.forcesUpdated2}
+                                        setForcesUpdated={this.setForcesUpdated2}
+                                        setPosition={this.setPosition2}
+                                        setVelocity={this.setVelocity2}
+                                        setAcceleration={this.setAcceleration2}
+                                    />
+                                )}
+                            </div>
+                            <div style={{ position: 'absolute', transformOrigin: 'top left', top: 0, left: 0, width: '100%', height: '100%' }}>
+                                {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane') &&
+                                    this.wallPositions?.map((element, index) => <Wall key={index} length={element.length} xPos={element.xPos} yPos={element.yPos} angleInDegrees={element.angleInDegrees} />)}
+                            </div>
+                        </div>
+                    </div>
+                    <div
+                        className="mechanicsSimulationEquationContainer"
+                        onWheel={e => this._props.isContentActive() && e.stopPropagation()}
+                        style={{ overflow: 'auto', height: `${Math.max(1, 800 / this._props.PanelWidth()) * 100}%`, transform: `scale(${Math.min(1, this._props.PanelWidth() / 850)})` }}>
+                        <div className="mechanicsSimulationControls">
+                            <Stack direction="row" spacing={1}>
+                                {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
+                                    <IconButton onClick={() => (this.dataDoc.simulation_paused = false)}>
+                                        <PlayArrowIcon />
+                                    </IconButton>
+                                )}
+                                {!this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
+                                    <IconButton onClick={() => (this.dataDoc.simulation_paused = true)}>
+                                        <PauseIcon />
+                                    </IconButton>
+                                )}
+                                {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
+                                    <IconButton onClick={action(() => this._simReset++)}>
+                                        <ReplayIcon />
+                                    </IconButton>
+                                )}
+                            </Stack>
+                            <div className="dropdownMenu">
+                                <select
+                                    value={StrCast(this.simulationType)}
+                                    onChange={event => {
+                                        this.dataDoc.simulation_type = event.target.value;
+                                        this.setupSimulation();
+                                    }}
+                                    style={{ height: '2em', width: '100%', fontSize: '16px' }}>
+                                    <option value="One Weight">Projectile</option>
+                                    <option value="Inclined Plane">Inclined Plane</option>
+                                    <option value="Pendulum">Pendulum</option>
+                                    <option value="Spring">Spring</option>
+                                    <option value="Circular Motion">Circular Motion</option>
+                                    <option value="Pulley">Pulley</option>
+                                    <option value="Suspension">Suspension</option>
+                                </select>
+                            </div>
+                            <div className="dropdownMenu">
+                                <select
+                                    value={this.simulationMode}
+                                    onChange={event => {
+                                        this.dataDoc.simulation_mode = event.target.value;
+                                        this.setupSimulation();
+                                    }}
+                                    style={{ height: '2em', width: '100%', fontSize: '16px' }}>
+                                    <option value="Tutorial">Tutorial Mode</option>
+                                    <option value="Freeform">Freeform Mode</option>
+                                    <option value="Review">Review Mode</option>
+                                </select>
+                            </div>
+                        </div>
+                        {this.simulationMode === 'Review' && this.simulationType !== 'Inclined Plane' && (
+                            <div className="wordProblemBox">
+                                <p>{this.simulationType} review problems in progress!</p>
+                                <hr />
+                            </div>
+                        )}
+                        {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && (
+                            <div>
+                                {!this.dataDoc.hintDialogueOpen && (
+                                    <IconButton
+                                        onClick={() => (this.dataDoc.hintDialogueOpen = true)}
+                                        sx={{
+                                            position: 'fixed',
+                                            left: this.xMax - 50 + 'px',
+                                            top: this.yMin + 14 + 'px',
+                                        }}>
+                                        <QuestionMarkIcon />
+                                    </IconButton>
+                                )}
+                                <Dialog maxWidth="sm" fullWidth open={BoolCast(this.dataDoc.hintDialogueOpen)} onClose={() => (this.dataDoc.hintDialogueOpen = false)}>
+                                    <DialogTitle>Hints</DialogTitle>
+                                    <DialogContent>
+                                        {this.selectedQuestion.hints?.map((hint: any, index: number) => (
+                                            <div key={index}>
+                                                <DialogContentText>
+                                                    <details>
+                                                        <summary>
+                                                            <b>
+                                                                Hint {index + 1}: {hint.description}
+                                                            </b>
+                                                        </summary>
+                                                        {hint.content}
+                                                    </details>
+                                                </DialogContentText>
+                                            </div>
+                                        ))}
+                                    </DialogContent>
+                                    <DialogActions>
+                                        <Button onClick={() => (this.dataDoc.hintDialogueOpen = false)}>Close</Button>
+                                    </DialogActions>
+                                </Dialog>
+                                <div className="wordProblemBox">
+                                    <div className="question">
+                                        <p>{this.questionPartOne}</p>
+                                        <p>{this.questionPartTwo}</p>
+                                    </div>
+                                    <div className="answers">
+                                        {this.selectedQuestion.answerParts.includes('force of gravity') && (
+                                            <InputField
+                                                label={<p>Gravity magnitude</p>}
+                                                lowerBound={0}
+                                                dataDoc={this.dataDoc}
+                                                prop="review_GravityMagnitude"
+                                                step={0.1}
+                                                unit="N"
+                                                upperBound={50}
+                                                value={NumCast(this.dataDoc.review_GravityMagnitude)}
+                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of gravity')]}
+                                                labelWidth="7em"
+                                            />
+                                        )}
+                                        {this.selectedQuestion.answerParts.includes('angle of gravity') && (
+                                            <InputField
+                                                label={<p>Gravity angle</p>}
+                                                lowerBound={0}
+                                                dataDoc={this.dataDoc}
+                                                prop="review_GravityAngle"
+                                                step={1}
+                                                unit="°"
+                                                upperBound={360}
+                                                value={NumCast(this.dataDoc.review_GravityAngle)}
+                                                radianEquivalent
+                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of gravity')]}
+                                                labelWidth="7em"
+                                            />
+                                        )}
+                                        {this.selectedQuestion.answerParts.includes('normal force') && (
+                                            <InputField
+                                                label={<p>Normal force magnitude</p>}
+                                                lowerBound={0}
+                                                dataDoc={this.dataDoc}
+                                                prop="review_NormalMagnitude"
+                                                step={0.1}
+                                                unit="N"
+                                                upperBound={50}
+                                                value={NumCast(this.dataDoc.review_NormalMagnitude)}
+                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('normal force')]}
+                                                labelWidth="7em"
+                                            />
+                                        )}
+                                        {this.selectedQuestion.answerParts.includes('angle of normal force') && (
+                                            <InputField
+                                                label={<p>Normal force angle</p>}
+                                                lowerBound={0}
+                                                dataDoc={this.dataDoc}
+                                                prop="review_NormalAngle"
+                                                step={1}
+                                                unit="°"
+                                                upperBound={360}
+                                                value={NumCast(this.dataDoc.review_NormalAngle)}
+                                                radianEquivalent
+                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of normal force')]}
+                                                labelWidth="7em"
+                                            />
+                                        )}
+                                        {this.selectedQuestion.answerParts.includes('force of static friction') && (
+                                            <InputField
+                                                label={<p>Static friction magnitude</p>}
+                                                lowerBound={0}
+                                                dataDoc={this.dataDoc}
+                                                prop="review_StaticMagnitude"
+                                                step={0.1}
+                                                unit="N"
+                                                upperBound={50}
+                                                value={NumCast(this.dataDoc.review_StaticMagnitude)}
+                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of static friction')]}
+                                                labelWidth="7em"
+                                            />
+                                        )}
+                                        {this.selectedQuestion.answerParts.includes('angle of static friction') && (
+                                            <InputField
+                                                label={<p>Static friction angle</p>}
+                                                lowerBound={0}
+                                                dataDoc={this.dataDoc}
+                                                prop="review_StaticAngle"
+                                                step={1}
+                                                unit="°"
+                                                upperBound={360}
+                                                value={NumCast(this.dataDoc.review_StaticAngle)}
+                                                radianEquivalent
+                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of static friction')]}
+                                                labelWidth="7em"
+                                            />
+                                        )}
+                                        {this.selectedQuestion.answerParts.includes('coefficient of static friction') && (
+                                            <InputField
+                                                label={
+                                                    <Box>
+                                                        &mu;<sub>s</sub>
+                                                    </Box>
+                                                }
+                                                lowerBound={0}
+                                                dataDoc={this.dataDoc}
+                                                prop="coefficientOfStaticFriction"
+                                                step={0.1}
+                                                unit=""
+                                                upperBound={1}
+                                                value={NumCast(this.dataDoc.coefficientOfStaticFriction)}
+                                                effect={this.updateReviewForcesBasedOnCoefficient}
+                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('coefficient of static friction')]}
+                                            />
+                                        )}
+                                        {this.selectedQuestion.answerParts.includes('wedge angle') && (
+                                            <InputField
+                                                label={<Box>&theta;</Box>}
+                                                lowerBound={0}
+                                                dataDoc={this.dataDoc}
+                                                prop="wedge_angle"
+                                                step={1}
+                                                unit="°"
+                                                upperBound={49}
+                                                value={this.wedgeAngle}
+                                                effect={(val: number) => {
+                                                    this.changeWedgeBasedOnNewAngle(val);
+                                                    this.updateReviewForcesBasedOnAngle(val);
+                                                }}
+                                                radianEquivalent
+                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('wedge angle')]}
+                                            />
+                                        )}
+                                    </div>
+                                </div>
+                            </div>
+                        )}
+                        {this.simulationMode === 'Tutorial' && (
+                            <div className="wordProblemBox">
+                                <div className="question">
+                                    <h2>Problem</h2>
+                                    <p>{this.tutorial.question}</p>
+                                </div>
+                                <div
+                                    style={{
+                                        display: 'flex',
+                                        justifyContent: 'spaceBetween',
+                                        width: '100%',
+                                    }}>
+                                    <IconButton
+                                        onClick={() => {
+                                            let step = NumCast(this.dataDoc.tutorial_stepNumber) - 1;
+                                            step = Math.max(step, 0);
+                                            step = Math.min(step, this.tutorial.steps.length - 1);
+                                            this.dataDoc.tutorial_stepNumber = step;
+                                            this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces);
+                                            this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces);
+                                            this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
+                                        }}
+                                        disabled={this.dataDoc.tutorial_stepNumber === 0}>
+                                        <ArrowLeftIcon />
+                                    </IconButton>
+                                    <div>
+                                        <h3>
+                                            Step {NumCast(this.dataDoc.tutorial_stepNumber) + 1}: {this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].description}
+                                        </h3>
+                                        <p>{this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].content}</p>
+                                    </div>
+                                    <IconButton
+                                        onClick={() => {
+                                            let step = NumCast(this.dataDoc.tutorial_stepNumber) + 1;
+                                            step = Math.max(step, 0);
+                                            step = Math.min(step, this.tutorial.steps.length - 1);
+                                            this.dataDoc.tutorial_stepNumber = step;
+                                            this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces);
+                                            this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces);
+                                            this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
+                                        }}
+                                        disabled={this.dataDoc.tutorial_stepNumber === this.tutorial.steps.length - 1}>
+                                        <ArrowRightIcon />
+                                    </IconButton>
+                                </div>
+                                <div>
+                                    {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && <p>Resources</p>}
+                                    {this.simulationType === 'One Weight' && (
+                                        <ul>
+                                            <li>
+                                                <a
+                                                    href="https://www.khanacademy.org/science/physics/one-dimensional-motion"
+                                                    target="_blank"
+                                                    rel="noreferrer"
+                                                    style={{
+                                                        color: 'blue',
+                                                        textDecoration: 'underline',
+                                                    }}>
+                                                    Khan Academy - One Dimensional Motion
+                                                </a>
+                                            </li>
+                                            <li>
+                                                <a
+                                                    href="https://www.khanacademy.org/science/physics/two-dimensional-motion"
+                                                    target="_blank"
+                                                    rel="noreferrer"
+                                                    style={{
+                                                        color: 'blue',
+                                                        textDecoration: 'underline',
+                                                    }}>
+                                                    Khan Academy - Two Dimensional Motion
+                                                </a>
+                                            </li>
+                                        </ul>
+                                    )}
+                                    {this.simulationType === 'Inclined Plane' && (
+                                        <ul>
+                                            <li>
+                                                <a
+                                                    href="https://www.khanacademy.org/science/physics/forces-newtons-laws#normal-contact-force"
+                                                    target="_blank"
+                                                    rel="noreferrer"
+                                                    style={{
+                                                        color: 'blue',
+                                                        textDecoration: 'underline',
+                                                    }}>
+                                                    Khan Academy - Normal Force
+                                                </a>
+                                            </li>
+                                            <li>
+                                                <a
+                                                    href="https://www.khanacademy.org/science/physics/forces-newtons-laws#inclined-planes-friction"
+                                                    target="_blank"
+                                                    rel="noreferrer"
+                                                    style={{
+                                                        color: 'blue',
+                                                        textDecoration: 'underline',
+                                                    }}>
+                                                    Khan Academy - Inclined Planes
+                                                </a>
+                                            </li>
+                                        </ul>
+                                    )}
+                                    {this.simulationType === 'Pendulum' && (
+                                        <ul>
+                                            <li>
+                                                <a
+                                                    href="https://www.khanacademy.org/science/physics/forces-newtons-laws#tension-tutorial"
+                                                    target="_blank"
+                                                    rel="noreferrer"
+                                                    style={{
+                                                        color: 'blue',
+                                                        textDecoration: 'underline',
+                                                    }}>
+                                                    Khan Academy - Tension
+                                                </a>
+                                            </li>
+                                        </ul>
+                                    )}
+                                </div>
+                            </div>
+                        )}
+                        {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && (
+                            <div
+                                style={{
+                                    display: 'flex',
+                                    justifyContent: 'space-between',
+                                    marginTop: '10px',
+                                }}>
+                                <p
+                                    style={{
+                                        color: 'blue',
+                                        textDecoration: 'underline',
+                                        cursor: 'pointer',
+                                    }}
+                                    onClick={() => (this.dataDoc.simulation_mode = 'Tutorial')}>
+                                    {' '}
+                                    Go to walkthrough{' '}
+                                </p>
+                                <div style={{ display: 'flex', flexDirection: 'column' }}>
+                                    <Button
+                                        onClick={action(() => {
+                                            this._simReset++;
+                                            this.checkAnswers();
+                                            this.dataDoc.simulation_showIcon = true;
+                                        })}
+                                        variant="outlined">
+                                        <p>Submit</p>
+                                    </Button>
+                                    <Button
+                                        onClick={() => {
+                                            this.generateNewQuestion();
+                                            this.dataDoc.simulation_showIcon = false;
+                                        }}
+                                        variant="outlined">
+                                        <p>New question</p>
+                                    </Button>
+                                </div>
+                            </div>
+                        )}
+                        {this.simulationMode === 'Freeform' && (
+                            <div className="vars">
+                                <FormControl component="fieldset">
+                                    <FormGroup>
+                                        {this.simulationType === 'One Weight' && (
+                                            <FormControlLabel
+                                                control={<Checkbox checked={BoolCast(this.dataDoc.elasticCollisions)} onChange={() => (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />}
+                                                label="Make collisions elastic"
+                                                labelPlacement="start"
+                                            />
+                                        )}
+                                        <FormControlLabel
+                                            control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showForces)} onChange={() => (this.dataDoc.simulation_showForces = !this.dataDoc.simulation_showForces)} />}
+                                            label="Show force vectors"
+                                            labelPlacement="start"
+                                        />
+                                        {(this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && (
+                                            <FormControlLabel
+                                                control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showComponentForces)} onChange={() => (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />}
+                                                label="Show component force vectors"
+                                                labelPlacement="start"
+                                            />
+                                        )}
+                                        <FormControlLabel
+                                            control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showAcceleration)} onChange={() => (this.dataDoc.simulation_showAcceleration = !this.dataDoc.simulation_showAcceleration)} />}
+                                            label="Show acceleration vector"
+                                            labelPlacement="start"
+                                        />
+                                        <FormControlLabel
+                                            control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showVelocity)} onChange={() => (this.dataDoc.simulation_showVelocity = !this.dataDoc.simulation_showVelocity)} />}
+                                            label="Show velocity vector"
+                                            labelPlacement="start"
+                                        />
+                                        <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit="x" upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth="5em" />
+                                        {this.dataDoc.simulation_paused && this.simulationType !== 'Circular Motion' && (
+                                            <InputField
+                                                label={<Box>Gravity</Box>}
+                                                lowerBound={-30}
+                                                dataDoc={this.dataDoc}
+                                                prop="gravity"
+                                                step={0.01}
+                                                unit="m/s2"
+                                                upperBound={0}
+                                                value={NumCast(this.dataDoc.simulation_gravity, -9.81)}
+                                                effect={(val: number) => this.setupSimulation()}
+                                                labelWidth="5em"
+                                            />
+                                        )}
+                                        {this.dataDoc.simulation_paused && this.simulationType !== 'Pulley' && (
+                                            <InputField
+                                                label={<Box>Mass</Box>}
+                                                lowerBound={1}
+                                                dataDoc={this.dataDoc}
+                                                prop="mass1"
+                                                step={0.1}
+                                                unit="kg"
+                                                upperBound={5}
+                                                value={this.mass1 ?? 1}
+                                                effect={(val: number) => this.setupSimulation()}
+                                                labelWidth="5em"
+                                            />
+                                        )}
+                                        {this.dataDoc.simulation_paused && this.simulationType === 'Pulley' && (
+                                            <InputField
+                                                label={<Box>Red mass</Box>}
+                                                lowerBound={1}
+                                                dataDoc={this.dataDoc}
+                                                prop="mass1"
+                                                step={0.1}
+                                                unit="kg"
+                                                upperBound={5}
+                                                value={this.mass1 ?? 1}
+                                                effect={(val: number) => this.setupSimulation()}
+                                                labelWidth="5em"
+                                            />
+                                        )}
+                                        {this.dataDoc.simulation_paused && this.simulationType === 'Pulley' && (
+                                            <InputField
+                                                label={<Box>Blue mass</Box>}
+                                                lowerBound={1}
+                                                dataDoc={this.dataDoc}
+                                                prop="mass2"
+                                                step={0.1}
+                                                unit="kg"
+                                                upperBound={5}
+                                                value={this.mass2 ?? 1}
+                                                effect={(val: number) => this.setupSimulation()}
+                                                labelWidth="5em"
+                                            />
+                                        )}
+                                        {this.dataDoc.simulation_paused && this.simulationType === 'Circular Motion' && (
+                                            <InputField
+                                                label={<Box>Rod length</Box>}
+                                                lowerBound={100}
+                                                dataDoc={this.dataDoc}
+                                                prop="circularMotionRadius"
+                                                step={5}
+                                                unit="m"
+                                                upperBound={250}
+                                                value={this.circularMotionRadius}
+                                                effect={(val: number) => this.setupSimulation()}
+                                                labelWidth="5em"
+                                            />
+                                        )}
+                                    </FormGroup>
+                                </FormControl>
+                                {this.simulationType === 'Spring' && this.dataDoc.simulation_paused && (
+                                    <div>
+                                        <InputField
+                                            label={<Typography color="inherit">Spring stiffness</Typography>}
+                                            lowerBound={0.1}
+                                            dataDoc={this.dataDoc}
+                                            prop="spring_constant"
+                                            step={1}
+                                            unit="N/m"
+                                            upperBound={500}
+                                            value={this.springConstant}
+                                            effect={action(() => this._simReset++)}
+                                            radianEquivalent={false}
+                                            mode="Freeform"
+                                            labelWidth="7em"
+                                        />
+                                        <InputField
+                                            label={<Typography color="inherit">Rest length</Typography>}
+                                            lowerBound={10}
+                                            dataDoc={this.dataDoc}
+                                            prop="spring_lengthRest"
+                                            step={100}
+                                            unit=""
+                                            upperBound={500}
+                                            value={this.springLengthRest}
+                                            effect={action(() => this._simReset++)}
+                                            radianEquivalent={false}
+                                            mode="Freeform"
+                                            labelWidth="7em"
+                                        />
+                                        <InputField
+                                            label={<Typography color="inherit">Starting displacement</Typography>}
+                                            lowerBound={-(this.springLengthRest - 10)}
+                                            dataDoc={this.dataDoc}
+                                            prop=""
+                                            step={10}
+                                            unit=""
+                                            upperBound={this.springLengthRest}
+                                            value={this.springLengthStart - this.springLengthRest}
+                                            effect={action((val: number) => {
+                                                this.dataDoc.mass1_positionYstart = this.springLengthRest + val;
+                                                this.dataDoc.spring_lengthStart = this.springLengthRest + val;
+                                                this._simReset++;
+                                            })}
+                                            radianEquivalent={false}
+                                            mode="Freeform"
+                                            labelWidth="7em"
+                                        />
+                                    </div>
+                                )}
+                                {this.simulationType === 'Inclined Plane' && this.dataDoc.simulation_paused && (
+                                    <div>
+                                        <InputField
+                                            label={<Box>&theta;</Box>}
+                                            lowerBound={0}
+                                            dataDoc={this.dataDoc}
+                                            prop="wedge_angle"
+                                            step={1}
+                                            unit="°"
+                                            upperBound={49}
+                                            value={this.wedgeAngle}
+                                            effect={action((val: number) => {
+                                                this.changeWedgeBasedOnNewAngle(val);
+                                                this._simReset++;
+                                            })}
+                                            radianEquivalent
+                                            mode="Freeform"
+                                            labelWidth="2em"
+                                        />
+                                        <InputField
+                                            label={
+                                                <Box>
+                                                    &mu;<sub>s</sub>
+                                                </Box>
+                                            }
+                                            lowerBound={0}
+                                            dataDoc={this.dataDoc}
+                                            prop="coefficientOfStaticFriction"
+                                            step={0.1}
+                                            unit=""
+                                            upperBound={1}
+                                            value={NumCast(this.dataDoc.coefficientOfStaticFriction) ?? 0}
+                                            effect={action((val: number) => {
+                                                this.updateForcesWithFriction(val);
+                                                if (val < NumCast(this.dataDoc.coefficientOfKineticFriction)) {
+                                                    this.dataDoc.soefficientOfKineticFriction = val;
+                                                }
+                                                this._simReset++;
+                                            })}
+                                            mode="Freeform"
+                                            labelWidth="2em"
+                                        />
+                                        <InputField
+                                            label={
+                                                <Box>
+                                                    &mu;<sub>k</sub>
+                                                </Box>
+                                            }
+                                            lowerBound={0}
+                                            dataDoc={this.dataDoc}
+                                            prop="coefficientOfKineticFriction"
+                                            step={0.1}
+                                            unit=""
+                                            upperBound={NumCast(this.dataDoc.coefficientOfStaticFriction)}
+                                            value={NumCast(this.dataDoc.coefficientOfKineticFriction) ?? 0}
+                                            effect={action(() => this._simReset++)}
+                                            mode="Freeform"
+                                            labelWidth="2em"
+                                        />
+                                    </div>
+                                )}
+                                {this.simulationType === 'Inclined Plane' && !this.dataDoc.simulation_paused && (
+                                    <Typography>
+                                        <>
+                                            &theta;: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad
+                                            <br />
+                                            &mu; <sub>s</sub>: {this.dataDoc.coefficientOfStaticFriction}
+                                            <br />
+                                            &mu; <sub>k</sub>: {this.dataDoc.coefficientOfKineticFriction}
+                                        </>
+                                    </Typography>
+                                )}
+                                {this.simulationType === 'Pendulum' && !this.dataDoc.simulation_paused && (
+                                    <Typography>
+                                        &theta;: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad
+                                    </Typography>
+                                )}
+                                {this.simulationType === 'Pendulum' && this.dataDoc.simulation_paused && (
+                                    <div>
+                                        <InputField
+                                            label={<Box>Angle</Box>}
+                                            lowerBound={0}
+                                            dataDoc={this.dataDoc}
+                                            prop="pendulum_angle"
+                                            step={1}
+                                            unit="°"
+                                            upperBound={59}
+                                            value={NumCast(this.dataDoc.pendulum_angle, 30)}
+                                            effect={action(value => {
+                                                this.dataDoc.pendulum_angleStart = value;
+                                                this.dataDoc.pendulum_lengthStart = this.dataDoc.pendulum_length;
+                                                if (this.simulationType === 'Pendulum') {
+                                                    const mag = this.mass1 * Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180);
+
+                                                    const forceOfTension: IForce = {
+                                                        description: 'Tension',
+                                                        magnitude: mag,
+                                                        directionInDegrees: 90 - value,
+                                                    };
+                                                    const gravityParallel: IForce = {
+                                                        description: 'Gravity Parallel Component',
+                                                        magnitude: Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180),
+                                                        directionInDegrees: 270 - value,
+                                                    };
+                                                    const gravityPerpendicular: IForce = {
+                                                        description: 'Gravity Perpendicular Component',
+                                                        magnitude: Math.abs(this.gravity) * Math.sin((value * Math.PI) / 180),
+                                                        directionInDegrees: -value,
+                                                    };
+
+                                                    const length = this.pendulumLength;
+                                                    const x = length * Math.cos(((90 - value) * Math.PI) / 180);
+                                                    const y = length * Math.sin(((90 - value) * Math.PI) / 180);
+                                                    const xPos = this.xMax / 2 - x - NumCast(this.dataDoc.radius);
+                                                    const yPos = y - NumCast(this.dataDoc.radius) - 5;
+                                                    this.dataDoc.mass1_positionXstart = xPos;
+                                                    this.dataDoc.mass1_positionYstart = yPos;
+
+                                                    this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
+                                                    this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
+                                                    this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]);
+                                                    this._simReset++;
+                                                }
+                                            })}
+                                            radianEquivalent
+                                            mode="Freeform"
+                                            labelWidth="5em"
+                                        />
+                                        <InputField
+                                            label={<Box>Rod length</Box>}
+                                            lowerBound={0}
+                                            dataDoc={this.dataDoc}
+                                            prop="pendulum_length"
+                                            step={1}
+                                            unit="m"
+                                            upperBound={400}
+                                            value={Math.round(this.pendulumLength)}
+                                            effect={action(value => {
+                                                if (this.simulationType === 'Pendulum') {
+                                                    this.dataDoc.pendulum_angleStart = this.pendulumAngle;
+                                                    this.dataDoc.pendulum_lengthStart = value;
+                                                    this._simReset++;
+                                                }
+                                            })}
+                                            radianEquivalent={false}
+                                            mode="Freeform"
+                                            labelWidth="5em"
+                                        />
+                                    </div>
+                                )}
+                            </div>
+                        )}
+                        <div className="mechanicsSimulationEquation">
+                            {this.simulationMode === 'Freeform' && (
+                                <table>
+                                    <tbody>
+                                        <tr>
+                                            <td>{this.simulationType === 'Pulley' ? 'Red Weight' : ''}</td>
+                                            <td>X</td>
+                                            <td>Y</td>
+                                        </tr>
+                                        <tr>
+                                            <td
+                                                style={{ cursor: 'help' }}
+                                                // onClick={() => {
+                                                //   window.open(
+                                                //     "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+                                                //   );
+                                                // }}
+                                            >
+                                                <Box>Position</Box>
+                                            </td>
+                                            {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && (
+                                                <td style={{ cursor: 'default' }}>{this.dataDoc.mass1_positionX + ''} m</td>
+                                            )}{' '}
+                                            {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && (
+                                                <td
+                                                    style={{
+                                                        cursor: 'default',
+                                                    }}>
+                                                    <InputField
+                                                        lowerBound={this.simulationType === 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15}
+                                                        dataDoc={this.dataDoc}
+                                                        prop="mass1_positionX"
+                                                        step={1}
+                                                        unit="m"
+                                                        upperBound={this.simulationType === 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15}
+                                                        value={NumCast(this.dataDoc.mass1_positionX)}
+                                                        effect={value => {
+                                                            this.dataDoc.mass1_xChange = value;
+                                                            if (this.simulationType === 'Suspension') {
+                                                                const x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+                                                                const x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+                                                                const deltaX1 = value + this.radius - x1rod;
+                                                                const deltaX2 = x2rod - (value + this.radius);
+                                                                const deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius;
+                                                                let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+                                                                let dir2T = Math.atan(deltaY / deltaX2);
+                                                                const tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+                                                                const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+                                                                dir1T = (dir1T * 180) / Math.PI;
+                                                                dir2T = (dir2T * 180) / Math.PI;
+                                                                const tensionForce1: IForce = {
+                                                                    description: 'Tension',
+                                                                    magnitude: tensionMag1,
+                                                                    directionInDegrees: dir1T,
+                                                                };
+                                                                const tensionForce2: IForce = {
+                                                                    description: 'Tension',
+                                                                    magnitude: tensionMag2,
+                                                                    directionInDegrees: dir2T,
+                                                                };
+                                                                const gravity = this.gravityForce(this.mass1);
+                                                                this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
+                                                            }
+                                                        }}
+                                                        small
+                                                        mode="Freeform"
+                                                    />
+                                                </td>
+                                            )}{' '}
+                                            {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && (
+                                                <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_positionY)} m`}</td>
+                                            )}{' '}
+                                            {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && (
+                                                <td
+                                                    style={{
+                                                        cursor: 'default',
+                                                    }}>
+                                                    <InputField
+                                                        lowerBound={1}
+                                                        dataDoc={this.dataDoc}
+                                                        prop="mass1_positionY"
+                                                        step={1}
+                                                        unit="m"
+                                                        upperBound={this.yMax - 110}
+                                                        value={NumCast(this.dataDoc.mass1_positionY)}
+                                                        effect={value => {
+                                                            this.dataDoc.mass1_yChange = value;
+                                                            if (this.simulationType === 'Suspension') {
+                                                                const x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+                                                                const x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+                                                                const deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod;
+                                                                const deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius);
+                                                                const deltaY = this.getYPosFromDisplay(value) + this.radius;
+                                                                let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+                                                                let dir2T = Math.atan(deltaY / deltaX2);
+                                                                const tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+                                                                const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+                                                                dir1T = (dir1T * 180) / Math.PI;
+                                                                dir2T = (dir2T * 180) / Math.PI;
+                                                                const tensionForce1: IForce = {
+                                                                    description: 'Tension',
+                                                                    magnitude: tensionMag1,
+                                                                    directionInDegrees: dir1T,
+                                                                };
+                                                                const tensionForce2: IForce = {
+                                                                    description: 'Tension',
+                                                                    magnitude: tensionMag2,
+                                                                    directionInDegrees: dir2T,
+                                                                };
+                                                                const gravity = this.gravityForce(this.mass1);
+                                                                this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
+                                                            }
+                                                        }}
+                                                        small
+                                                        mode="Freeform"
+                                                    />
+                                                </td>
+                                            )}{' '}
+                                        </tr>
+                                        <tr>
+                                            <td
+                                                style={{ cursor: 'help' }}
+                                                // onClick={() => {
+                                                //   window.open(
+                                                //     "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+                                                //   );
+                                                // }}
+                                            >
+                                                <Box>Velocity</Box>
+                                            </td>
+                                            {(!this.dataDoc.simulation_paused || (this.simulationType !== 'One Weight' && this.simulationType !== 'Circular Motion')) && (
+                                                <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_velocityX)} m/s`}</td>
+                                            )}{' '}
+                                            {this.dataDoc.simulation_paused && (this.simulationType === 'One Weight' || this.simulationType === 'Circular Motion') && (
+                                                <td
+                                                    style={{
+                                                        cursor: 'default',
+                                                    }}>
+                                                    <InputField
+                                                        lowerBound={-50}
+                                                        dataDoc={this.dataDoc}
+                                                        prop="mass1_velocityX"
+                                                        step={1}
+                                                        unit="m/s"
+                                                        upperBound={50}
+                                                        value={NumCast(this.dataDoc.mass1_velocityX)}
+                                                        effect={action(value => {
+                                                            this.dataDoc.mass1_velocityXstart = value;
+                                                            this._simReset++;
+                                                        })}
+                                                        small
+                                                        mode="Freeform"
+                                                    />
+                                                </td>
+                                            )}{' '}
+                                            {(!this.dataDoc.simulation_paused || this.simulationType !== 'One Weight') && <td style={{ cursor: 'default' }}>{this.dataDoc.mass1_velocityY + ''} m/s</td>}{' '}
+                                            {this.dataDoc.simulation_paused && this.simulationType === 'One Weight' && (
+                                                <td
+                                                    style={{
+                                                        cursor: 'default',
+                                                    }}>
+                                                    <InputField
+                                                        lowerBound={-50}
+                                                        dataDoc={this.dataDoc}
+                                                        prop="mass1_velocityY"
+                                                        step={1}
+                                                        unit="m/s"
+                                                        upperBound={50}
+                                                        value={NumCast(this.dataDoc.mass1_velocityY)}
+                                                        effect={value => {
+                                                            this.dataDoc.mass1_velocityYstart = -value;
+                                                        }}
+                                                        small
+                                                        mode="Freeform"
+                                                    />
+                                                </td>
+                                            )}{' '}
+                                        </tr>
+                                        <tr>
+                                            <td
+                                                style={{ cursor: 'help' }}
+                                                // onClick={() => {
+                                                //   window.open(
+                                                //     "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+                                                //   );
+                                                // }}
+                                            >
+                                                <Box>Acceleration</Box>
+                                            </td>
+                                            <td style={{ cursor: 'default' }}>
+                                                {this.dataDoc.mass1_accelerationX + ''} m/s<sup>2</sup>
+                                            </td>
+                                            <td style={{ cursor: 'default' }}>
+                                                {this.dataDoc.mass1_accelerationY + ''} m/s<sup>2</sup>
+                                            </td>
+                                        </tr>
+                                        <tr>
+                                            <td>
+                                                <Box>Momentum</Box>
+                                            </td>
+                                            <td>{Math.round(NumCast(this.dataDoc.mass1_velocityX) * this.mass1 * 10) / 10} kg*m/s</td>
+                                            <td>{Math.round(NumCast(this.dataDoc.mass1_velocityY) * this.mass1 * 10) / 10} kg*m/s</td>
+                                        </tr>
+                                    </tbody>
+                                </table>
+                            )}
+                            {this.simulationMode === 'Freeform' && this.simulationType === 'Pulley' && (
+                                <table>
+                                    <tbody>
+                                        <tr>
+                                            <td>Blue Weight</td>
+                                            <td>X</td>
+                                            <td>Y</td>
+                                        </tr>
+                                        <tr>
+                                            <td>
+                                                <Box>Position</Box>
+                                            </td>
+                                            <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionX} m`}</td>
+                                            <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionY} m`}</td>
+                                        </tr>
+                                        <tr>
+                                            <td>
+                                                <Box>Velocity</Box>
+                                            </td>
+                                            <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionX} m/s`}</td>
+                                            <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionY} m/s`}</td>
+                                        </tr>
+                                        <tr>
+                                            <td>
+                                                <Box>Acceleration</Box>
+                                            </td>
+                                            <td style={{ cursor: 'default' }}>
+                                                {this.dataDoc.mass2_accelerationX + ''} m/s<sup>2</sup>
+                                            </td>
+                                            <td style={{ cursor: 'default' }}>
+                                                {this.dataDoc.mass2_accelerationY + ''} m/s<sup>2</sup>
+                                            </td>
+                                        </tr>
+                                        <tr>
+                                            <td>
+                                                <Box>Momentum</Box>
+                                            </td>
+                                            <td>{Math.round(NumCast(this.dataDoc.mass2_velocityX) * this.mass1 * 10) / 10} kg*m/s</td>
+                                            <td>{Math.round(NumCast(this.dataDoc.mass2_velocityY) * this.mass1 * 10) / 10} kg*m/s</td>
+                                        </tr>
+                                    </tbody>
+                                </table>
+                            )}
+                        </div>
+                        {this.simulationType !== 'Pendulum' && this.simulationType !== 'Spring' && (
+                            <div>
+                                <p>Kinematic Equations</p>
+                                <ul>
+                                    <li>
+                                        Position: x<sub>1</sub>=x<sub>0</sub>+v<sub>0</sub>t+
+                                        <sup>1</sup>&frasl;
+                                        <sub>2</sub>at
+                                        <sup>2</sup>
+                                    </li>
+                                    <li>
+                                        Velocity: v<sub>1</sub>=v<sub>0</sub>+at
+                                    </li>
+                                    <li>Acceleration: a = F/m</li>
+                                </ul>
+                            </div>
+                        )}
+                        {this.simulationType === 'Spring' && (
+                            <div>
+                                <p>Harmonic Motion Equations: Spring</p>
+                                <ul>
+                                    <li>
+                                        Spring force: F<sub>s</sub>=kd
+                                    </li>
+                                    <li>
+                                        Spring period: T<sub>s</sub>=2&pi;&#8730;<sup>m</sup>&frasl;
+                                        <sub>k</sub>
+                                    </li>
+                                    <li>Equilibrium displacement for vertical spring: d = mg/k</li>
+                                    <li>
+                                        Elastic potential energy: U<sub>s</sub>=<sup>1</sup>&frasl;
+                                        <sub>2</sub>kd<sup>2</sup>
+                                    </li>
+                                    <ul>
+                                        <li>Maximum when system is at maximum displacement, 0 when system is at 0 displacement</li>
+                                    </ul>
+                                    <li>
+                                        Translational kinetic energy: K=<sup>1</sup>&frasl;
+                                        <sub>2</sub>mv<sup>2</sup>
+                                    </li>
+                                    <ul>
+                                        <li>Maximum when system is at maximum/minimum velocity (at 0 displacement), 0 when velocity is 0 (at maximum displacement)</li>
+                                    </ul>
+                                </ul>
+                            </div>
+                        )}
+                        {this.simulationType === 'Pendulum' && (
+                            <div>
+                                <p>Harmonic Motion Equations: Pendulum</p>
+                                <ul>
+                                    <li>
+                                        Pendulum period: T<sub>p</sub>=2&pi;&#8730;<sup>l</sup>&frasl;
+                                        <sub>g</sub>
+                                    </li>
+                                </ul>
+                            </div>
+                        )}
+                    </div>
+                </div>
+                <div
+                    style={{
+                        position: 'fixed',
+                        top: this.yMax - 120 + 20 + 'px',
+                        left: this.xMin + 90 - 80 + 'px',
+                        zIndex: -10000,
+                    }}>
+                    <svg width={100 + 'px'} height={100 + 'px'}>
+                        <defs>
+                            <marker id="miniArrow" markerWidth="20" markerHeight="20" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
+                                <path d="M0,0 L0,6 L9,3 z" fill="#000000" />
+                            </marker>
+                        </defs>
+                        <line x1={20} y1={70} x2={70} y2={70} stroke="#000000" strokeWidth="2" markerEnd="url(#miniArrow)" />
+                        <line x1={20} y1={70} x2={20} y2={20} stroke="#000000" strokeWidth="2" markerEnd="url(#miniArrow)" />
+                    </svg>
+                    <p
+                        style={{
+                            position: 'fixed',
+                            top: this.yMax - 120 + 40 + 'px',
+                            left: this.xMin + 90 - 80 + 'px',
+                        }}>
+                        {this.simulationType === 'Circular Motion' ? 'Z' : 'Y'}
+                    </p>
+                    <p
+                        style={{
+                            position: 'fixed',
+                            top: this.yMax - 120 + 80 + 'px',
+                            left: this.xMin + 90 - 40 + 'px',
+                        }}>
+                        X
+                    </p>
+                </div>
             </div>
         );
     }
 }
 
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function selectMainMenu(doc: Doc) {
-    MainView.Instance.selectMenu(doc);
+Docs.Prototypes.TemplateMap.set(DocumentType.SIMULATION, {
+    data: '',
+    layout: { view: PhysicsSimulationBox, dataField: 'data' },
+    options: { acl: '', _width: 1000, _height: 800, mass1: '', mass2: '', layout_nativeDimEditable: true, position: '', acceleration: '', pendulum: '', spring: '', wedge: '', simulation: '', review: '', systemIcon: 'BsShareFill' },
 });
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function createNewPresentation() {
-    return MainView.Instance.createNewPresentation();
-}, 'creates a new presentation when called');
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function openPresentation(pres: Doc) {
-    return MainView.Instance.openPresentation(pres);
-}, 'creates a new presentation when called');
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function createNewFolder() {
-    return MainView.Instance.createNewFolder();
-}, 'creates a new folder in myFiles when called');
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b8257ff31..467191735 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -7,7 +7,6 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
-import { TbAlpha } from 'react-icons/tb';
 import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
 import { ActiveEraserWidth, ActiveInkWidth, Doc, DocListCast, Field, FieldType, Opt, SetActiveInkColor, SetActiveInkWidth } from '../../../../fields/Doc';
@@ -56,7 +55,7 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
-import { SmartDrawHandler } from './SmartDrawHandler';
+import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
 
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -120,6 +119,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
     @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.  could be used for performance improvement
     @observable _showAnimTimeline = false;
+    @observable _showDrawingEditor = false;
     @observable _deleteList: DocumentView[] = [];
     @observable _timelineRef = React.createRef<Timeline>();
     @observable _marqueeViewRef = React.createRef<MarqueeView>();
@@ -514,7 +514,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         e.stopPropagation();
                         break;
                     case InkTool.SmartDraw:
-                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.createDrawing, hit !== -1);
+                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.showSmartDraw, hit !== -1);
                         e.stopPropagation();
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
@@ -566,6 +566,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
     };
+
     @action
     onEraserUp = (): void => {
         this._deleteList.lastElement()?._props.removeDocument?.(this._deleteList.map(ink => ink.Document));
@@ -607,12 +608,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     _eraserPts: number[][] = []; // keep track of the last few eraserPts to make the eraser circle 'stretch'
 
     erase = (e: PointerEvent, delta: number[]) => {
+        e.stopImmediatePropagation();
         const currPoint = { X: e.clientX, Y: e.clientY };
         this._eraserPts.push([currPoint.X, currPoint.Y]);
         this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
         if (Doc.ActiveTool === InkTool.RadiusEraser) {
             const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
-
             strokeMap.forEach((intersects, stroke) => {
                 if (!this._deleteList.includes(stroke)) {
                     this._deleteList.push(stroke);
@@ -682,9 +683,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     onEraserClick = (e: PointerEvent, doubleTap?: boolean) => {
+        e.preventDefault();
+        e.stopImmediatePropagation();
         this.erase(e, [0, 0]);
-        e.stopPropagation();
-        return false;
     };
 
     /**
@@ -696,32 +697,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * @param delta
      * @returns
      */
-    @action
-    onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
-        const currPoint = { X: e.clientX, Y: e.clientY };
-        this._eraserPts.push([currPoint.X, currPoint.Y]);
-        this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
-        const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
-
-        strokeMap.forEach((intersects, stroke) => {
-            if (!this._deleteList.includes(stroke)) {
-                this._deleteList.push(stroke);
-                SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
-                SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
-                const segments = this.radiusErase(stroke, intersects.sort());
-                segments?.forEach(segment =>
-                    this.forceStrokeGesture(
-                        e,
-                        Gestures.Stroke,
-                        segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
-                    )
-                );
-            }
-            stroke.layoutDoc.opacity = 0;
-            stroke.layoutDoc.dontIntersect = true;
-        });
-        return false;
-    };
+    // @action
+    // onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
+    //     const currPoint = { X: e.clientX, Y: e.clientY };
+    //     this._eraserPts.push([currPoint.X, currPoint.Y]);
+    //     this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
+    //     const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
+
+    //     strokeMap.forEach((intersects, stroke) => {
+    //         if (!this._deleteList.includes(stroke)) {
+    //             this._deleteList.push(stroke);
+    //             SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
+    //             SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
+    //             const segments = this.radiusErase(stroke, intersects.sort());
+    //             segments?.forEach(segment =>
+    //                 this.forceStrokeGesture(
+    //                     e,
+    //                     Gestures.Stroke,
+    //                     segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
+    //                 )
+    //             );
+    //         }
+    //         stroke.layoutDoc.opacity = 0;
+    //         stroke.layoutDoc.dontIntersect = true;
+    //     });
+    //     return false;
+    // };
 
     forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
         this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
@@ -1263,15 +1264,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    createDrawing = (e: PointerEvent, doubleTap?: boolean) => {
-        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createInkStrokes);
+    showSmartDraw = (e: PointerEvent, doubleTap?: boolean) => {
+        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing);
     };
 
+    _drawing: Doc[] = [];
     @undoBatch
-    createInkStrokes = (strokeData: [InkData, string, string][]) => {
+    createDrawing = (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
         strokeData.forEach((stroke: [InkData, string, string]) => {
-            // const points: InkData = FitCurve(inkData, 20) as InkData;
-            // const allPts = GenerateControlPoints(inkData, alpha);
             const bounds = InkField.getBounds(stroke[0]);
             const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
             const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
@@ -1288,8 +1288,33 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 undefined,
                 stroke[2] === 'none' ? undefined : stroke[2]
             );
+            this._drawing.push(inkDoc);
             this.addDocument(inkDoc);
         });
+        // const collection = this._marqueeViewRef.current?.collection(undefined, false, this._drawing);
+        // if (collection) {
+        //     const docData = collection[DocData];
+        //     docData.title = opts.text;
+        //     docData.drawingInput = opts.text;
+        //     docData.drawingComplexity = opts.complexity;
+        //     docData.drawingColored = opts.autoColor;
+        //     docData.drawingSize = opts.size;
+        //     docData.drawingData = gptRes;
+        // }
+        this._batch?.end();
+    };
+
+    removeDrawing = (doc?: Doc) => {
+        this._batch = UndoManager.StartBatch('regenerateDrawing');
+        if (doc) {
+            const docData: Doc = doc[DocData];
+            const children = docData.data as unknown as Doc[];
+            this._props.removeDocument?.(doc);
+            this._props.removeDocument?.(children);
+        } else {
+            this._props.removeDocument?.(this._drawing);
+        }
+        this._drawing = [];
     };
 
     @action
@@ -1995,6 +2020,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }),
                 icon: 'eye',
             });
+        optionItems.push({
+            description: (this._showDrawingEditor ? 'Close' : 'Show') + ' Drawing Editor',
+            event: action(() => {
+                this._showDrawingEditor = !this._showDrawingEditor;
+                this._showDrawingEditor ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate();
+            }),
+            icon: 'pen-to-square',
+        });
         this._props.renderDepth &&
             optionItems.push({
                 description: 'Use Background Color as Default',
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index dc15c83c5..23cf487ec 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -36,6 +36,7 @@ import { CollectionFreeFormView } from './CollectionFreeFormView';
 import { ImageLabelHandler } from './ImageLabelHandler';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './MarqueeView.scss';
+import { collectionOf } from '@turf/turf';
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -426,6 +427,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this._props.selectDocuments([newCollection]);
         MarqueeOptionsMenu.Instance.fadeOut(true);
         this.hideMarquee();
+        return newCollection;
     });
 
     /**
diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
deleted file mode 100644
index edb814172..000000000
--- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx
+++ /dev/null
@@ -1,295 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, makeObservable, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import React from 'react';
-import { SettingsManager } from '../../../util/SettingsManager';
-import { ObservableReactComponent } from '../../ObservableReactComponent';
-import { Button, IconButton, Size } from 'browndash-components';
-import ReactLoading from 'react-loading';
-import { AiOutlineSend } from 'react-icons/ai';
-import './ImageLabelHandler.scss';
-import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
-import { InkData } from '../../../../fields/InkField';
-import { SVGToBezier } from '../../../util/bezierFit';
-const { parse } = require('svgson');
-import { Slider, Switch } from '@mui/material';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { Flex } from '@adobe/react-spectrum';
-import { Row } from 'react-aria-components';
-import { UndoManager } from '../../../util/UndoManager';
-import e from 'cors';
-
-@observer
-export class SmartDrawHandler extends ObservableReactComponent<{}> {
-    static Instance: SmartDrawHandler;
-
-    @observable private _display: boolean = false;
-    @observable private _pageX: number = 0;
-    @observable private _pageY: number = 0;
-    @observable private _yRelativeToTop: boolean = true;
-    @observable private _isLoading: boolean = false;
-    @observable private _userInput: string = '';
-    @observable private _showOptions: boolean = false;
-    @observable private _menuIcon: string = 'caret-right';
-    @observable private _complexity: number = 5;
-    @observable private _size: number = 300;
-    @observable private _autoColor: boolean = true;
-    @observable private _showRegenerate: boolean = false;
-    private _addToDocFunc: (strokeList: [InkData, string, string][]) => void = () => {};
-    private _lastX: number = 0;
-    private _lastY: number = 0;
-
-    constructor(props: any) {
-        super(props);
-        makeObservable(this);
-        SmartDrawHandler.Instance = this;
-    }
-
-    @action
-    setUserInput = (input: string) => {
-        this._userInput = input;
-    };
-
-    @action
-    displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeData: [InkData, string, string][]) => void) => {
-        this._pageX = x;
-        this._pageY = y;
-        this._display = true;
-        this._addToDocFunc = addToDoc;
-    };
-
-    hideSmartDrawHandler = () => {
-        this._showRegenerate = false;
-        this._display = false;
-        this._isLoading = false;
-        this._showOptions = false;
-        this._menuIcon = 'caret-right';
-    };
-
-    hideRegenerate = () => {
-        this._showRegenerate = false;
-        this._userInput = '';
-        this._complexity = 5;
-        this._size = 300;
-        this._autoColor = true;
-        this._isLoading = false;
-    };
-
-    toggleMenu = () => {
-        this._showOptions = !this._showOptions;
-        this._menuIcon === 'caret-right' ? (this._menuIcon = 'caret-down') : (this._menuIcon = 'caret-right');
-    };
-
-    @action
-    drawWithGPT = async (e: React.MouseEvent<Element, MouseEvent>, startPoint: { X: number; Y: number }, input: string, regenerate: boolean = false) => {
-        if (this._userInput === '') return;
-        e.stopPropagation();
-        this._lastX = startPoint.X;
-        this._lastY = startPoint.Y;
-        this._isLoading = true;
-        this._showOptions = false;
-        try {
-            const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW);
-            if (!res) {
-                console.error('GPT call failed');
-                return;
-            }
-            const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
-            if (svg) {
-                const svgObject = await parse(svg[0]);
-                const svgStrokes: any = svgObject.children;
-                const strokeData: [InkData, string, string][] = [];
-                svgStrokes.forEach((child: any) => {
-                    const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
-                    strokeData.push([
-                        convertedBezier.map(point => {
-                            return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
-                        }),
-                        this._autoColor ? child.attributes.stroke : undefined,
-                        this._autoColor ? child.attributes.fill : undefined,
-                    ]);
-                });
-                if (regenerate) UndoManager.Undo();
-                this._addToDocFunc(strokeData);
-            }
-        } catch (err) {
-            console.error('GPT call failed', err);
-        }
-        this.hideSmartDrawHandler();
-        this._showRegenerate = true;
-    };
-
-    regenerate = (e: React.MouseEvent<Element, MouseEvent>) => {
-        this.drawWithGPT(e, { X: this._lastX, Y: this._lastY }, `Regenerate the item "${this._userInput}"`, true);
-    };
-
-    render() {
-        if (this._display) {
-            return (
-                <div
-                    id="label-handler"
-                    className="contextMenu-cont"
-                    style={{
-                        display: this._display ? '' : 'none',
-                        left: this._pageX,
-                        ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
-                        background: SettingsManager.userBackgroundColor,
-                        color: SettingsManager.userColor,
-                    }}>
-                    <div>
-                        <IconButton
-                            tooltip={'Cancel'}
-                            onClick={() => {
-                                this.hideSmartDrawHandler();
-                                this.hideRegenerate();
-                            }}
-                            icon={<FontAwesomeIcon icon="xmark" />}
-                            color={SettingsManager.userColor}
-                            style={{ width: '19px' }}
-                        />
-                        <input
-                            aria-label="label-input"
-                            id="new-label"
-                            type="text"
-                            style={{ color: 'black' }}
-                            value={this._userInput}
-                            onChange={e => {
-                                this.setUserInput(e.target.value);
-                            }}
-                            placeholder="Enter item to draw"
-                        />
-                        <IconButton
-                            tooltip="Advanced Options"
-                            icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />}
-                            color={SettingsManager.userColor}
-                            style={{ width: '14px' }}
-                            onClick={() => {
-                                this._showOptions = !this._showOptions;
-                            }}
-                        />
-                        <Button
-                            style={{ alignSelf: 'flex-end' }}
-                            text="Send"
-                            icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                            iconPlacement="right"
-                            color={SettingsManager.userColor}
-                            onClick={e => {
-                                this.drawWithGPT(e, { X: e.clientX, Y: e.clientY }, this._userInput);
-                            }}
-                        />
-                    </div>
-                    {this._showOptions && (
-                        <>
-                            <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
-                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}>
-                                    Auto color
-                                    <Switch
-                                        sx={{
-                                            '& .MuiSwitch-switchBase.Mui-checked': {
-                                                color: SettingsManager.userColor,
-                                            },
-                                            '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
-                                                backgroundColor: SettingsManager.userVariantColor,
-                                            },
-                                        }}
-                                        defaultChecked={true}
-                                        size="small"
-                                        onChange={() => (this._autoColor = !this._autoColor)}
-                                    />
-                                </div>
-                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}>
-                                    Complexity
-                                    <Slider
-                                        sx={{
-                                            '& .MuiSlider-thumb': {
-                                                color: SettingsManager.userColor,
-                                                '&.Mui-focusVisible, &:hover, &.Mui-active': {
-                                                    boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
-                                                },
-                                            },
-                                            '& .MuiSlider-track': {
-                                                color: SettingsManager.userVariantColor,
-                                            },
-                                            '& .MuiSlider-rail': {
-                                                color: SettingsManager.userColor,
-                                            },
-                                        }}
-                                        style={{ width: '80%' }}
-                                        min={1}
-                                        max={10}
-                                        step={1}
-                                        size="small"
-                                        value={this._complexity}
-                                        onChange={(e, val) => {
-                                            this._complexity = val as number;
-                                        }}
-                                        valueLabelDisplay="auto"
-                                    />
-                                </div>
-                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}>
-                                    Size (in pixels)
-                                    <Slider
-                                        sx={{
-                                            '& .MuiSlider-thumb': {
-                                                color: SettingsManager.userColor,
-                                                '&.Mui-focusVisible, &:hover, &.Mui-active': {
-                                                    boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
-                                                },
-                                            },
-                                            '& .MuiSlider-track': {
-                                                color: SettingsManager.userVariantColor,
-                                            },
-                                            '& .MuiSlider-rail': {
-                                                color: SettingsManager.userColor,
-                                            },
-                                        }}
-                                        style={{ width: '80%' }}
-                                        min={50}
-                                        max={700}
-                                        step={10}
-                                        size="small"
-                                        value={this._size}
-                                        onChange={(e, val) => {
-                                            this._size = val as number;
-                                        }}
-                                        valueLabelDisplay="auto"
-                                    />
-                                </div>
-                            </div>
-                        </>
-                    )}
-                </div>
-            );
-        } else if (this._showRegenerate) {
-            return (
-                <div
-                    id="smartdraw-options-menu"
-                    className="contextMenu-cont"
-                    style={{
-                        left: this._pageX,
-                        ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
-                        background: SettingsManager.userBackgroundColor,
-                        color: SettingsManager.userColor,
-                    }}>
-                    <div
-                        style={{
-                            display: 'flex',
-                            flexDirection: 'row',
-                        }}>
-                        <IconButton tooltip="Cancel" onClick={this.hideRegenerate} icon={<FontAwesomeIcon icon="xmark" />} color={SettingsManager.userColor} style={{ width: '19px' }} />
-                        <IconButton
-                            tooltip="Regenerate"
-                            icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
-                            color={SettingsManager.userColor}
-                            onClick={e => {
-                                this.regenerate(e);
-                            }}
-                        />
-                    </div>
-                </div>
-            );
-        } else {
-            return <></>;
-        }
-    }
-}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 7bca1230f..6e24b2931 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -20,15 +20,27 @@
         top: 0;
         left: 0;
 
+        .pdfBox-sidebarBtn-container {
+            display: flex;
+            flex-direction: row;
+            position: absolute;
+            width: 53px;
+            height: 33px;
+            right: 5px;
+            align-items: center;
+            justify-content: space-between;
+            z-index: 1;
+        }
+
         // glr: This should really be the same component as text and PDFs
         .pdfBox-sidebarBtn {
             background: $black;
             height: 25px;
             width: 25px;
-            right: 5px;
+            // right: 5px;
             color: $white;
             display: flex;
-            position: absolute;
+            // position: absolute;
             align-items: center;
             justify-content: center;
             border-radius: 3px;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 7a89b143b..8dd48f10f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,6 +1,8 @@
 /* eslint-disable jsx-a11y/no-static-element-interactions */
 /* eslint-disable jsx-a11y/control-has-associated-label */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { IconButton } from 'browndash-components';
+import { black } from 'colors';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as Pdfjs from 'pdfjs-dist';
@@ -503,17 +505,30 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
     @computed get sidebarHandle() {
         return (
-            <div
-                className="pdfBox-sidebarBtn"
-                key="sidebar"
-                title="Toggle Sidebar"
-                style={{
-                    display: !this._props.isContentActive() ? 'none' : undefined,
-                    top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
-                    backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
-                }}
-                onPointerDown={e => this.sidebarBtnDown(e, true)}>
-                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />
+            <div className="pdfBox-sidebarBtn-container">
+                <div
+                    className="pdfBox-sidebarBtn"
+                    key="sidebar"
+                    title="Toggle Sidebar"
+                    style={{
+                        display: !this._props.isContentActive() ? 'none' : undefined,
+                        top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
+                        backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+                    }}>
+                    {/* // onPointerDown={e => this.sidebarBtnDown(e, true)} */}
+                    <IconButton tooltip="Toggle Annotation Palette" icon={<FontAwesomeIcon style={{ color: Colors.WHITE }} icon="palette" />} onPointerDown={e => this.sidebarBtnDown(e, true)} />
+                </div>
+                <div
+                    className="pdfBox-sidebarBtn"
+                    key="sidebar"
+                    title="Toggle Sidebar"
+                    style={{
+                        display: !this._props.isContentActive() ? 'none' : undefined,
+                        top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
+                        backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+                    }}>
+                    <IconButton tooltip="Toggle Sidebar" icon={<FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />} onPointerDown={e => this.sidebarBtnDown(e, true)} />
+                </div>
             </div>
         );
     }
diff --git a/src/client/views/smartdraw/DrawingPalette.scss b/src/client/views/smartdraw/DrawingPalette.scss
new file mode 100644
index 000000000..0f1152b71
--- /dev/null
+++ b/src/client/views/smartdraw/DrawingPalette.scss
@@ -0,0 +1,11 @@
+.drawing-palette {
+    display: grid;
+    grid-template-columns: auto;
+    position: absolute;
+    right: 14px;
+    width: 170px;
+    height: 170px;
+    top: 50px;
+    border-radius: 5px;
+    background-color: white;
+}
diff --git a/src/client/views/smartdraw/DrawingPalette.tsx b/src/client/views/smartdraw/DrawingPalette.tsx
new file mode 100644
index 000000000..87a39bc85
--- /dev/null
+++ b/src/client/views/smartdraw/DrawingPalette.tsx
@@ -0,0 +1,89 @@
+import { computed, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { returnAll, returnFalse, returnOne, returnZero } from '../../../ClientUtils';
+import { Doc, StrListCast } from '../../../fields/Doc';
+import { emptyFunction } from '../../../Utils';
+import { CollectionViewType } from '../../documents/DocumentTypes';
+import { MarqueeView } from '../collections/collectionFreeForm';
+import { CollectionGridView } from '../collections/collectionGrid';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
+import { DocumentView } from '../nodes/DocumentView';
+import { FieldViewProps } from '../nodes/FieldView';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import './DrawingPalette.scss';
+
+@observer
+export class DrawingPalette extends ObservableReactComponent<{}> {
+    @observable private _savedDrawings: Doc[] = [];
+    @observable _marqueeViewRef = React.createRef<MarqueeView>();
+    private _stackRef = React.createRef<CollectionStackingView>();
+
+    constructor(props: any) {
+        super(props);
+        makeObservable(this);
+    }
+
+    panelWidth = () => 100;
+    panelHeight = () => 100;
+
+    getCollection = () => {
+        return this._marqueeViewRef.current?.collection(undefined, false, this._savedDrawings) || new Doc();
+    };
+
+    @computed get savedDrawingAnnos() {
+        // const savedAnnos = Doc.MyDrawingAnnos;
+        return (
+            <div className="collectionMenu-contMenuButtons" style={{ height: '100%' }}>
+                {/* <DocumentView PanelHeight={this.panelWidth} PanelWidth={this.panelHeight} Document={savedAnnos} renderDepth={2} isContentActive={returnFalse} childFilters={this.childFilters} /> */}
+                {/* <CollectionStackingView
+                    {...this._props}
+                    Document={savedAnnos}
+                    // setContentViewBox={emptyFunction}
+                    // NativeWidth={returnZero}
+                    // NativeHeight={returnZero}
+                    ref={this._stackRef}
+                    PanelHeight={this.panelWidth}
+                    PanelWidth={this.panelHeight}
+                    // childFilters={this.childFilters}
+                    // sortFunc={this.sortByLinkAnchorY}
+                    // setHeight={this.setHeightCallback}
+                    // isAnnotationOverlay={false}
+                    // select={emptyFunction}
+                    NativeDimScaling={returnOne}
+                    // childlayout_showTitle={this.layout_showTitle}
+                    isContentActive={returnFalse}
+                    isSelected={returnFalse}
+                    isAnyChildContentActive={returnFalse}
+                    // childDocumentsActive={this._props.isContentActive}
+                    whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
+                    childHideDecorationTitle
+                    // ScreenToLocalTransform={this.screenToLocalTransform}
+                    renderDepth={this._props.renderDepth + 1}
+                    type_collection={CollectionViewType.Stacking}
+                    // fieldKey={'drawing-palette'}
+                    pointerEvents={returnAll}
+                /> */}
+            </div>
+        );
+    }
+
+    render() {
+        return (
+            <div className="drawing-palette">
+                {/* {this._savedDrawings.map(doc => {
+                    return <DocumentView 
+                    Document={doc} 
+                    renderDepth={0} 
+                    PanelWidth={this.panelWidth} 
+                    PanelHeight={this.panelHeight} 
+                    isContentActive={this.isContentActive} />;
+                })} */}
+                {/* <CollectionGridView {...this._props} /> */}
+                {}
+                {/* <DocumentView Document={this.getCollection()} /> */}
+                {this.savedDrawingAnnos}
+            </div>
+        );
+    }
+}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
new file mode 100644
index 000000000..6d2cc0593
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -0,0 +1,407 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import React from 'react';
+import { SettingsManager } from '../../util/SettingsManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { Button, IconButton } from 'browndash-components';
+import ReactLoading from 'react-loading';
+import { AiOutlineSend } from 'react-icons/ai';
+// import './ImageLabelHandler.scss';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { InkData } from '../../../fields/InkField';
+import { SVGToBezier } from '../../util/bezierFit';
+const { parse } = require('svgson');
+import { Slider, Switch } from '@mui/material';
+import { Doc } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { DocumentView } from '../nodes/DocumentView';
+
+export interface DrawingOptions {
+    text: string;
+    complexity: number;
+    size: number;
+    autoColor: boolean;
+    x: number;
+    y: number;
+}
+
+@observer
+export class SmartDrawHandler extends ObservableReactComponent<{}> {
+    static Instance: SmartDrawHandler;
+
+    @observable private _display: boolean = false;
+    @observable private _pageX: number = 0;
+    @observable private _pageY: number = 0;
+    @observable private _yRelativeToTop: boolean = true;
+    @observable private _isLoading: boolean = false;
+    @observable private _userInput: string = '';
+    @observable private _showOptions: boolean = false;
+    @observable private _showEditBox: boolean = false;
+    @observable private _showRegenerate: boolean = false;
+    @observable private _complexity: number = 5;
+    @observable private _size: number = 200;
+    @observable private _autoColor: boolean = true;
+    @observable private _regenInput: string = '';
+    private _addFunc: (e: React.PointerEvent<Element>, strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void = () => {};
+    private _deleteFunc: (doc?: Doc) => void = () => {};
+    private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 300, autoColor: true, x: 0, y: 0 };
+    private _lastResponse: string = '';
+    private _selectedDoc: Doc | undefined = undefined;
+
+    constructor(props: any) {
+        super(props);
+        makeObservable(this);
+        SmartDrawHandler.Instance = this;
+    }
+
+    @action
+    setUserInput = (input: string) => {
+        this._userInput = input;
+    };
+
+    @action
+    setRegenInput = (input: string) => {
+        this._regenInput = input;
+    };
+
+    @action
+    setShowOptions = () => {
+        this._showOptions = !this._showOptions;
+    };
+
+    @action
+    setComplexity = (val: number) => {
+        this._complexity = val;
+    };
+
+    @action
+    setSize = (val: number) => {
+        this._size = val;
+    };
+
+    @action
+    setAutoColor = () => {
+        this._autoColor = !this._autoColor;
+    };
+
+    @action
+    displaySmartDrawHandler = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void, deleteFunc: (doc?: Doc) => void) => {
+        this._pageX = x;
+        this._pageY = y;
+        this._display = true;
+        this._addFunc = addFunc;
+        this._deleteFunc = deleteFunc;
+    };
+
+    @action
+    displayRegenerate = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void, deleteFunc: (doc?: Doc) => void) => {
+        const selectedDoc: Doc = DocumentView.SelectedDocs().lastElement();
+        const docData = selectedDoc[DocData];
+        this._addFunc = addFunc;
+        this._deleteFunc = deleteFunc;
+        this._pageX = x;
+        this._pageY = y;
+        this._showRegenerate = true;
+        this._lastResponse = docData.drawingData as string;
+        this._lastInput = { text: docData.drawingInput as string, complexity: docData.drawingComplexity as number, size: docData.drawingSize as number, autoColor: docData.drawingColored as boolean, x: this._pageX, y: this._pageY };
+    };
+
+    @action
+    hideSmartDrawHandler = () => {
+        this._showRegenerate = false;
+        this._display = false;
+        this._isLoading = false;
+        this._showOptions = false;
+        this._userInput = '';
+        this._complexity = 5;
+        this._size = 300;
+        this._autoColor = true;
+        // this._regenInput = ''
+    };
+
+    @action
+    hideRegenerate = () => {
+        this._showRegenerate = false;
+        this._isLoading = false;
+        this._regenInput = '';
+    };
+
+    _errorOccurredOnce = false;
+    @action
+    drawWithGPT = async (e: React.PointerEvent<Element>, input: string) => {
+        if (input === '') return;
+        this._lastInput = { text: input, complexity: this._complexity, size: this._size, autoColor: this._autoColor, x: e.clientX, y: e.clientY };
+        this._isLoading = true;
+        this._showOptions = false;
+        try {
+            const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW, undefined, true);
+            if (!res) {
+                console.error('GPT call failed');
+                return;
+            }
+            console.log(res);
+            await this.parseResponse(e, res, { X: e.clientX, Y: e.clientY }, false);
+            this.hideSmartDrawHandler();
+            this._showRegenerate = true;
+            this._errorOccurredOnce = false;
+        } catch (err) {
+            if (this._errorOccurredOnce) {
+                console.error('GPT call failed', err);
+                this._errorOccurredOnce = false;
+            } else {
+                this._errorOccurredOnce = true;
+                this.drawWithGPT(e, input);
+            }
+        }
+        this._isLoading = false;
+    };
+
+    @action
+    edit = () => {
+        this._showEditBox = !this._showEditBox;
+    };
+
+    @action
+    regenerate = async (e: React.PointerEvent<Element>) => {
+        this._isLoading = true;
+        try {
+            let res;
+            if (this._regenInput !== '') {
+                const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
+                res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
+                this._lastInput.text = `${this._lastInput.text} + ${this._regenInput}`;
+            } else {
+                res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+            }
+            if (!res) {
+                console.error('GPT call failed');
+                return;
+            }
+            console.log(res);
+            this.parseResponse(e, res, { X: this._lastInput.x, Y: this._lastInput.y }, true);
+        } catch (err) {
+            console.error('GPT call failed', err);
+        }
+        this._isLoading = false;
+        this._regenInput = '';
+        this._showEditBox = false;
+    };
+
+    @action
+    parseResponse = async (e: React.PointerEvent<Element>, res: string, startPoint: { X: number; Y: number }, regenerate: boolean) => {
+        const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+        console.log('start point is', startPoint);
+        if (svg) {
+            this._lastResponse = svg[0];
+            const svgObject = await parse(svg[0]);
+            const svgStrokes: any = svgObject.children;
+            const strokeData: [InkData, string, string][] = [];
+            console.log('autocolor is', this._autoColor);
+            svgStrokes.forEach((child: any) => {
+                const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
+                strokeData.push([
+                    convertedBezier.map(point => {
+                        return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
+                    }),
+                    (regenerate ? this._lastInput.autoColor : this._autoColor) ? child.attributes.stroke : undefined,
+                    (regenerate ? this._lastInput.autoColor : this._autoColor) ? child.attributes.fill : undefined,
+                ]);
+            });
+            if (regenerate) {
+                this._deleteFunc(this._selectedDoc);
+            }
+            this._addFunc(e, strokeData, this._lastInput, svg[0]);
+        }
+    };
+
+    render() {
+        if (this._display) {
+            return (
+                <div
+                    id="label-handler"
+                    className="contextMenu-cont"
+                    style={{
+                        display: this._display ? '' : 'none',
+                        left: this._pageX,
+                        ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+                        background: SettingsManager.userBackgroundColor,
+                        color: SettingsManager.userColor,
+                    }}>
+                    <div>
+                        <IconButton
+                            tooltip={'Cancel'}
+                            onClick={() => {
+                                this.hideSmartDrawHandler();
+                                this.hideRegenerate();
+                            }}
+                            icon={<FontAwesomeIcon icon="xmark" />}
+                            color={SettingsManager.userColor}
+                            style={{ width: '19px' }}
+                        />
+                        <input
+                            aria-label="label-input"
+                            id="new-label"
+                            type="text"
+                            style={{ color: 'black' }}
+                            value={this._userInput}
+                            onChange={e => {
+                                this.setUserInput(e.target.value);
+                            }}
+                            placeholder="Enter item to draw"
+                        />
+                        <Button
+                            style={{ alignSelf: 'flex-end' }}
+                            text="Send"
+                            icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            color={SettingsManager.userColor}
+                            onClick={e => {
+                                this.drawWithGPT(e as React.PointerEvent<Element>, this._userInput);
+                            }}
+                        />
+                    </div>
+                    {this._showOptions && (
+                        <>
+                            <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
+                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}>
+                                    Auto color
+                                    <Switch
+                                        sx={{
+                                            '& .MuiSwitch-switchBase.Mui-checked': {
+                                                color: SettingsManager.userColor,
+                                            },
+                                            '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
+                                                backgroundColor: SettingsManager.userVariantColor,
+                                            },
+                                        }}
+                                        defaultChecked={true}
+                                        size="small"
+                                        onChange={this.setAutoColor}
+                                    />
+                                </div>
+                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}>
+                                    Complexity
+                                    <Slider
+                                        sx={{
+                                            '& .MuiSlider-thumb': {
+                                                color: SettingsManager.userColor,
+                                                '&.Mui-focusVisible, &:hover, &.Mui-active': {
+                                                    boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
+                                                },
+                                            },
+                                            '& .MuiSlider-track': {
+                                                color: SettingsManager.userVariantColor,
+                                            },
+                                            '& .MuiSlider-rail': {
+                                                color: SettingsManager.userColor,
+                                            },
+                                        }}
+                                        style={{ width: '80%' }}
+                                        min={1}
+                                        max={10}
+                                        step={1}
+                                        size="small"
+                                        value={this._complexity}
+                                        onChange={(e, val) => {
+                                            this.setComplexity(val as number);
+                                        }}
+                                        valueLabelDisplay="auto"
+                                    />
+                                </div>
+                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}>
+                                    Size (in pixels)
+                                    <Slider
+                                        sx={{
+                                            '& .MuiSlider-thumb': {
+                                                color: SettingsManager.userColor,
+                                                '&.Mui-focusVisible, &:hover, &.Mui-active': {
+                                                    boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
+                                                },
+                                            },
+                                            '& .MuiSlider-track': {
+                                                color: SettingsManager.userVariantColor,
+                                            },
+                                            '& .MuiSlider-rail': {
+                                                color: SettingsManager.userColor,
+                                            },
+                                        }}
+                                        style={{ width: '80%' }}
+                                        min={50}
+                                        max={700}
+                                        step={10}
+                                        size="small"
+                                        value={this._size}
+                                        onChange={(e, val) => {
+                                            this.setSize(val as number);
+                                        }}
+                                        valueLabelDisplay="auto"
+                                    />
+                                </div>
+                            </div>
+                        </>
+                    )}
+                </div>
+            );
+        } else if (this._showRegenerate) {
+            return (
+                <div
+                    id="smartdraw-options-menu"
+                    className="contextMenu-cont"
+                    style={{
+                        left: this._pageX,
+                        ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+                        background: SettingsManager.userBackgroundColor,
+                        color: SettingsManager.userColor,
+                    }}>
+                    <div
+                        style={{
+                            display: 'flex',
+                            flexDirection: 'row',
+                        }}>
+                        <IconButton
+                            tooltip="Regenerate"
+                            icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
+                            color={SettingsManager.userColor}
+                            onClick={e => {
+                                this.regenerate(e as React.PointerEvent<Element>);
+                            }}
+                        />
+                        <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} />
+                        {this._showEditBox && (
+                            <div
+                                style={{
+                                    display: 'flex',
+                                    flexDirection: 'row',
+                                }}>
+                                <input
+                                    aria-label="Edit instructions input"
+                                    id="regen-input"
+                                    type="text"
+                                    style={{ color: 'black' }}
+                                    value={this._regenInput}
+                                    onChange={e => {
+                                        this.setRegenInput(e.target.value);
+                                    }}
+                                    placeholder="Edit instructions"
+                                />
+                                <Button
+                                    style={{ alignSelf: 'flex-end' }}
+                                    text="Send"
+                                    icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                    iconPlacement="right"
+                                    color={SettingsManager.userColor}
+                                    onClick={e => {
+                                        this.regenerate(e as React.PointerEvent<Element>);
+                                    }}
+                                />
+                            </div>
+                        )}
+                    </div>
+                </div>
+            );
+        } else {
+            return <></>;
+        }
+    }
+}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 4a469dfe2..bc1abd26e 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -276,6 +276,7 @@ export class Doc extends RefField {
     public static get MyPublishedDocs()       { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs).concat(DocListCast(DocCast(Doc.UserDoc().myPublishedDocs)?.data)); } // prettier-ignore
     public static get MyDashboards()          { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore
     public static get MyTemplates()           { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore
+    public static get MyDrawingAnnos()        { return DocCast(Doc.UserDoc().myDrawingAnnos); } // prettier-ignore
     public static get MyImports()             { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore
     public static get MyFilesystem()          { return DocCast(Doc.UserDoc().myFilesystem); } // prettier-ignore
     public static get MyTools()               { return DocCast(Doc.UserDoc().myTools); } // prettier-ignore
-- 
cgit v1.2.3-70-g09d2


From f84a8d79c07bdd55e48acc976227a2ceeb457e5f Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 2 Jul 2024 14:35:58 -0400
Subject: bug creating drawing groups

---
 src/client/views/MainView.tsx                      | 2964 +++++++-------------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   22 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |   14 +-
 3 files changed, 1070 insertions(+), 1930 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 f88eb3bca..dd11ac684 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,1991 +1,1129 @@
-/* eslint-disable camelcase */
-/* eslint-disable jsx-a11y/control-has-associated-label */
-/* eslint-disable @typescript-eslint/no-unused-vars */
-/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
-/* eslint-disable react/no-array-index-key */
-/* eslint-disable react/jsx-props-no-spreading */
-/* eslint-disable no-return-assign */
-import ArrowLeftIcon from '@mui/icons-material/ArrowLeft';
-import ArrowRightIcon from '@mui/icons-material/ArrowRight';
-import PauseIcon from '@mui/icons-material/Pause';
-import PlayArrowIcon from '@mui/icons-material/PlayArrow';
-import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
-import ReplayIcon from '@mui/icons-material/Replay';
-import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material';
-import Typography from '@mui/material/Typography';
-import { IReactionDisposer, action, computed, observable, reaction } from 'mobx';
+/* eslint-disable node/no-unpublished-import */
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons';
+import * as far from '@fortawesome/free-regular-svg-icons';
+import * as fa from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { NumListCast } from '../../../../fields/Doc';
-import { List } from '../../../../fields/List';
-import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
-import { ViewBoxAnnotatableComponent } from '../../DocComponent';
-import { FieldView, FieldViewProps } from '../FieldView';
-import './PhysicsSimulationBox.scss';
-import InputField from './PhysicsSimulationInputField';
-import questions from './PhysicsSimulationQuestions.json';
-import tutorials from './PhysicsSimulationTutorial.json';
-import Wall from './PhysicsSimulationWall';
-import Weight from './PhysicsSimulationWeight';
-import { Docs } from '../../../documents/Documents';
-import { DocumentType } from '../../../documents/DocumentTypes';
+// eslint-disable-next-line import/no-relative-packages
+import '../../../node_modules/browndash-components/dist/styles/global.min.css';
+import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
+import { Doc, DocListCast, GetDocFromUrl, Opt } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
+import { Id } from '../../fields/FieldSymbols';
+import { DocCast, StrCast, toList } from '../../fields/Types';
+import { DocServer } from '../DocServer';
+import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
+import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { Docs } from '../documents/Documents';
+import { CalendarManager } from '../util/CalendarManager';
+import { CaptureManager } from '../util/CaptureManager';
+import { DocumentManager } from '../util/DocumentManager';
+import { DragManager } from '../util/DragManager';
+import { dropActionType } from '../util/DropActionTypes';
+import { GroupManager } from '../util/GroupManager';
+import { HistoryUtil } from '../util/History';
+import { Hypothesis } from '../util/HypothesisUtils';
+import { UPDATE_SERVER_CACHE } from '../util/LinkManager';
+import { RTFMarkup } from '../util/RTFMarkup';
+import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { ServerStats } from '../util/ServerStats';
+import { SettingsManager } from '../util/SettingsManager';
+import { SharingManager } from '../util/SharingManager';
+import { SnappingManager } from '../util/SnappingManager';
+import { Transform } from '../util/Transform';
+import { ReportManager } from '../util/reportManager/ReportManager';
+import { ComponentDecorations } from './ComponentDecorations';
+import { ContextMenu } from './ContextMenu';
+import { DashboardView } from './DashboardView';
+import { DictationOverlay } from './DictationOverlay';
+import { DocumentDecorations } from './DocumentDecorations';
+import { GestureOverlay } from './GestureOverlay';
+import { LightboxView } from './LightboxView';
+import './MainView.scss';
+import { ObservableReactComponent } from './ObservableReactComponent';
+import { PreviewCursor } from './PreviewCursor';
+import { PropertiesView } from './PropertiesView';
+import { DashboardStyleProvider, DefaultStyleProvider, returnEmptyDocViewList } from './StyleProvider';
+import { TimelineMenu } from './animationtimeline/TimelineMenu';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+import { CollectionMenu } from './collections/CollectionMenu';
+import { TabDocView } from './collections/TabDocView';
+import './collections/TreeView.scss';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
+import { ImageLabelHandler } from './collections/collectionFreeForm/ImageLabelHandler';
+import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
+import { CollectionLinearView } from './collections/collectionLinear';
+import { LinkMenu } from './linking/LinkMenu';
+import { AudioBox } from './nodes/AudioBox';
+import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
+import { DocButtonState } from './nodes/DocumentLinksButton';
+import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
+import { ImageEditorData as ImageEditor } from './nodes/ImageBox';
+import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
+import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
+import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu';
+import { MapAnchorMenu } from './nodes/MapBox/MapAnchorMenu';
+import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
+import { TaskCompletionBox } from './nodes/TaskCompletedBox';
+import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
+import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
+import GenerativeFill from './nodes/generativeFill/GenerativeFill';
+import { PresBox } from './nodes/trails';
+import { AnchorMenu } from './pdf/AnchorMenu';
+import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
+import { TopBar } from './topbar/TopBar';
+import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
 
-interface IWallProps {
-    length: number;
-    xPos: number;
-    yPos: number;
-    angleInDegrees: number;
-}
-interface IForce {
-    description: string;
-    magnitude: number;
-    directionInDegrees: number;
-}
-interface VectorTemplate {
-    top: number;
-    left: number;
-    width: number;
-    height: number;
-    x1: number;
-    y1: number;
-    x2: number;
-    y2: number;
-    weightX: number;
-    weightY: number;
-}
-interface QuestionTemplate {
-    questionSetup: string[];
-    variablesForQuestionSetup: string[];
-    question: string;
-    answerParts: string[];
-    answerSolutionDescriptions: string[];
-    goal: string;
-    hints: { description: string; content: string }[];
-}
-
-interface TutorialTemplate {
-    question: string;
-    steps: {
-        description: string;
-        content: string;
-        forces: {
-            description: string;
-            magnitude: number;
-            directionInDegrees: number;
-            component: boolean;
-        }[];
-        showMagnitude: boolean;
-    }[];
-}
+const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
+const _global = (window /* browser */ || global) /* node */ as any;
 
 @observer
-export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
-    public static LayoutString(fieldKey: string) {
-        return FieldView.LayoutString(PhysicsSimulationBox, fieldKey);
-    }
-
-    _widthDisposer: IReactionDisposer | undefined;
-    @observable _simReset = 0;
-
-    // semi-Constants
-    xMin = 0;
-    yMin = 0;
-    xMax = this._props.PanelWidth() * 0.6;
-    yMax = this._props.PanelHeight();
-    color = `rgba(0,0,0,0.5)`;
-    radius = 50;
-    wallPositions: IWallProps[] = [];
-
-    @computed get circularMotionRadius() {
-        return (NumCast(this.dataDoc.circularMotionRadius, 150) * this._props.PanelWidth()) / 1000;
-    }
-    @computed get gravity() {
-        return NumCast(this.dataDoc.simulation_gravity, -9.81);
-    }
-    @computed get simulationType() {
-        return StrCast(this.dataDoc.simulation_type, 'Inclined Plane');
-    }
-    @computed get simulationMode() {
-        return StrCast(this.dataDoc.simulation_mode, 'Freeform');
-    }
-    // Used for spring simulation
-    @computed get springConstant() {
-        return NumCast(this.dataDoc.spring_constant, 0.5);
-    }
-    @computed get springLengthRest() {
-        return NumCast(this.dataDoc.spring_lengthRest, 200);
-    }
-    @computed get springLengthStart() {
-        return NumCast(this.dataDoc.spring_lengthStart, 200);
-    }
-
-    @computed get pendulumAngle() {
-        return NumCast(this.dataDoc.pendulum_angle);
-    }
-    @computed get pendulumAngleStart() {
-        return NumCast(this.dataDoc.pendulum_angleStart);
-    }
-    @computed get pendulumLength() {
-        return NumCast(this.dataDoc.pendulum_length);
-    }
-    @computed get pendulumLengthStart() {
-        return NumCast(this.dataDoc.pendulum_lengthStart);
-    }
+export class MainView extends ObservableReactComponent<{}> {
+    // eslint-disable-next-line no-use-before-define
+    public static Instance: MainView;
+    public static Live: boolean = false;
+    private _docBtnRef = React.createRef<HTMLDivElement>();
 
-    // Used for wedge simulation
-    @computed get wedgeAngle() {
-        return NumCast(this.dataDoc.wedge_angle, 26);
-    }
-    @computed get wedgeHeight() {
-        return NumCast(this.dataDoc.wedge_height, Math.tan((26 * Math.PI) / 180) * this.xMax * 0.5);
-    }
-    @computed get wedgeWidth() {
-        return NumCast(this.dataDoc.wedge_width, this.xMax * 0.5);
-    }
-    @computed get mass1() {
-        return NumCast(this.dataDoc.mass1, 1);
-    }
-    @computed get mass2() {
-        return NumCast(this.dataDoc.mass2, 1);
+    @observable private _windowWidth: number = 0;
+    @observable private _windowHeight: number = 0;
+    @observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row)
+    @observable private _dashUIHeight: number = 0; // height of entire main dashboard region including top menu buttons
+    @observable private _panelContent: string = 'none';
+    @observable private _sidebarContent: any = Doc.MyLeftSidebarPanel;
+    @observable private _leftMenuFlyoutWidth: number = 0;
+    @computed get _hideUI() {
+        return this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking;
     }
 
-    @computed get mass1Radius() {
-        return NumCast(this.dataDoc.mass1_radius, 30);
-    }
-    @computed get mass1PosXStart() {
-        return NumCast(this.dataDoc.mass1_positionXstart);
-    }
-    @computed get mass1PosYStart() {
-        return NumCast(this.dataDoc.mass1_positionYstart);
+    @computed private get dashboardTabHeight() {
+        return this._hideUI ? 0 : 27;
+    } //  27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
+    @computed private get topOfDashUI() {
+        return this._hideUI || DocumentView.LightboxDoc() ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
     }
-    @computed get mass1VelXStart() {
-        return NumCast(this.dataDoc.mass1_velocityXstart);
+    @computed private get topOfHeaderBarDoc() {
+        return this.topOfDashUI;
     }
-    @computed get mass1VelYStart() {
-        return NumCast(this.dataDoc.mass1_velocityYstart);
+    @computed private get topOfSidebarDoc() {
+        return this.topOfDashUI + this.topMenuHeight();
     }
-
-    @computed get mass2PosXStart() {
-        return NumCast(this.dataDoc.mass2_positionXstart);
-    }
-    @computed get mass2PosYStart() {
-        return NumCast(this.dataDoc.mass2_positionYstart);
+    @computed private get topOfMainDoc() {
+        return this.topOfDashUI + this.topMenuHeight() + this.headerBarDocHeight();
     }
-    @computed get mass2VelXStart() {
-        return NumCast(this.dataDoc.mass2_velocityXstart);
+    @computed private get topOfMainDocContent() {
+        return this.topOfMainDoc + this.dashboardTabHeight;
     }
-    @computed get mass2VelYStart() {
-        return NumCast(this.dataDoc.mass2_velocityYstart);
-    }
-
-    @computed get selectedQuestion() {
-        return this.dataDoc.selectedQuestion ? (JSON.parse(StrCast(this.dataDoc.selectedQuestion)) as QuestionTemplate) : questions.inclinePlane[0];
+    @computed private get leftScreenOffsetOfMainDocView() {
+        return this.leftMenuWidth() - 2;
     }
-    @computed get tutorial() {
-        return this.dataDoc.tutorial ? (JSON.parse(StrCast(this.dataDoc.tutorial)) as TutorialTemplate) : tutorials.inclinePlane;
+    @computed private get userDoc() {
+        return Doc.UserDoc();
     }
-    @computed get selectedSolutions() {
-        return NumListCast(this.dataDoc.selectedSolutions);
+    @observable mainDoc: Opt<Doc> = undefined;
+    @computed private get mainContainer() {
+        if (window.location.pathname.startsWith('/doc/') && ClientUtils.CurrentUserEmail() === 'guest') {
+            DocServer.GetRefField(window.location.pathname.substring('/doc/'.length)).then(main =>
+                runInAction(() => {
+                    this.mainDoc = main as Doc;
+                })
+            );
+            return this.mainDoc;
+        }
+        return this.userDoc ? Doc.ActiveDashboard : Doc.GuestDashboard;
     }
-    @computed get questionPartOne() {
-        return StrCast(this.dataDoc.questionPartOne);
+    @computed private get headerBarDoc() {
+        return Doc.MyHeaderBar;
     }
-    @computed get questionPartTwo() {
-        return StrCast(this.dataDoc.questionPartTwo);
+    @computed public get mainFreeform(): Opt<Doc> {
+        return (docs => (docs?.length > 1 ? docs[1] : undefined))(DocListCast(this.mainContainer!.data));
     }
+    @observable public headerBarHeight: number = 0;
+    headerBarHeightFunc = () => this.headerBarHeight;
 
-    componentWillUnmount() {
-        this._widthDisposer?.();
-    }
+    @action
+    toggleTopBar = () => {
+        if (this.headerBarHeight > 0) {
+            this.headerBarHeight = 0;
+        } else {
+            this.headerBarHeight = 60;
+        }
+    };
+    headerBarDocWidth = () => this.mainDocViewWidth();
+    headerBarDocHeight = () => (this._hideUI ? 0 : this.headerBarHeight ?? 0);
+    topMenuHeight = () => (this._hideUI ? 0 : 35);
+    topMenuWidth = returnZero; // value is ignored ...
+    leftMenuWidth = () => (this._hideUI ? 0 : Number(LEFT_MENU_WIDTH.replace('px', '')));
+    leftMenuHeight = () => this._dashUIHeight;
+    leftMenuFlyoutWidth = () => this._leftMenuFlyoutWidth;
+    leftMenuFlyoutHeight = () => this._dashUIHeight;
+    propertiesWidth = () => Math.max(0, Math.min(this._dashUIWidth - 50, SnappingManager.PropertiesWidth || 0));
+    propertiesHeight = () => this._dashUIHeight;
+    mainDocViewWidth = () => this._dashUIWidth - this.propertiesWidth() - this.leftMenuWidth() - this.leftMenuFlyoutWidth();
+    mainDocViewHeight = () => this._dashUIHeight - this.headerBarDocHeight();
 
     componentDidMount() {
-        // Setup and update simulation
-        this._widthDisposer = reaction(() => [this._props.PanelWidth(), this._props.PanelHeight()], this.setupSimulation, { fireImmediately: true });
+        // Utils.TraceConsoleLog();
+        reaction(
+            // when a multi-selection occurs, remove focus from all active elements to allow keyboad input to go only to global key manager to act upon selection
+            () => DocumentView.Selected().slice(),
+            views => views.length > 1 && (document.activeElement as any)?.blur !== undefined && (document.activeElement as any)!.blur()
+        );
+        reaction(
+            () => Doc.MyDockedBtns.linearView_IsOpen,
+            open => SnappingManager.SetPrintToConsole(!!open)
+        );
+        const scriptTag = document.createElement('script');
+        scriptTag.setAttribute('type', 'text/javascript');
+        scriptTag.setAttribute('src', 'https://www.bing.com/api/maps/mapcontrol?callback=makeMap');
+        scriptTag.async = true;
+        scriptTag.defer = true;
+        document.body.appendChild(scriptTag);
+        document.getElementById('root')?.addEventListener('scroll', () =>
+            (ele => {
+                ele.scrollLeft = ele.scrollTop = 0;
+            })(document.getElementById('root')!)
+        );
+        const ele = document.getElementById('loader');
+        const prog = document.getElementById('dash-progress');
+        if (ele && prog) {
+            // remove from DOM
+            setTimeout(() => {
+                prog.style.transition = '1s';
+                prog.style.width = '100%';
+            }, 0);
+            setTimeout(() => {
+                ele.outerHTML = '';
+            }, 1000);
+        }
+        this._sidebarContent.proto = undefined;
+        if (!MainView.Live) {
+            DocServer.setLivePlaygroundFields([
+                'dataTransition',
+                'viewTransition',
+                'treeView_Open',
+                'treeView_ExpandedView',
+                'carousel_index',
+                'itemIndex', // for changing slides in presentations
+                'layout_sidebarWidthPercent',
+                'layout_currentTimecode',
+                'layout_timelineHeightPercent',
+                'layout_hideMinimap',
+                'layout_showSidebar',
+                'layout_scrollTop',
+                'layout_fitWidth',
+                'layout_curPage',
+                'presStatus',
+                'freeform_panX',
+                'freeform_panY',
+                'freeform_scale',
+                'overlayX',
+                'overlayY',
+                'text_scrollHeight',
+                'text_height',
+                'hidden',
+                // 'type_collection',
+                'chromeHidden',
+                'currentFrame',
+            ]); // can play with these fields on someone else's
+        }
 
-        // Create walls
-        this.wallPositions = [
-            { length: 100, xPos: 0, yPos: 0, angleInDegrees: 0 },
-            { length: 100, xPos: 0, yPos: 100, angleInDegrees: 0 },
-            { length: 100, xPos: 0, yPos: 0, angleInDegrees: 90 },
-            { length: 100, xPos: (this.xMax / this._props.PanelWidth()) * 100, yPos: 0, angleInDegrees: 90 },
-        ];
+        const tag = document.createElement('script');
+        tag.src = 'https://www.youtube.com/iframe_api';
+        const firstScriptTag = document.getElementsByTagName('script')[0];
+        firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
+        document.addEventListener('dash', (e: any) => {
+            // event used by chrome plugin to tell Dash which document to focus on
+            const id = GetDocFromUrl(e.detail);
+            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentView.showDocument(doc, { willPan: false }) : null));
+        });
+        document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
+        this.initEventListeners();
     }
 
-    componentDidUpdate(prevProps: Readonly<FieldViewProps>) {
-        super.componentDidUpdate(prevProps);
-        if (this.xMax !== this._props.PanelWidth() * 0.6 || this.yMax !== this._props.PanelHeight()) {
-            this.xMax = this._props.PanelWidth() * 0.6;
-            this.yMax = this._props.PanelHeight();
-            this.setupSimulation();
-        }
+    componentWillUnMount() {
+        // window.removeEventListener('keyup', KeyManager.Instance.unhandle);
+        // window.removeEventListener('keydown', KeyManager.Instance.handle);
+        // window.removeEventListener('pointerdown', this.globalPointerDown, true);
+        // window.removeEventListener('pointermove', this.globalPointerMove, true);
+        // window.removeEventListener('pointerup', this.globalPointerClick, true);
+        // window.removeEventListener('paste', KeyManager.Instance.paste as any);
+        // document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener);
     }
 
-    gravityForce = (mass: number): IForce => ({
-        description: 'Gravity',
-        magnitude: mass * Math.abs(this.gravity),
-        directionInDegrees: 270,
-    });
+    constructor(props: any) {
+        super(props);
+        makeObservable(this);
+        DocumentViewInternal.addDocTabFunc = MainView.addDocTabFunc_impl;
+        MainView.Instance = this;
+        DashboardView._urlState = HistoryUtil.parseUrl(window.location) || ({} as any);
 
-    @action
-    setupSimulation = () => {
-        const { simulationType } = this;
-        const mode = this.simulationMode;
-        this.dataDoc.simulation_paused = true;
-        if (simulationType !== 'Circular Motion') {
-            this.dataDoc.mass1_velocityXstart = 0;
-            this.dataDoc.mass1_velocityYstart = 0;
-            this.dataDoc.mass1_velocityX = 0;
-            this.dataDoc.mass1_velocityY = 0;
-        }
-        if (mode === 'Freeform') {
-            this.dataDoc.simulation_showForceMagnitudes = true;
-            // prettier-ignore
-            switch (simulationType) {
-                case 'One Weight': 
-                    this.dataDoc.simulation_showComponentForces = false;
-                    this.dataDoc.mass1_positionYstart = this.yMin + this.mass1Radius;
-                    this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
-                    this.dataDoc.mass1_positionY = this.getDisplayYPos(this.yMin + this.mass1Radius);
-                    this.dataDoc.mass1_positionX = (this.xMax + this.xMin) / 2 - this.mass1Radius;
-                    this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
-                    this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
-                    break;
-                case 'Inclined Plane':  this.setupInclinedPlane(); break;
-                case 'Pendulum':        this.setupPendulum(); break;
-                case 'Spring':          this.setupSpring(); break;
-                case 'Circular Motion': this.setupCircular(20); break;
-                case 'Pulley':          this.setupPulley(); break;
-                case 'Suspension':      this.setupSuspension();break;
-                default:
-            }
-            this._simReset++;
-        } else if (mode === 'Review') {
-            this.dataDoc.simulation_showComponentForces = false;
-            this.dataDoc.simulation_showForceMagnitudes = true;
-            this.dataDoc.simulation_showAcceleration = false;
-            this.dataDoc.simulation_showVelocity = false;
-            this.dataDoc.simulation_showForces = true;
-            this.generateNewQuestion();
-            // prettier-ignore
-            switch (simulationType) {
-                case 'One Weight' : break;// TODO - one weight review problems
-                case 'Spring':  this.setupSpring(); break; // TODO - spring review problems
-                case 'Inclined Plane': this.dataDoc.mass1_forcesUpdated =  this.dataDoc.mass1_forcesStart = ''; break;
-                case 'Pendulum':  this.setupPendulum(); break;  // TODO - pendulum review problems
-                case 'Circular Motion':  this.setupCircular(0); break;  // TODO - circular motion review problems
-                case 'Pulley':  this.setupPulley(); break; // TODO - pulley tutorial review problems
-                case 'Suspension': this.setupSuspension(); break;  // TODO - suspension tutorial review problems
-                default:
-            }
-        } else if (mode === 'Tutorial') {
-            this.dataDoc.simulation_showComponentForces = false;
-            this.dataDoc.tutorial_stepNumber = 0;
-            this.dataDoc.simulation_showAcceleration = false;
-            if (this.simulationType !== 'Circular Motion') {
-                this.dataDoc.mass1_velocityX = 0;
-                this.dataDoc.mass1_velocityY = 0;
-                this.dataDoc.simulation_showVelocity = false;
-            } else {
-                this.dataDoc.mass1_velocityX = 20;
-                this.dataDoc.mass1_velocityY = 0;
-                this.dataDoc.simulation_showVelocity = true;
-            }
+        // causes errors to be generated when modifying an observable outside of an action
+        configure({ enforceActions: 'observed' });
 
-            switch (this.simulationType) {
-                case 'One Weight':
-                    this.dataDoc.simulation_showForces = true;
-                    this.dataDoc.mass1_positionYstart = this.yMax - 100;
-                    this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
-                    this.dataDoc.tutorial = JSON.stringify(tutorials.freeWeight);
-                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.freeWeight.steps[0].forces);
-                    this.dataDoc.simulation_showForceMagnitudes = tutorials.freeWeight.steps[0].showMagnitude;
-                    break;
-                case 'Spring':
-                    this.dataDoc.simulation_showForces = true;
-                    this.setupSpring();
-                    this.dataDoc.mass1_positionYstart = this.yMin + 200 + 19.62;
-                    this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
-                    this.dataDoc.tutorial = JSON.stringify(tutorials.spring);
-                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.spring.steps[0].forces);
-                    this.dataDoc.simulation_showForceMagnitudes = tutorials.spring.steps[0].showMagnitude;
-                    break;
-                case 'Pendulum':
-                    this.setupPendulum();
-                    this.dataDoc.tutorial = JSON.stringify(tutorials.pendulum);
-                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.pendulum.steps[0].forces);
-                    this.dataDoc.simulation_showForceMagnitudes = tutorials.pendulum.steps[0].showMagnitude;
-                    break;
-                case 'Inclined Plane':
-                    this.dataDoc.wedge_angle = 26;
-                    this.setupInclinedPlane();
-                    this.dataDoc.simulation_showForces = true;
-                    this.dataDoc.tutorial = JSON.stringify(tutorials.inclinePlane);
-                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.inclinePlane.steps[0].forces);
-                    this.dataDoc.simulation_showForceMagnitudes = tutorials.inclinePlane.steps[0].showMagnitude;
-                    break;
-                case 'Circular Motion':
-                    this.dataDoc.simulation_showForces = true;
-                    this.setupCircular(40);
-                    this.dataDoc.tutorial = JSON.stringify(tutorials.circular);
-                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.circular.steps[0].forces);
-                    this.dataDoc.simulation_showForceMagnitudes = tutorials.circular.steps[0].showMagnitude;
-                    break;
-                case 'Pulley':
-                    this.dataDoc.simulation_showForces = true;
-                    this.setupPulley();
-                    this.dataDoc.tutorial = JSON.stringify(tutorials.pulley);
-                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.pulley.steps[0].forces);
-                    this.dataDoc.simulation_showForceMagnitudes = tutorials.pulley.steps[0].showMagnitude;
-                    break;
-                case 'Suspension':
-                    this.dataDoc.simulation_showForces = true;
-                    this.setupSuspension();
-                    this.dataDoc.tutorial = JSON.stringify(tutorials.suspension);
-                    this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.suspension.steps[0].forces);
-                    this.dataDoc.simulation_showForceMagnitudes = tutorials.suspension.steps[0].showMagnitude;
-                    break;
-                default:
+        if (window.location.pathname !== '/home') {
+            const pathname = window.location.pathname.substr(1).split('/');
+            if (pathname.length > 1 && pathname[0] === 'doc') {
+                DocServer.GetRefField(pathname[1]).then(
+                    action(field => {
+                        if (field instanceof Doc && field._type_collection !== CollectionViewType.Docking) {
+                            Doc.GuestTarget = field;
+                        }
+                    })
+                );
             }
-            this._simReset++;
         }
-    };
 
-    // Helper function to go between display and real values
-    getDisplayYPos = (yPos: number) => this.yMax - yPos - 2 * this.mass1Radius + 5;
-    getYPosFromDisplay = (yDisplay: number) => this.yMax - yDisplay - 2 * this.mass1Radius + 5;
+        library.add(
+            ...[
+                fa.faExclamationCircle,
+                fa.faEdit,
+                fa.faArrowDownShortWide,
+                fa.faTrash,
+                fa.faTrashAlt,
+                fa.faShare,
+                fa.faTaxi,
+                fa.faDownload,
+                fa.faPallet,
+                fa.faExpandArrowsAlt,
+                fa.faAmbulance,
+                fa.faLayerGroup,
+                fa.faExternalLinkAlt,
+                fa.faCalendar,
+                fa.faSquare,
+                far.faSquare as any,
+                fa.faConciergeBell,
+                fa.faWindowRestore,
+                fa.faFolder,
+                fa.faFolderOpen,
+                fa.faFolderPlus,
+                fa.faFolderClosed,
+                fa.faBook,
+                fa.faMapPin,
+                fa.faMapMarker,
+                fa.faFingerprint,
+                fa.faCrosshairs,
+                fa.faDesktop,
+                fa.faUnlock,
+                fa.faLock,
+                fa.faLaptopCode,
+                fa.faMale,
+                fa.faCopy,
+                fa.faHome,
+                fa.faHandPointLeft,
+                fa.faHandPointRight,
+                fa.faCompass,
+                fa.faSnowflake,
+                fa.faStar,
+                fa.faSplotch,
+                fa.faMicrophone,
+                fa.faCircleHalfStroke,
+                fa.faKeyboard,
+                fa.faQuestion,
+                fa.faTasks,
+                fa.faPalette,
+                fa.faAngleLeft,
+                fa.faAngleRight,
+                fa.faBell,
+                fa.faCamera,
+                fa.faExpand,
+                fa.faCaretDown,
+                fa.faCaretLeft,
+                fa.faCaretRight,
+                fa.faCaretSquareDown,
+                fa.faCaretSquareRight,
+                fa.faArrowsAltH,
+                fa.faPlus,
+                fa.faMinus,
+                fa.faTerminal,
+                fa.faToggleOn,
+                fa.faFile,
+                fa.faLocationArrow,
+                fa.faSearch,
+                fa.faFileDownload,
+                fa.faFileUpload,
+                fa.faStop,
+                fa.faCalculator,
+                fa.faWindowMaximize,
+                fa.faIdCard,
+                fa.faAddressCard,
+                fa.faQuestionCircle,
+                fa.faArrowLeft,
+                fa.faArrowRight,
+                fa.faArrowDown,
+                fa.faArrowUp,
+                fa.faBolt,
+                fa.faBullseye,
+                fa.faTurnUp,
+                fa.faTurnDown,
+                fa.faCaretUp,
+                fa.faCat,
+                fa.faCheck,
+                fa.faChevronRight,
+                fa.faChevronLeft,
+                fa.faChevronDown,
+                fa.faChevronUp,
+                fa.faClone,
+                fa.faCloudUploadAlt,
+                fa.faCommentAlt,
+                fa.faCommentDots,
+                fa.faCompressArrowsAlt,
+                fa.faCut,
+                fa.faEllipsisV,
+                fa.faEraser,
+                fa.faDeleteLeft,
+                fa.faXmarksLines,
+                fa.faCircleXmark,
+                fa.faXmark,
+                fa.faExclamation,
+                fa.faFileAlt,
+                fa.faFileAudio,
+                fa.faFileVideo,
+                fa.faFilePdf,
+                fa.faFilm,
+                fa.faFilter,
+                fa.faFont,
+                fa.faGlobeAmericas,
+                fa.faGlobeAsia,
+                fa.faHighlighter,
+                fa.faLongArrowAltRight,
+                fa.faMousePointer,
+                fa.faMusic,
+                fa.faObjectGroup,
+                fa.faArrowsLeftRight,
+                fa.faPause,
+                fa.faPen,
+                fa.faUserPen,
+                fa.faPenNib,
+                fa.faPhone,
+                fa.faPlay,
+                fa.faPortrait,
+                fa.faRedoAlt,
+                fa.faStamp,
+                fa.faStickyNote,
+                fa.faArrowsAltV,
+                fa.faTimesCircle,
+                fa.faThumbtack,
+                fa.faTree,
+                fa.faTv,
+                fa.faUndoAlt,
+                fa.faVideoSlash,
+                fa.faVideo,
+                fa.faAsterisk,
+                fa.faBrain,
+                fa.faImage,
+                fa.faPaintBrush,
+                fa.faTimes,
+                fa.faFlag,
+                fa.faScroll,
+                fa.faEye,
+                fa.faArrowsAlt,
+                fa.faQuoteLeft,
+                fa.faSortAmountDown,
+                fa.faAlignLeft,
+                fa.faAlignCenter,
+                fa.faAlignRight,
+                fa.faHeading,
+                fa.faRulerCombined,
+                fa.faFillDrip,
+                fa.faLink,
+                fa.faUnlink,
+                fa.faBold,
+                fa.faItalic,
+                fa.faClipboard,
+                fa.faUnderline,
+                fa.faStrikethrough,
+                fa.faSuperscript,
+                fa.faSubscript,
+                fa.faIndent,
+                fa.faEyeDropper,
+                fa.faPaintRoller,
+                fa.faBars,
+                fa.faBarsStaggered,
+                fa.faBrush,
+                fa.faShapes,
+                fa.faEllipsisH,
+                fa.faHandPaper,
+                fa.faMap,
+                fa.faUser,
+                faHireAHelper as any,
+                fa.faTrashRestore,
+                fa.faUsers,
+                fa.faWrench,
+                fa.faCog,
+                fa.faMap,
+                fa.faBellSlash,
+                fa.faExpandAlt,
+                fa.faArchive,
+                fa.faBezierCurve,
+                fa.faCircle,
+                far.faCircle as any,
+                fa.faLongArrowAltRight,
+                fa.faPenFancy,
+                fa.faAngleDoubleRight,
+                fa.faAngleDoubleDown,
+                fa.faAngleDoubleLeft,
+                fa.faAngleDoubleUp,
+                faBuffer as any,
+                fa.faExpand,
+                fa.faUndo,
+                fa.faSlidersH,
+                fa.faAngleUp,
+                fa.faAngleDown,
+                fa.faPlayCircle,
+                fa.faClock,
+                fa.faRoute,
+                fa.faRocket,
+                fa.faExchangeAlt,
+                fa.faHashtag,
+                fa.faAlignJustify,
+                fa.faCheckSquare,
+                fa.faListUl,
+                fa.faWindowMinimize,
+                fa.faWindowRestore,
+                fa.faTextWidth,
+                fa.faTextHeight,
+                fa.faClosedCaptioning,
+                fa.faInfoCircle,
+                fa.faTag,
+                fa.faSyncAlt,
+                fa.faPhotoVideo,
+                fa.faArrowAltCircleDown,
+                fa.faArrowAltCircleUp,
+                fa.faArrowAltCircleLeft,
+                fa.faArrowAltCircleRight,
+                fa.faStopCircle,
+                fa.faCheckCircle,
+                fa.faGripVertical,
+                fa.faSortUp,
+                fa.faSortDown,
+                fa.faTable,
+                fa.faTableCells,
+                fa.faTableColumns,
+                fa.faTh,
+                fa.faThList,
+                fa.faProjectDiagram,
+                fa.faSignature,
+                fa.faColumns,
+                fa.faChevronCircleUp,
+                fa.faUpload,
+                fa.faBorderAll,
+                fa.faBraille,
+                fa.faPersonChalkboard,
+                fa.faChalkboard,
+                fa.faPencilAlt,
+                fa.faEyeSlash,
+                fa.faSmile,
+                fa.faIndent,
+                fa.faOutdent,
+                fa.faChartBar,
+                fa.faBan,
+                fa.faPhoneSlash,
+                fa.faGripLines,
+                fa.faSave,
+                fa.faBook,
+                fa.faBookmark,
+                fa.faList,
+                fa.faListOl,
+                fa.faLightbulb,
+                fa.faBookOpen,
+                fa.faMapMarkerAlt,
+                fa.faSearchPlus,
+                fa.faSolarPanel,
+                fa.faVolumeUp,
+                fa.faVolumeDown,
+                fa.faSquareRootAlt,
+                fa.faVolumeMute,
+                fa.faUserCircle,
+                fa.faHeart,
+                fa.faHeartBroken,
+                fa.faHighlighter,
+                fa.faRemoveFormat,
+                fa.faHandPointUp,
+                fa.faXRay,
+                fa.faZ,
+                fa.faArrowsUpToLine,
+                fa.faArrowsDownToLine,
+                fa.faPalette,
+                fa.faHourglassHalf,
+                fa.faRobot,
+                fa.faSatellite,
+                fa.faStar,
+            ]
+        );
+    }
 
-    // Update forces when coefficient of static friction changes in freeform mode
-    updateForcesWithFriction = (coefficient: number, width = this.wedgeWidth, height = this.wedgeHeight) => {
-        const normalForce: IForce = {
-            description: 'Normal Force',
-            magnitude: Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1,
-            directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
-        };
-        const frictionForce: IForce = {
-            description: 'Static Friction Force',
-            magnitude: coefficient * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1,
-            directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
-        };
-        // reduce magnitude or friction force if necessary such that block cannot slide up plane
-        let yForce = -Math.abs(this.gravity) * this.mass1;
-        yForce += normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
-        yForce += frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
-        if (yForce > 0) {
-            frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + Math.abs(this.gravity) * this.mass1) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+    private longPressTimer: NodeJS.Timeout | undefined;
+    globalPointerClick = action(() => {
+        this.longPressTimer && clearTimeout(this.longPressTimer);
+        DocumentView.LongPress = false;
+    });
+    globalPointerMove = action((e: PointerEvent) => {
+        if (e.movementX > 3 || e.movementY > 3) this.longPressTimer && clearTimeout(this.longPressTimer);
+    });
+    globalPointerDown = action((e: PointerEvent) => {
+        DocumentView.LongPress = false;
+        this.longPressTimer = setTimeout(
+            action(() => {
+                DocumentView.LongPress = true;
+            }),
+            1000
+        );
+        DocumentManager.removeOverlayViews();
+        Doc.linkFollowUnhighlight();
+        AudioBox.Enabled = true;
+        const targets = document.elementsFromPoint(e.x, e.y);
+        if (targets.length) {
+            let targClass = targets[0].className.toString();
+            for (let i = 0; i < targets.length - 1; i++) {
+                if (typeof targets[i].className === 'object') targClass = targets[i + 1].className.toString();
+                else break;
+            }
+            !targClass.includes('contextMenu') && ContextMenu.Instance.closeMenu();
+            !['timeline-menu-desc', 'timeline-menu-item', 'timeline-menu-input'].includes(targClass) && TimelineMenu.Instance.closeMenu();
         }
+    });
 
-        const normalForceComponent: IForce = {
-            description: 'Normal Force',
-            magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)),
-            directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
-        };
-        const gravityParallel: IForce = {
-            description: 'Gravity Parallel Component',
-            magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin(Math.PI / 2 - Math.atan(height / width)),
-            directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI + 180,
-        };
-        const gravityPerpendicular: IForce = {
-            description: 'Gravity Perpendicular Component',
-            magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(Math.PI / 2 - Math.atan(height / width)),
-            directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI,
-        };
-        const gravityForce = this.gravityForce(this.mass1);
-        if (coefficient !== 0) {
-            this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce, frictionForce]);
-            this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce, frictionForce]);
-            this.dataDoc.mass1_componentForces = JSON.stringify([frictionForce, normalForceComponent, gravityParallel, gravityPerpendicular]);
-        } else {
-            this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce]);
-            this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce]);
-            this.dataDoc.mass1_componentForces = JSON.stringify([normalForceComponent, gravityParallel, gravityPerpendicular]);
-        }
+    initEventListeners = () => {
+        window.addEventListener('beforeunload', UPDATE_SERVER_CACHE);
+        window.addEventListener('drop', e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page
+        window.addEventListener('dragover', e => e.preventDefault(), false);
+        document.addEventListener('pointerdown', this.globalPointerDown, true);
+        document.addEventListener('pointermove', this.globalPointerMove, true);
+        document.addEventListener('pointerup', this.globalPointerClick, true);
+        document.addEventListener(
+            'click',
+            (e: MouseEvent) => {
+                if (!e.cancelBubble) {
+                    const pathstr = (e as any)?.path?.map((p: any) => p.classList?.toString()).join();
+                    if (pathstr?.includes('libraryFlyout')) {
+                        DocumentView.DeselectAll();
+                    }
+                }
+            },
+            false
+        );
+        document.oncontextmenu = () => false;
     };
 
-    // Change wedge height and width and weight position to match new wedge angle
-    changeWedgeBasedOnNewAngle = (angle: number) => {
-        const radAng = (angle * Math.PI) / 180;
-        this.dataDoc.wedge_width = this.xMax * 0.5;
-        this.dataDoc.wedge_height = Math.tan(radAng) * this.dataDoc.wedge_width;
-
-        // update weight position based on updated wedge width/height
-        const yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius;
-        const xPos = this.xMax * 0.25 + this.mass1Radius * Math.sin(radAng) - this.mass1Radius;
-
-        this.dataDoc.mass1_positionXstart = xPos;
-        this.dataDoc.mass1_positionYstart = yPos;
-        if (this.simulationMode === 'Freeform') {
-            this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction), this.dataDoc.wedge_width, Math.tan(radAng) * this.dataDoc.wedge_width);
-        }
+    @action
+    createNewPresentation = () => {
+        const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
+        CollectionDockingView.AddSplit(pres, OpenWhereMod.right);
+        Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', pres); // Doc.MyTrails should be created in createDashboard
+        Doc.ActivePresentation = pres;
     };
 
-    // In review mode, update forces when coefficient of static friction changed
-    updateReviewForcesBasedOnCoefficient = (coefficient: number) => {
-        let theta = this.wedgeAngle;
-        const index = this.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45');
-        if (index >= 0) {
-            theta = NumListCast(this.dataDoc.questionVariables)[index];
-        }
-        if (isNaN(theta)) {
-            return;
-        }
-        this.dataDoc.review_GravityMagnitude = Math.abs(this.gravity);
-        this.dataDoc.review_GravityAngle = 270;
-        this.dataDoc.review_NormalMagnitude = Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180);
-        this.dataDoc.review_NormalAngle = 90 - theta;
-        let yForce = -Math.abs(this.gravity);
-        yForce += Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((90 - theta) * Math.PI) / 180);
-        yForce += coefficient * Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((180 - theta) * Math.PI) / 180);
-        let friction = coefficient * Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180);
-        if (yForce > 0) {
-            friction = (-(Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180)) * Math.sin(((90 - theta) * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin(((180 - theta) * Math.PI) / 180);
+    @action
+    openPresentation = (pres: Doc) => {
+        if (pres.type === DocumentType.PRES) {
+            CollectionDockingView.AddSplit(pres, OpenWhereMod.right, undefined, PresBox.PanelName);
+            Doc.MyTrails && (Doc.ActivePresentation = pres);
+            Doc.AddDocToList(Doc.MyTrails, 'data', pres);
+            this.closeFlyout();
         }
-        this.dataDoc.review_StaticMagnitude = friction;
-        this.dataDoc.review_StaticAngle = 180 - theta;
     };
 
-    // In review mode, update forces when wedge angle changed
-    updateReviewForcesBasedOnAngle = (angle: number) => {
-        this.dataDoc.review_GravityMagnitude = Math.abs(this.gravity);
-        this.dataDoc.review_GravityAngle = 270;
-        this.dataDoc.review_NormalMagnitude = Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180);
-        this.dataDoc.review_NormalAngle = 90 - angle;
-        let yForce = -Math.abs(this.gravity);
-        yForce += Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180) * Math.sin(((90 - angle) * Math.PI) / 180);
-        yForce += NumCast(this.dataDoc.review_Coefficient) * Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180) * Math.sin(((180 - angle) * Math.PI) / 180);
-        let friction = NumCast(this.dataDoc.review_Coefficient) * Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180);
-        if (yForce > 0) {
-            friction = (-(Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180)) * Math.sin(((90 - angle) * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin(((180 - angle) * Math.PI) / 180);
-        }
-        this.dataDoc.review_StaticMagnitude = friction;
-        this.dataDoc.review_StaticAngle = 180 - angle;
+    @action
+    createNewFolder = async () => {
+        const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true });
+        Doc.AddDocToList(Doc.MyFilesystem, 'data', folder);
     };
 
-    // Solve for the correct answers to the generated problem
-    getAnswersToQuestion = (question: QuestionTemplate, questionVars: number[]) => {
-        const solutions: number[] = [];
+    waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
+    headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1);
+    mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1);
+    addHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true);
+    removeHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true);
+    @computed get headerBarDocView() {
+        return (
+            <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}>
+                <DocumentView
+                    key="headerBarDoc"
+                    Document={this.headerBarDoc}
+                    addDocTab={DocumentViewInternal.addDocTabFunc}
+                    pinToPres={emptyFunction}
+                    containerViewPath={returnEmptyDoclist}
+                    styleProvider={DefaultStyleProvider}
+                    addDocument={this.addHeaderDoc}
+                    removeDocument={this.removeHeaderDoc}
+                    fitContentsToBox={returnTrue}
+                    isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events)
+                    isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive
+                    ScreenToLocalTransform={this.headerBarScreenXf}
+                    childHideResizeHandles
+                    childDragAction={dropActionType.move}
+                    dontRegisterView
+                    hideResizeHandles
+                    PanelWidth={this.headerBarDocWidth}
+                    PanelHeight={this.headerBarDocHeight}
+                    renderDepth={0}
+                    focus={emptyFunction}
+                    whenChildContentsActiveChanged={emptyFunction}
+                    childFilters={returnEmptyFilter}
+                    childFiltersByRanges={returnEmptyFilter}
+                    searchFilterDocs={returnEmptyDoclist}
+                />
+            </div>
+        );
+    }
+    @computed get mainDocView() {
+        const headerBar = this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView;
+        return (
+            <>
+                {headerBar}
+                <DocumentView
+                    key="main"
+                    Document={this.mainContainer!}
+                    addDocument={undefined}
+                    addDocTab={DocumentViewInternal.addDocTabFunc}
+                    pinToPres={emptyFunction}
+                    containerViewPath={returnEmptyDoclist}
+                    styleProvider={this._hideUI ? DefaultStyleProvider : undefined}
+                    isContentActive={returnTrue}
+                    removeDocument={undefined}
+                    ScreenToLocalTransform={this._hideUI ? this.mainScreenToLocalXf : Transform.Identity}
+                    PanelWidth={this.mainDocViewWidth}
+                    PanelHeight={this.mainDocViewHeight}
+                    focus={emptyFunction}
+                    whenChildContentsActiveChanged={emptyFunction}
+                    childFilters={returnEmptyFilter}
+                    childFiltersByRanges={returnEmptyFilter}
+                    searchFilterDocs={returnEmptyDoclist}
+                    suppressSetHeight
+                    renderDepth={this._hideUI ? 0 : -1}
+                />
+            </>
+        );
+    }
 
-        let theta = this.wedgeAngle;
-        let index = question.variablesForQuestionSetup.indexOf('theta - max 45');
-        if (index >= 0) {
-            theta = questionVars[index];
-        }
-        let muS: number = NumCast(this.dataDoc.coefficientOfStaticFriction);
-        index = question.variablesForQuestionSetup.indexOf('coefficient of static friction');
-        if (index >= 0) {
-            muS = questionVars[index];
-        }
+    @computed get dockingContent() {
+        return (
+            <GestureOverlay isActive={!DocumentView.LightboxDoc()}>
+                <div
+                    key="docking"
+                    className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? '-flyout' : ''}`}
+                    onDrop={e => {
+                        e.stopPropagation();
+                        e.preventDefault();
+                    }}
+                    style={{
+                        width: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
+                        minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
+                        transform: DocumentView.LightboxDoc() ? 'scale(0.0001)' : undefined,
+                    }}>
+                    {!this.mainContainer ? null : this.mainDocView}
+                </div>
+            </GestureOverlay>
+        );
+    }
 
-        for (let i = 0; i < question.answerSolutionDescriptions.length; i++) {
-            const description = question.answerSolutionDescriptions[i];
-            if (!isNaN(NumCast(description))) {
-                solutions.push(NumCast(description));
-            } else if (description === 'solve normal force angle from wedge angle') {
-                solutions.push(90 - theta);
-            } else if (description === 'solve normal force magnitude from wedge angle') {
-                solutions.push(Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI));
-            } else if (description === 'solve static force magnitude from wedge angle given equilibrium') {
-                const normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
-                const normalForceAngle = 90 - theta;
-                const frictionForceAngle = 180 - theta;
-                const frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
-                solutions.push(frictionForceMagnitude);
-            } else if (description === 'solve static force angle from wedge angle given equilibrium') {
-                solutions.push(180 - theta);
-            } else if (description === 'solve minimum static coefficient from wedge angle given equilibrium') {
-                const normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
-                const normalForceAngle = 90 - theta;
-                const frictionForceAngle = 180 - theta;
-                const frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
-                const frictionCoefficient = frictionForceMagnitude / normalForceMagnitude;
-                solutions.push(frictionCoefficient);
-            } else if (description === 'solve maximum wedge angle from coefficient of static friction given equilibrium') {
-                solutions.push((Math.atan(muS) * 180) / Math.PI);
-            }
-        }
-        this.dataDoc.selectedSolutions = new List<number>(solutions);
-        return solutions;
+    @action
+    onPropertiesPointerDown = (e: React.PointerEvent) => {
+        setupMoveUpEvents(
+            this,
+            e,
+            action(() => {
+                SnappingManager.SetPropertiesWidth(Math.max(0, this._dashUIWidth - e.clientX));
+                return !SnappingManager.PropertiesWidth;
+            }),
+            action(() => {
+                SnappingManager.PropertiesWidth < 5 && SnappingManager.SetPropertiesWidth(0);
+            }),
+            action(() => {
+                SnappingManager.SetPropertiesWidth(this.propertiesWidth() < 15 ? Math.min(this._dashUIWidth - 50, 250) : 0);
+            }),
+            false
+        );
     };
 
-    // In review mode, check if input answers match correct answers and optionally generate alert
-    checkAnswers = (showAlert: boolean = true) => {
-        let error: boolean = false;
-        const epsilon: number = 0.01;
-        if (this.selectedQuestion) {
-            for (let i = 0; i < this.selectedQuestion.answerParts.length; i++) {
-                if (this.selectedQuestion.answerParts[i] === 'force of gravity') {
-                    if (Math.abs(NumCast(this.dataDoc.review_GravityMagnitude) - this.selectedSolutions[i]) > epsilon) {
-                        error = true;
-                    }
-                } else if (this.selectedQuestion.answerParts[i] === 'angle of gravity') {
-                    if (Math.abs(NumCast(this.dataDoc.review_GravityAngle) - this.selectedSolutions[i]) > epsilon) {
-                        error = true;
-                    }
-                } else if (this.selectedQuestion.answerParts[i] === 'normal force') {
-                    if (Math.abs(NumCast(this.dataDoc.review_NormalMagnitude) - this.selectedSolutions[i]) > epsilon) {
-                        error = true;
-                    }
-                } else if (this.selectedQuestion.answerParts[i] === 'angle of normal force') {
-                    if (Math.abs(NumCast(this.dataDoc.review_NormalAngle) - this.selectedSolutions[i]) > epsilon) {
-                        error = true;
-                    }
-                } else if (this.selectedQuestion.answerParts[i] === 'force of static friction') {
-                    if (Math.abs(NumCast(this.dataDoc.review_StaticMagnitude) - this.selectedSolutions[i]) > epsilon) {
-                        error = true;
-                    }
-                } else if (this.selectedQuestion.answerParts[i] === 'angle of static friction') {
-                    if (Math.abs(NumCast(this.dataDoc.review_StaticAngle) - this.selectedSolutions[i]) > epsilon) {
-                        error = true;
-                    }
-                } else if (this.selectedQuestion.answerParts[i] === 'coefficient of static friction') {
-                    if (Math.abs(NumCast(this.dataDoc.coefficientOfStaticFriction) - this.selectedSolutions[i]) > epsilon) {
-                        error = true;
-                    }
-                } else if (this.selectedQuestion.answerParts[i] === 'wedge angle') {
-                    if (Math.abs(this.wedgeAngle - this.selectedSolutions[i]) > epsilon) {
-                        error = true;
-                    }
-                }
-            }
-        }
-        if (showAlert) {
-            this.dataDoc.simulation_paused = false;
-            setTimeout(() => (this.dataDoc.simulation_paused = true), 3000);
-        }
-        if (this.selectedQuestion.goal === 'noMovement') {
-            this.dataDoc.noMovement = !error;
-        }
+    @action
+    onFlyoutPointerDown = (e: React.PointerEvent) => {
+        setupMoveUpEvents(
+            this,
+            e,
+            action(ev => {
+                this._leftMenuFlyoutWidth = Math.max(ev.clientX - 58, 0);
+                return false;
+            }),
+            () => this._leftMenuFlyoutWidth < 5 && this.closeFlyout(),
+            this.closeFlyout
+        );
     };
 
-    // Reset all review values to default
-    resetReviewValuesToDefault = () => {
-        this.dataDoc.review_GravityMagnitude = 0;
-        this.dataDoc.review_GravityAngle = 0;
-        this.dataDoc.review_NormalMagnitude = 0;
-        this.dataDoc.review_NormalAngle = 0;
-        this.dataDoc.review_StaticMagnitude = 0;
-        this.dataDoc.review_StaticAngle = 0;
-        this.dataDoc.coefficientOfKineticFriction = 0;
-        this.dataDoc.simulation_paused = true;
+    sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
+    mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
+    static addDocTabFunc_impl = (docs: Doc | Doc[], location: OpenWhere): boolean => {
+        const doc = toList(docs).lastElement();
+        const whereFields = location.split(':');
+        const keyValue = whereFields.includes(OpenWhereMod.keyvalue);
+        const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
+        const panelName = whereFields.length > 1 ? whereFields.lastElement() : '';
+        if (doc.dockingConfig && !keyValue) return DashboardView.openDashboard(doc);
+        switch (whereFields[0]) {
+            case OpenWhere.lightbox:   return LightboxView.Instance.AddDocTab(doc, location);
+            case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
+            case OpenWhere.toggle:     return CollectionDockingView.ToggleSplit(doc, whereMods, undefined, TabDocView.DontSelectOnActivate); // bcz: hack!  mark the toggle so that it won't be selected on activation- this is needed so that the backlinks menu can toggle views of targets on and off without selecting them
+            case OpenWhere.replace:    return CollectionDockingView.ReplaceTab(doc, whereMods, undefined, panelName);
+            case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, undefined, undefined, keyValue);
+        } // prettier-ignore
     };
 
-    // In review mode, reset problem variables and generate a new question
-    generateNewQuestion = () => {
-        this.resetReviewValuesToDefault();
-
-        const vars: number[] = [];
-        let question: QuestionTemplate = questions.inclinePlane[0];
-
-        if (this.simulationType === 'Inclined Plane') {
-            this.dataDoc.questionNumber = (NumCast(this.dataDoc.questionNumber) + 1) % questions.inclinePlane.length;
-            question = questions.inclinePlane[NumCast(this.dataDoc.questionNumber)];
+    @computed get flyout() {
+        return !this._leftMenuFlyoutWidth ? (
+            <div key="flyout" className="mainView-libraryFlyout-out">
+                {this.docButtons}
+            </div>
+        ) : (
+            <div key="libFlyout" className="mainView-libraryFlyout" style={{ minWidth: this._leftMenuFlyoutWidth, width: this._leftMenuFlyoutWidth }}>
+                <div className="mainView-contentArea">
+                    <DocumentView
+                        Document={this._sidebarContent.proto || this._sidebarContent}
+                        addDocument={undefined}
+                        addDocTab={DocumentViewInternal.addDocTabFunc}
+                        pinToPres={DocumentView.PinDoc}
+                        containerViewPath={returnEmptyDoclist}
+                        styleProvider={this._sidebarContent.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem || this._sidebarContent.proto === Doc.MyTrails ? DashboardStyleProvider : DefaultStyleProvider}
+                        removeDocument={returnFalse}
+                        ScreenToLocalTransform={this.mainContainerXf}
+                        PanelWidth={this.leftMenuFlyoutWidth}
+                        PanelHeight={this.leftMenuFlyoutHeight}
+                        renderDepth={0}
+                        isContentActive={returnTrue}
+                        focus={emptyFunction}
+                        whenChildContentsActiveChanged={emptyFunction}
+                        childFilters={returnEmptyFilter}
+                        childFiltersByRanges={returnEmptyFilter}
+                        searchFilterDocs={returnEmptyDoclist}
+                    />
+                </div>
+                {this.docButtons}
+            </div>
+        );
+    }
 
-            let coefficient = 0;
-            let wedge_angle = 0;
+    @computed get leftMenuPanel() {
+        return (
+            <div key="menu" className="mainView-leftMenuPanel" style={{ background: SnappingManager.userBackgroundColor, display: DocumentView.LightboxDoc() ? 'none' : undefined }}>
+                <DocumentView
+                    Document={Doc.MyLeftSidebarMenu}
+                    addDocument={undefined}
+                    addDocTab={DocumentViewInternal.addDocTabFunc}
+                    pinToPres={emptyFunction}
+                    removeDocument={returnFalse}
+                    ScreenToLocalTransform={this.sidebarScreenToLocal}
+                    PanelWidth={this.leftMenuWidth}
+                    PanelHeight={this.leftMenuHeight}
+                    renderDepth={0}
+                    containerViewPath={returnEmptyDoclist}
+                    focus={emptyFunction}
+                    styleProvider={DefaultStyleProvider}
+                    isContentActive={returnTrue}
+                    whenChildContentsActiveChanged={emptyFunction}
+                    childFilters={returnEmptyFilter}
+                    childFiltersByRanges={returnEmptyFilter}
+                    searchFilterDocs={returnEmptyDoclist}
+                />
+            </div>
+        );
+    }
 
-            for (let i = 0; i < question.variablesForQuestionSetup.length; i++) {
-                if (question.variablesForQuestionSetup[i] === 'theta - max 45') {
-                    const randValue = Math.floor(Math.random() * 44 + 1);
-                    vars.push(randValue);
-                    wedge_angle = randValue;
-                } else if (question.variablesForQuestionSetup[i] === 'coefficient of static friction') {
-                    const randValue = Math.round(Math.random() * 1000) / 1000;
-                    vars.push(randValue);
-                    coefficient = randValue;
-                }
-            }
-            this.dataDoc.wedge_angle = wedge_angle;
-            this.changeWedgeBasedOnNewAngle(wedge_angle);
-            this.dataDoc.coefficientOfStaticFriction = coefficient;
-            this.dataDoc.review_Coefficient = coefficient;
-        }
-        let q = '';
-        for (let i = 0; i < question.questionSetup.length; i++) {
-            q += question.questionSetup[i];
-            if (i !== question.questionSetup.length - 1) {
-                q += vars[i];
-                if (question.variablesForQuestionSetup[i].includes('theta')) {
-                    q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)';
-                }
+    @action
+    selectMenu = (button: Doc) => {
+        const title = StrCast(button[DocData].title);
+        const willOpen = !this._leftMenuFlyoutWidth || this._panelContent !== title;
+        this.closeFlyout();
+        if (willOpen) {
+            switch ((this._panelContent = title)) {
+                case 'Settings':
+                    SettingsManager.Instance.openMgr();
+                    break;
+                case 'Help':
+                    break;
+                default:
+                    this.expandFlyout(button);
             }
         }
-        this.dataDoc.questionVariables = new List<number>(vars);
-        this.dataDoc.selectedQuestion = JSON.stringify(question);
-        this.dataDoc.questionPartOne = q;
-        this.dataDoc.questionPartTwo = question.question;
-        this.dataDoc.answers = new List<number>(this.getAnswersToQuestion(question, vars));
-        // this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset);
+        return true;
     };
 
-    // Default setup for uniform circular motion simulation
-    @action
-    setupCircular = (value: number) => {
-        this.dataDoc.simulation_showComponentForces = false;
-        this.dataDoc.mass1_velocityYstart = 0;
-        this.dataDoc.mass1_velocityXstart = value;
-        const xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
-        const yPos = (this.yMax + this.yMin) / 2 + this.circularMotionRadius - this.mass1Radius;
-        this.dataDoc.mass1_positionYstart = yPos;
-        this.dataDoc.mass1_positionXstart = xPos;
-        const tensionForce: IForce = {
-            description: 'Centripetal Force',
-            magnitude: (this.dataDoc.mass1_velocityXstart ** 2 * this.mass1) / this.circularMotionRadius,
-            directionInDegrees: 90,
-        };
-        this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce]);
-        this.dataDoc.mass1_forcesStart = JSON.stringify([tensionForce]);
-        this._simReset++;
-    };
+    @computed get mainInnerContent() {
+        const leftMenuFlyoutWidth = this._leftMenuFlyoutWidth + this.leftMenuWidth();
+        const width = this.propertiesWidth() + leftMenuFlyoutWidth;
+        return (
+            <>
+                {this._hideUI ? null : this.leftMenuPanel}
+                <div key="inner" className="mainView-innerContent">
+                    {this.flyout}
+                    <div
+                        className="mainView-libraryHandle"
+                        style={{ background: SnappingManager.userBackgroundColor, left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }}
+                        onPointerDown={this.onFlyoutPointerDown}>
+                        <FontAwesomeIcon icon="chevron-left" color={SnappingManager.userColor} style={{ opacity: '50%' }} size="sm" />
+                    </div>
+                    <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}>
+                        {this.dockingContent}
 
-    setupInclinedPlane = () => {
-        this.changeWedgeBasedOnNewAngle(this.wedgeAngle);
-        this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
-        this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction));
-    };
+                        {this._hideUI ? null : (
+                            <div
+                                className={`mainView-propertiesDragger${this.propertiesWidth() < 10 ? '-minified' : ''}`}
+                                key="props"
+                                onPointerDown={this.onPropertiesPointerDown}
+                                style={{ background: SnappingManager.userVariantColor, right: this.propertiesWidth() - 1 }}>
+                                <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={SnappingManager.userColor} size="sm" />
+                            </div>
+                        )}
+                        <div className="properties-container" style={{ width: this.propertiesWidth(), color: SnappingManager.userColor }}>
+                            <div style={{ display: this.propertiesWidth() < 10 ? 'none' : undefined }}>
+                                <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </>
+        );
+    }
 
-    // Default setup for pendulum simulation
-    setupPendulum = () => {
-        const length = (300 * this._props.PanelWidth()) / 1000;
-        const angle = 30;
-        const x = length * Math.cos(((90 - angle) * Math.PI) / 180);
-        const y = length * Math.sin(((90 - angle) * Math.PI) / 180);
-        const xPos = this.xMax / 2 - x - this.mass1Radius;
-        const yPos = y - this.mass1Radius - 5;
-        this.dataDoc.mass1_positionXstart = xPos;
-        this.dataDoc.mass1_positionYstart = yPos;
-        const forceOfTension: IForce = {
-            description: 'Tension',
-            magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin((60 * Math.PI) / 180),
-            directionInDegrees: 90 - angle,
-        };
-        const gravityParallel: IForce = {
-            description: 'Gravity Parallel Component',
-            magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin(((90 - angle) * Math.PI) / 180),
-            directionInDegrees: -angle - 90,
-        };
-        const gravityPerpendicular: IForce = {
-            description: 'Gravity Perpendicular Component',
-            magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(((90 - angle) * Math.PI) / 180),
-            directionInDegrees: -angle,
-        };
+    @computed get mainDashboardArea() {
+        return !this.userDoc ? null : (
+            <div
+                className="mainView-dashboardArea"
+                ref={r => {
+                    r &&
+                        new _global.ResizeObserver(
+                            action(() => {
+                                this._dashUIWidth = r.getBoundingClientRect().width;
+                                this._dashUIHeight = r.getBoundingClientRect().height;
+                            })
+                        ).observe(r);
+                }}
+                style={{
+                    color: 'black',
+                    height: `calc(100% - ${this.topOfDashUI + this.topMenuHeight()}px)`,
+                    width: '100%',
+                }}>
+                {this.mainInnerContent}
+            </div>
+        );
+    }
 
-        this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]);
-        this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
-        this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
-        this.dataDoc.pendulum_angle = this.dataDoc.pendulum_angleStart = 30;
-        this.dataDoc.pendulum_length = this.dataDoc.pendulum_lengthStart = 300;
-    };
+    expandFlyout = action((button: Doc) => {
+        // bcz: What's going on here!? --- may be fixed now, so commenting out ...
+        // Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab.  All of the div below the CollectionFreeFormView's main div
+        // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be.
+        // interactively adjusting the flyout fixes the problem.  So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);)
+        this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250;
+        // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
 
-    // Default setup for spring simulation
-    @action
-    setupSpring = () => {
-        this.dataDoc.simulation_showComponentForces = false;
-        this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
-        this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
-        this.dataDoc.mass1_positionXstart = this.xMax / 2 - this.mass1Radius;
-        this.dataDoc.mass1_positionYstart = 200;
-        this.dataDoc.spring_constant = 0.5;
-        this.dataDoc.spring_lengthRest = 200;
-        this.dataDoc.spring_lengthStart = 200;
-        this._simReset++;
-    };
+        this._sidebarContent.proto = DocCast(button.target);
+        SnappingManager.SetLastPressedBtn(button[Id]);
+    });
 
-    // Default setup for suspension simulation
-    @action
-    setupSuspension = () => {
-        const xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
-        const yPos = this.yMin + 200;
-        this.dataDoc.mass1_positionYstart = yPos;
-        this.dataDoc.mass1_positionXstart = xPos;
-        this.dataDoc.mass1_positionY = this.getDisplayYPos(yPos);
-        this.dataDoc.mass1_positionX = xPos;
-        const tensionMag = (this.mass1 * Math.abs(this.gravity)) / (2 * Math.sin(Math.PI / 4));
-        const tensionForce1: IForce = {
-            description: 'Tension',
-            magnitude: tensionMag,
-            directionInDegrees: 45,
-        };
-        const tensionForce2: IForce = {
-            description: 'Tension',
-            magnitude: tensionMag,
-            directionInDegrees: 135,
-        };
-        const gravity = this.gravityForce(this.mass1);
-        this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
-        this.dataDoc.mass1_forcesStart = JSON.stringify([tensionForce1, tensionForce2, gravity]);
-        this._simReset++;
-    };
+    closeFlyout = action(() => {
+        SnappingManager.SetLastPressedBtn('');
+        this._panelContent = 'none';
+        this._sidebarContent.proto = undefined;
+        this._leftMenuFlyoutWidth = 0;
+    });
 
-    // Default setup for pulley simulation
-    @action
-    setupPulley = () => {
-        this.dataDoc.simulation_showComponentForces = false;
-        this.dataDoc.mass1_positionYstart = (this.yMax + this.yMin) / 2;
-        this.dataDoc.mass1_positionXstart = (this.xMin + this.xMax) / 2 - 2 * this.mass1Radius - 5;
-        this.dataDoc.mass1_positionY = this.getDisplayYPos((this.yMax + this.yMin) / 2);
-        this.dataDoc.mass1_positionX = (this.xMin + this.xMax) / 2 - 2 * this.mass1Radius - 5;
-        const a = (-1 * ((this.mass1 - this.mass2) * Math.abs(this.gravity))) / (this.mass1 + this.mass2);
-        const gravityForce1 = this.gravityForce(this.mass1);
-        const tensionForce1: IForce = {
-            description: 'Tension',
-            magnitude: this.mass1 * a + this.mass1 * Math.abs(this.gravity),
-            directionInDegrees: 90,
-        };
-        this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce1, tensionForce1]);
-        this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce1, tensionForce1]);
+    remButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true);
+    moveButtonDoc = (docs: Doc | Doc[], targetCol: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(docs) && addDocument(docs);
+    addButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true);
 
-        const gravityForce2 = this.gravityForce(this.mass2);
-        const tensionForce2: IForce = {
-            description: 'Tension',
-            magnitude: -this.mass2 * a + this.mass2 * Math.abs(this.gravity),
-            directionInDegrees: 90,
-        };
-        this.dataDoc.mass2_positionYstart = (this.yMax + this.yMin) / 2;
-        this.dataDoc.mass2_positionXstart = (this.xMin + this.xMax) / 2 + 5;
-        this.dataDoc.mass2_positionY = this.getDisplayYPos((this.yMax + this.yMin) / 2);
-        this.dataDoc.mass2_positionX = (this.xMin + this.xMax) / 2 + 5;
-        this.dataDoc.mass2_forcesUpdated = JSON.stringify([gravityForce2, tensionForce2]);
-        this.dataDoc.mass2_forcesStart = JSON.stringify([gravityForce2, tensionForce2]);
-        this._simReset++;
+    buttonBarXf = () => {
+        if (!this._docBtnRef.current) return Transform.Identity();
+        const { scale, translateX, translateY } = ClientUtils.GetScreenTransform(this._docBtnRef.current);
+        return new Transform(-translateX, -translateY, 1 / scale);
     };
 
-    public static parseJSON(json: string) {
-        return !json ? [] : (JSON.parse(json) as IForce[]);
+    @computed get docButtons() {
+        return !Doc.MyDockedBtns ? null : (
+            <div className="mainView-docButtons" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }} ref={this._docBtnRef}>
+                <CollectionLinearView
+                    Document={Doc.MyDockedBtns}
+                    docViewPath={returnEmptyDocViewList}
+                    fieldKey="data"
+                    dropAction={dropActionType.embed}
+                    styleProvider={DefaultStyleProvider}
+                    select={emptyFunction}
+                    isAnyChildContentActive={returnFalse}
+                    isContentActive={emptyFunction}
+                    isSelected={returnFalse}
+                    moveDocument={this.moveButtonDoc}
+                    addDocument={this.addButtonDoc}
+                    addDocTab={DocumentViewInternal.addDocTabFunc}
+                    pinToPres={emptyFunction}
+                    removeDocument={this.remButtonDoc}
+                    ScreenToLocalTransform={this.buttonBarXf}
+                    PanelWidth={this.leftMenuFlyoutWidth}
+                    PanelHeight={this.leftMenuFlyoutHeight}
+                    renderDepth={0}
+                    focus={emptyFunction}
+                    whenChildContentsActiveChanged={emptyFunction}
+                    childFilters={returnEmptyFilter}
+                    childFiltersByRanges={returnEmptyFilter}
+                    searchFilterDocs={returnEmptyDoclist}
+                />
+                {['watching', 'recording'].includes(StrCast(this.userDoc?.presentationMode)) ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : null}
+            </div>
+        );
+    }
+    @computed get snapLines() {
+        const dragged = DragManager.docsBeingDragged.lastElement() ?? DocumentView.SelectedDocs().lastElement();
+        const dragPar = dragged ? CollectionFreeFormView.from(DocumentView.getViews(dragged).lastElement()) : undefined;
+        return !dragPar?.layoutDoc.freeform_snapLines ? null : (
+            <div className="mainView-snapLines">
+                <svg style={{ width: '100%', height: '100%' }}>
+                    {[
+                        ...SnappingManager.HorizSnapLines.map((l, i) => (
+                            // eslint-disable-next-line react/no-array-index-key
+                            <line key={'horiz' + i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                        )),
+                        ...SnappingManager.VertSnapLines.map((l, i) => (
+                            // eslint-disable-next-line react/no-array-index-key
+                            <line key={'vert' + i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                        )),
+                    ]}
+                </svg>
+            </div>
+        );
+    }
+
+    @computed get inkResources() {
+        return (
+            <svg width={0} height={0}>
+                <defs>
+                    <filter id="inkSelectionHalo">
+                        <feColorMatrix
+                            type="matrix"
+                            result="color"
+                            values="1 0 0 0 0
+                0 0 0 0 0
+                0 0 0 0 0
+                0 0 0 1 0"
+                        />
+                        <feGaussianBlur in="color" stdDeviation="4" result="blur" />
+                        <feOffset in="blur" dx="0" dy="0" result="offset" />
+                        <feMerge>
+                            <feMergeNode in="bg" />
+                            <feMergeNode in="offset" />
+                            <feMergeNode in="SourceGraphic" />
+                        </feMerge>
+                    </filter>
+                </defs>
+            </svg>
+        );
     }
 
-    // Handle force change in review mode
-    updateReviewModeValues = () => {
-        const forceOfGravityReview: IForce = {
-            description: 'Gravity',
-            magnitude: NumCast(this.dataDoc.review_GravityMagnitude),
-            directionInDegrees: NumCast(this.dataDoc.review_GravityAngle),
-        };
-        const normalForceReview: IForce = {
-            description: 'Normal Force',
-            magnitude: NumCast(this.dataDoc.review_NormalMagnitude),
-            directionInDegrees: NumCast(this.dataDoc.review_NormalAngle),
-        };
-        const staticFrictionForceReview: IForce = {
-            description: 'Static Friction Force',
-            magnitude: NumCast(this.dataDoc.review_StaticMagnitude),
-            directionInDegrees: NumCast(this.dataDoc.review_StaticAngle),
-        };
-        this.dataDoc.mass1_forcesStart = JSON.stringify([forceOfGravityReview, normalForceReview, staticFrictionForceReview]);
-        this.dataDoc.mass1_forcesUpdated = JSON.stringify([forceOfGravityReview, normalForceReview, staticFrictionForceReview]);
+    togglePropertiesFlyout = () => {
+        if (MainView.Instance.propertiesWidth() > 0) {
+            SnappingManager.SetPropertiesWidth(0);
+        } else {
+            SnappingManager.SetPropertiesWidth(300);
+        }
     };
 
-    pause = () => (this.dataDoc.simulation_paused = true);
-    componentForces1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_componentForces));
-    setComponentForces1 = (forces: IForce[]) => (this.dataDoc.mass1_componentForces = JSON.stringify(forces));
-    componentForces2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_componentForces));
-    setComponentForces2 = (forces: IForce[]) => (this.dataDoc.mass2_componentForces = JSON.stringify(forces));
-    startForces1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_forcesStart));
-    startForces2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_forcesStart));
-    forcesUpdated1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_forcesUpdated));
-    setForcesUpdated1 = (forces: IForce[]) => (this.dataDoc.mass1_forcesUpdated = JSON.stringify(forces));
-    forcesUpdated2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_forcesUpdated));
-    setForcesUpdated2 = (forces: IForce[]) => (this.dataDoc.mass2_forcesUpdated = JSON.stringify(forces));
-    setPosition1 = (xPos: number | undefined, yPos: number | undefined) => {
-        yPos !== undefined && (this.dataDoc.mass1_positionY = Math.round(yPos * 100) / 100);
-        xPos !== undefined && (this.dataDoc.mass1_positionX = Math.round(xPos * 100) / 100);
-    };
-    setPosition2 = (xPos: number | undefined, yPos: number | undefined) => {
-        yPos !== undefined && (this.dataDoc.mass2_positionY = Math.round(yPos * 100) / 100);
-        xPos !== undefined && (this.dataDoc.mass2_positionX = Math.round(xPos * 100) / 100);
-    };
-    setVelocity1 = (xVel: number | undefined, yVel: number | undefined) => {
-        yVel !== undefined && (this.dataDoc.mass1_velocityY = (-1 * Math.round(yVel * 100)) / 100);
-        xVel !== undefined && (this.dataDoc.mass1_velocityX = Math.round(xVel * 100) / 100);
-    };
-    setVelocity2 = (xVel: number | undefined, yVel: number | undefined) => {
-        yVel !== undefined && (this.dataDoc.mass2_velocityY = (-1 * Math.round(yVel * 100)) / 100);
-        xVel !== undefined && (this.dataDoc.mass2_velocityX = Math.round(xVel * 100) / 100);
-    };
-    setAcceleration1 = (xAccel: number, yAccel: number) => {
-        this.dataDoc.mass1_accelerationY = yAccel;
-        this.dataDoc.mass1_accelerationX = xAccel;
-    };
-    setAcceleration2 = (xAccel: number, yAccel: number) => {
-        this.dataDoc.mass2_accelerationY = yAccel;
-        this.dataDoc.mass2_accelerationX = xAccel;
-    };
-    setPendulumAngle = (angle: number | undefined, length: number | undefined) => {
-        angle !== undefined && (this.dataDoc.pendulum_angle = angle);
-        length !== undefined && (this.dataDoc.pendulum_length = length);
-    };
-    setSpringLength = (length: number) => {
-        this.dataDoc.spring_lengthStart = length;
-    };
-    resetRequest = () => this._simReset;
+    lightboxMaxBorder = [200, 50];
     render() {
-        const commonWeightProps = {
-            pause: this.pause,
-            paused: BoolCast(this.dataDoc.simulation_paused),
-            panelWidth: this._props.PanelWidth,
-            panelHeight: this._props.PanelHeight,
-            resetRequest: this.resetRequest,
-            xMax: this.xMax,
-            xMin: this.xMin,
-            yMax: this.yMax,
-            yMin: this.yMin,
-            wallPositions: this.wallPositions,
-            gravity: Math.abs(this.gravity),
-            timestepSize: 0.05,
-            showComponentForces: BoolCast(this.dataDoc.simulation_showComponentForces),
-            coefficientOfKineticFriction: NumCast(this.dataDoc.coefficientOfKineticFriction),
-            elasticCollisions: BoolCast(this.dataDoc.elasticCollisions),
-            simulationMode: this.simulationMode,
-            noMovement: BoolCast(this.dataDoc.noMovement),
-            circularMotionRadius: this.circularMotionRadius,
-            wedgeHeight: this.wedgeHeight,
-            wedgeWidth: this.wedgeWidth,
-            springConstant: this.springConstant,
-            springStartLength: this.springLengthStart,
-            springRestLength: this.springLengthRest,
-            setSpringLength: this.setSpringLength,
-            setPendulumAngle: this.setPendulumAngle,
-            pendulumAngle: this.pendulumAngle,
-            pendulumLength: this.pendulumLength,
-            startPendulumAngle: this.pendulumAngleStart,
-            startPendulumLength: this.pendulumLengthStart,
-            radius: this.mass1Radius,
-            simulationSpeed: NumCast(this.dataDoc.simulation_speed, 2),
-            showAcceleration: BoolCast(this.dataDoc.simulation_showAcceleration),
-            showForceMagnitudes: BoolCast(this.dataDoc.simulation_showForceMagnitudes),
-            showForces: BoolCast(this.dataDoc.simulation_showForces),
-            showVelocity: BoolCast(this.dataDoc.simulation_showVelocity),
-            simulationType: this.simulationType,
-        };
         return (
-            <div className="physicsSimApp">
-                <div className="mechanicsSimulationContainer">
-                    <div className="mechanicsSimulationContentContainer">
-                        <div className="mechanicsSimulationButtonsAndElements">
-                            <div className="mechanicsSimulationButtons">
-                                {!this.dataDoc.simulation_paused && (
-                                    <div
-                                        style={{
-                                            position: 'fixed',
-                                            left: 0.1 * this._props.PanelWidth() + 'px',
-                                            top: 0.95 * this._props.PanelHeight() + 'px',
-                                            width: 0.5 * this._props.PanelWidth() + 'px',
-                                        }}>
-                                        <LinearProgress />
-                                    </div>
-                                )}
-                            </div>
-                            <div
-                                className="mechanicsSimulationElements"
-                                style={{
-                                    //
-                                    width: '60%',
-                                    height: '100%',
-                                    position: 'absolute',
-                                    background: 'yellow',
-                                }}>
-                                <Weight
-                                    {...commonWeightProps}
-                                    color="red"
-                                    componentForces={this.componentForces1}
-                                    setComponentForces={this.setComponentForces1}
-                                    displayXVelocity={NumCast(this.dataDoc.mass1_velocityX)}
-                                    displayYVelocity={NumCast(this.dataDoc.mass1_velocityY)}
-                                    mass={this.mass1}
-                                    startForces={this.startForces1}
-                                    startPosX={this.mass1PosXStart}
-                                    startPosY={this.mass1PosYStart}
-                                    startVelX={this.mass1VelXStart}
-                                    startVelY={this.mass1VelYStart}
-                                    updateMassPosX={NumCast(this.dataDoc.mass1_xChange)}
-                                    updateMassPosY={NumCast(this.dataDoc.mass1_yChange)}
-                                    forcesUpdated={this.forcesUpdated1}
-                                    setForcesUpdated={this.setForcesUpdated1}
-                                    setPosition={this.setPosition1}
-                                    setVelocity={this.setVelocity1}
-                                    setAcceleration={this.setAcceleration1}
-                                />
-                                {this.simulationType === 'Pulley' && (
-                                    <Weight
-                                        {...commonWeightProps}
-                                        color="green"
-                                        componentForces={this.componentForces2}
-                                        setComponentForces={this.setComponentForces2}
-                                        displayXVelocity={NumCast(this.dataDoc.mass2_velocityX)}
-                                        displayYVelocity={NumCast(this.dataDoc.mass2_velocityY)}
-                                        mass={this.mass2}
-                                        startForces={this.startForces2}
-                                        startPosX={this.mass2PosXStart}
-                                        startPosY={this.mass2PosYStart}
-                                        startVelX={this.mass2VelXStart}
-                                        startVelY={this.mass2VelYStart}
-                                        updateMassPosX={NumCast(this.dataDoc.mass2_xChange)}
-                                        updateMassPosY={NumCast(this.dataDoc.mass2_yChange)}
-                                        forcesUpdated={this.forcesUpdated2}
-                                        setForcesUpdated={this.setForcesUpdated2}
-                                        setPosition={this.setPosition2}
-                                        setVelocity={this.setVelocity2}
-                                        setAcceleration={this.setAcceleration2}
-                                    />
-                                )}
-                            </div>
-                            <div style={{ position: 'absolute', transformOrigin: 'top left', top: 0, left: 0, width: '100%', height: '100%' }}>
-                                {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane') &&
-                                    this.wallPositions?.map((element, index) => <Wall key={index} length={element.length} xPos={element.xPos} yPos={element.yPos} angleInDegrees={element.angleInDegrees} />)}
-                            </div>
-                        </div>
-                    </div>
-                    <div
-                        className="mechanicsSimulationEquationContainer"
-                        onWheel={e => this._props.isContentActive() && e.stopPropagation()}
-                        style={{ overflow: 'auto', height: `${Math.max(1, 800 / this._props.PanelWidth()) * 100}%`, transform: `scale(${Math.min(1, this._props.PanelWidth() / 850)})` }}>
-                        <div className="mechanicsSimulationControls">
-                            <Stack direction="row" spacing={1}>
-                                {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
-                                    <IconButton onClick={() => (this.dataDoc.simulation_paused = false)}>
-                                        <PlayArrowIcon />
-                                    </IconButton>
-                                )}
-                                {!this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
-                                    <IconButton onClick={() => (this.dataDoc.simulation_paused = true)}>
-                                        <PauseIcon />
-                                    </IconButton>
-                                )}
-                                {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
-                                    <IconButton onClick={action(() => this._simReset++)}>
-                                        <ReplayIcon />
-                                    </IconButton>
-                                )}
-                            </Stack>
-                            <div className="dropdownMenu">
-                                <select
-                                    value={StrCast(this.simulationType)}
-                                    onChange={event => {
-                                        this.dataDoc.simulation_type = event.target.value;
-                                        this.setupSimulation();
-                                    }}
-                                    style={{ height: '2em', width: '100%', fontSize: '16px' }}>
-                                    <option value="One Weight">Projectile</option>
-                                    <option value="Inclined Plane">Inclined Plane</option>
-                                    <option value="Pendulum">Pendulum</option>
-                                    <option value="Spring">Spring</option>
-                                    <option value="Circular Motion">Circular Motion</option>
-                                    <option value="Pulley">Pulley</option>
-                                    <option value="Suspension">Suspension</option>
-                                </select>
-                            </div>
-                            <div className="dropdownMenu">
-                                <select
-                                    value={this.simulationMode}
-                                    onChange={event => {
-                                        this.dataDoc.simulation_mode = event.target.value;
-                                        this.setupSimulation();
-                                    }}
-                                    style={{ height: '2em', width: '100%', fontSize: '16px' }}>
-                                    <option value="Tutorial">Tutorial Mode</option>
-                                    <option value="Freeform">Freeform Mode</option>
-                                    <option value="Review">Review Mode</option>
-                                </select>
-                            </div>
-                        </div>
-                        {this.simulationMode === 'Review' && this.simulationType !== 'Inclined Plane' && (
-                            <div className="wordProblemBox">
-                                <p>{this.simulationType} review problems in progress!</p>
-                                <hr />
-                            </div>
-                        )}
-                        {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && (
-                            <div>
-                                {!this.dataDoc.hintDialogueOpen && (
-                                    <IconButton
-                                        onClick={() => (this.dataDoc.hintDialogueOpen = true)}
-                                        sx={{
-                                            position: 'fixed',
-                                            left: this.xMax - 50 + 'px',
-                                            top: this.yMin + 14 + 'px',
-                                        }}>
-                                        <QuestionMarkIcon />
-                                    </IconButton>
-                                )}
-                                <Dialog maxWidth="sm" fullWidth open={BoolCast(this.dataDoc.hintDialogueOpen)} onClose={() => (this.dataDoc.hintDialogueOpen = false)}>
-                                    <DialogTitle>Hints</DialogTitle>
-                                    <DialogContent>
-                                        {this.selectedQuestion.hints?.map((hint: any, index: number) => (
-                                            <div key={index}>
-                                                <DialogContentText>
-                                                    <details>
-                                                        <summary>
-                                                            <b>
-                                                                Hint {index + 1}: {hint.description}
-                                                            </b>
-                                                        </summary>
-                                                        {hint.content}
-                                                    </details>
-                                                </DialogContentText>
-                                            </div>
-                                        ))}
-                                    </DialogContent>
-                                    <DialogActions>
-                                        <Button onClick={() => (this.dataDoc.hintDialogueOpen = false)}>Close</Button>
-                                    </DialogActions>
-                                </Dialog>
-                                <div className="wordProblemBox">
-                                    <div className="question">
-                                        <p>{this.questionPartOne}</p>
-                                        <p>{this.questionPartTwo}</p>
-                                    </div>
-                                    <div className="answers">
-                                        {this.selectedQuestion.answerParts.includes('force of gravity') && (
-                                            <InputField
-                                                label={<p>Gravity magnitude</p>}
-                                                lowerBound={0}
-                                                dataDoc={this.dataDoc}
-                                                prop="review_GravityMagnitude"
-                                                step={0.1}
-                                                unit="N"
-                                                upperBound={50}
-                                                value={NumCast(this.dataDoc.review_GravityMagnitude)}
-                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
-                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of gravity')]}
-                                                labelWidth="7em"
-                                            />
-                                        )}
-                                        {this.selectedQuestion.answerParts.includes('angle of gravity') && (
-                                            <InputField
-                                                label={<p>Gravity angle</p>}
-                                                lowerBound={0}
-                                                dataDoc={this.dataDoc}
-                                                prop="review_GravityAngle"
-                                                step={1}
-                                                unit="°"
-                                                upperBound={360}
-                                                value={NumCast(this.dataDoc.review_GravityAngle)}
-                                                radianEquivalent
-                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
-                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of gravity')]}
-                                                labelWidth="7em"
-                                            />
-                                        )}
-                                        {this.selectedQuestion.answerParts.includes('normal force') && (
-                                            <InputField
-                                                label={<p>Normal force magnitude</p>}
-                                                lowerBound={0}
-                                                dataDoc={this.dataDoc}
-                                                prop="review_NormalMagnitude"
-                                                step={0.1}
-                                                unit="N"
-                                                upperBound={50}
-                                                value={NumCast(this.dataDoc.review_NormalMagnitude)}
-                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
-                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('normal force')]}
-                                                labelWidth="7em"
-                                            />
-                                        )}
-                                        {this.selectedQuestion.answerParts.includes('angle of normal force') && (
-                                            <InputField
-                                                label={<p>Normal force angle</p>}
-                                                lowerBound={0}
-                                                dataDoc={this.dataDoc}
-                                                prop="review_NormalAngle"
-                                                step={1}
-                                                unit="°"
-                                                upperBound={360}
-                                                value={NumCast(this.dataDoc.review_NormalAngle)}
-                                                radianEquivalent
-                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
-                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of normal force')]}
-                                                labelWidth="7em"
-                                            />
-                                        )}
-                                        {this.selectedQuestion.answerParts.includes('force of static friction') && (
-                                            <InputField
-                                                label={<p>Static friction magnitude</p>}
-                                                lowerBound={0}
-                                                dataDoc={this.dataDoc}
-                                                prop="review_StaticMagnitude"
-                                                step={0.1}
-                                                unit="N"
-                                                upperBound={50}
-                                                value={NumCast(this.dataDoc.review_StaticMagnitude)}
-                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
-                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of static friction')]}
-                                                labelWidth="7em"
-                                            />
-                                        )}
-                                        {this.selectedQuestion.answerParts.includes('angle of static friction') && (
-                                            <InputField
-                                                label={<p>Static friction angle</p>}
-                                                lowerBound={0}
-                                                dataDoc={this.dataDoc}
-                                                prop="review_StaticAngle"
-                                                step={1}
-                                                unit="°"
-                                                upperBound={360}
-                                                value={NumCast(this.dataDoc.review_StaticAngle)}
-                                                radianEquivalent
-                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
-                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of static friction')]}
-                                                labelWidth="7em"
-                                            />
-                                        )}
-                                        {this.selectedQuestion.answerParts.includes('coefficient of static friction') && (
-                                            <InputField
-                                                label={
-                                                    <Box>
-                                                        &mu;<sub>s</sub>
-                                                    </Box>
-                                                }
-                                                lowerBound={0}
-                                                dataDoc={this.dataDoc}
-                                                prop="coefficientOfStaticFriction"
-                                                step={0.1}
-                                                unit=""
-                                                upperBound={1}
-                                                value={NumCast(this.dataDoc.coefficientOfStaticFriction)}
-                                                effect={this.updateReviewForcesBasedOnCoefficient}
-                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
-                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('coefficient of static friction')]}
-                                            />
-                                        )}
-                                        {this.selectedQuestion.answerParts.includes('wedge angle') && (
-                                            <InputField
-                                                label={<Box>&theta;</Box>}
-                                                lowerBound={0}
-                                                dataDoc={this.dataDoc}
-                                                prop="wedge_angle"
-                                                step={1}
-                                                unit="°"
-                                                upperBound={49}
-                                                value={this.wedgeAngle}
-                                                effect={(val: number) => {
-                                                    this.changeWedgeBasedOnNewAngle(val);
-                                                    this.updateReviewForcesBasedOnAngle(val);
-                                                }}
-                                                radianEquivalent
-                                                showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
-                                                correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('wedge angle')]}
-                                            />
-                                        )}
-                                    </div>
-                                </div>
-                            </div>
-                        )}
-                        {this.simulationMode === 'Tutorial' && (
-                            <div className="wordProblemBox">
-                                <div className="question">
-                                    <h2>Problem</h2>
-                                    <p>{this.tutorial.question}</p>
-                                </div>
-                                <div
-                                    style={{
-                                        display: 'flex',
-                                        justifyContent: 'spaceBetween',
-                                        width: '100%',
-                                    }}>
-                                    <IconButton
-                                        onClick={() => {
-                                            let step = NumCast(this.dataDoc.tutorial_stepNumber) - 1;
-                                            step = Math.max(step, 0);
-                                            step = Math.min(step, this.tutorial.steps.length - 1);
-                                            this.dataDoc.tutorial_stepNumber = step;
-                                            this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces);
-                                            this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces);
-                                            this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
-                                        }}
-                                        disabled={this.dataDoc.tutorial_stepNumber === 0}>
-                                        <ArrowLeftIcon />
-                                    </IconButton>
-                                    <div>
-                                        <h3>
-                                            Step {NumCast(this.dataDoc.tutorial_stepNumber) + 1}: {this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].description}
-                                        </h3>
-                                        <p>{this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].content}</p>
-                                    </div>
-                                    <IconButton
-                                        onClick={() => {
-                                            let step = NumCast(this.dataDoc.tutorial_stepNumber) + 1;
-                                            step = Math.max(step, 0);
-                                            step = Math.min(step, this.tutorial.steps.length - 1);
-                                            this.dataDoc.tutorial_stepNumber = step;
-                                            this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces);
-                                            this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces);
-                                            this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
-                                        }}
-                                        disabled={this.dataDoc.tutorial_stepNumber === this.tutorial.steps.length - 1}>
-                                        <ArrowRightIcon />
-                                    </IconButton>
-                                </div>
-                                <div>
-                                    {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && <p>Resources</p>}
-                                    {this.simulationType === 'One Weight' && (
-                                        <ul>
-                                            <li>
-                                                <a
-                                                    href="https://www.khanacademy.org/science/physics/one-dimensional-motion"
-                                                    target="_blank"
-                                                    rel="noreferrer"
-                                                    style={{
-                                                        color: 'blue',
-                                                        textDecoration: 'underline',
-                                                    }}>
-                                                    Khan Academy - One Dimensional Motion
-                                                </a>
-                                            </li>
-                                            <li>
-                                                <a
-                                                    href="https://www.khanacademy.org/science/physics/two-dimensional-motion"
-                                                    target="_blank"
-                                                    rel="noreferrer"
-                                                    style={{
-                                                        color: 'blue',
-                                                        textDecoration: 'underline',
-                                                    }}>
-                                                    Khan Academy - Two Dimensional Motion
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    )}
-                                    {this.simulationType === 'Inclined Plane' && (
-                                        <ul>
-                                            <li>
-                                                <a
-                                                    href="https://www.khanacademy.org/science/physics/forces-newtons-laws#normal-contact-force"
-                                                    target="_blank"
-                                                    rel="noreferrer"
-                                                    style={{
-                                                        color: 'blue',
-                                                        textDecoration: 'underline',
-                                                    }}>
-                                                    Khan Academy - Normal Force
-                                                </a>
-                                            </li>
-                                            <li>
-                                                <a
-                                                    href="https://www.khanacademy.org/science/physics/forces-newtons-laws#inclined-planes-friction"
-                                                    target="_blank"
-                                                    rel="noreferrer"
-                                                    style={{
-                                                        color: 'blue',
-                                                        textDecoration: 'underline',
-                                                    }}>
-                                                    Khan Academy - Inclined Planes
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    )}
-                                    {this.simulationType === 'Pendulum' && (
-                                        <ul>
-                                            <li>
-                                                <a
-                                                    href="https://www.khanacademy.org/science/physics/forces-newtons-laws#tension-tutorial"
-                                                    target="_blank"
-                                                    rel="noreferrer"
-                                                    style={{
-                                                        color: 'blue',
-                                                        textDecoration: 'underline',
-                                                    }}>
-                                                    Khan Academy - Tension
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    )}
-                                </div>
-                            </div>
-                        )}
-                        {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && (
-                            <div
-                                style={{
-                                    display: 'flex',
-                                    justifyContent: 'space-between',
-                                    marginTop: '10px',
-                                }}>
-                                <p
-                                    style={{
-                                        color: 'blue',
-                                        textDecoration: 'underline',
-                                        cursor: 'pointer',
-                                    }}
-                                    onClick={() => (this.dataDoc.simulation_mode = 'Tutorial')}>
-                                    {' '}
-                                    Go to walkthrough{' '}
-                                </p>
-                                <div style={{ display: 'flex', flexDirection: 'column' }}>
-                                    <Button
-                                        onClick={action(() => {
-                                            this._simReset++;
-                                            this.checkAnswers();
-                                            this.dataDoc.simulation_showIcon = true;
-                                        })}
-                                        variant="outlined">
-                                        <p>Submit</p>
-                                    </Button>
-                                    <Button
-                                        onClick={() => {
-                                            this.generateNewQuestion();
-                                            this.dataDoc.simulation_showIcon = false;
-                                        }}
-                                        variant="outlined">
-                                        <p>New question</p>
-                                    </Button>
-                                </div>
-                            </div>
-                        )}
-                        {this.simulationMode === 'Freeform' && (
-                            <div className="vars">
-                                <FormControl component="fieldset">
-                                    <FormGroup>
-                                        {this.simulationType === 'One Weight' && (
-                                            <FormControlLabel
-                                                control={<Checkbox checked={BoolCast(this.dataDoc.elasticCollisions)} onChange={() => (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />}
-                                                label="Make collisions elastic"
-                                                labelPlacement="start"
-                                            />
-                                        )}
-                                        <FormControlLabel
-                                            control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showForces)} onChange={() => (this.dataDoc.simulation_showForces = !this.dataDoc.simulation_showForces)} />}
-                                            label="Show force vectors"
-                                            labelPlacement="start"
-                                        />
-                                        {(this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && (
-                                            <FormControlLabel
-                                                control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showComponentForces)} onChange={() => (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />}
-                                                label="Show component force vectors"
-                                                labelPlacement="start"
-                                            />
-                                        )}
-                                        <FormControlLabel
-                                            control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showAcceleration)} onChange={() => (this.dataDoc.simulation_showAcceleration = !this.dataDoc.simulation_showAcceleration)} />}
-                                            label="Show acceleration vector"
-                                            labelPlacement="start"
-                                        />
-                                        <FormControlLabel
-                                            control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showVelocity)} onChange={() => (this.dataDoc.simulation_showVelocity = !this.dataDoc.simulation_showVelocity)} />}
-                                            label="Show velocity vector"
-                                            labelPlacement="start"
-                                        />
-                                        <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit="x" upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth="5em" />
-                                        {this.dataDoc.simulation_paused && this.simulationType !== 'Circular Motion' && (
-                                            <InputField
-                                                label={<Box>Gravity</Box>}
-                                                lowerBound={-30}
-                                                dataDoc={this.dataDoc}
-                                                prop="gravity"
-                                                step={0.01}
-                                                unit="m/s2"
-                                                upperBound={0}
-                                                value={NumCast(this.dataDoc.simulation_gravity, -9.81)}
-                                                effect={(val: number) => this.setupSimulation()}
-                                                labelWidth="5em"
-                                            />
-                                        )}
-                                        {this.dataDoc.simulation_paused && this.simulationType !== 'Pulley' && (
-                                            <InputField
-                                                label={<Box>Mass</Box>}
-                                                lowerBound={1}
-                                                dataDoc={this.dataDoc}
-                                                prop="mass1"
-                                                step={0.1}
-                                                unit="kg"
-                                                upperBound={5}
-                                                value={this.mass1 ?? 1}
-                                                effect={(val: number) => this.setupSimulation()}
-                                                labelWidth="5em"
-                                            />
-                                        )}
-                                        {this.dataDoc.simulation_paused && this.simulationType === 'Pulley' && (
-                                            <InputField
-                                                label={<Box>Red mass</Box>}
-                                                lowerBound={1}
-                                                dataDoc={this.dataDoc}
-                                                prop="mass1"
-                                                step={0.1}
-                                                unit="kg"
-                                                upperBound={5}
-                                                value={this.mass1 ?? 1}
-                                                effect={(val: number) => this.setupSimulation()}
-                                                labelWidth="5em"
-                                            />
-                                        )}
-                                        {this.dataDoc.simulation_paused && this.simulationType === 'Pulley' && (
-                                            <InputField
-                                                label={<Box>Blue mass</Box>}
-                                                lowerBound={1}
-                                                dataDoc={this.dataDoc}
-                                                prop="mass2"
-                                                step={0.1}
-                                                unit="kg"
-                                                upperBound={5}
-                                                value={this.mass2 ?? 1}
-                                                effect={(val: number) => this.setupSimulation()}
-                                                labelWidth="5em"
-                                            />
-                                        )}
-                                        {this.dataDoc.simulation_paused && this.simulationType === 'Circular Motion' && (
-                                            <InputField
-                                                label={<Box>Rod length</Box>}
-                                                lowerBound={100}
-                                                dataDoc={this.dataDoc}
-                                                prop="circularMotionRadius"
-                                                step={5}
-                                                unit="m"
-                                                upperBound={250}
-                                                value={this.circularMotionRadius}
-                                                effect={(val: number) => this.setupSimulation()}
-                                                labelWidth="5em"
-                                            />
-                                        )}
-                                    </FormGroup>
-                                </FormControl>
-                                {this.simulationType === 'Spring' && this.dataDoc.simulation_paused && (
-                                    <div>
-                                        <InputField
-                                            label={<Typography color="inherit">Spring stiffness</Typography>}
-                                            lowerBound={0.1}
-                                            dataDoc={this.dataDoc}
-                                            prop="spring_constant"
-                                            step={1}
-                                            unit="N/m"
-                                            upperBound={500}
-                                            value={this.springConstant}
-                                            effect={action(() => this._simReset++)}
-                                            radianEquivalent={false}
-                                            mode="Freeform"
-                                            labelWidth="7em"
-                                        />
-                                        <InputField
-                                            label={<Typography color="inherit">Rest length</Typography>}
-                                            lowerBound={10}
-                                            dataDoc={this.dataDoc}
-                                            prop="spring_lengthRest"
-                                            step={100}
-                                            unit=""
-                                            upperBound={500}
-                                            value={this.springLengthRest}
-                                            effect={action(() => this._simReset++)}
-                                            radianEquivalent={false}
-                                            mode="Freeform"
-                                            labelWidth="7em"
-                                        />
-                                        <InputField
-                                            label={<Typography color="inherit">Starting displacement</Typography>}
-                                            lowerBound={-(this.springLengthRest - 10)}
-                                            dataDoc={this.dataDoc}
-                                            prop=""
-                                            step={10}
-                                            unit=""
-                                            upperBound={this.springLengthRest}
-                                            value={this.springLengthStart - this.springLengthRest}
-                                            effect={action((val: number) => {
-                                                this.dataDoc.mass1_positionYstart = this.springLengthRest + val;
-                                                this.dataDoc.spring_lengthStart = this.springLengthRest + val;
-                                                this._simReset++;
-                                            })}
-                                            radianEquivalent={false}
-                                            mode="Freeform"
-                                            labelWidth="7em"
-                                        />
-                                    </div>
-                                )}
-                                {this.simulationType === 'Inclined Plane' && this.dataDoc.simulation_paused && (
-                                    <div>
-                                        <InputField
-                                            label={<Box>&theta;</Box>}
-                                            lowerBound={0}
-                                            dataDoc={this.dataDoc}
-                                            prop="wedge_angle"
-                                            step={1}
-                                            unit="°"
-                                            upperBound={49}
-                                            value={this.wedgeAngle}
-                                            effect={action((val: number) => {
-                                                this.changeWedgeBasedOnNewAngle(val);
-                                                this._simReset++;
-                                            })}
-                                            radianEquivalent
-                                            mode="Freeform"
-                                            labelWidth="2em"
-                                        />
-                                        <InputField
-                                            label={
-                                                <Box>
-                                                    &mu;<sub>s</sub>
-                                                </Box>
-                                            }
-                                            lowerBound={0}
-                                            dataDoc={this.dataDoc}
-                                            prop="coefficientOfStaticFriction"
-                                            step={0.1}
-                                            unit=""
-                                            upperBound={1}
-                                            value={NumCast(this.dataDoc.coefficientOfStaticFriction) ?? 0}
-                                            effect={action((val: number) => {
-                                                this.updateForcesWithFriction(val);
-                                                if (val < NumCast(this.dataDoc.coefficientOfKineticFriction)) {
-                                                    this.dataDoc.soefficientOfKineticFriction = val;
-                                                }
-                                                this._simReset++;
-                                            })}
-                                            mode="Freeform"
-                                            labelWidth="2em"
-                                        />
-                                        <InputField
-                                            label={
-                                                <Box>
-                                                    &mu;<sub>k</sub>
-                                                </Box>
-                                            }
-                                            lowerBound={0}
-                                            dataDoc={this.dataDoc}
-                                            prop="coefficientOfKineticFriction"
-                                            step={0.1}
-                                            unit=""
-                                            upperBound={NumCast(this.dataDoc.coefficientOfStaticFriction)}
-                                            value={NumCast(this.dataDoc.coefficientOfKineticFriction) ?? 0}
-                                            effect={action(() => this._simReset++)}
-                                            mode="Freeform"
-                                            labelWidth="2em"
-                                        />
-                                    </div>
-                                )}
-                                {this.simulationType === 'Inclined Plane' && !this.dataDoc.simulation_paused && (
-                                    <Typography>
-                                        <>
-                                            &theta;: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad
-                                            <br />
-                                            &mu; <sub>s</sub>: {this.dataDoc.coefficientOfStaticFriction}
-                                            <br />
-                                            &mu; <sub>k</sub>: {this.dataDoc.coefficientOfKineticFriction}
-                                        </>
-                                    </Typography>
-                                )}
-                                {this.simulationType === 'Pendulum' && !this.dataDoc.simulation_paused && (
-                                    <Typography>
-                                        &theta;: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad
-                                    </Typography>
-                                )}
-                                {this.simulationType === 'Pendulum' && this.dataDoc.simulation_paused && (
-                                    <div>
-                                        <InputField
-                                            label={<Box>Angle</Box>}
-                                            lowerBound={0}
-                                            dataDoc={this.dataDoc}
-                                            prop="pendulum_angle"
-                                            step={1}
-                                            unit="°"
-                                            upperBound={59}
-                                            value={NumCast(this.dataDoc.pendulum_angle, 30)}
-                                            effect={action(value => {
-                                                this.dataDoc.pendulum_angleStart = value;
-                                                this.dataDoc.pendulum_lengthStart = this.dataDoc.pendulum_length;
-                                                if (this.simulationType === 'Pendulum') {
-                                                    const mag = this.mass1 * Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180);
-
-                                                    const forceOfTension: IForce = {
-                                                        description: 'Tension',
-                                                        magnitude: mag,
-                                                        directionInDegrees: 90 - value,
-                                                    };
-                                                    const gravityParallel: IForce = {
-                                                        description: 'Gravity Parallel Component',
-                                                        magnitude: Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180),
-                                                        directionInDegrees: 270 - value,
-                                                    };
-                                                    const gravityPerpendicular: IForce = {
-                                                        description: 'Gravity Perpendicular Component',
-                                                        magnitude: Math.abs(this.gravity) * Math.sin((value * Math.PI) / 180),
-                                                        directionInDegrees: -value,
-                                                    };
-
-                                                    const length = this.pendulumLength;
-                                                    const x = length * Math.cos(((90 - value) * Math.PI) / 180);
-                                                    const y = length * Math.sin(((90 - value) * Math.PI) / 180);
-                                                    const xPos = this.xMax / 2 - x - NumCast(this.dataDoc.radius);
-                                                    const yPos = y - NumCast(this.dataDoc.radius) - 5;
-                                                    this.dataDoc.mass1_positionXstart = xPos;
-                                                    this.dataDoc.mass1_positionYstart = yPos;
-
-                                                    this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
-                                                    this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
-                                                    this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]);
-                                                    this._simReset++;
-                                                }
-                                            })}
-                                            radianEquivalent
-                                            mode="Freeform"
-                                            labelWidth="5em"
-                                        />
-                                        <InputField
-                                            label={<Box>Rod length</Box>}
-                                            lowerBound={0}
-                                            dataDoc={this.dataDoc}
-                                            prop="pendulum_length"
-                                            step={1}
-                                            unit="m"
-                                            upperBound={400}
-                                            value={Math.round(this.pendulumLength)}
-                                            effect={action(value => {
-                                                if (this.simulationType === 'Pendulum') {
-                                                    this.dataDoc.pendulum_angleStart = this.pendulumAngle;
-                                                    this.dataDoc.pendulum_lengthStart = value;
-                                                    this._simReset++;
-                                                }
-                                            })}
-                                            radianEquivalent={false}
-                                            mode="Freeform"
-                                            labelWidth="5em"
-                                        />
-                                    </div>
-                                )}
-                            </div>
-                        )}
-                        <div className="mechanicsSimulationEquation">
-                            {this.simulationMode === 'Freeform' && (
-                                <table>
-                                    <tbody>
-                                        <tr>
-                                            <td>{this.simulationType === 'Pulley' ? 'Red Weight' : ''}</td>
-                                            <td>X</td>
-                                            <td>Y</td>
-                                        </tr>
-                                        <tr>
-                                            <td
-                                                style={{ cursor: 'help' }}
-                                                // onClick={() => {
-                                                //   window.open(
-                                                //     "https://www.khanacademy.org/science/physics/two-dimensional-motion"
-                                                //   );
-                                                // }}
-                                            >
-                                                <Box>Position</Box>
-                                            </td>
-                                            {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && (
-                                                <td style={{ cursor: 'default' }}>{this.dataDoc.mass1_positionX + ''} m</td>
-                                            )}{' '}
-                                            {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && (
-                                                <td
-                                                    style={{
-                                                        cursor: 'default',
-                                                    }}>
-                                                    <InputField
-                                                        lowerBound={this.simulationType === 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15}
-                                                        dataDoc={this.dataDoc}
-                                                        prop="mass1_positionX"
-                                                        step={1}
-                                                        unit="m"
-                                                        upperBound={this.simulationType === 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15}
-                                                        value={NumCast(this.dataDoc.mass1_positionX)}
-                                                        effect={value => {
-                                                            this.dataDoc.mass1_xChange = value;
-                                                            if (this.simulationType === 'Suspension') {
-                                                                const x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
-                                                                const x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
-                                                                const deltaX1 = value + this.radius - x1rod;
-                                                                const deltaX2 = x2rod - (value + this.radius);
-                                                                const deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius;
-                                                                let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
-                                                                let dir2T = Math.atan(deltaY / deltaX2);
-                                                                const tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
-                                                                const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
-                                                                dir1T = (dir1T * 180) / Math.PI;
-                                                                dir2T = (dir2T * 180) / Math.PI;
-                                                                const tensionForce1: IForce = {
-                                                                    description: 'Tension',
-                                                                    magnitude: tensionMag1,
-                                                                    directionInDegrees: dir1T,
-                                                                };
-                                                                const tensionForce2: IForce = {
-                                                                    description: 'Tension',
-                                                                    magnitude: tensionMag2,
-                                                                    directionInDegrees: dir2T,
-                                                                };
-                                                                const gravity = this.gravityForce(this.mass1);
-                                                                this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
-                                                            }
-                                                        }}
-                                                        small
-                                                        mode="Freeform"
-                                                    />
-                                                </td>
-                                            )}{' '}
-                                            {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && (
-                                                <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_positionY)} m`}</td>
-                                            )}{' '}
-                                            {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && (
-                                                <td
-                                                    style={{
-                                                        cursor: 'default',
-                                                    }}>
-                                                    <InputField
-                                                        lowerBound={1}
-                                                        dataDoc={this.dataDoc}
-                                                        prop="mass1_positionY"
-                                                        step={1}
-                                                        unit="m"
-                                                        upperBound={this.yMax - 110}
-                                                        value={NumCast(this.dataDoc.mass1_positionY)}
-                                                        effect={value => {
-                                                            this.dataDoc.mass1_yChange = value;
-                                                            if (this.simulationType === 'Suspension') {
-                                                                const x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
-                                                                const x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
-                                                                const deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod;
-                                                                const deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius);
-                                                                const deltaY = this.getYPosFromDisplay(value) + this.radius;
-                                                                let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
-                                                                let dir2T = Math.atan(deltaY / deltaX2);
-                                                                const tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
-                                                                const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
-                                                                dir1T = (dir1T * 180) / Math.PI;
-                                                                dir2T = (dir2T * 180) / Math.PI;
-                                                                const tensionForce1: IForce = {
-                                                                    description: 'Tension',
-                                                                    magnitude: tensionMag1,
-                                                                    directionInDegrees: dir1T,
-                                                                };
-                                                                const tensionForce2: IForce = {
-                                                                    description: 'Tension',
-                                                                    magnitude: tensionMag2,
-                                                                    directionInDegrees: dir2T,
-                                                                };
-                                                                const gravity = this.gravityForce(this.mass1);
-                                                                this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
-                                                            }
-                                                        }}
-                                                        small
-                                                        mode="Freeform"
-                                                    />
-                                                </td>
-                                            )}{' '}
-                                        </tr>
-                                        <tr>
-                                            <td
-                                                style={{ cursor: 'help' }}
-                                                // onClick={() => {
-                                                //   window.open(
-                                                //     "https://www.khanacademy.org/science/physics/two-dimensional-motion"
-                                                //   );
-                                                // }}
-                                            >
-                                                <Box>Velocity</Box>
-                                            </td>
-                                            {(!this.dataDoc.simulation_paused || (this.simulationType !== 'One Weight' && this.simulationType !== 'Circular Motion')) && (
-                                                <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_velocityX)} m/s`}</td>
-                                            )}{' '}
-                                            {this.dataDoc.simulation_paused && (this.simulationType === 'One Weight' || this.simulationType === 'Circular Motion') && (
-                                                <td
-                                                    style={{
-                                                        cursor: 'default',
-                                                    }}>
-                                                    <InputField
-                                                        lowerBound={-50}
-                                                        dataDoc={this.dataDoc}
-                                                        prop="mass1_velocityX"
-                                                        step={1}
-                                                        unit="m/s"
-                                                        upperBound={50}
-                                                        value={NumCast(this.dataDoc.mass1_velocityX)}
-                                                        effect={action(value => {
-                                                            this.dataDoc.mass1_velocityXstart = value;
-                                                            this._simReset++;
-                                                        })}
-                                                        small
-                                                        mode="Freeform"
-                                                    />
-                                                </td>
-                                            )}{' '}
-                                            {(!this.dataDoc.simulation_paused || this.simulationType !== 'One Weight') && <td style={{ cursor: 'default' }}>{this.dataDoc.mass1_velocityY + ''} m/s</td>}{' '}
-                                            {this.dataDoc.simulation_paused && this.simulationType === 'One Weight' && (
-                                                <td
-                                                    style={{
-                                                        cursor: 'default',
-                                                    }}>
-                                                    <InputField
-                                                        lowerBound={-50}
-                                                        dataDoc={this.dataDoc}
-                                                        prop="mass1_velocityY"
-                                                        step={1}
-                                                        unit="m/s"
-                                                        upperBound={50}
-                                                        value={NumCast(this.dataDoc.mass1_velocityY)}
-                                                        effect={value => {
-                                                            this.dataDoc.mass1_velocityYstart = -value;
-                                                        }}
-                                                        small
-                                                        mode="Freeform"
-                                                    />
-                                                </td>
-                                            )}{' '}
-                                        </tr>
-                                        <tr>
-                                            <td
-                                                style={{ cursor: 'help' }}
-                                                // onClick={() => {
-                                                //   window.open(
-                                                //     "https://www.khanacademy.org/science/physics/two-dimensional-motion"
-                                                //   );
-                                                // }}
-                                            >
-                                                <Box>Acceleration</Box>
-                                            </td>
-                                            <td style={{ cursor: 'default' }}>
-                                                {this.dataDoc.mass1_accelerationX + ''} m/s<sup>2</sup>
-                                            </td>
-                                            <td style={{ cursor: 'default' }}>
-                                                {this.dataDoc.mass1_accelerationY + ''} m/s<sup>2</sup>
-                                            </td>
-                                        </tr>
-                                        <tr>
-                                            <td>
-                                                <Box>Momentum</Box>
-                                            </td>
-                                            <td>{Math.round(NumCast(this.dataDoc.mass1_velocityX) * this.mass1 * 10) / 10} kg*m/s</td>
-                                            <td>{Math.round(NumCast(this.dataDoc.mass1_velocityY) * this.mass1 * 10) / 10} kg*m/s</td>
-                                        </tr>
-                                    </tbody>
-                                </table>
-                            )}
-                            {this.simulationMode === 'Freeform' && this.simulationType === 'Pulley' && (
-                                <table>
-                                    <tbody>
-                                        <tr>
-                                            <td>Blue Weight</td>
-                                            <td>X</td>
-                                            <td>Y</td>
-                                        </tr>
-                                        <tr>
-                                            <td>
-                                                <Box>Position</Box>
-                                            </td>
-                                            <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionX} m`}</td>
-                                            <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionY} m`}</td>
-                                        </tr>
-                                        <tr>
-                                            <td>
-                                                <Box>Velocity</Box>
-                                            </td>
-                                            <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionX} m/s`}</td>
-                                            <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionY} m/s`}</td>
-                                        </tr>
-                                        <tr>
-                                            <td>
-                                                <Box>Acceleration</Box>
-                                            </td>
-                                            <td style={{ cursor: 'default' }}>
-                                                {this.dataDoc.mass2_accelerationX + ''} m/s<sup>2</sup>
-                                            </td>
-                                            <td style={{ cursor: 'default' }}>
-                                                {this.dataDoc.mass2_accelerationY + ''} m/s<sup>2</sup>
-                                            </td>
-                                        </tr>
-                                        <tr>
-                                            <td>
-                                                <Box>Momentum</Box>
-                                            </td>
-                                            <td>{Math.round(NumCast(this.dataDoc.mass2_velocityX) * this.mass1 * 10) / 10} kg*m/s</td>
-                                            <td>{Math.round(NumCast(this.dataDoc.mass2_velocityY) * this.mass1 * 10) / 10} kg*m/s</td>
-                                        </tr>
-                                    </tbody>
-                                </table>
-                            )}
-                        </div>
-                        {this.simulationType !== 'Pendulum' && this.simulationType !== 'Spring' && (
-                            <div>
-                                <p>Kinematic Equations</p>
-                                <ul>
-                                    <li>
-                                        Position: x<sub>1</sub>=x<sub>0</sub>+v<sub>0</sub>t+
-                                        <sup>1</sup>&frasl;
-                                        <sub>2</sub>at
-                                        <sup>2</sup>
-                                    </li>
-                                    <li>
-                                        Velocity: v<sub>1</sub>=v<sub>0</sub>+at
-                                    </li>
-                                    <li>Acceleration: a = F/m</li>
-                                </ul>
-                            </div>
-                        )}
-                        {this.simulationType === 'Spring' && (
-                            <div>
-                                <p>Harmonic Motion Equations: Spring</p>
-                                <ul>
-                                    <li>
-                                        Spring force: F<sub>s</sub>=kd
-                                    </li>
-                                    <li>
-                                        Spring period: T<sub>s</sub>=2&pi;&#8730;<sup>m</sup>&frasl;
-                                        <sub>k</sub>
-                                    </li>
-                                    <li>Equilibrium displacement for vertical spring: d = mg/k</li>
-                                    <li>
-                                        Elastic potential energy: U<sub>s</sub>=<sup>1</sup>&frasl;
-                                        <sub>2</sub>kd<sup>2</sup>
-                                    </li>
-                                    <ul>
-                                        <li>Maximum when system is at maximum displacement, 0 when system is at 0 displacement</li>
-                                    </ul>
-                                    <li>
-                                        Translational kinetic energy: K=<sup>1</sup>&frasl;
-                                        <sub>2</sub>mv<sup>2</sup>
-                                    </li>
-                                    <ul>
-                                        <li>Maximum when system is at maximum/minimum velocity (at 0 displacement), 0 when velocity is 0 (at maximum displacement)</li>
-                                    </ul>
-                                </ul>
-                            </div>
-                        )}
-                        {this.simulationType === 'Pendulum' && (
-                            <div>
-                                <p>Harmonic Motion Equations: Pendulum</p>
-                                <ul>
-                                    <li>
-                                        Pendulum period: T<sub>p</sub>=2&pi;&#8730;<sup>l</sup>&frasl;
-                                        <sub>g</sub>
-                                    </li>
-                                </ul>
-                            </div>
-                        )}
-                    </div>
-                </div>
-                <div
-                    style={{
-                        position: 'fixed',
-                        top: this.yMax - 120 + 20 + 'px',
-                        left: this.xMin + 90 - 80 + 'px',
-                        zIndex: -10000,
-                    }}>
-                    <svg width={100 + 'px'} height={100 + 'px'}>
-                        <defs>
-                            <marker id="miniArrow" markerWidth="20" markerHeight="20" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
-                                <path d="M0,0 L0,6 L9,3 z" fill="#000000" />
-                            </marker>
-                        </defs>
-                        <line x1={20} y1={70} x2={70} y2={70} stroke="#000000" strokeWidth="2" markerEnd="url(#miniArrow)" />
-                        <line x1={20} y1={70} x2={20} y2={20} stroke="#000000" strokeWidth="2" markerEnd="url(#miniArrow)" />
-                    </svg>
-                    <p
-                        style={{
-                            position: 'fixed',
-                            top: this.yMax - 120 + 40 + 'px',
-                            left: this.xMin + 90 - 80 + 'px',
-                        }}>
-                        {this.simulationType === 'Circular Motion' ? 'Z' : 'Y'}
-                    </p>
-                    <p
-                        style={{
-                            position: 'fixed',
-                            top: this.yMax - 120 + 80 + 'px',
-                            left: this.xMin + 90 - 40 + 'px',
-                        }}>
-                        X
-                    </p>
-                </div>
+            <div
+                className="mainView-container"
+                style={{
+                    color: SnappingManager.userColor,
+                    background: SnappingManager.userBackgroundColor,
+                }}
+                onScroll={() =>
+                    (ele => {
+                        ele.scrollTop = ele.scrollLeft = 0;
+                    })(document.getElementById('root')!)
+                }
+                ref={r => {
+                    r &&
+                        new _global.ResizeObserver(
+                            action(() => {
+                                this._windowWidth = r.getBoundingClientRect().width;
+                                this._windowHeight = r.getBoundingClientRect().height;
+                            })
+                        ).observe(r);
+                }}>
+                {this.inkResources}
+                <DictationOverlay />
+                <SharingManager />
+                <CalendarManager />
+                <ServerStats />
+                <RTFMarkup />
+                <SettingsManager />
+                <ReportManager />
+                <CaptureManager />
+                <GroupManager />
+                <GoogleAuthenticationManager />
+                <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfSidebarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} />
+                <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} />
+                {this._hideUI ? null : <TopBar />}
+                <LinkDescriptionPopup />
+                {DocButtonState.Instance.LinkEditorDocView ? (
+                    <LinkMenu
+                        clearLinkEditor={action(() => {
+                            DocButtonState.Instance.LinkEditorDocView = undefined;
+                        })}
+                        docView={DocButtonState.Instance.LinkEditorDocView}
+                    />
+                ) : null}
+                {LinkInfo.Instance?.LinkInfo ? (
+                    // eslint-disable-next-line react/jsx-props-no-spreading
+                    <LinkDocPreview {...LinkInfo.Instance.LinkInfo} />
+                ) : null}
+                {((page: string) => {
+                    // prettier-ignore
+                    switch (page) {
+                        case 'home': return <DashboardView />;
+                        case 'dashboard':
+                        default: return (<>
+                                        <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || DocumentView.LightboxDoc() ? 'none' : undefined, zIndex: 2001 }}>
+                                            <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} togglePropertiesFlyout={this.togglePropertiesFlyout} toggleTopBar={this.toggleTopBar} topBarHeight={this.headerBarHeightFunc}/>
+                                        </div>
+                                        {this.mainDashboardArea}
+                                    </> );
+                    }
+                })(Doc.ActivePage)}
+                <PreviewCursor />
+                <TaskCompletionBox />
+                <ContextMenu />
+                <ImageLabelHandler />
+                <SmartDrawHandler />
+                <AnchorMenu />
+                <MapAnchorMenu />
+                <DirectionsAnchorMenu />
+                <DashFieldViewMenu />
+                <MarqueeOptionsMenu />
+                <TimelineMenu />
+                <RichTextMenu />
+                {this.snapLines}
+                <LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
+                <GPTPopup key="gptpopup" />
+                <SchemaCSVPopUp key="schemacsvpopup" />
+                <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
             </div>
         );
     }
 }
 
-Docs.Prototypes.TemplateMap.set(DocumentType.SIMULATION, {
-    data: '',
-    layout: { view: PhysicsSimulationBox, dataField: 'data' },
-    options: { acl: '', _width: 1000, _height: 800, mass1: '', mass2: '', layout_nativeDimEditable: true, position: '', acceleration: '', pendulum: '', spring: '', wedge: '', simulation: '', review: '', systemIcon: 'BsShareFill' },
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function selectMainMenu(doc: Doc) {
+    MainView.Instance.selectMenu(doc);
 });
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function createNewPresentation() {
+    return MainView.Instance.createNewPresentation();
+}, 'creates a new presentation when called');
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function openPresentation(pres: Doc) {
+    return MainView.Instance.openPresentation(pres);
+}, 'creates a new presentation when called');
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function createNewFolder() {
+    return MainView.Instance.createNewFolder();
+}, 'creates a new folder in myFiles when called');
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 467191735..09bb3649d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1270,7 +1270,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     _drawing: Doc[] = [];
     @undoBatch
-    createDrawing = (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+    createDrawing = (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => {
         strokeData.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
             const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
@@ -1291,16 +1291,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this._drawing.push(inkDoc);
             this.addDocument(inkDoc);
         });
-        // const collection = this._marqueeViewRef.current?.collection(undefined, false, this._drawing);
-        // if (collection) {
-        //     const docData = collection[DocData];
-        //     docData.title = opts.text;
-        //     docData.drawingInput = opts.text;
-        //     docData.drawingComplexity = opts.complexity;
-        //     docData.drawingColored = opts.autoColor;
-        //     docData.drawingSize = opts.size;
-        //     docData.drawingData = gptRes;
-        // }
+        const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+        if (collection) {
+            const docData = collection[DocData];
+            docData.title = opts.text;
+            docData.drawingInput = opts.text;
+            docData.drawingComplexity = opts.complexity;
+            docData.drawingColored = opts.autoColor;
+            docData.drawingSize = opts.size;
+            docData.drawingData = gptRes;
+        }
         this._batch?.end();
     };
 
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 6d2cc0593..d24cc9d50 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -43,7 +43,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _size: number = 200;
     @observable private _autoColor: boolean = true;
     @observable private _regenInput: string = '';
-    private _addFunc: (e: React.PointerEvent<Element>, strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void = () => {};
+    private _addFunc: (e: React.PointerEvent<Element>, strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
     private _deleteFunc: (doc?: Doc) => void = () => {};
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 300, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
@@ -86,7 +86,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    displaySmartDrawHandler = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void, deleteFunc: (doc?: Doc) => void) => {
+    displaySmartDrawHandler = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
         this._pageX = x;
         this._pageY = y;
         this._display = true;
@@ -95,9 +95,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    displayRegenerate = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void, deleteFunc: (doc?: Doc) => void) => {
-        const selectedDoc: Doc = DocumentView.SelectedDocs().lastElement();
-        const docData = selectedDoc[DocData];
+    displayRegenerate = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+        this._selectedDoc = DocumentView.SelectedDocs().lastElement();
+        const docData = this._selectedDoc[DocData];
         this._addFunc = addFunc;
         this._deleteFunc = deleteFunc;
         this._pageX = x;
@@ -210,8 +210,10 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             });
             if (regenerate) {
                 this._deleteFunc(this._selectedDoc);
+                this._addFunc(e, strokeData, this._lastInput, svg[0], this._selectedDoc);
+            } else {
+                this._addFunc(e, strokeData, this._lastInput, svg[0]);
             }
-            this._addFunc(e, strokeData, this._lastInput, svg[0]);
         }
     };
 
-- 
cgit v1.2.3-70-g09d2


From 22ef5658b825281f5dd51e3c51a62c3d53756dc0 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 2 Jul 2024 14:49:54 -0400
Subject: fixed pointer event problem

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 4 ++--
 1 file changed, 2 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 09bb3649d..0bdcc8450 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -518,8 +518,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         e.stopPropagation();
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
-                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1);
-                            e.stopPropagation();
+                            const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
                         }
                         break;
                     default:
-- 
cgit v1.2.3-70-g09d2

