From baf156f58856004279223b2e1f858c5ff7e88686 Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Tue, 4 Jun 2024 13:54:56 -0400 Subject: small fixes --- src/client/apis/gpt/GPT.ts | 6 ++++++ src/client/util/CurrentUserUtils.ts | 1 + src/client/views/MainView.tsx | 1 + src/client/views/SmartDraw.tsx | 0 src/client/views/collections/CollectionMenu.scss | 2 +- .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 6 ++++-- src/client/views/global/globalScripts.ts | 4 ++-- src/fields/Doc.ts | 4 ++-- 8 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 src/client/views/SmartDraw.tsx (limited to 'src') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 05007960d..3a5e49731 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -53,6 +53,12 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { temp: 0, prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct', }, + draw: { + 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', + } }; let lastCall = ''; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index e095bc659..4e379219f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -745,6 +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()"}}, ]; } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 31d88fb87..8430db883 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -394,6 +394,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faArrowsLeftRight, fa.faPause, fa.faPen, + fa.faUserPen, fa.faPenNib, fa.faPhone, fa.faPlay, diff --git a/src/client/views/SmartDraw.tsx b/src/client/views/SmartDraw.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 3ec875df4..45d9394ed 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -6,7 +6,7 @@ align-content: center; justify-content: space-between; background-color: $dark-gray; - height: 35px; + height: 40px; border-bottom: $standard-border; padding: 0 10px; align-items: center; 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 { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK && setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, }], [ 'eraserWidth', { - checkResult: () => ActiveEraserWidth(), + checkResult: () => ActiveEraserWidth() === 0 ? 1 : ActiveEraserWidth(), setInk: (doc: Doc) => { }, - setMode: () => { SetEraserWidth(value.toString());}, + setMode: () => { SetEraserWidth(value);}, }] ]); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 1b3d963e8..4a469dfe2 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -185,7 +185,7 @@ export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.acti export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore -export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth); } // prettier-ignore +export function ActiveEraserWidth(): number { return NumCast(ActiveInkPen()?.eraserWidth); } // prettier-ignore export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); @@ -218,7 +218,7 @@ export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); } export function SetEraserWidth(width: number): void { - ActiveInkPen() && (ActiveInkPen().eraserWidth = width); + !isNaN(width) && ActiveInkPen() && (ActiveInkPen().eraserWidth = width); } @scriptingGlobal -- cgit v1.2.3-70-g09d2 From 2f5757ffaebaec9d459404fec266295abeebd2b0 Mon Sep 17 00:00:00 2001 From: eleanor-park 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') 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<{}> { + 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 { + 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 ( +
+
+ } color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> + { + this.setUserInput(e.target.value); + }} + placeholder="Enter item to draw" + /> +
+
+ {/* {this._labelGroups.map(group => { + return ( +
+

{group}

+ { + this.removeLabel(group); + }} + icon={'x'} + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '19px' }} + /> +
+ ); + })} */} +
+
+ ); + } 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; -- cgit v1.2.3-70-g09d2 From 2277349fc4d5460e94a7a6b705b56488c0efb184 Mon Sep 17 00:00:00 2001 From: eleanor-park 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') 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 { 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 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') 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; + savedTapes: () => ObservableMap; selectionText: () => string; annotationLayer: HTMLDivElement; addDocument: (doc: Doc) => boolean; @@ -73,7 +74,7 @@ export class MarqueeAnnotator extends ObservableReactComponent): Opt => { + // 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(annoRects); + // textRegionAnnoProto.opacity = 0; + // textRegionAnnoProto.layout_unrendered = true; + // savedAnnoMap.clear(); + // return textRegionAnno; + // }; + + @undoBatch + makeTapeDocument = (color: string, isLinkButton?: boolean, savedTapes?: ObservableMap): Opt => { + // 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(annoRects); + // textRegionAnnoProto.opacity = 0; + // textRegionAnnoProto.layout_unrendered = true; + // savedAnnoMap.clear(); + // return textRegionAnno; + }; + @action highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap, addAsAnnotation?: boolean) => { // creates annotation documents for current highlights @@ -136,6 +271,15 @@ export class MarqueeAnnotator extends ObservableReactComponent, 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, annotationLayer: HTMLDivElement, div: HTMLDivElement, page: number) => { div.style.backgroundColor = '#ACCEF7'; div.style.opacity = '0.5'; @@ -182,6 +326,7 @@ export class MarqueeAnnotator extends ObservableReactComponent 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 /* , 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 { + 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 = 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 { - 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 { - 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 { @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); }} /> {/* { public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public Highlight: (color: string) => Opt = (/* color: string */) => undefined; + public Tape: (color: string) => Opt = (/* color: string */) => undefined; public GetAnchor: (savedAnnotations: Opt>, addAsAnnotation: boolean) => Opt = emptyFunction; public Delete: () => void = unimplementedFunction; public PinToPres: () => void = unimplementedFunction; @@ -172,6 +173,12 @@ export class AnchorMenu extends AntimodeMenu { AnchorMenu.Instance.fadeOut(true); }; + @action + tapeClicked = () => { + this.Tape(this.highlightColor); + // AnchorMenu.Instance.fadeOut(true); + }; + @computed get highlighter() { return ( @@ -182,6 +189,13 @@ export class AnchorMenu extends AntimodeMenu { colorPicker={this.highlightColor} color={SettingsManager.userColor} /> + } + onClick={this.tapeClicked} + colorPicker={this.highlightColor} + color={SettingsManager.userColor} + /> ); 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 { @observable _pageSizes: { width: number; height: number }[] = []; @observable _savedAnnotations = new ObservableMap(); + @observable _savedTapes = new ObservableMap(); @observable _textSelecting = true; @observable _showWaiting = true; @observable Index: number = -1; @@ -583,6 +584,7 @@ export class PDFViewer extends ObservableReactComponent { return
; } 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 { 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 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') 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<{}> { - 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 ViewDefResult[] }> { @@ -144,10 +145,7 @@ export class CollectionFreeFormView extends CollectionSubView { this.erase(e, [0, 0]); + e.stopPropagation(); return false; }; @@ -1265,19 +1264,20 @@ export class CollectionFreeFormView extends CollectionSubView { - 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, }}>
- } color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> + } color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> { }} placeholder="Enter item to draw" /> + } color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '14px' }} onClick={this.changeDrawingType} /> + {/* } + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '14px' }} + onClick={() => { + this._alpha = 0; + }} + /> */}
-
- {/* {this._labelGroups.map(group => { - return ( -
-

{group}

- { - this.removeLabel(group); - }} - icon={'x'} - color={MarqueeOptionsMenu.Instance.userColor} - style={{ width: '19px' }} - /> -
- ); - })} */}
); -- cgit v1.2.3-70-g09d2 From bd64bbd29a38ae4979b2165d1fa9b9c76c2600d5 Mon Sep 17 00:00:00 2001 From: eleanor-park 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') 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 { + 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 { 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(/]*>([\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 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') 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 { - 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, 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(/]*>([\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) => { + 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, }}>
- } color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> + { + this.hideSmartDrawHandler(); + this.hideRegenerate(); + }} + icon={} + color={SettingsManager.userColor} + style={{ width: '19px' }} + /> { }} placeholder="Enter item to draw" /> - } color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '14px' }} onClick={this.changeDrawingType} /> - {/* } - color={MarqueeOptionsMenu.Instance.userColor} + } + color={SettingsManager.userColor} style={{ width: '14px' }} onClick={() => { - this._alpha = 0; + this._showOptions = !this._showOptions; }} - /> */} + />
+ {this._showOptions && ( + <> +
+
+ Auto color + (this._autoColor = !this._autoColor)} + /> +
+
+ Complexity + { + this._complexity = val as number; + }} + valueLabelDisplay="auto" + /> +
+
+ Size (in pixels) + { + this._size = val as number; + }} + valueLabelDisplay="auto" + /> +
+
+ + )} + + ); + } else if (this._showRegenerate) { + return ( +
+
+ } color={SettingsManager.userColor} style={{ width: '19px' }} /> + : } + color={SettingsManager.userColor} onClick={e => { - this.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._userInput); + this.regenerate(e); }} />
-- cgit v1.2.3-70-g09d2 From 33f416349c00bdec405455340befb0ca85b2fee4 Mon Sep 17 00:00:00 2001 From: eleanor-park 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') 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 { @observable private _doc: Opt = undefined; @observable private _docTarget: Opt = undefined; @observable private _docView: Opt = 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 { 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 { {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 && }
); } 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(); +export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent() { + 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 = 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 { - 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) { + 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 ( -
- -
- ); - } - @computed get mainDocView() { - const headerBar = this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView; - return ( - <> - {headerBar} - - - ); - } + // Solve for the correct answers to the generated problem + getAnswersToQuestion = (question: QuestionTemplate, questionVars: number[]) => { + const solutions: number[] = []; - @computed get dockingContent() { - return ( - -
{ - 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} -
-
- ); - } + 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(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 ? ( -
- {this.docButtons} -
- ) : ( -
-
- -
- {this.docButtons} -
- ); - } + // In review mode, reset problem variables and generate a new question + generateNewQuestion = () => { + this.resetReviewValuesToDefault(); - @computed get leftMenuPanel() { - return ( -
- -
- ); - } + 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(vars); + this.dataDoc.selectedQuestion = JSON.stringify(question); + this.dataDoc.questionPartOne = q; + this.dataDoc.questionPartTwo = question.question; + this.dataDoc.answers = new List(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} -
- {this.flyout} -
- -
-
- {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 : ( -
- -
- )} -
-
- -
-
-
-
- - ); - } + 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 : ( -
{ - 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} -
- ); - } + // 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 : ( -
- - {['watching', 'recording'].includes(StrCast(this.userDoc?.presentationMode)) ?
{StrCast(this.userDoc?.presentationMode)}
: null} -
- ); - } - @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 : ( -
- - {[ - ...SnappingManager.HorizSnapLines.map((l, i) => ( - // eslint-disable-next-line react/no-array-index-key - - )), - ...SnappingManager.VertSnapLines.map((l, i) => ( - // eslint-disable-next-line react/no-array-index-key - - )), - ]} - -
- ); - } - - @computed get inkResources() { - return ( - - - - - - - - - - - - - - - ); + 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 ( -
- (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} - - - - - - - - - - - - - {this._hideUI ? null : } - - {DocButtonState.Instance.LinkEditorDocView ? ( - { - DocButtonState.Instance.LinkEditorDocView = undefined; - })} - docView={DocButtonState.Instance.LinkEditorDocView} - /> - ) : null} - {LinkInfo.Instance?.LinkInfo ? ( - // eslint-disable-next-line react/jsx-props-no-spreading - - ) : null} - {((page: string) => { - // prettier-ignore - switch (page) { - case 'home': return ; - case 'dashboard': - default: return (<> -
- -
- {this.mainDashboardArea} - ); - } - })(Doc.ActivePage)} - - - - - - - - - - - - - {this.snapLines} - - - - +
+
+
+
+
+ {!this.dataDoc.simulation_paused && ( +
+ +
+ )} +
+
+ + {this.simulationType === 'Pulley' && ( + + )} +
+
+ {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane') && + this.wallPositions?.map((element, index) => )} +
+
+
+
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)})` }}> +
+ + {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( + (this.dataDoc.simulation_paused = false)}> + + + )} + {!this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( + (this.dataDoc.simulation_paused = true)}> + + + )} + {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( + this._simReset++)}> + + + )} + +
+ +
+
+ +
+
+ {this.simulationMode === 'Review' && this.simulationType !== 'Inclined Plane' && ( +
+

{this.simulationType} review problems in progress!

+
+
+ )} + {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && ( +
+ {!this.dataDoc.hintDialogueOpen && ( + (this.dataDoc.hintDialogueOpen = true)} + sx={{ + position: 'fixed', + left: this.xMax - 50 + 'px', + top: this.yMin + 14 + 'px', + }}> + + + )} + (this.dataDoc.hintDialogueOpen = false)}> + Hints + + {this.selectedQuestion.hints?.map((hint: any, index: number) => ( +
+ +
+ + + Hint {index + 1}: {hint.description} + + + {hint.content} +
+
+
+ ))} +
+ + + +
+
+
+

{this.questionPartOne}

+

{this.questionPartTwo}

+
+
+ {this.selectedQuestion.answerParts.includes('force of gravity') && ( + Gravity magnitude

} + 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') && ( + Gravity angle

} + 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') && ( + Normal force magnitude

} + 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') && ( + Normal force angle

} + 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') && ( + Static friction magnitude

} + 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') && ( + Static friction angle

} + 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') && ( + + μs + + } + 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') && ( + θ} + 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')]} + /> + )} +
+
+
+ )} + {this.simulationMode === 'Tutorial' && ( +
+
+

Problem

+

{this.tutorial.question}

+
+
+ { + 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}> + + +
+

+ Step {NumCast(this.dataDoc.tutorial_stepNumber) + 1}: {this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].description} +

+

{this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].content}

+
+ { + 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}> + + +
+
+ {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') &&

Resources

} + {this.simulationType === 'One Weight' && ( + + )} + {this.simulationType === 'Inclined Plane' && ( + + )} + {this.simulationType === 'Pendulum' && ( + + )} +
+
+ )} + {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && ( +
+

(this.dataDoc.simulation_mode = 'Tutorial')}> + {' '} + Go to walkthrough{' '} +

+
+ + +
+
+ )} + {this.simulationMode === 'Freeform' && ( +
+ + + {this.simulationType === 'One Weight' && ( + (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />} + label="Make collisions elastic" + labelPlacement="start" + /> + )} + (this.dataDoc.simulation_showForces = !this.dataDoc.simulation_showForces)} />} + label="Show force vectors" + labelPlacement="start" + /> + {(this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && ( + (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />} + label="Show component force vectors" + labelPlacement="start" + /> + )} + (this.dataDoc.simulation_showAcceleration = !this.dataDoc.simulation_showAcceleration)} />} + label="Show acceleration vector" + labelPlacement="start" + /> + (this.dataDoc.simulation_showVelocity = !this.dataDoc.simulation_showVelocity)} />} + label="Show velocity vector" + labelPlacement="start" + /> + Speed} 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' && ( + Gravity} + 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' && ( + Mass} + 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' && ( + Red mass} + 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' && ( + Blue mass} + 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' && ( + Rod length} + lowerBound={100} + dataDoc={this.dataDoc} + prop="circularMotionRadius" + step={5} + unit="m" + upperBound={250} + value={this.circularMotionRadius} + effect={(val: number) => this.setupSimulation()} + labelWidth="5em" + /> + )} + + + {this.simulationType === 'Spring' && this.dataDoc.simulation_paused && ( +
+ Spring stiffness} + 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" + /> + Rest length} + 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" + /> + Starting displacement} + 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" + /> +
+ )} + {this.simulationType === 'Inclined Plane' && this.dataDoc.simulation_paused && ( +
+ θ} + 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" + /> + + μs + + } + 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" + /> + + μk + + } + 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" + /> +
+ )} + {this.simulationType === 'Inclined Plane' && !this.dataDoc.simulation_paused && ( + + <> + θ: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad +
+ μ s: {this.dataDoc.coefficientOfStaticFriction} +
+ μ k: {this.dataDoc.coefficientOfKineticFriction} + +
+ )} + {this.simulationType === 'Pendulum' && !this.dataDoc.simulation_paused && ( + + θ: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad + + )} + {this.simulationType === 'Pendulum' && this.dataDoc.simulation_paused && ( +
+ Angle} + 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" + /> + Rod length} + 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" + /> +
+ )} +
+ )} +
+ {this.simulationMode === 'Freeform' && ( + + + + + + + + + + {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && ( + + )}{' '} + {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && ( + + )}{' '} + {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && ( + + )}{' '} + {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && ( + + )}{' '} + + + + {(!this.dataDoc.simulation_paused || (this.simulationType !== 'One Weight' && this.simulationType !== 'Circular Motion')) && ( + + )}{' '} + {this.dataDoc.simulation_paused && (this.simulationType === 'One Weight' || this.simulationType === 'Circular Motion') && ( + + )}{' '} + {(!this.dataDoc.simulation_paused || this.simulationType !== 'One Weight') && }{' '} + {this.dataDoc.simulation_paused && this.simulationType === 'One Weight' && ( + + )}{' '} + + + + + + + + + + + + +
{this.simulationType === 'Pulley' ? 'Red Weight' : ''}XY
{ + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + Position + {this.dataDoc.mass1_positionX + ''} m + { + 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" + /> + {`${NumCast(this.dataDoc.mass1_positionY)} m`} + { + 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" + /> +
{ + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + Velocity + {`${NumCast(this.dataDoc.mass1_velocityX)} m/s`} + { + this.dataDoc.mass1_velocityXstart = value; + this._simReset++; + })} + small + mode="Freeform" + /> + {this.dataDoc.mass1_velocityY + ''} m/s + { + this.dataDoc.mass1_velocityYstart = -value; + }} + small + mode="Freeform" + /> +
{ + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + Acceleration + + {this.dataDoc.mass1_accelerationX + ''} m/s2 + + {this.dataDoc.mass1_accelerationY + ''} m/s2 +
+ Momentum + {Math.round(NumCast(this.dataDoc.mass1_velocityX) * this.mass1 * 10) / 10} kg*m/s{Math.round(NumCast(this.dataDoc.mass1_velocityY) * this.mass1 * 10) / 10} kg*m/s
+ )} + {this.simulationMode === 'Freeform' && this.simulationType === 'Pulley' && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Blue WeightXY
+ Position + {`${this.dataDoc.mass2_positionX} m`}{`${this.dataDoc.mass2_positionY} m`}
+ Velocity + {`${this.dataDoc.mass2_positionX} m/s`}{`${this.dataDoc.mass2_positionY} m/s`}
+ Acceleration + + {this.dataDoc.mass2_accelerationX + ''} m/s2 + + {this.dataDoc.mass2_accelerationY + ''} m/s2 +
+ Momentum + {Math.round(NumCast(this.dataDoc.mass2_velocityX) * this.mass1 * 10) / 10} kg*m/s{Math.round(NumCast(this.dataDoc.mass2_velocityY) * this.mass1 * 10) / 10} kg*m/s
+ )} +
+ {this.simulationType !== 'Pendulum' && this.simulationType !== 'Spring' && ( +
+

Kinematic Equations

+
    +
  • + Position: x1=x0+v0t+ + 1⁄ + 2at + 2 +
  • +
  • + Velocity: v1=v0+at +
  • +
  • Acceleration: a = F/m
  • +
+
+ )} + {this.simulationType === 'Spring' && ( +
+

Harmonic Motion Equations: Spring

+
    +
  • + Spring force: Fs=kd +
  • +
  • + Spring period: Ts=2π√m⁄ + k +
  • +
  • Equilibrium displacement for vertical spring: d = mg/k
  • +
  • + Elastic potential energy: Us=1⁄ + 2kd2 +
  • +
      +
    • Maximum when system is at maximum displacement, 0 when system is at 0 displacement
    • +
    +
  • + Translational kinetic energy: K=1⁄ + 2mv2 +
  • +
      +
    • Maximum when system is at maximum/minimum velocity (at 0 displacement), 0 when velocity is 0 (at maximum displacement)
    • +
    +
+
+ )} + {this.simulationType === 'Pendulum' && ( +
+

Harmonic Motion Equations: Pendulum

+
    +
  • + Pendulum period: Tp=2π√l⁄ + g +
  • +
+
+ )} +
+
+
+ + + + + + + + + +

+ {this.simulationType === 'Circular Motion' ? 'Z' : 'Y'} +

+

+ X +

+
); } } -// 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(); @observable _marqueeViewRef = React.createRef(); @@ -514,7 +514,7 @@ export class CollectionFreeFormView extends CollectionSubView { this._deleteList.lastElement()?._props.removeDocument?.(this._deleteList.map(ink => ink.Document)); @@ -607,12 +608,12 @@ export class CollectionFreeFormView extends CollectionSubView { + 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 = 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 { + e.preventDefault(); + e.stopImmediatePropagation(); this.erase(e, [0, 0]); - e.stopPropagation(); - return false; }; /** @@ -696,32 +697,32 @@ export class CollectionFreeFormView extends CollectionSubView { - 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 = 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 = 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 { - 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, 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 { + 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 { + 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 { - 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, 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(/]*>([\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) => { - this.drawWithGPT(e, { X: this._lastX, Y: this._lastY }, `Regenerate the item "${this._userInput}"`, true); - }; - - render() { - if (this._display) { - return ( -
-
- { - this.hideSmartDrawHandler(); - this.hideRegenerate(); - }} - icon={} - color={SettingsManager.userColor} - style={{ width: '19px' }} - /> - { - this.setUserInput(e.target.value); - }} - placeholder="Enter item to draw" - /> - } - color={SettingsManager.userColor} - style={{ width: '14px' }} - onClick={() => { - this._showOptions = !this._showOptions; - }} - /> -
- {this._showOptions && ( - <> -
-
- Auto color - (this._autoColor = !this._autoColor)} - /> -
-
- Complexity - { - this._complexity = val as number; - }} - valueLabelDisplay="auto" - /> -
-
- Size (in pixels) - { - this._size = val as number; - }} - valueLabelDisplay="auto" - /> -
-
- - )} -
- ); - } else if (this._showRegenerate) { - return ( -
-
- } color={SettingsManager.userColor} style={{ width: '19px' }} /> - : } - color={SettingsManager.userColor} - onClick={e => { - this.regenerate(e); - }} - /> -
-
- ); - } 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() { } @computed get sidebarHandle() { return ( -
this.sidebarBtnDown(e, true)}> - +
+
+ {/* // onPointerDown={e => this.sidebarBtnDown(e, true)} */} + } onPointerDown={e => this.sidebarBtnDown(e, true)} /> +
+
+ } onPointerDown={e => this.sidebarBtnDown(e, true)} /> +
); } 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(); + private _stackRef = React.createRef(); + + 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 ( +
+ {/* */} + {/* */} +
+ ); + } + + render() { + return ( +
+ {/* {this._savedDrawings.map(doc => { + return ; + })} */} + {/* */} + {} + {/* */} + {this.savedDrawingAnnos} +
+ ); + } +} 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, 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, 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, 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, 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) => { + 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, res: string, startPoint: { X: number; Y: number }, regenerate: boolean) => { + const svg = res.match(/]*>([\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 ( +
+
+ { + this.hideSmartDrawHandler(); + this.hideRegenerate(); + }} + icon={} + color={SettingsManager.userColor} + style={{ width: '19px' }} + /> + { + this.setUserInput(e.target.value); + }} + placeholder="Enter item to draw" + /> +
+ {this._showOptions && ( + <> +
+
+ Auto color + +
+
+ Complexity + { + this.setComplexity(val as number); + }} + valueLabelDisplay="auto" + /> +
+
+ Size (in pixels) + { + this.setSize(val as number); + }} + valueLabelDisplay="auto" + /> +
+
+ + )} +
+ ); + } else if (this._showRegenerate) { + return ( +
+
+ : } + color={SettingsManager.userColor} + onClick={e => { + this.regenerate(e as React.PointerEvent); + }} + /> + } color={SettingsManager.userColor} onClick={this.edit} /> + {this._showEditBox && ( +
+ { + this.setRegenInput(e.target.value); + }} + placeholder="Edit instructions" + /> +
+ )} +
+
+ ); + } 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 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') 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() { - 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(); - // 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 = 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 { + 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) { - 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 ( +
+ +
+ ); + } + @computed get mainDocView() { + const headerBar = this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView; + return ( + <> + {headerBar} + + + ); + } - 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 ( + +
{ + 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} +
+
+ ); + } - 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(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 ? ( +
+ {this.docButtons} +
+ ) : ( +
+
+ +
+ {this.docButtons} +
+ ); + } - let coefficient = 0; - let wedge_angle = 0; + @computed get leftMenuPanel() { + return ( +
+ +
+ ); + } - 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(vars); - this.dataDoc.selectedQuestion = JSON.stringify(question); - this.dataDoc.questionPartOne = q; - this.dataDoc.questionPartTwo = question.question; - this.dataDoc.answers = new List(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} +
+ {this.flyout} +
+ +
+
+ {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 : ( +
+ +
+ )} +
+
+ +
+
+
+
+ + ); + } - // 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 : ( +
{ + 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} +
+ ); + } - 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 : ( +
+ + {['watching', 'recording'].includes(StrCast(this.userDoc?.presentationMode)) ?
{StrCast(this.userDoc?.presentationMode)}
: null} +
+ ); + } + @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 : ( +
+ + {[ + ...SnappingManager.HorizSnapLines.map((l, i) => ( + // eslint-disable-next-line react/no-array-index-key + + )), + ...SnappingManager.VertSnapLines.map((l, i) => ( + // eslint-disable-next-line react/no-array-index-key + + )), + ]} + +
+ ); + } + + @computed get inkResources() { + return ( + + + + + + + + + + + + + + + ); } - // 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 ( -
-
-
-
-
- {!this.dataDoc.simulation_paused && ( -
- -
- )} -
-
- - {this.simulationType === 'Pulley' && ( - - )} -
-
- {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane') && - this.wallPositions?.map((element, index) => )} -
-
-
-
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)})` }}> -
- - {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( - (this.dataDoc.simulation_paused = false)}> - - - )} - {!this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( - (this.dataDoc.simulation_paused = true)}> - - - )} - {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && ( - this._simReset++)}> - - - )} - -
- -
-
- -
-
- {this.simulationMode === 'Review' && this.simulationType !== 'Inclined Plane' && ( -
-

{this.simulationType} review problems in progress!

-
-
- )} - {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && ( -
- {!this.dataDoc.hintDialogueOpen && ( - (this.dataDoc.hintDialogueOpen = true)} - sx={{ - position: 'fixed', - left: this.xMax - 50 + 'px', - top: this.yMin + 14 + 'px', - }}> - - - )} - (this.dataDoc.hintDialogueOpen = false)}> - Hints - - {this.selectedQuestion.hints?.map((hint: any, index: number) => ( -
- -
- - - Hint {index + 1}: {hint.description} - - - {hint.content} -
-
-
- ))} -
- - - -
-
-
-

