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