import "./PhysicsSimulationBox.scss"; import { FieldView, FieldViewProps } from './FieldView'; import React = require('react'); import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { observer } from 'mobx-react'; import "./PhysicsSimulationBox.scss"; import Weight from "./PhysicsSimulationWeight"; import Wall from "./PhysicsSimulationWall" import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CheckBox } from "../search/CheckBox"; import PauseIcon from "@mui/icons-material/Pause"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import ReplayIcon from "@mui/icons-material/Replay"; import QuestionMarkIcon from "@mui/icons-material/QuestionMark"; import ArrowLeftIcon from "@mui/icons-material/ArrowLeft"; import ArrowRightIcon from "@mui/icons-material/ArrowRight"; import EditIcon from "@mui/icons-material/Edit"; import EditOffIcon from "@mui/icons-material/EditOff"; import { Box, Button, Checkbox, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack, } from "@mui/material"; import Typography from "@mui/material/Typography"; import React, { useEffect, useState } from "react"; import "./App.scss"; import { InputField } from "./InputField"; import questions from "./PhysicsSimulationQuestions.json"; import tutorials from "./PhysicsSimulationTutorial.json"; import { IWallProps, Wall } from "./Wall"; import { IForce, Weight } from "./Weight"; 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 default class PhysicsSimulationBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PhysicsSimulationBox, fieldKey); } constructor(props: any) { super(props); } // Constants const xMin = 0; const yMin = 0; const xMax = window.innerWidth * 0.7; const yMax = window.innerHeight * 0.8; const color = `rgba(0,0,0,0.5)`; const radius = 50; const wallPositions: IWallProps[] = []; componentDidMount() { this.wallPositions.push({ length: 70, xPos: 0, yPos: 0, angleInDegrees: 0 }); this.wallPositions.push({ length: 70, xPos: 0, yPos: 80, angleInDegrees: 0 }); this.wallPositions.push({ length: 80, xPos: 0, yPos: 0, angleInDegrees: 90 }); this.wallPositions.push({ length: 80, xPos: 69.5, yPos: 0, angleInDegrees: 90 }); // Used throughout sims this.xMax = this.layoutDoc._width; this.yMax = this.layoutDoc._height; this.radius = 0.1*this.layoutDoc._height; this.dataDoc.reviewCoefficient = this.dataDoc.reviewCoefficient ?? 0; this.dataDoc.questionVariables = this.dataDoc.questionVariables ?? []; this.dataDoc.accelerationXDisplay = this.dataDoc.accelerationXDisplay ?? 0; this.dataDoc.accelerationYDisplay = this.dataDoc.accelerationYDisplay ?? 0; this.dataDoc.componentForces = this.dataDoc.componentForces ?? []; this.dataDoc.displayChange = this.dataDoc.displayChange ?? { xDisplay: 0, yDisplay: 0 }; this.dataDoc.elasticCollisions = this.dataDoc.elasticCollisions ?? false; this.dataDoc.gravity = this.dataDoc.gravity ?? -9.81; this.dataDoc.mass = this.dataDoc.mass ?? 1; this.dataDoc.mode = this.dataDoc.mode ?? "Freeform"; this.dataDoc.positionXDisplay = this.dataDoc.positionXDisplay ?? 0; this.dataDoc.positionYDisplay = this.dataDoc.positionYDisplay ?? 0; this.dataDoc.resetAll = this.dataDoc.resetAll ?? true; this.dataDoc.showAcceleration = this.dataDoc.showAcceleration ?? false; this.dataDoc.showComponentForces = this.dataDoc.showComponentForces ?? false; this.dataDoc.showForces = this.dataDoc.showForces ?? true; this.dataDoc.showForceMagnitudes = this.dataDoc.showForceMagnitudes ?? true; this.dataDoc.showVelocity = this.dataDoc.showVelocity ?? false; this.dataDoc.simulationPaused = this.dataDoc.simulationPaused ?? true; this.dataDoc.simulationReset = this.dataDoc.simulationReset ?? false; this.dataDoc.simulationSpeed = this.dataDoc.simulationSpeed ?? 2; this.dataDoc.simulationType = this.dataDoc.simulationType ?? "Inclined Plane"; this.dataDoc.startForces = this.dataDoc.startForces ?? []; this.dataDoc.startPosX = this.dataDoc.startPosX ?? 0; this.dataDoc.startPosY = this.dataDoc.startPosY ?? 0; this.dataDoc.startVelX = this.dataDoc.startVelX ?? 0; this.dataDoc.startVelY = this.dataDoc.startVelY ?? 0; this.dataDoc.stepNumber = this.dataDoc.stepNumber ?? 0; this.dataDoc.timer = this.dataDoc.timer ?? 0; this.dataDoc.updatedForces = this.dataDoc.updatedForces ?? []; this.dataDoc.velocityXDisplay = this.dataDoc.velocityXDisplay ?? 0; this.dataDoc.velocityYDisplay = this.dataDoc.velocityYDisplay ?? 0; // Used for review mode this.dataDoc.answerInputFields = this.dataDoc.answerInputFields ??
; this.dataDoc.currentForceSketch = this.dataDoc.currentForceSketch ?? null; this.dataDoc.deleteMode = this.dataDoc.deleteMode ?? false; this.dataDoc.forceSketches = this.dataDoc.forceSketches ?? []; this.dataDoc.hintDialogueOpen = this.dataDoc.hintDialogueOpen ?? false; this.dataDoc.noMovement = this.dataDoc.noMovement ?? false; this.dataDoc.questionNumber = this.dataDoc.questionNumber ?? 0; this.dataDoc.questionPartOne = this.dataDoc.questionPartOne ?? ""; this.dataDoc.questionPartTwo = this.dataDoc.questionPartTwo ?? ""; this.dataDoc.reviewGravityAngle = this.dataDoc.reviewGravityAngle ?? 0; this.dataDoc.reviewGravityMagnitude = this.dataDoc.reviewGravityMagnitude ?? 0; this.dataDoc.reviewNormalAngle = this.dataDoc.reviewNormalAngle ?? 0; this.dataDoc.reviewNormalMagnitude = this.dataDoc.reviewNormalMagnitude ?? 0; this.dataDoc.reviewStaticAngle = this.dataDoc.reviewStaticAngle ?? 0; this.dataDoc.reviewStaticMagnitude = this.dataDoc.reviewStaticMagnitude ?? 0; this.dataDoc.selectedSolutions = this.dataDoc.selectedSolutions ?? []; this.dataDoc.selectedQuestion = this.dataDoc.selectedQuestion ?? questions.inclinePlane[0]; this.dataDoc.sketching = this.dataDoc.sketching ?? false; // Used for tutorial mode this.dataDoc.selectedTutorial = this.dataDoc.selectedTutorial ?? tutorials.inclinePlane; // Used for uniform circular motion this.dataDoc.circularMotionRadius = this.dataDoc.circularMotionRadius ?? 150; // Used for spring simulation this.dataDoc.springConstant = this.dataDoc.springConstant ?? 0.5; this.dataDoc.springRestLength = this.dataDoc.springRestLength ?? 200; this.dataDoc.springStartLength = this.dataDoc.springStartLength ?? 200; // Used for pendulum simulation this.dataDoc.adjustPendulumAngle = this.dataDoc.adjustPendulumAngle ?? { angle: 0, length: 0 }; this.dataDoc.pendulumAngle = this.dataDoc.pendulumAngle ?? 0; this.dataDoc.pendulumLength = this.dataDoc.pendulumLength ?? 300; this.dataDoc.startPendulumAngle = this.dataDoc.startPendulumAngle ?? 0; // Used for wedge simulation this.dataDoc.coefficientOfKineticFriction = this.dataDoc.coefficientOfKineticFriction ?? 0; this.dataDoc.coefficientOfStaticFriction = this.dataDoc.coefficientOfStaticFriction ?? 0; this.dataDoc.wedgeAngle = this.dataDoc.wedgeAngle ?? 26; this.dataDoc.wedgeHeight = this.dataDoc.wedgeHeight ?? Math.tan((26 * Math.PI) / 180) * 400; this.dataDoc.wedgeWidth = this.dataDoc.wedgeWidth ?? 400; // Used for pulley simulation this.dataDoc.positionXDisplay2 = this.dataDoc.positionXDisplay2 ?? 0; this.dataDoc.velocityXDisplay2 = this.dataDoc.velocityXDisplay2 ?? 0; this.dataDoc.accelerationXDisplay2 = this.dataDoc.accelerationXDisplay2 ?? 0; this.dataDoc.positionYDisplay2 = this.dataDoc.positionYDisplay2 ?? 0; this.dataDoc.velocityYDisplay2 = this.dataDoc.velocityYDisplay2 ?? 0; this.dataDoc.accelerationYDisplay2 = this.dataDoc.accelerationYDisplay2 ?? 0; this.dataDoc.startPosX2 = this.dataDoc.startPosX2 ?? 0; this.dataDoc.startPosY2 = this.dataDoc.startPosY2 ?? 0; this.dataDoc.displayChange2 = this.dataDoc.displayChange2 ?? { xDisplay: 0, yDisplay: 0 }; this.dataDoc.startForces2 = this.dataDoc.startForces2 ?? []; this.dataDoc.updatedForces2 = this.dataDoc.updatedForces2 ?? []; this.dataDoc.mass2 = this.dataDoc.mass2 ?? 1; } componentDidUpdate() { this.xMax = this.layoutDoc._width; this.yMax = this.layoutDoc._height; this.radius = 0.1*this.layoutDoc._height; } // Helper function to go between display and real values getDisplayYPos = (yPos: number) => { return this.yMax - this.dataDoc.yPos - 2 * this.radius + 5; }; getYPosFromDisplay = (yDisplay: number) => { return this.yMax - this.dataDoc.yDisplay - 2 * this.radius + 5; }; // Update forces when coefficient of static friction changes in freeform mode updateForcesWithFriction = ( coefficient: number, width: number = this.wedgeWidth, height: number = this.wedgeHeight ) => { const normalForce: IForce = { description: "Normal Force", magnitude: Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass, directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI, component: false, }; let frictionForce: IForce = { description: "Static Friction Force", magnitude: coefficient * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass, directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI, component: false, }; // reduce magnitude or friction force if necessary such that block cannot slide up plane let yForce = -Math.abs(this.gravity) * this.mass; 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.mass) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); } const frictionForceComponent: IForce = { description: "Static Friction Force", magnitude: coefficient * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)), directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI, component: true, }; const normalForceComponent: IForce = { description: "Normal Force", magnitude: Math.abs(this.gravity) * Math.cos(Math.atan(height / width)), directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI, component: true, }; const gravityParallel: IForce = { description: "Gravity Parallel Component", magnitude: this.mass * Math.abs(this.gravity) * Math.sin(Math.PI / 2 - Math.atan(height / width)), directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI + 180, component: true, }; const gravityPerpendicular: IForce = { description: "Gravity Perpendicular Component", magnitude: this.mass * Math.abs(this.gravity) * Math.cos(Math.PI / 2 - Math.atan(height / width)), directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI, component: true, }; const gravityForce: IForce = { description: "Gravity", magnitude: this.mass * Math.abs(this.gravity), directionInDegrees: 270, component: false, }; if (coefficient != 0) { this.dataDoc.startForces = [gravityForce, normalForce, frictionForce]; this.dataDoc.updatedForces = [gravityForce, normalForce, frictionForce]; this.dataDoc.componentForces = [ frictionForceComponent, normalForceComponent, gravityParallel, gravityPerpendicular, ]; } else { this.dataDoc.startForces = [gravityForce, normalForce]; this.dataDoc.updatedForces = [gravityForce, normalForce]; this.dataDoc.componentForces = [ normalForceComponent, gravityParallel, gravityPerpendicular, ]; } }; // Change wedge height and width and weight position to match new wedge angle changeWedgeBasedOnNewAngle = (angle: number) => { let width = 0; let height = 0; if (angle < 50) { width = 400; height = Math.tan((angle * Math.PI) / 180) * 400; this.dataDoc.wedgeWidth = width; this.dataDoc.wedgeHeight = height; } else if (angle < 70) { width = 200; height = Math.tan((angle * Math.PI) / 180) * 200; this.dataDoc.wedgeWidth = width; this.dataDoc.wedgeHeight = height; } else { width = 100; height = Math.tan((angle * Math.PI) / 180) * 100; this.dataDoc.wedgeWidth = width; this.dataDoc.wedgeHeight = height; } // update weight position based on updated wedge width/height let yPos = (width - this.radius) * Math.tan((angle * Math.PI) / 180); if (angle < 40) { yPos += Math.sqrt(angle); } else if (angle < 58) { yPos += angle / 2; } else if (angle < 68) { yPos += angle; } else if (angle < 70) { yPos += angle * 1.3; } else if (angle < 75) { yPos += angle * 1.5; } else if (angle < 78) { yPos += angle * 2; } else if (angle < 79) { yPos += angle * 2.25; } else if (angle < 80) { yPos += angle * 2.6; } else { yPos += angle * 3; } this.dataDoc.startPosX = Math.round((this.xMax * 0.5 - 200) * 10) / 10; this.dataDoc.startPosY = this.getDisplayYPos(yPos); if (this.dataDoc.mode == "Freeform") { this.updateForcesWithFriction( Number(this.dataDoc.coefficientOfStaticFriction), width, height ); } }; // In review mode, update forces when coefficient of static friction changed updateReviewForcesBasedOnCoefficient = (coefficient: number) => { let theta: number = Number(this.dataDoc.wedgeAngle); let index = this.dataDoc.selectedQuestion.variablesForQuestionSetup.indexOf("theta - max 45"); if (index >= 0) { theta = this.dataDoc.questionVariables[index]; } if (isNaN(theta)) { return; } this.dataDoc.reviewGravityMagnitude = (Math.abs(this.dataDoc.gravity)); this.dataDoc.reviewGravityAngle = (270); this.dataDoc.reviewNormalMagnitude = ( Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180) ); this.dataDoc.reviewNormalAngle = (90 - theta); let yForce = -Math.abs(this.dataDoc.gravity); yForce += Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((90 - theta) * Math.PI) / 180); yForce += coefficient * Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((180 - theta) * Math.PI) / 180); let friction = coefficient * Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180); if (yForce > 0) { friction = (-(Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180)) * Math.sin(((90 - theta) * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin(((180 - theta) * Math.PI) / 180); } this.dataDoc.reviewStaticMagnitude = (friction); this.dataDoc.reviewStaticAngle = (180 - theta); }; // In review mode, update forces when wedge angle changed updateReviewForcesBasedOnAngle = (angle: number) => { this.dataDoc.reviewGravityMagnitude = (Math.abs(this.dataDoc.gravity)); this.dataDoc.reviewGravityAngle = (270); this.dataDoc.reviewNormalMagnitude = ( Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180) ); this.dataDoc.reviewNormalAngle = (90 - angle); let yForce = -Math.abs(this.dataDoc.gravity); yForce += Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180) * Math.sin(((90 - Number(angle)) * Math.PI) / 180); yForce += this.dataDoc.reviewCoefficient * Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180) * Math.sin(((180 - Number(angle)) * Math.PI) / 180); let friction = this.dataDoc.reviewCoefficient * Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180); if (yForce > 0) { friction = (-(Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180)) * Math.sin(((90 - Number(angle)) * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin(((180 - Number(angle)) * Math.PI) / 180); } this.dataDoc.reviewStaticMagnitude = (friction); this.dataDoc.reviewStaticAngle = (180 - angle); }; render () { }