{this.questionPartOne}

-

{this.questionPartTwo}

-
-
- {this.selectedQuestion.answerParts.includes('force of gravity') && ( - Gravity magnitude

} - 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') && ( - Gravity angle

} - 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') && ( - Normal force magnitude

} - 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') && ( - Normal force angle

} - 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') && ( - Static friction magnitude

} - 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') && ( - Static friction angle

} - 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') && ( - - μs - - } - 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') && ( - θ} - 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')]} - /> - )} -
-
-
- )} - {this.simulationMode === 'Tutorial' && ( -
-
-

Problem

-

{this.tutorial.question}

-
-
- { - 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}> - - -
-

- Step {NumCast(this.dataDoc.tutorial_stepNumber) + 1}: {this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].description} -

-

{this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].content}

-
- { - 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}> - - -
-
- {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') &&

Resources

} - {this.simulationType === 'One Weight' && ( - - )} - {this.simulationType === 'Inclined Plane' && ( - - )} - {this.simulationType === 'Pendulum' && ( - - )} -
-
- )} - {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && ( -
-

(this.dataDoc.simulation_mode = 'Tutorial')}> - {' '} - Go to walkthrough{' '} -

-
- - -
-
- )} - {this.simulationMode === 'Freeform' && ( -
- - - {this.simulationType === 'One Weight' && ( - (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />} - label="Make collisions elastic" - labelPlacement="start" - /> - )} - (this.dataDoc.simulation_showForces = !this.dataDoc.simulation_showForces)} />} - label="Show force vectors" - labelPlacement="start" - /> - {(this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && ( - (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />} - label="Show component force vectors" - labelPlacement="start" - /> - )} - (this.dataDoc.simulation_showAcceleration = !this.dataDoc.simulation_showAcceleration)} />} - label="Show acceleration vector" - labelPlacement="start" - /> - (this.dataDoc.simulation_showVelocity = !this.dataDoc.simulation_showVelocity)} />} - label="Show velocity vector" - labelPlacement="start" - /> - Speed} 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' && ( - Gravity} - 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' && ( - Mass} - 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' && ( - Red mass} - 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' && ( - Blue mass} - 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' && ( - Rod length} - lowerBound={100} - dataDoc={this.dataDoc} - prop="circularMotionRadius" - step={5} - unit="m" - upperBound={250} - value={this.circularMotionRadius} - effect={(val: number) => this.setupSimulation()} - labelWidth="5em" - /> - )} - - - {this.simulationType === 'Spring' && this.dataDoc.simulation_paused && ( -
- Spring stiffness} - 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" - /> - Rest length} - 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" - /> - Starting displacement} - 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" - /> -
- )} - {this.simulationType === 'Inclined Plane' && this.dataDoc.simulation_paused && ( -
- θ} - 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" - /> - - μs - - } - 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" - /> - - μk - - } - 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" - /> -
- )} - {this.simulationType === 'Inclined Plane' && !this.dataDoc.simulation_paused && ( - - <> - θ: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad -
- μ s: {this.dataDoc.coefficientOfStaticFriction} -
- μ k: {this.dataDoc.coefficientOfKineticFriction} - -
- )} - {this.simulationType === 'Pendulum' && !this.dataDoc.simulation_paused && ( - - θ: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad - - )} - {this.simulationType === 'Pendulum' && this.dataDoc.simulation_paused && ( -
- Angle} - 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" - /> - Rod length} - 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" - /> -
- )} -
- )} -
- {this.simulationMode === 'Freeform' && ( - - - - - - - - - - {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && ( - - )}{' '} - {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && ( - - )}{' '} - {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && ( - - )}{' '} - {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && ( - - )}{' '} - - - - {(!this.dataDoc.simulation_paused || (this.simulationType !== 'One Weight' && this.simulationType !== 'Circular Motion')) && ( - - )}{' '} - {this.dataDoc.simulation_paused && (this.simulationType === 'One Weight' || this.simulationType === 'Circular Motion') && ( - - )}{' '} - {(!this.dataDoc.simulation_paused || this.simulationType !== 'One Weight') && }{' '} - {this.dataDoc.simulation_paused && this.simulationType === 'One Weight' && ( - - )}{' '} - - - - - - - - - - - - -
{this.simulationType === 'Pulley' ? 'Red Weight' : ''}XY
{ - // window.open( - // "https://www.khanacademy.org/science/physics/two-dimensional-motion" - // ); - // }} - > - Position - {this.dataDoc.mass1_positionX + ''} m - { - 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" - /> - {`${NumCast(this.dataDoc.mass1_positionY)} m`} - { - 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" - /> -
{ - // window.open( - // "https://www.khanacademy.org/science/physics/two-dimensional-motion" - // ); - // }} - > - Velocity - {`${NumCast(this.dataDoc.mass1_velocityX)} m/s`} - { - this.dataDoc.mass1_velocityXstart = value; - this._simReset++; - })} - small - mode="Freeform" - /> - {this.dataDoc.mass1_velocityY + ''} m/s - { - this.dataDoc.mass1_velocityYstart = -value; - }} - small - mode="Freeform" - /> -
{ - // window.open( - // "https://www.khanacademy.org/science/physics/two-dimensional-motion" - // ); - // }} - > - Acceleration - - {this.dataDoc.mass1_accelerationX + ''} m/s2 - - {this.dataDoc.mass1_accelerationY + ''} m/s2 -
- Momentum - {Math.round(NumCast(this.dataDoc.mass1_velocityX) * this.mass1 * 10) / 10} kg*m/s{Math.round(NumCast(this.dataDoc.mass1_velocityY) * this.mass1 * 10) / 10} kg*m/s
- )} - {this.simulationMode === 'Freeform' && this.simulationType === 'Pulley' && ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Blue WeightXY
- Position - {`${this.dataDoc.mass2_positionX} m`}{`${this.dataDoc.mass2_positionY} m`}
- Velocity - {`${this.dataDoc.mass2_positionX} m/s`}{`${this.dataDoc.mass2_positionY} m/s`}
- Acceleration - - {this.dataDoc.mass2_accelerationX + ''} m/s2 - - {this.dataDoc.mass2_accelerationY + ''} m/s2 -
- Momentum - {Math.round(NumCast(this.dataDoc.mass2_velocityX) * this.mass1 * 10) / 10} kg*m/s{Math.round(NumCast(this.dataDoc.mass2_velocityY) * this.mass1 * 10) / 10} kg*m/s
- )} -
- {this.simulationType !== 'Pendulum' && this.simulationType !== 'Spring' && ( -
-

Kinematic Equations

-
    -
  • - Position: x1=x0+v0t+ - 1⁄ - 2at - 2 -
  • -
  • - Velocity: v1=v0+at -
  • -
  • Acceleration: a = F/m
  • -
-
- )} - {this.simulationType === 'Spring' && ( -
-

Harmonic Motion Equations: Spring

-
    -
  • - Spring force: Fs=kd -
  • -
  • - Spring period: Ts=2π√m⁄ - k -
  • -
  • Equilibrium displacement for vertical spring: d = mg/k
  • -
  • - Elastic potential energy: Us=1⁄ - 2kd2 -
  • -
      -
    • Maximum when system is at maximum displacement, 0 when system is at 0 displacement
    • -
    -
  • - Translational kinetic energy: K=1⁄ - 2mv2 -
  • -
      -
    • Maximum when system is at maximum/minimum velocity (at 0 displacement), 0 when velocity is 0 (at maximum displacement)
    • -
    -
-
- )} - {this.simulationType === 'Pendulum' && ( -
-

Harmonic Motion Equations: Pendulum

-
    -
  • - Pendulum period: Tp=2π√l⁄ - g -
  • -
-
- )} -
-
-
- - - - - - - - - -

- {this.simulationType === 'Circular Motion' ? 'Z' : 'Y'} -

-

- X -

-
+
+ (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} + + + + + + + + + + + + + {this._hideUI ? null : } + + {DocButtonState.Instance.LinkEditorDocView ? ( + { + DocButtonState.Instance.LinkEditorDocView = undefined; + })} + docView={DocButtonState.Instance.LinkEditorDocView} + /> + ) : null} + {LinkInfo.Instance?.LinkInfo ? ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ) : null} + {((page: string) => { + // prettier-ignore + switch (page) { + case 'home': return ; + case 'dashboard': + default: return (<> +
+ +
+ {this.mainDashboardArea} + ); + } + })(Doc.ActivePage)} + + + + + + + + + + + + + {this.snapLines} + + + +
); } } -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, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => { + createDrawing = (e: React.PointerEvent, 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 { @observable private _size: number = 200; @observable private _autoColor: boolean = true; @observable private _regenInput: string = ''; - private _addFunc: (e: React.PointerEvent, strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void = () => {}; + private _addFunc: (e: React.PointerEvent, 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, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void, deleteFunc: (doc?: Doc) => void) => { + displaySmartDrawHandler = (x: number, y: number, addFunc: (e: React.PointerEvent, 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, 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, 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 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') 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 Date: Tue, 2 Jul 2024 17:00:12 -0400 Subject: fixed displaying marquee groups when Bounds isn't set. --- src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 23cf487ec..5aff3ed6f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -375,8 +375,8 @@ export class MarqueeView extends ObservableReactComponent