diff options
Diffstat (limited to 'src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx')
-rw-r--r-- | src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx | 1674 |
1 files changed, 1674 insertions, 0 deletions
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx new file mode 100644 index 000000000..88af37791 --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx @@ -0,0 +1,1674 @@ +import { Doc } from '../../../../fields/Doc'; +import React = require('react'); +import "./PhysicsSimulationBox.scss"; + +interface IWallProps { + length: number; + xPos: number; + yPos: number; + angleInDegrees: number; +} +interface IForce { + description: string; + magnitude: number; + directionInDegrees: number; + component: boolean; +} +export interface IWeightProps { + dataDoc: Doc; + layoutDoc: Doc; + adjustPendulumAngle: { angle: number; length: number }; + circularMotionRadius: number; + coefficientOfKineticFriction: number; + color: string; + componentForces: IForce[]; + displayXVelocity: number; + displayYVelocity: number; + elasticCollisions: boolean; + gravity: number; + mass: number; + mode: string; + noMovement: boolean; + paused: boolean; + pendulumAngle: number; + pendulumLength: number; + radius: number; + reset: boolean; + showAcceleration: boolean; + showComponentForces: boolean; + showForceMagnitudes: boolean; + showForces: boolean; + showVelocity: boolean; + simulationSpeed: number; + simulationType: string; + springConstant: number; + springRestLength: number; + springStartLength: number; + startForces: IForce[]; + startPendulumAngle: number; + startPosX: number; + startPosY: number; + startVelX: number; + startVelY: number; + timestepSize: number; + updateDisplay: { xDisplay: number; yDisplay: number }; + updatedForces: IForce[]; + wallPositions: IWallProps[]; + wedgeHeight: number; + wedgeWidth: number; + xMax: number; + xMin: number; + yMax: number; + yMin: number; +} + +interface IState { + angleLabel: number, + clickPositionX: number, + clickPositionY: number, + coordinates: string, + dragging: boolean, + kineticFriction: boolean, + maxPosYConservation: number, + timer: number, + updatedStartPosX: any, + updatedStartPosY: any, + walls: IWallProps[], + xPosition: any, + xVelocity: number, + yPosition: any, + yVelocity: number, + xAccel: number, + yAccel: number, +} + +export default class Weight extends React.Component<IWeightProps, IState> { + + constructor(props: any) { + super(props) + this.state = { + angleLabel: 0, + clickPositionX: 0, + clickPositionY: 0, + coordinates: "", + dragging: false, + kineticFriction: false, + maxPosYConservation: 0, + timer: 0, + updatedStartPosX: this.props.startPosX ?? 0, + updatedStartPosY: this.props.startPosY ?? 0, + walls: [], + xPosition: this.props.startPosX ?? 0, + xVelocity: this.props.startVelX ? this.props.startVelX: 0, + yPosition: this.props.startPosY ?? 0, + yVelocity: this.props.startVelY ? this.props.startVelY: 0, + xAccel: 0, + yAccel: 0, + } + } + + componentDidMount() { + // Timer for animating the simulation + setInterval(() => { + this.setState({timer: this.state.timer + 1}); + }, 50); + } + + // Constants + draggable = + this.props.dataDoc['simulationType'] != "Inclined Plane" && + this.props.dataDoc['simulationType'] != "Pendulum" && + this.props.dataDoc['mode'] == "Freeform"; + epsilon = 0.0001; + labelBackgroundColor = `rgba(255,255,255,0.5)`; + + // Variables + weightStyle = { + alignItems: "center", + backgroundColor: this.props.color, + borderColor: "black", + borderRadius: 50 + "%", + borderStyle: "solid", + display: "flex", + height: 2 * this.props.radius + "px", + justifyContent: "center", + left: this.props.startPosX + "px", + position: "absolute" as "absolute", + top: this.props.startPosY + "px", + touchAction: "none", + width: 2 * this.props.radius + "px", + zIndex: 5, + }; + + // Helper function to go between display and real values + getDisplayYPos = (yPos: number) => { + return this.props.yMax - yPos - 2 * this.props.radius + 5; + }; + getYPosFromDisplay = (yDisplay: number) => { + return this.props.yMax - yDisplay - 2 * this.props.radius + 5; + }; + + // Set display values based on real values + setYPosDisplay = (yPos: number) => { + const displayPos = this.getDisplayYPos(yPos); + if (this.props.color == 'red') { + this.props.dataDoc['positionYDisplay'] = Math.round(displayPos * 100) / 100 + } else { + this.props.dataDoc['positionYDisplay2'] = Math.round(displayPos * 100) / 100 + } + }; + setXPosDisplay = (xPos: number) => { + if (this.props.color == 'red') { + this.props.dataDoc['positionXDisplay'] = Math.round(xPos * 100) / 100; + } else { + this.props.dataDoc['positionXDisplay2'] = Math.round(xPos * 100) / 100;} + }; + setYVelDisplay = (yVel: number) => { + if (this.props.color == 'red') { + this.props.dataDoc['velocityYDisplay'] = (-1 * Math.round(yVel * 100)) / 100; + } else { + this.props.dataDoc['velocityYDisplay2'] = (-1 * Math.round(yVel * 100)) / 100;} + }; + setXVelDisplay = (xVel: number) => { + if (this.props.color == 'red') { + this.props.dataDoc['velocityXDisplay'] = Math.round(xVel * 100) / 100; + } else { + this.props.dataDoc['velocityXDisplay2'] = Math.round(xVel * 100) / 100;} + }; + + // Update display values when simulation updates + setDisplayValues = ( + xPos: number = this.state.xPosition, + yPos: number = this.state.yPosition, + xVel: number = this.state.xVelocity, + yVel: number = this.state.yVelocity + ) => { + this.setYPosDisplay(yPos); + this.setXPosDisplay(xPos); + this.setYVelDisplay(yVel); + this.setXVelDisplay(xVel); + if (this.props.color == 'red') { + this.props.dataDoc['accelerationYDisplay'] = + (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100 + ; + this.props.dataDoc['accelerationXDisplay'] = + Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100 + ; + } else { + this.props.dataDoc['accelerationYDisplay2'] = + (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100 + ; + this.props.dataDoc['accelerationXDisplay2'] = + Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100 + ;} + + this.setState({xAccel : (Math.round(this.getNewAccelerationX(this.props.updatedForces) * 100) / 100)}) + this.setState({yAccel : + (-1 * Math.round(this.getNewAccelerationY(this.props.updatedForces) * 100)) / 100 + }); + }; + + componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void { + // Change pendulum angle from input field + if (prevProps.adjustPendulumAngle != this.props.adjustPendulumAngle) { + let length = this.props.adjustPendulumAngle.length; + const x = + length * Math.cos(((90 - this.props.adjustPendulumAngle.angle) * Math.PI) / 180); + const y = + length * Math.sin(((90 - this.props.adjustPendulumAngle.angle) * Math.PI) / 180); + const xPos = this.props.xMax / 2 - x - this.props.radius; + const yPos = y - this.props.radius - 5; + this.setState({xPosition: xPos}) + this.setState({yPosition: yPos}) + this.setState({updatedStartPosX: xPos}) + this.setState({updatedStartPosY: yPos}) + this.props.dataDoc['pendulumAngle'] = this.props.adjustPendulumAngle.angle + this.props.dataDoc['pendulumLength'] = this.props.adjustPendulumAngle.length + } + + // When display values updated by user, update real values + if (prevProps.updateDisplay != this.props.updateDisplay) { + if (this.props.updateDisplay.xDisplay != this.state.xPosition) { + let x = this.props.updateDisplay.xDisplay; + x = Math.max(0, x); + x = Math.min(x, this.props.xMax - 2 * this.props.radius); + this.setState({updatedStartPosX: x}) + this.setState({xPosition: x}) + if (this.props.color == 'red') { + this.props.dataDoc['positionXDisplay'] = x + } else { + this.props.dataDoc['positionXDisplay2'] = x + } + } + + if (this.props.updateDisplay.yDisplay != this.getDisplayYPos(this.state.yPosition)) { + let y = this.props.updateDisplay.yDisplay; + y = Math.max(0, y); + y = Math.min(y, this.props.yMax - 2 * this.props.radius); + let coordinatePosition = this.getYPosFromDisplay(y); + this.setState({updatedStartPosY: coordinatePosition}) + this.setState({yPosition: coordinatePosition}) + if (this.props.color == 'red') { + this.props.dataDoc['positionYDisplay'] = y + } else { + this.props.dataDoc['positionYDisplay2'] = y + } + } + + if (this.props.displayXVelocity != this.state.xVelocity) { + let x = this.props.displayXVelocity; + this.setState({xVelocity: x}) + if (this.props.color == 'red') { + this.props.dataDoc['velocityXDisplay'] = x + } else { + this.props.dataDoc['velocityXDisplay2'] = x + } + } + + if (this.props.displayYVelocity != -this.state.yVelocity) { + let y = this.props.displayYVelocity; + this.setState({yVelocity: -y}) + if (this.props.color == 'red') { + this.props.dataDoc['velocityYDisplay'] = y + } else { + this.props.dataDoc['velocityYDisplay2'] = y + } + } + } + + // Prevent bug when switching between sims + if (prevProps.startForces != this.props.startForces) { + this.setState({xVelocity: this.props.startVelX}) + this.setState({yVelocity: this.props.startVelY}) + this.setDisplayValues(); + } + + // Make sure weight doesn't go above max height + if ((prevState.updatedStartPosY != this.state.updatedStartPosY || prevProps.startVelY != this.props.startVelY) && !isNaN(this.state.updatedStartPosY) && !isNaN(this.props.startVelY)){ + if (this.props.dataDoc['simulationType'] == "One Weight") { + let maxYPos = this.state.updatedStartPosY; + if (this.props.startVelY != 0) { + maxYPos -= (this.props.startVelY * this.props.startVelY) / (2 * Math.abs(this.props.gravity)); + } + if (maxYPos < 0) { + maxYPos = 0; + } + this.setState({maxPosYConservation: maxYPos}) + } + } + + // Check for collisions and update + if (!this.props.paused) { + if (prevState.timer != this.state.timer) { + if (!this.props.noMovement) { + let collisions = false; + if ( + this.props.dataDoc['simulationType'] == "One Weight" || + this.props.dataDoc['simulationType'] == "Inclined Plane" + ) { + const collisionsWithGround = this.checkForCollisionsWithGround(); + const collisionsWithWalls = this.checkForCollisionsWithWall(); + collisions = collisionsWithGround || collisionsWithWalls; + } + if (this.props.dataDoc['simulationType'] == "Pulley") { + if (this.state.yPosition <= this.props.yMin + 100 || this.state.yPosition >= this.props.yMax - 100) { + collisions = true; + } + } + if (!collisions) { + this.update(); + } + this.setDisplayValues(); + } + } + } + + // Reset everything on reset button click + if (prevProps.reset != this.props.reset) { + this.resetEverything(); + } + + // Convert from static to kinetic friction if/when weight slips on inclined plane + if (prevState.xVelocity != this.state.xVelocity) { + if ( + this.props.dataDoc['simulationType'] == "Inclined Plane" && + Math.abs(this.state.xVelocity) > 0.1 && + this.props.dataDoc['mode'] != "Review" && + !this.state.kineticFriction + ) { + this.setState({kineticFriction: true}) + const normalForce: IForce = { + description: "Normal Force", + magnitude: + this.props.mass * + Math.abs(this.props.gravity) * + Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: false, + }; + let frictionForce: IForce = { + description: "Kinetic Friction Force", + magnitude: + this.props.mass * + this.props.coefficientOfKineticFriction * + Math.abs(this.props.gravity) * + Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: false, + }; + // reduce magnitude of friction force if necessary such that block cannot slide up plane + let yForce = -Math.abs(this.props.gravity); + 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.props.gravity)) / + Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + } + + const frictionForceComponent: IForce = { + description: "Kinetic Friction Force", + + magnitude: + this.props.mass * + this.props.coefficientOfKineticFriction * + Math.abs(this.props.gravity) * + Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: true, + }; + const normalForceComponent: IForce = { + description: "Normal Force", + magnitude: + this.props.mass * + Math.abs(this.props.gravity) * + Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: true, + }; + const gravityParallel: IForce = { + description: "Gravity Parallel Component", + magnitude: + this.props.mass * + Math.abs(this.props.gravity) * + Math.sin(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 180 - + 90 - + (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI + + 180, + component: true, + }; + const gravityPerpendicular: IForce = { + description: "Gravity Perpendicular Component", + magnitude: + this.props.mass * + Math.abs(this.props.gravity) * + Math.cos(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: + 360 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + component: true, + }; + const gravityForce: IForce = { + description: "Gravity", + magnitude: this.props.mass * Math.abs(this.props.gravity), + directionInDegrees: 270, + component: false, + }; + if (this.props.coefficientOfKineticFriction != 0) { + this.props.dataDoc['updatedForces'] = ([gravityForce, normalForce, frictionForce]); + this.props.dataDoc['componentForces'] = ([ + frictionForceComponent, + normalForceComponent, + gravityParallel, + gravityPerpendicular, + ]); + } else { + this.props.dataDoc['updatedForces'] = ([gravityForce, normalForce]); + this.props.dataDoc['componentForces'] = ([ + normalForceComponent, + gravityParallel, + gravityPerpendicular, + ]); + } + } + } + + // Add/remove walls when simulation type changes + if (prevProps.simulationType != this.props.simulationType) { + let w: IWallProps[] = []; + if (this.props.dataDoc['simulationType'] == "One Weight" || this.props.dataDoc['simulationType'] == "Inclined Plane") { + w = this.props.wallPositions + } + this.setState({walls: w}) + } + + // Update x position when start pos x changes + if (prevProps.startPosX != this.props.startPosX) { + if (this.props.paused && !isNaN(this.props.startPosX)) { + this.setState({xPosition: this.props.startPosX}) + this.setState({updatedStartPosX: this.props.startPosX}) + this.setXPosDisplay(this.props.startPosX) + } + } + + // Update y position when start pos y changes TODO debug + if (prevProps.startPosY != this.props.startPosY) { + if (this.props.paused && !isNaN(this.props.startPosY)) { + this.setState({yPosition: this.props.startPosY}) + this.setState({updatedStartPosY: this.props.startPosY ?? 0}) + this.setYPosDisplay(this.props.startPosY ?? 0) + } + } + + // Update wedge coordinates + if (prevProps.wedgeWidth != this.props.wedgeWidth || prevProps.wedgeHeight != this.props.wedgeHeight) { + const left = this.props.xMax * 0.25; + const coordinatePair1 = Math.round(left) + "," + this.props.yMax + " "; + const coordinatePair2 = Math.round(left + this.props.wedgeWidth) + "," + this.props.yMax + " "; + const coordinatePair3 = Math.round(left) + "," + (this.props.yMax - this.props.wedgeHeight); + const coord = coordinatePair1 + coordinatePair2 + coordinatePair3; + this.setState({coordinates: coord}) + } + + if (this.state.xPosition != prevState.xPosition || this.state.yPosition != prevState.yPosition) { + this.weightStyle = { + alignItems: "center", + backgroundColor: this.props.color, + borderColor: "black", + borderRadius: 50 + "%", + borderStyle: "solid", + display: "flex", + height: 2 * this.props.radius + "px", + justifyContent: "center", + left: this.state.xPosition + "px", + position: "absolute" as "absolute", + top: this.state.yPosition + "px", + touchAction: "none", + width: 2 * this.props.radius + "px", + zIndex: 5, + }; + } + } + + // Reset simulation on reset button click + resetEverything = () => { + this.setState({kineticFriction: false}) + this.setState({xPosition: this.state.updatedStartPosX}) + this.setState({yPosition: this.state.updatedStartPosY}) + this.setState({xVelocity: this.props.startVelX ?? 0}) + this.setState({yVelocity: this.props.startVelY ?? 0}) + this.props.dataDoc['pendulumAngle'] = this.props.dataDoc['startPendulumAngle'] + this.props.dataDoc['updatedForces'] = (this.props.dataDoc['startForces']) + this.props.dataDoc['updatedForces2'] = (this.props.dataDoc['startForces2']) + if (this.props.color == 'red') { + this.props.dataDoc['positionXDisplay'] = this.state.updatedStartPosX + this.props.dataDoc['positionYDisplay'] = this.state.updatedStartPosY + this.props.dataDoc['velocityXDisplay'] = this.props.startVelX ?? 0 + this.props.dataDoc['velocityYDisplay'] = this.props.startVelY ?? 0 + this.props.dataDoc['accelerationXDisplay'] = 0 + this.props.dataDoc['accelerationYDisplay'] = 0 + } else { + this.props.dataDoc['positionXDisplay2'] = this.state.updatedStartPosX + this.props.dataDoc['positionYDisplay2'] = this.state.updatedStartPosY + this.props.dataDoc['velocityXDisplay2'] = this.props.startVelX ?? 0 + this.props.dataDoc['velocityYDisplay2'] = this.props.startVelY ?? 0 + this.props.dataDoc['accelerationXDisplay2'] = 0 + this.props.dataDoc['accelerationYDisplay2'] = 0 + } + this.setState({angleLabel: Math.round(this.props.dataDoc['pendulumAngle'] ?? 0 * 100) / 100}) + }; + + // Compute x acceleration from forces, F=ma + getNewAccelerationX = (forceList: IForce[]) => { + let newXAcc = 0; + if (forceList) { + forceList.forEach((force) => { + newXAcc += + (force.magnitude * + Math.cos((force.directionInDegrees * Math.PI) / 180)) / + this.props.mass; + }); + } + return newXAcc; + }; + + + // Compute y acceleration from forces, F=ma + getNewAccelerationY = (forceList: IForce[]) => { + let newYAcc = 0; + if (forceList) { + forceList.forEach((force) => { + newYAcc += + (-1 * + (force.magnitude * + Math.sin((force.directionInDegrees * Math.PI) / 180))) / + this.props.mass; + }); + } + return newYAcc; + }; + + // Compute uniform circular motion forces given x, y positions + getNewCircularMotionForces = (xPos: number, yPos: number) => { + let deltaX = (this.props.xMin + this.props.xMax) / 2 - (xPos + this.props.radius); + let deltaY = yPos + this.props.radius - (this.props.yMin + this.props.yMax) / 2; + let dir = (Math.atan2(deltaY, deltaX) * 180) / Math.PI; + const tensionForce: IForce = { + description: "Centripetal Force", + magnitude: (this.props.startVelX ** 2 * this.props.mass) / this.props.circularMotionRadius, + directionInDegrees: dir, + component: false, + }; + return [tensionForce]; + }; + + // Compute spring forces given y position + getNewSpringForces = (yPos: number) => { + let springForce: IForce = { + description: "Spring Force", + magnitude: 0, + directionInDegrees: 90, + component: false, + }; + if (yPos - this.props.springRestLength > 0) { + springForce = { + description: "Spring Force", + magnitude: this.props.springConstant * (yPos - this.props.springRestLength), + directionInDegrees: 90, + component: false, + }; + } else if (yPos - this.props.springRestLength < 0) { + springForce = { + description: "Spring Force", + magnitude: this.props.springConstant * (this.props.springRestLength - yPos), + directionInDegrees: 270, + component: false, + }; + } + return [ + { + description: "Gravity", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: 270, + component: false, + }, + springForce, + ]; + }; + + // Compute pendulum forces given position, velocity + getNewPendulumForces = ( + xPos: number, + yPos: number, + xVel: number, + yVel: number + ) => { + const x = this.props.xMax / 2 - xPos - this.props.radius; + const y = yPos + this.props.radius + 5; + let angle = (Math.atan(y / x) * 180) / Math.PI; + if (angle < 0) { + angle += 180; + } + let oppositeAngle = 90 - angle; + if (oppositeAngle < 0) { + oppositeAngle = 90 - (180 - angle); + } + + const pendulumLength = Math.sqrt(x * x + y * y); + this.props.dataDoc['pendulumAngle'] = oppositeAngle; + + const mag = + this.props.mass * Math.abs(this.props.gravity) * Math.cos((oppositeAngle * Math.PI) / 180) + + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength; + + const forceOfTension: IForce = { + description: "Tension", + magnitude: mag, + directionInDegrees: angle, + component: false, + }; + + return [ + { + description: "Gravity", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: 270, + component: false, + }, + forceOfTension, + ]; + }; + + // Check for collisions in x direction + checkForCollisionsWithWall = () => { + let collision = false; + const minX = this.state.xPosition; + const maxX = this.state.xPosition + 2 * this.props.radius; + if (this.state.xVelocity != 0) { + this.state.walls.forEach((wall) => { + if (wall.angleInDegrees == 90) { + const wallX = (wall.xPos / 100) * this.props.layoutDoc._width; + if (wall.xPos < 0.35) { + if (minX <= wallX) { + this.setState({xPosition: wallX+0.01}); + if (this.props.elasticCollisions) { + this.setState({xVelocity: -this.state.xVelocity}); + } else { + this.setState({xVelocity: 0}); + } + collision = true; + } + } else { + if (maxX >= wallX) { + this.setState({xPosition: wallX- 2 * this.props.radius-0.01}); + if (this.props.elasticCollisions) { + this.setState({xVelocity: -this.state.xVelocity}); + } else { + this.setState({xVelocity: 0}); + } + collision = true; + } + } + } + }); + } + return collision; + }; + + // Check for collisions in y direction + checkForCollisionsWithGround = () => { + let collision = false; + const minY = this.state.yPosition; + const maxY = this.state.yPosition + 2 * this.props.radius; + if (this.state.yVelocity > 0) { + this.state.walls.forEach((wall) => { + if (wall.angleInDegrees == 0 && wall.yPos > 0.4) { + const groundY = (wall.yPos / 100) * this.props.layoutDoc._height; + if (maxY > groundY) { + this.setState({yPosition: groundY- 2 * this.props.radius-0.01}); + if (this.props.elasticCollisions) { + this.setState({yVelocity: -this.state.yVelocity}); + } else { + this.setState({yVelocity: 0}); + if (this.props.dataDoc['simulationType'] != "Two Weights") { + const forceOfGravity: IForce = { + description: "Gravity", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: 270, + component: false, + }; + const normalForce: IForce = { + description: "Normal force", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: wall.angleInDegrees + 90, + component: false, + }; + this.props.dataDoc['updatedForces'] = ([forceOfGravity, normalForce]); + if (this.props.dataDoc['simulationType'] == "Inclined Plane") { + const forceOfGravityC: IForce = { + description: "Gravity", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: 270, + component: true, + }; + const normalForceC: IForce = { + description: "Normal force", + magnitude: Math.abs(this.props.gravity) * this.props.mass, + directionInDegrees: wall.angleInDegrees + 90, + component: true, + }; + this.props.dataDoc['componentForces'] = ([forceOfGravityC, normalForceC]); + } + } + } + collision = true; + } + } + }); + } + if (this.state.yVelocity < 0) { + this.state.walls.forEach((wall) => { + if (wall.angleInDegrees == 0 && wall.yPos < 0.4) { + const groundY = (wall.yPos / 100) * this.props.layoutDoc._height; + if (minY < groundY) { + this.setState({yPosition: groundY + 0.01}); + if (this.props.elasticCollisions) { + this.setState({yVelocity: -this.state.yVelocity}); + } else { + this.setState({yVelocity: 0}); + } + collision = true; + } + } + }); + } + return collision; + }; + + // Called at each RK4 step + evaluate = ( + currentXPos: number, + currentYPos: number, + currentXVel: number, + currentYVel: number, + deltaXPos: number, + deltaYPos: number, + deltaXVel: number, + deltaYVel: number, + dt: number + ) => { + const newXPos = currentXPos + deltaXPos * dt; + const newYPos = currentYPos + deltaYPos * dt; + const newXVel = currentXVel + deltaXVel * dt; + const newYVel = currentYVel + deltaYVel * dt; + const newDeltaXPos = newXVel; + const newDeltaYPos = newYVel; + let forces = this.props.updatedForces; + if (this.props.dataDoc['simulationType'] == "Pendulum") { + forces = this.getNewPendulumForces(newXPos, newYPos, newXVel, newYVel); + } else if (this.props.dataDoc['simulationType'] == "Spring") { + forces = this.getNewSpringForces(newYPos); + } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { + forces = this.getNewCircularMotionForces(newXPos, newYPos); + } + const newDeltaXVel = this.getNewAccelerationX(forces); + const newDeltaYVel = this.getNewAccelerationY(forces); + return { + xPos: newXPos, + yPos: newYPos, + xVel: newXVel, + yVel: newYVel, + deltaXPos: newDeltaXPos, + deltaYPos: newDeltaYPos, + deltaXVel: newDeltaXVel, + deltaYVel: newDeltaYVel, + }; + }; + + // Update position, velocity using RK4 method + update = () => { + let startXVel = this.state.xVelocity; + let startYVel = this.state.yVelocity; + let xPos = this.state.xPosition; + let yPos = this.state.yPosition; + let xVel = this.state.xVelocity; + let yVel = this.state.yVelocity; + let forces: IForce[] = this.props.dataDoc['updatedForces']; + if (this.props.dataDoc['simulationType'] == "Pendulum") { + forces = this.getNewPendulumForces(xPos, yPos, xVel, yVel); + } else if (this.props.dataDoc['simulationType'] == "Spring") { + forces = this.getNewSpringForces(yPos); + } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { + forces = this.getNewCircularMotionForces(xPos, yPos); + } + const xAcc = this.getNewAccelerationX(forces); + const yAcc = this.getNewAccelerationY(forces); + for (let i = 0; i < this.props.simulationSpeed; i++) { + const k1 = this.evaluate(xPos, yPos, xVel, yVel, xVel, yVel, xAcc, yAcc, 0); + const k2 = this.evaluate( + xPos, + yPos, + xVel, + yVel, + k1.deltaXPos, + k1.deltaYPos, + k1.deltaXVel, + k1.deltaYVel, + this.props.timestepSize * 0.5 + ); + const k3 = this.evaluate( + xPos, + yPos, + xVel, + yVel, + k2.deltaXPos, + k2.deltaYPos, + k2.deltaXVel, + k2.deltaYVel, + this.props.timestepSize * 0.5 + ); + const k4 = this.evaluate( + xPos, + yPos, + xVel, + yVel, + k3.deltaXPos, + k3.deltaYPos, + k3.deltaXVel, + k3.deltaYVel, + this.props.timestepSize + ); + + xVel += + ((this.props.timestepSize * 1.0) / 6.0) * + (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel); + yVel += + ((this.props.timestepSize * 1.0) / 6.0) * + (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel); + xPos += + ((this.props.timestepSize * 1.0) / 6.0) * + (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos); + yPos += + ((this.props.timestepSize * 1.0) / 6.0) * + (k1.deltaYPos + 2 * (k2.deltaYPos + k3.deltaYPos) + k4.deltaYPos); + } + // make sure harmonic motion maintained and errors don't propagate + if (this.props.dataDoc['simulationType'] == "Spring") { + if (startYVel < 0 && yVel > 0 && yPos < this.props.springRestLength) { + let equilibriumPos = + this.props.springRestLength + (this.props.mass * Math.abs(this.props.gravity)) / this.props.springConstant; + let amplitude = Math.abs(equilibriumPos - this.props.springStartLength); + yPos = equilibriumPos - amplitude; + } else if (startYVel > 0 && yVel < 0 && yPos > this.props.springRestLength) { + let equilibriumPos = + this.props.springRestLength + (this.props.mass * Math.abs(this.props.gravity)) / this.props.springConstant; + let amplitude = Math.abs(equilibriumPos - this.props.springStartLength); + yPos = equilibriumPos + amplitude; + } + } + if (this.props.dataDoc['simulationType'] == "Pendulum") { + let startX = this.state.updatedStartPosX; + if (startXVel <= 0 && xVel > 0) { + xPos = this.state.updatedStartPosX; + if (this.state.updatedStartPosX > this.props.xMax / 2) { + xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius; + } + yPos = this.props.startPosY; + } else if (startXVel >= 0 && xVel < 0) { + xPos = this.state.updatedStartPosX; + if (this.state.updatedStartPosX < this.props.xMax / 2) { + xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius; + } + yPos = this.props.startPosY; + } + } + if (this.props.dataDoc['simulationType'] == "One Weight") { + if (yPos < this.state.maxPosYConservation) { + yPos = this.state.maxPosYConservation; + } + } + this.setState({xVelocity: xVel}); + this.setState({yVelocity: yVel}); + this.setState({xPosition: xPos}); + this.setState({yPosition: yPos}); + let forcesn = this.props.dataDoc['updatedForces'] + if (this.props.dataDoc['simulationType'] == "Pendulum") { + forcesn = this.getNewPendulumForces(xPos, yPos, xVel, yVel); + } else if (this.props.dataDoc['simulationType'] == "Spring") { + forcesn = this.getNewSpringForces(yPos); + } else if (this.props.dataDoc['simulationType'] == "Circular Motion") { + forcesn = this.getNewCircularMotionForces(xPos, yPos); + } + this.props.dataDoc['updatedForces'] = (forcesn); + + // set component forces if they change + if (this.props.dataDoc['simulationType'] == "Pendulum") { + let x = this.props.xMax / 2 - xPos - this.props.radius; + let y = yPos + this.props.radius + 5; + let angle = (Math.atan(y / x) * 180) / Math.PI; + if (angle < 0) { + angle += 180; + } + let oppositeAngle = 90 - angle; + if (oppositeAngle < 0) { + oppositeAngle = 90 - (180 - angle); + } + + const pendulumLength = Math.sqrt(x * x + y * y); + + const mag = + this.props.mass * Math.abs(this.props.gravity) * Math.cos((oppositeAngle * Math.PI) / 180) + + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength; + + const tensionComponent: IForce = { + description: "Tension", + magnitude: mag, + directionInDegrees: angle, + component: true, + }; + const gravityParallel: IForce = { + description: "Gravity Parallel Component", + magnitude: Math.abs(this.props.gravity) * Math.cos(((90 - angle) * Math.PI) / 180), + directionInDegrees: 270 - (90 - angle), + component: true, + }; + const gravityPerpendicular: IForce = { + description: "Gravity Perpendicular Component", + magnitude: Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180), + directionInDegrees: -(90 - angle), + component: true, + }; + if (Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180) < 0) { + gravityPerpendicular.magnitude = Math.abs( + Math.abs(this.props.gravity) * Math.sin(((90 - angle) * Math.PI) / 180) + ); + gravityPerpendicular.directionInDegrees = 180 - (90 - angle); + } + this.props.dataDoc['componentForces'] = ([ + tensionComponent, + gravityParallel, + gravityPerpendicular, + ]); + } + }; + + // Render weight, spring, rod(s), vectors + render () { + return ( + <div> + <div + className="weightContainer" + onPointerDown={(e) => { + if (this.draggable) { + this.props.dataDoc['paused'] = true; + this.setState({dragging: true}); + this.setState({clickPositionX: e.clientX}); + this.setState({clickPositionY: e.clientY}); + } + }} + onPointerMove={(e) => { + if (this.state.dragging) { + let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; + if (newY > this.props.yMax - 2 * this.props.radius - 10) { + newY = this.props.yMax - 2 * this.props.radius - 10; + } else if (newY < 10) { + newY = 10; + } + + let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; + if (newX > this.props.xMax - 2 * this.props.radius - 10) { + newX = this.props.xMax - 2 * this.props.radius - 10; + } else if (newX < 10) { + newX = 10; + } + if (this.props.dataDoc['simulationType'] == "Suspension") { + if (newX < (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15) { + newX = (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15; + } else if (newX > (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15) { + newX = (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15; + } + } + + this.setState({yPosition: newY}); + this.props.dataDoc['positionYDisplay'] = + Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100 + if (this.props.dataDoc['simulationType'] != "Pulley") { + this.setState({xPosition: newX}); + this.props.dataDoc['positionXDisplay'] = newX + } + if (this.props.dataDoc['simulationType'] != "Suspension") { + if (this.props.dataDoc['simulationType'] != "Pulley") { + this.setState({updatedStartPosX: newX}); + } + this.setState({updatedStartPosY: newY}); + } + this.setState({clickPositionX: e.clientX}); + this.setState({clickPositionY: e.clientY}); + this.setDisplayValues(); + } + }} + onPointerUp={(e) => { + if (this.state.dragging) { + if ( + this.props.dataDoc['simulationType'] != "Pendulum" && + this.props.dataDoc['simulationType'] != "Suspension" + ) { + this.resetEverything(); + } + this.setState({dragging: false}); + let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; + if (newY > this.props.yMax - 2 * this.props.radius - 10) { + newY = this.props.yMax - 2 * this.props.radius - 10; + } else if (newY < 10) { + newY = 10; + } + + let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; + if (newX > this.props.xMax - 2 * this.props.radius - 10) { + newX = this.props.xMax - 2 * this.props.radius - 10; + } else if (newX < 10) { + newX = 10; + } + if (this.props.dataDoc['simulationType'] == "Spring") { + this.props.dataDoc.springStartLength = newY + } + if (this.props.dataDoc['simulationType'] == "Suspension") { + let x1rod = (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200; + let x2rod = (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius; + let deltaX1 = this.state.xPosition + this.props.radius - x1rod; + let deltaX2 = x2rod - (this.state.xPosition + this.props.radius); + let deltaY = this.state.yPosition + this.props.radius; + let dir1T = Math.PI - Math.atan(deltaY / deltaX1); + let dir2T = Math.atan(deltaY / deltaX2); + let tensionMag2 = + (this.props.mass * Math.abs(this.props.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, + component: false, + }; + const tensionForce2: IForce = { + description: "Tension", + magnitude: tensionMag2, + directionInDegrees: dir2T, + component: false, + }; + const grav: IForce = { + description: "Gravity", + magnitude: this.props.mass * Math.abs(this.props.gravity), + directionInDegrees: 270, + component: false, + }; + this.props.dataDoc['updatedForces'] = ([tensionForce1, tensionForce2, grav]); + } + } + }} + > + <div className="weight" style={this.weightStyle}> + <p className="weightLabel">{this.props.mass} kg</p> + </div> + </div> + {this.props.dataDoc['simulationType'] == "Spring" && ( + <div + className="spring" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((val) => { + const count = 10; + let xPos1; + let yPos1; + let xPos2; + let yPos2; + if (val % 2 == 0) { + xPos1 = this.state.xPosition + this.props.radius - 20; + xPos2 = this.state.xPosition + this.props.radius + 20; + } else { + xPos1 = this.state.xPosition + this.props.radius + 20; + xPos2 = this.state.xPosition + this.props.radius - 20; + } + yPos1 = (val * this.state.yPosition) / count; + yPos2 = ((val + 1) * this.state.yPosition) / count; + return ( + <line + key={val} + x1={xPos1} + y1={yPos1} + x2={xPos2} + y2={yPos2} + stroke={"#808080"} + strokeWidth="10" + /> + ); + })} + </svg> + </div> + )} + + {this.props.dataDoc['simulationType'] == "Pulley" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.state.xPosition + this.props.radius} + y2={this.props.yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Pulley" && ( + <div + className="wheel" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <circle + cx={(this.props.xMax + this.props.xMin) / 2} + cy={this.props.radius} + r={this.props.radius * 1.5} + fill={"#808080"} + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Suspension" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200} + y2={this.props.yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + <p + style={{ + position: "absolute", + left: (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200 + 80 + "px", + top: 10 + "px", + backgroundColor: this.labelBackgroundColor, + }} + > + {Math.round( + ((Math.atan( + (this.state.yPosition + this.props.radius) / + (this.state.xPosition + + this.props.radius - + ((this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200)) + ) * + 180) / + Math.PI) * + 100 + ) / 100} + ° + </p> + <svg width={this.props.layoutDoc._width + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius} + y2={this.props.yMin} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + + <p + style={{ + position: "absolute", + left: (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius - 80 + "px", + top: 10 + "px", + backgroundColor: this.labelBackgroundColor, + }} + > + {Math.round( + ((Math.atan( + (this.state.yPosition + this.props.radius) / + ((this.props.xMax + this.props.xMin) / 2 + + this.props.yMin + + 200 + + this.props.radius - + (this.state.xPosition + this.props.radius)) + ) * + 180) / + Math.PI) * + 100 + ) / 100} + ° + </p> + </div> + )} + {this.props.dataDoc['simulationType'] == "Circular Motion" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMin + this.props.xMax) / 2} + y2={(this.props.yMin + this.props.yMax) / 2} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + </div> + )} + {this.props.dataDoc['simulationType'] == "Pendulum" && ( + <div + className="rod" + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.props.xMax / 2} + y2={-5} + stroke={"#deb887"} + strokeWidth="10" + /> + </svg> + {!this.state.dragging && ( + <div> + <p + style={{ + position: "absolute", + zIndex: 5, + left: this.state.xPosition + "px", + top: this.state.yPosition - 70 + "px", + backgroundColor: this.labelBackgroundColor, + }} + > + {Math.round(this.props.pendulumLength)} m + </p> + <p + style={{ + position: "absolute", + left: this.props.xMax / 2 + "px", + top: 30 + "px", + backgroundColor: this.labelBackgroundColor, + }} + > + {Math.round(this.props.pendulumAngle * 100) / 100}° + </p> + </div> + )} + </div> + )} + {this.props.dataDoc['simulationType'] == "Inclined Plane" && ( + <div> + <div + style={{ position: "absolute", left: "0", top: "0" }} + > + <svg width={this.props.xMax + "px"} height={this.props.yMax + "px"}> + <polygon points={this.state.coordinates} style={{ fill: "burlywood" }} /> + </svg> + </div> + + <p + style={{ + position: "absolute", + left: Math.round(this.props.xMax * 0.5 - 200 + this.props.wedgeWidth - 80) + "px", + top: Math.round(this.props.yMax - 40) + "px", + }} + > + {Math.round( + ((Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI) * 100 + ) / 100} + ° + </p> + </div> + )} + {!this.state.dragging && this.props.showAcceleration && ( + <div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <defs> + <marker + id="accArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill="green" /> + </marker> + </defs> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.state.xPosition + this.props.radius + this.getNewAccelerationX(this.props.updatedForces) * 15} + y2={this.state.yPosition + this.props.radius + this.getNewAccelerationY(this.props.updatedForces) * 15} + stroke={"green"} + strokeWidth="5" + markerEnd="url(#accArrow)" + /> + </svg> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: this.state.xPosition + this.props.radius + this.state.xAccel * 15 + 25 + "px", + top: this.state.yPosition + this.props.radius + this.state.yAccel * 15 + 70 + "px", + lineHeight: 1, + }} + > + <p> + {Math.round( + 100 * Math.sqrt(this.state.xAccel * this.state.xAccel + this.state.yAccel * this.state.yAccel) + ) / 100}{" "} + m/s + <sup>2</sup> + </p> + </div> + </div> + </div> + )} + {!this.state.dragging && this.props.showVelocity && ( + <div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: 0, + top: 0, + }} + > + <svg width={this.props.xMax + "px"} height={this.props.layoutDoc._height + "px"}> + <defs> + <marker + id="velArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill="blue" /> + </marker> + </defs> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.state.xPosition + this.props.radius + this.state.xVelocity * 7} + y2={this.state.yPosition + this.props.radius + this.state.yVelocity * 7} + stroke={"blue"} + strokeWidth="5" + markerEnd="url(#velArrow)" + /> + </svg> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: this.state.xPosition + this.props.radius + this.state.xVelocity * 7 + 25 + "px", + top: this.state.yPosition + this.props.radius + this.state.yVelocity * 7 + "px", + lineHeight: 1, + }} + > + <p> + {Math.round( + 100 * + Math.sqrt( + this.props.displayXVelocity * this.props.displayXVelocity + + this.props.displayYVelocity * this.props.displayYVelocity + ) + ) / 100}{" "} + m/s + </p> + </div> + </div> + </div> + )} + {!this.state.dragging && + this.props.showComponentForces && + this.props.componentForces.map((force, index) => { + if (force.magnitude < this.epsilon) { + return; + } + let arrowStartY: number = this.state.yPosition + this.props.radius; + const arrowStartX: number = this.state.xPosition + this.props.radius; + let arrowEndY: number = + arrowStartY - + Math.abs(force.magnitude) * + 20 * + Math.sin((force.directionInDegrees * Math.PI) / 180); + const arrowEndX: number = + arrowStartX + + Math.abs(force.magnitude) * + 20 * + Math.cos((force.directionInDegrees * Math.PI) / 180); + + let color = "#0d0d0d"; + + let labelTop = arrowEndY; + let labelLeft = arrowEndX; + if (force.directionInDegrees > 90 && force.directionInDegrees < 270) { + labelLeft -= 120; + } else { + labelLeft += 30; + } + if (force.directionInDegrees >= 0 && force.directionInDegrees < 180) { + labelTop += 40; + } else { + labelTop -= 40; + } + labelTop = Math.min(labelTop, this.props.yMax + 50); + labelTop = Math.max(labelTop, this.props.yMin); + labelLeft = Math.min(labelLeft, this.props.xMax - 60); + labelLeft = Math.max(labelLeft, this.props.xMin); + + return ( + <div key={index}> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: this.props.xMin, + top: this.props.yMin, + }} + > + <svg + width={this.props.xMax - this.props.xMin + "px"} + height={this.props.layoutDoc._height + "px"} + > + <defs> + <marker + id="forceArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill={color} /> + </marker> + </defs> + {force.component == true && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + strokeDasharray="10,10" + markerEnd="url(#forceArrow)" + /> + )} + {force.component == false && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + markerEnd="url(#forceArrow)" + /> + )} + </svg> + </div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: labelLeft + "px", + top: labelTop + "px", + lineHeight: 1, + backgroundColor: this.labelBackgroundColor, + }} + > + {force.description && <p>{force.description}</p>} + {!force.description && <p>Force</p>} + {this.props.showForceMagnitudes && ( + <p>{Math.round(100 * force.magnitude) / 100} N</p> + )} + </div> + </div> + ); + })} + {!this.state.dragging && + this.props.showForces && this.props.updatedForces && + this.props.updatedForces.map((force, index) => { + if (force.magnitude < this.epsilon) { + return; + } + let arrowStartY: number = this.state.yPosition + this.props.radius; + const arrowStartX: number = this.state.xPosition + this.props.radius; + let arrowEndY: number = + arrowStartY - + Math.abs(force.magnitude) * + 20 * + Math.sin((force.directionInDegrees * Math.PI) / 180); + const arrowEndX: number = + arrowStartX + + Math.abs(force.magnitude) * + 20 * + Math.cos((force.directionInDegrees * Math.PI) / 180); + + let color = "#0d0d0d"; + + let labelTop = arrowEndY; + let labelLeft = arrowEndX; + if (force.directionInDegrees > 90 && force.directionInDegrees < 270) { + labelLeft -= 120; + } else { + labelLeft += 30; + } + if (force.directionInDegrees >= 0 && force.directionInDegrees < 180) { + labelTop += 40; + } else { + labelTop -= 40; + } + labelTop = Math.min(labelTop, this.props.yMax + 50); + labelTop = Math.max(labelTop, this.props.yMin); + labelLeft = Math.min(labelLeft, this.props.xMax - 60); + labelLeft = Math.max(labelLeft, this.props.xMin); + + return ( + <div key={index}> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: this.props.xMin, + top: this.props.yMin, + }} + > + <svg + width={this.props.xMax - this.props.xMin + "px"} + height={this.props.layoutDoc._height + "px"} + > + <defs> + <marker + id="forceArrow" + markerWidth="10" + markerHeight="10" + refX="0" + refY="3" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M0,0 L0,6 L9,3 z" fill={color} /> + </marker> + </defs> + {force.component == true && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + strokeDasharray="10,10" + markerEnd="url(#forceArrow)" + /> + )} + {force.component == false && ( + <line + x1={arrowStartX} + y1={arrowStartY} + x2={arrowEndX} + y2={arrowEndY} + stroke={color} + strokeWidth="5" + markerEnd="url(#forceArrow)" + /> + )} + </svg> + </div> + <div + style={{ + pointerEvents: "none", + position: "absolute", + left: labelLeft + "px", + top: labelTop + "px", + lineHeight: 1, + backgroundColor: this.labelBackgroundColor, + }} + > + {force.description && <p>{force.description}</p>} + {!force.description && <p>Force</p>} + {this.props.showForceMagnitudes && ( + <p>{Math.round(100 * force.magnitude) / 100} N</p> + )} + </div> + </div> + ); + })} + </div> + )} +}; |