aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx')
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx1987
1 files changed, 1987 insertions, 0 deletions
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
new file mode 100644
index 000000000..2c06282ed
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
@@ -0,0 +1,1987 @@
+// 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 { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import { NumListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from './../FieldView';
+import './PhysicsSimulationBox.scss';
+import InputField from './PhysicsSimulationInputField';
+import * as questions from './PhysicsSimulationQuestions.json';
+import * as tutorials from './PhysicsSimulationTutorial.json';
+import Wall from './PhysicsSimulationWall';
+import Weight from './PhysicsSimulationWeight';
+import React = require('react');
+
+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 PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(PhysicsSimulationBox, fieldKey);
+ }
+
+ _widthDisposer: IReactionDisposer | undefined;
+ @observable _simReset = 0;
+
+ // semi-Constants
+ xMin = 0;
+ yMin = 0;
+ xMax = this.props.PanelWidth() * 0.6;
+ yMax = this.props.PanelHeight();
+ color = `rgba(0,0,0,0.5)`;
+ radius = 50;
+ wallPositions: IWallProps[] = [];
+
+ @computed get circularMotionRadius() {
+ return (NumCast(this.dataDoc.circularMotionRadius, 150) * this.props.PanelWidth()) / 1000;
+ }
+ @computed get gravity() {
+ return NumCast(this.dataDoc.simulation_gravity, -9.81);
+ }
+ @computed get simulationType() {
+ return StrCast(this.dataDoc.simulation_type, 'Inclined Plane');
+ }
+ @computed get simulationMode() {
+ return StrCast(this.dataDoc.simulation_mode, 'Freeform');
+ }
+ // Used for spring simulation
+ @computed get springConstant() {
+ return NumCast(this.dataDoc.spring_constant, 0.5);
+ }
+ @computed get springLengthRest() {
+ return NumCast(this.dataDoc.spring_lengthRest, 200);
+ }
+ @computed get springLengthStart() {
+ return NumCast(this.dataDoc.spring_lengthStart, 200);
+ }
+
+ @computed get pendulumAngle() {
+ return NumCast(this.dataDoc.pendulum_angle);
+ }
+ @computed get pendulumAngleStart() {
+ return NumCast(this.dataDoc.pendulum_angleStart);
+ }
+ @computed get pendulumLength() {
+ return NumCast(this.dataDoc.pendulum_length);
+ }
+ @computed get pendulumLengthStart() {
+ return NumCast(this.dataDoc.pendulum_lengthStart);
+ }
+
+ // Used for wedge simulation
+ @computed get wedgeAngle() {
+ return NumCast(this.dataDoc.wedge_angle, 26);
+ }
+ @computed get wedgeHeight() {
+ return NumCast(this.dataDoc.wedge_height, Math.tan((26 * Math.PI) / 180) * this.xMax * 0.5);
+ }
+ @computed get wedgeWidth() {
+ return NumCast(this.dataDoc.wedge_width, this.xMax * 0.5);
+ }
+ @computed get mass1() {
+ return NumCast(this.dataDoc.mass1, 1);
+ }
+ @computed get mass2() {
+ return NumCast(this.dataDoc.mass2, 1);
+ }
+
+ @computed get mass1Radius() {
+ return NumCast(this.dataDoc.mass1_radius, 30);
+ }
+ @computed get mass1PosXStart() {
+ return NumCast(this.dataDoc.mass1_positionXstart);
+ }
+ @computed get mass1PosYStart() {
+ return NumCast(this.dataDoc.mass1_positionYstart);
+ }
+ @computed get mass1VelXStart() {
+ return NumCast(this.dataDoc.mass1_velocityXstart);
+ }
+ @computed get mass1VelYStart() {
+ return NumCast(this.dataDoc.mass1_velocityYstart);
+ }
+
+ @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() {
+ // Setup and update simulation
+ this._widthDisposer = reaction(() => [this.props.PanelWidth(), this.props.PanelHeight()], this.setupSimulation, { fireImmediately: true });
+
+ // 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 },
+ ];
+ }
+
+ componentDidUpdate() {
+ 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();
+ }
+ }
+
+ gravityForce = (mass: number): IForce => ({
+ description: 'Gravity',
+ magnitude: mass * Math.abs(this.gravity),
+ directionInDegrees: 270,
+ });
+
+ @action
+ setupSimulation = () => {
+ const simulationType = this.simulationType;
+ 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;
+ }
+ 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
+ }
+ } 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;
+ }
+
+ 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;
+ }
+ this._simReset++;
+ }
+ };
+
+ // Helper function to go between display and real values
+ getDisplayYPos = (yPos: number) => this.yMax - yPos - 2 * this.mass1Radius + 5;
+ getYPosFromDisplay = (yDisplay: number) => this.yMax - yDisplay - 2 * this.mass1Radius + 5;
+
+ // 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,
+ };
+ let 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);
+ }
+
+ 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]);
+ }
+ };
+
+ // 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
+ let yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius;
+ let 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);
+ }
+ };
+
+ // In review mode, update forces when coefficient of static friction changed
+ updateReviewForcesBasedOnCoefficient = (coefficient: number) => {
+ let theta = this.wedgeAngle;
+ let 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;
+ };
+
+ // 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;
+ };
+
+ // Solve for the correct answers to the generated problem
+ getAnswersToQuestion = (question: QuestionTemplate, questionVars: number[]) => {
+ const solutions: number[] = [];
+
+ 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];
+ }
+
+ 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') {
+ let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
+ let normalForceAngle = 90 - theta;
+ let frictionForceAngle = 180 - theta;
+ let 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') {
+ let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
+ let normalForceAngle = 90 - theta;
+ let frictionForceAngle = 180 - theta;
+ let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+ let 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;
+ };
+
+ // In review mode, check if input answers match correct answers and optionally generate alert
+ checkAnswers = (showAlert: boolean = true) => {
+ let error: boolean = false;
+ let 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;
+ }
+ };
+
+ // 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;
+ };
+
+ // In review mode, reset problem variables and generate a new question
+ generateNewQuestion = () => {
+ this.resetReviewValuesToDefault();
+
+ const vars: number[] = [];
+ let question: QuestionTemplate = questions.inclinePlane[0];
+
+ if (this.simulationType === 'Inclined Plane') {
+ this.dataDoc.questionNumber = (NumCast(this.dataDoc.questionNumber) + 1) % questions.inclinePlane.length;
+ question = questions.inclinePlane[NumCast(this.dataDoc.questionNumber)];
+
+ let coefficient = 0;
+ let wedge_angle = 0;
+
+ for (let i = 0; i < question.variablesForQuestionSetup.length; i++) {
+ if (question.variablesForQuestionSetup[i] == 'theta - max 45') {
+ let randValue = Math.floor(Math.random() * 44 + 1);
+ vars.push(randValue);
+ wedge_angle = randValue;
+ } else if (question.variablesForQuestionSetup[i] == 'coefficient of static friction') {
+ let randValue = Math.round(Math.random() * 1000) / 1000;
+ vars.push(randValue);
+ coefficient = randValue;
+ }
+ }
+ this.dataDoc.wedge_angle = wedge_angle;
+ this.changeWedgeBasedOnNewAngle(wedge_angle);
+ this.dataDoc.coefficientOfStaticFriction = coefficient;
+ this.dataDoc.review_Coefficient = coefficient;
+ }
+ let q = '';
+ for (let i = 0; i < question.questionSetup.length; i++) {
+ q += question.questionSetup[i];
+ if (i != question.questionSetup.length - 1) {
+ q += vars[i];
+ if (question.variablesForQuestionSetup[i].includes('theta')) {
+ q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)';
+ }
+ }
+ }
+ 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);
+ };
+
+ // 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;
+ let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+ let 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++;
+ };
+
+ setupInclinedPlane = () => {
+ this.changeWedgeBasedOnNewAngle(this.wedgeAngle);
+ this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
+ this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction));
+ };
+
+ // 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,
+ };
+
+ 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;
+ };
+
+ // 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++;
+ };
+
+ // Default setup for suspension simulation
+ @action
+ setupSuspension = () => {
+ let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+ let 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;
+ let 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++;
+ };
+
+ // 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]);
+
+ 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++;
+ };
+
+ public static parseJSON(json: string) {
+ return !json ? [] : (JSON.parse(json) as IForce[]);
+ }
+
+ // 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]);
+ };
+
+ 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="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={true} 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={true}
+ 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={true}
+ 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={true}
+ 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={true}
+ 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={true}
+ 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={true}
+ 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') {
+ let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+ let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+ let deltaX1 = value + this.radius - x1rod;
+ let deltaX2 = x2rod - (value + this.radius);
+ let deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius;
+ let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+ let dir2T = Math.atan(deltaY / deltaX2);
+ let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+ let 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={true}
+ 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') {
+ let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+ let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+ let deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod;
+ let deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius);
+ let deltaY = this.getYPosFromDisplay(value) + this.radius;
+ let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+ let dir2T = Math.atan(deltaY / deltaX2);
+ let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+ let 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={true}
+ 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={true}
+ 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={true}
+ 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>
+ );
+ }
+}