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