From 7b2553514bb000eb7f618eb0f0d653baee78742c Mon Sep 17 00:00:00 2001 From: Eric Date: Sun, 13 Aug 2023 15:43:53 -0400 Subject: 1 --- .../nodes/PhysicsBox/PhysicsSimulationBox.tsx | 1987 ++++++++++++++++++++ 1 file changed, 1987 insertions(+) create mode 100644 src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx (limited to 'src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx') 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() { + 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(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(vars); + this.dataDoc.selectedQuestion = JSON.stringify(question); + this.dataDoc.questionPartOne = q; + this.dataDoc.questionPartTwo = question.question; + this.dataDoc.answers = new List(this.getAnswersToQuestion(question, vars)); + //this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset); + }; + + // 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 ( +
+
+
+
+
+ {!this.dataDoc.simulation_paused && ( +
+ +
+ )} +
+
+ + {this.simulationType == 'Pulley' && ( + + )} +
+
+ {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane') && + this.wallPositions?.map((element, index) => )} +
+
+
+
this.props.isContentActive() && e.stopPropagation()} + style={{ overflow: 'auto', height: `${Math.max(1, 800 / this.props.PanelWidth()) * 100}%`, transform: `scale(${Math.min(1, this.props.PanelWidth() / 850)})` }}> +
+ + {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + (this.dataDoc.simulation_paused = false)}> + {/* */} + + )} + {!this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + (this.dataDoc.simulation_paused = true)}> + {/* */} + + )} + {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + this._simReset++)}> + + + )} + +
+ +
+
+ +
+
+ {this.simulationMode == 'Review' && this.simulationType != 'Inclined Plane' && ( +
+

+ <>{this.simulationType} review problems in progress! +

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

{this.questionPartOne}

+

{this.questionPartTwo}

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

} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_GravityMagnitude" + step={0.1} + unit={'N'} + upperBound={50} + value={NumCast(this.dataDoc.review_GravityMagnitude)} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of gravity')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('angle of gravity') && ( + Gravity angle

} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_GravityAngle" + step={1} + unit={'°'} + upperBound={360} + value={NumCast(this.dataDoc.review_GravityAngle)} + radianEquivalent={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') && ( + Normal force magnitude

} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_NormalMagnitude" + step={0.1} + unit={'N'} + upperBound={50} + value={NumCast(this.dataDoc.review_NormalMagnitude)} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('normal force')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('angle of normal force') && ( + Normal force angle

} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_NormalAngle" + step={1} + unit={'°'} + upperBound={360} + value={NumCast(this.dataDoc.review_NormalAngle)} + radianEquivalent={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') && ( + Static friction magnitude

} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_StaticMagnitude" + step={0.1} + unit={'N'} + upperBound={50} + value={NumCast(this.dataDoc.review_StaticMagnitude)} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of static friction')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('angle of static friction') && ( + Static friction angle

} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_StaticAngle" + step={1} + unit={'°'} + upperBound={360} + value={NumCast(this.dataDoc.review_StaticAngle)} + radianEquivalent={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') && ( + + μs + + } + lowerBound={0} + dataDoc={this.dataDoc} + prop="coefficientOfStaticFriction" + step={0.1} + unit={''} + upperBound={1} + value={NumCast(this.dataDoc.coefficientOfStaticFriction)} + effect={this.updateReviewForcesBasedOnCoefficient} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('coefficient of static friction')]} + /> + )} + {this.selectedQuestion.answerParts.includes('wedge angle') && ( + θ} + lowerBound={0} + dataDoc={this.dataDoc} + prop="wedge_angle" + step={1} + unit={'°'} + upperBound={49} + value={this.wedgeAngle} + effect={(val: number) => { + this.changeWedgeBasedOnNewAngle(val); + this.updateReviewForcesBasedOnAngle(val); + }} + radianEquivalent={true} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('wedge angle')]} + /> + )} +
+
+
+ )} + {this.simulationMode == 'Tutorial' && ( +
+
+

Problem

+

{this.tutorial.question}

+
+
+ { + let step = NumCast(this.dataDoc.tutorial_stepNumber) - 1; + step = Math.max(step, 0); + step = Math.min(step, this.tutorial.steps.length - 1); + this.dataDoc.tutorial_stepNumber = step; + this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces); + this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces); + this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude; + }} + disabled={this.dataDoc.tutorial_stepNumber == 0}> + {/* */} + +
+

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

+

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

+
+ { + let step = NumCast(this.dataDoc.tutorial_stepNumber) + 1; + step = Math.max(step, 0); + step = Math.min(step, this.tutorial.steps.length - 1); + this.dataDoc.tutorial_stepNumber = step; + this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces); + this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces); + this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude; + }} + disabled={this.dataDoc.tutorial_stepNumber === this.tutorial.steps.length - 1}> + {/* */} + +
+
+ {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') &&

Resources

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

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

+
+ + +
+
+ )} + {this.simulationMode == 'Freeform' && ( +
+ + + {this.simulationType == 'One Weight' && ( + (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />} + label="Make collisions elastic" + labelPlacement="start" + /> + )} + (this.dataDoc.simulation_showForces = !this.dataDoc.simulation_showForces)} />} + label="Show force vectors" + labelPlacement="start" + /> + {(this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && ( + (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />} + label="Show component force vectors" + labelPlacement="start" + /> + )} + (this.dataDoc.simulation_showAcceleration = !this.dataDoc.simulation_showAcceleration)} />} + label="Show acceleration vector" + labelPlacement="start" + /> + (this.dataDoc.simulation_showVelocity = !this.dataDoc.simulation_showVelocity)} />} + label="Show velocity vector" + labelPlacement="start" + /> + Speed} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit={'x'} upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth={'5em'} /> + {this.dataDoc.simulation_paused && this.simulationType != 'Circular Motion' && ( + Gravity} + lowerBound={-30} + dataDoc={this.dataDoc} + prop="gravity" + step={0.01} + unit={'m/s2'} + upperBound={0} + value={NumCast(this.dataDoc.simulation_gravity, -9.81)} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulation_paused && this.simulationType != 'Pulley' && ( + Mass} + lowerBound={1} + dataDoc={this.dataDoc} + prop="mass1" + step={0.1} + unit={'kg'} + upperBound={5} + value={this.mass1 ?? 1} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && ( + Red mass} + lowerBound={1} + dataDoc={this.dataDoc} + prop="mass1" + step={0.1} + unit={'kg'} + upperBound={5} + value={this.mass1 ?? 1} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && ( + Blue mass} + lowerBound={1} + dataDoc={this.dataDoc} + prop="mass2" + step={0.1} + unit={'kg'} + upperBound={5} + value={this.mass2 ?? 1} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulation_paused && this.simulationType == 'Circular Motion' && ( + Rod length} + lowerBound={100} + dataDoc={this.dataDoc} + prop="circularMotionRadius" + step={5} + unit={'m'} + upperBound={250} + value={this.circularMotionRadius} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + + + {this.simulationType == 'Spring' && this.dataDoc.simulation_paused && ( +
+ Spring stiffness} + lowerBound={0.1} + dataDoc={this.dataDoc} + prop="spring_constant" + step={1} + unit={'N/m'} + upperBound={500} + value={this.springConstant} + effect={action(() => this._simReset++)} + radianEquivalent={false} + mode={'Freeform'} + labelWidth={'7em'} + /> + Rest length} + lowerBound={10} + dataDoc={this.dataDoc} + prop="spring_lengthRest" + step={100} + unit="" + upperBound={500} + value={this.springLengthRest} + effect={action(() => this._simReset++)} + radianEquivalent={false} + mode="Freeform" + labelWidth={'7em'} + /> + Starting displacement} + lowerBound={-(this.springLengthRest - 10)} + dataDoc={this.dataDoc} + prop="" + step={10} + unit="" + upperBound={this.springLengthRest} + value={this.springLengthStart - this.springLengthRest} + effect={action((val: number) => { + this.dataDoc.mass1_positionYstart = this.springLengthRest + val; + this.dataDoc.spring_lengthStart = this.springLengthRest + val; + this._simReset++; + })} + radianEquivalent={false} + mode="Freeform" + labelWidth={'7em'} + /> +
+ )} + {this.simulationType == 'Inclined Plane' && this.dataDoc.simulation_paused && ( +
+ θ} + lowerBound={0} + dataDoc={this.dataDoc} + prop="wedge_angle" + step={1} + unit={'°'} + upperBound={49} + value={this.wedgeAngle} + effect={action((val: number) => { + this.changeWedgeBasedOnNewAngle(val); + this._simReset++; + })} + radianEquivalent={true} + mode={'Freeform'} + labelWidth={'2em'} + /> + + μs + + } + lowerBound={0} + dataDoc={this.dataDoc} + prop="coefficientOfStaticFriction" + step={0.1} + unit={''} + upperBound={1} + value={NumCast(this.dataDoc.coefficientOfStaticFriction) ?? 0} + effect={action((val: number) => { + this.updateForcesWithFriction(val); + if (val < NumCast(this.dataDoc.coefficientOfKineticFriction)) { + this.dataDoc.soefficientOfKineticFriction = val; + } + this._simReset++; + })} + mode={'Freeform'} + labelWidth={'2em'} + /> + + μk + + } + lowerBound={0} + dataDoc={this.dataDoc} + prop="coefficientOfKineticFriction" + step={0.1} + unit={''} + upperBound={NumCast(this.dataDoc.coefficientOfStaticFriction)} + value={NumCast(this.dataDoc.coefficientOfKineticFriction) ?? 0} + effect={action(() => this._simReset++)} + mode={'Freeform'} + labelWidth={'2em'} + /> +
+ )} + {this.simulationType == 'Inclined Plane' && !this.dataDoc.simulation_paused && ( + + <> + θ: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad +
+ μ s: {this.dataDoc.coefficientOfStaticFriction} +
+ μ k: {this.dataDoc.coefficientOfKineticFriction} + +
+ )} + {this.simulationType == 'Pendulum' && !this.dataDoc.simulation_paused && ( + + θ: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad + + )} + {this.simulationType == 'Pendulum' && this.dataDoc.simulation_paused && ( +
+ Angle} + lowerBound={0} + dataDoc={this.dataDoc} + prop="pendulum_angle" + step={1} + unit={'°'} + upperBound={59} + value={NumCast(this.dataDoc.pendulum_angle, 30)} + effect={action(value => { + this.dataDoc.pendulum_angleStart = value; + this.dataDoc.pendulum_lengthStart = this.dataDoc.pendulum_length; + if (this.simulationType == 'Pendulum') { + const mag = this.mass1 * Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180); + + const forceOfTension: IForce = { + description: 'Tension', + magnitude: mag, + directionInDegrees: 90 - value, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180), + directionInDegrees: 270 - value, + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: Math.abs(this.gravity) * Math.sin((value * Math.PI) / 180), + directionInDegrees: -value, + }; + + const length = this.pendulumLength; + const x = length * Math.cos(((90 - value) * Math.PI) / 180); + const y = length * Math.sin(((90 - value) * Math.PI) / 180); + const xPos = this.xMax / 2 - x - NumCast(this.dataDoc.radius); + const yPos = y - NumCast(this.dataDoc.radius) - 5; + this.dataDoc.mass1_positionXstart = xPos; + this.dataDoc.mass1_positionYstart = yPos; + + this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]); + this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]); + this._simReset++; + } + })} + radianEquivalent={true} + mode="Freeform" + labelWidth="5em" + /> + Rod length} + lowerBound={0} + dataDoc={this.dataDoc} + prop="pendulum_length" + step={1} + unit="m" + upperBound={400} + value={Math.round(this.pendulumLength)} + effect={action(value => { + if (this.simulationType == 'Pendulum') { + this.dataDoc.pendulum_angleStart = this.pendulumAngle; + this.dataDoc.pendulum_lengthStart = value; + this._simReset++; + } + })} + radianEquivalent={false} + mode="Freeform" + labelWidth="5em" + /> +
+ )} +
+ )} +
+ {this.simulationMode == 'Freeform' && ( + + + + + + + + + + {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && ( + + )}{' '} + {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && ( + + )}{' '} + {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && ( + + )}{' '} + {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && ( + + )}{' '} + + + + {(!this.dataDoc.simulation_paused || (this.simulationType != 'One Weight' && this.simulationType != 'Circular Motion')) && ( + + )}{' '} + {this.dataDoc.simulation_paused && (this.simulationType == 'One Weight' || this.simulationType == 'Circular Motion') && ( + + )}{' '} + {(!this.dataDoc.simulation_paused || this.simulationType != 'One Weight') && ( + + )}{' '} + {this.dataDoc.simulation_paused && this.simulationType == 'One Weight' && ( + + )}{' '} + + + + + + + + + + + + +
{this.simulationType == 'Pulley' ? 'Red Weight' : ''}XY
{ + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + Position + + <>{this.dataDoc.mass1_positionX} m + + { + this.dataDoc.mass1_xChange = value; + if (this.simulationType == 'Suspension') { + 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" + /> + {`${NumCast(this.dataDoc.mass1_positionY)} m`} + { + 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" + /> +
{ + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + Velocity + {`${NumCast(this.dataDoc.mass1_velocityX)} m/s`} + { + this.dataDoc.mass1_velocityXstart = value; + this._simReset++; + })} + small={true} + mode="Freeform" + /> + + <>{this.dataDoc.mass1_velocityY} m/s + + { + this.dataDoc.mass1_velocityYstart = -value; + }} + small={true} + mode="Freeform" + /> +
{ + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + Acceleration + + <> + {this.dataDoc.mass1_accelerationX} m/s2 + + + <> + {this.dataDoc.mass1_accelerationY} m/s2 + +
+ Momentum + {Math.round(NumCast(this.dataDoc.mass1_velocityX) * this.mass1 * 10) / 10} kg*m/s{Math.round(NumCast(this.dataDoc.mass1_velocityY) * this.mass1 * 10) / 10} kg*m/s
+ )} + {this.simulationMode == 'Freeform' && this.simulationType == 'Pulley' && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Blue WeightXY
+ Position + {`${this.dataDoc.mass2_positionX} m`}{`${this.dataDoc.mass2_positionY} m`}
+ Velocity + {`${this.dataDoc.mass2_positionX} m/s`}{`${this.dataDoc.mass2_positionY} m/s`}
+ Acceleration + + <> + {this.dataDoc.mass2_accelerationX} m/s2 + + + <> + {this.dataDoc.mass2_accelerationY} m/s2 + +
+ Momentum + {Math.round(NumCast(this.dataDoc.mass2_velocityX) * this.mass1 * 10) / 10} kg*m/s{Math.round(NumCast(this.dataDoc.mass2_velocityY) * this.mass1 * 10) / 10} kg*m/s
+ )} +
+ {this.simulationType != 'Pendulum' && this.simulationType != 'Spring' && ( +
+

Kinematic Equations

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

Harmonic Motion Equations: Spring

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

Harmonic Motion Equations: Pendulum

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

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

+

+ X +

+
+
+ ); + } +} -- cgit v1.2.3-70-g09d2