From 3d6cbd57be8ebf648bf00a850399034b1b72aec4 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 21 Jun 2024 14:17:54 -0700 Subject: [PATCH 01/14] ghost body creation function --- fission/.gitignore | 3 + fission/README.md | 45 +- fission/bun.lockb | Bin 274767 -> 295551 bytes fission/src/mirabuf/MirabufSceneObject.ts | 229 ++-- fission/src/systems/physics/PhysicsSystem.ts | 1125 +++++++++++------- fission/src/test/PhysicsSystem.test.ts | 194 +-- 6 files changed, 972 insertions(+), 624 deletions(-) diff --git a/fission/.gitignore b/fission/.gitignore index a547bf36d8..449d7eb050 100644 --- a/fission/.gitignore +++ b/fission/.gitignore @@ -1,3 +1,6 @@ +public/Downloadables + + # Logs logs *.log diff --git a/fission/README.md b/fission/README.md index a2fae7aefe..7f94a3b56b 100644 --- a/fission/README.md +++ b/fission/README.md @@ -1,59 +1,71 @@ # Fission: Synthesis' web-based robot simulator ## Gettings Started + ### Requirements + 1. NPM (v10.2.4 recommended) 2. NodeJS (v20.10.0 recommended) -3. TypeScript (v4.8.4 recommended) *Unknown if this is actually required* +3. TypeScript (v4.8.4 recommended) _Unknown if this is actually required_ ### Assets + For the asset pack that will be available in production, download the asset pack [here](https://synthesis.autodesk.com/Downloadables/assetpack.zip) and unzip it. Make sure that the Downloadables directory is placed inside of the public directory like so: + ``` /fission/public/Downloadables/ ``` This can be accomplished with the `assetpack` npm script: + ``` npm run assetpack ``` ### Building + To build, install all dependencies: + ```bash npm i ``` + ### NPM Scripts -| Script | Description | -| ------ | ----------- | -| `dev` | Starts the development server used for testing. Supports hotloading (though finicky with WASM module loading). | -| `test` | Runs the unit tests via vitest. | -| `build` | Builds the project into it's packaged form. Uses root base path. | -| `build:prod` | Builds the project into it's packaged form. Uses the `/fission/` base path. | -| `preview` | Runs the built project for preview locally before deploying. | -| `lint` | Runs eslint on the project. | -| `lint:fix` | Attempts to fix issues found with eslint. | -| `prettier` | Runs prettier on the project as a check. | +| Script | Description | +| -------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `dev` | Starts the development server used for testing. Supports hotloading (though finicky with WASM module loading). | +| `test` | Runs the unit tests via vitest. | +| `build` | Builds the project into it's packaged form. Uses root base path. | +| `build:prod` | Builds the project into it's packaged form. Uses the `/fission/` base path. | +| `preview` | Runs the built project for preview locally before deploying. | +| `lint` | Runs eslint on the project. | +| `lint:fix` | Attempts to fix issues found with eslint. | +| `prettier` | Runs prettier on the project as a check. | | `prettier:fix` | Runs prettier on the project to fix any issues with formating. **DO NOT USE**, I don't like the current format it uses. | -| `format` | Runs `prettier:fix` and `lint:fix`. **Do not use** for the same reasons as `prettier:fix`. | -| `assetpack` | Downloads the assetpack and unzips/installs it in the correct location. | +| `format` | Runs `prettier:fix` and `lint:fix`. **Do not use** for the same reasons as `prettier:fix`. | +| `assetpack` | Downloads the assetpack and unzips/installs it in the correct location. | ### Autodesk Platform Services To test/enable the use of Autodesk Platform Services (APS), please follow instructions for development web server (Closed Source). ## Core Systems + These core systems make up the bulk of the vital technologies to make Synthesis work. The idea is that these systems will serve as a jumping off point for anything requiring real-time simulation. ### World + The world serves as a hub for all of the core systems. It is a static class that handles system update execution order and lifetime. ### Scene Renderer + The Scene Renderer is our interface with rendering within the Canvas. This is primarily done via ThreeJS, however can be extended later on. ### Physics System + This Physics System is our interface with Jolt, ensuring objects are properly handled and provides utility functions that are more custom fit to our purposes. [Jolt Physics Architecture](https://jrouwe.github.io/JoltPhysics/) @@ -63,18 +75,23 @@ This Physics System is our interface with Jolt, ensuring objects are properly ha ### UI System ## Additional Systems + These systems will extend off of the core systems to build out features in Synthesis. ### Simulation System + The Simulation System articulates dynamic elements of the scene via the Physics System. At it's core there are 3 main components: #### Driver + Drivers are mostly write-only. They take in values to know how to articulate the physics objects and contraints. #### Stimulus -Stimulu are mostly read-only. They read values from given physics objects and constraints. + +Stimuli are mostly read-only. They read values from given physics objects and constraints. #### Brain + Brains are the controllers of the mechanisms. They use a combination of Drivers and Stimuli to control a given mechanism. For basic user control of the mechanisms, we'll have a Synthesis Brain. By the end of Summer 2024, I hope to have an additional brain, the WPIBrain for facilitating WPILib code control over the mechanisms inside of Synthesis. diff --git a/fission/bun.lockb b/fission/bun.lockb index 8ca9f4a4ab44ab72622fa81f2e042ca8b100e1fb..240d4be8bfb155b5efe34f7d21ed39dee0461cd0 100755 GIT binary patch delta 71667 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 delta 59376 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 o$Ea#+X^g5P{%=R?L`&`INR_DxT{tGTp | null + private _physicsLayerReserve: LayerReserve | undefined = undefined - private _mirabufInstance: MirabufInstance; - private _debugBodies: Map | null; - private _physicsLayerReserve: LayerReserve | undefined = undefined; - - private _mechanism: Mechanism; + private _mechanism: Mechanism public constructor(mirabufInstance: MirabufInstance) { super() this._mirabufInstance = mirabufInstance - this._mechanism = World.PhysicsSystem.CreateMechanismFromParser(this._mirabufInstance.parser) + this._mechanism = World.PhysicsSystem.CreateMechanismFromParser( + this._mirabufInstance.parser + ) if (this._mechanism.layerReserve) { this._physicsLayerReserve = this._mechanism.layerReserve } - + this._debugBodies = null } public Setup(): void { // Rendering - this._mirabufInstance.AddToScene(World.SceneRenderer.scene); + this._mirabufInstance.AddToScene(World.SceneRenderer.scene) if (DEBUG_BODIES) { - this._debugBodies = new Map(); + this._debugBodies = new Map() this._mechanism.nodeToBody.forEach((bodyId, rnName) => { + const body = World.PhysicsSystem.GetBody(bodyId) + + const colliderMesh = this.CreateMeshForShape(body.GetShape()) + const comMesh = World.SceneRenderer.CreateSphere(0.05) + World.SceneRenderer.scene.add(colliderMesh) + World.SceneRenderer.scene.add(comMesh) + ;(comMesh.material as THREE.Material).depthTest = false + this._debugBodies!.set(rnName, { + colliderMesh: colliderMesh, + comMesh: comMesh, + }) + }) + } - const body = World.PhysicsSystem.GetBody(bodyId); - - const colliderMesh = this.CreateMeshForShape(body.GetShape()); - const comMesh = World.SceneRenderer.CreateSphere(0.05); - World.SceneRenderer.scene.add(colliderMesh); - World.SceneRenderer.scene.add(comMesh); - (comMesh.material as THREE.Material).depthTest = false; - this._debugBodies!.set(rnName, { colliderMesh: colliderMesh, comMesh: comMesh }); - }); + // God mode testing: add ghost object to all spawned mirabuf files + const nodeId = this._mechanism.rootBody + const anchorBody = this._mechanism.GetBodyByNodeId(nodeId) + if (anchorBody == undefined) { + console.error("Failed to anchor to mechanism") } + const bodyId = World.PhysicsSystem.GetBody(anchorBody!) + World.PhysicsSystem.CreateGodModeBody(bodyId) // Simulation - World.SimulationSystem.RegisterMechanism(this._mechanism); - const simLayer = World.SimulationSystem.GetSimulationLayer(this._mechanism)!; - const brain = new SynthesisBrain(this._mechanism); - simLayer.SetBrain(brain); + World.SimulationSystem.RegisterMechanism(this._mechanism) + const simLayer = World.SimulationSystem.GetSimulationLayer( + this._mechanism + )! + const brain = new SynthesisBrain(this._mechanism) + simLayer.SetBrain(brain) } public Update(): void { this._mirabufInstance.parser.rigidNodes.forEach(rn => { - const body = World.PhysicsSystem.GetBody(this._mechanism.GetBodyByNodeId(rn.id)!); - const transform = JoltMat44_ThreeMatrix4(body.GetWorldTransform()); + const body = World.PhysicsSystem.GetBody( + this._mechanism.GetBodyByNodeId(rn.id)! + ) + const transform = JoltMat44_ThreeMatrix4(body.GetWorldTransform()) rn.parts.forEach(part => { - const partTransform = this._mirabufInstance.parser.globalTransforms.get(part)!.clone().premultiply(transform); + const partTransform = + this._mirabufInstance.parser.globalTransforms + .get(part)! + .clone() + .premultiply(transform) this._mirabufInstance.meshes.get(part)!.forEach(mesh => { - mesh.position.setFromMatrixPosition(partTransform); - mesh.rotation.setFromRotationMatrix(partTransform); - }); - }); + mesh.position.setFromMatrixPosition(partTransform) + mesh.rotation.setFromRotationMatrix(partTransform) + }) + }) if (isNaN(body.GetPosition().GetX())) { - const vel = body.GetLinearVelocity(); - const pos = body.GetPosition(); - console.warn(`Invalid Position.\nPosition => ${pos.GetX()}, ${pos.GetY()}, ${pos.GetZ()}\nVelocity => ${vel.GetX()}, ${vel.GetY()}, ${vel.GetZ()}`); + const vel = body.GetLinearVelocity() + const pos = body.GetPosition() + console.warn( + `Invalid Position.\nPosition => ${pos.GetX()}, ${pos.GetY()}, ${pos.GetZ()}\nVelocity => ${vel.GetX()}, ${vel.GetY()}, ${vel.GetZ()}` + ) } // console.debug(`POSITION: ${body.GetPosition().GetX()}, ${body.GetPosition().GetY()}, ${body.GetPosition().GetZ()}`) if (this._debugBodies) { - const { colliderMesh, comMesh } = this._debugBodies.get(rn.id)!; - colliderMesh.position.setFromMatrixPosition(transform); - colliderMesh.rotation.setFromRotationMatrix(transform); - - const comTransform = JoltMat44_ThreeMatrix4(body.GetCenterOfMassTransform()); - comMesh.position.setFromMatrixPosition(comTransform); - comMesh.rotation.setFromRotationMatrix(comTransform); + const { colliderMesh, comMesh } = this._debugBodies.get(rn.id)! + colliderMesh.position.setFromMatrixPosition(transform) + colliderMesh.rotation.setFromRotationMatrix(transform) + + const comTransform = JoltMat44_ThreeMatrix4( + body.GetCenterOfMassTransform() + ) + comMesh.position.setFromMatrixPosition(comTransform) + comMesh.rotation.setFromRotationMatrix(comTransform) } - }); + }) } public Dispose(): void { World.SimulationSystem.UnregisterMechanism(this._mechanism) - World.PhysicsSystem.DestroyMechanism(this._mechanism); - this._mirabufInstance.Dispose(World.SceneRenderer.scene); + World.PhysicsSystem.DestroyMechanism(this._mechanism) + this._mirabufInstance.Dispose(World.SceneRenderer.scene) this._debugBodies?.forEach(x => { - World.SceneRenderer.scene.remove(x.colliderMesh, x.comMesh); - x.colliderMesh.geometry.dispose(); - x.comMesh.geometry.dispose(); - (x.colliderMesh.material as THREE.Material).dispose(); - (x.comMesh.material as THREE.Material).dispose(); - }); - this._debugBodies?.clear(); - this._physicsLayerReserve?.Release(); + World.SceneRenderer.scene.remove(x.colliderMesh, x.comMesh) + x.colliderMesh.geometry.dispose() + x.comMesh.geometry.dispose() + ;(x.colliderMesh.material as THREE.Material).dispose() + ;(x.comMesh.material as THREE.Material).dispose() + }) + this._debugBodies?.clear() + this._physicsLayerReserve?.Release() } private CreateMeshForShape(shape: Jolt.Shape): THREE.Mesh { - const scale = new JOLT.Vec3(1, 1, 1); - const triangleContext = new JOLT.ShapeGetTriangles(shape, JOLT.AABox.prototype.sBiggest(), shape.GetCenterOfMass(), JOLT.Quat.prototype.sIdentity(), scale); - JOLT.destroy(scale); - - const vertices = new Float32Array(JOLT.HEAP32.buffer, triangleContext.GetVerticesData(), triangleContext.GetVerticesSize() / Float32Array.BYTES_PER_ELEMENT); - const buffer = new THREE.BufferAttribute(vertices, 3).clone(); - JOLT.destroy(triangleContext); - - const geometry = new THREE.BufferGeometry(); - geometry.setAttribute('position', buffer); - geometry.computeVertexNormals(); - - const material = new THREE.MeshStandardMaterial({ color: 0x33ff33, wireframe: true }) - const mesh = new THREE.Mesh(geometry, material); - mesh.castShadow = true; - - return mesh; + const scale = new JOLT.Vec3(1, 1, 1) + const triangleContext = new JOLT.ShapeGetTriangles( + shape, + JOLT.AABox.prototype.sBiggest(), + shape.GetCenterOfMass(), + JOLT.Quat.prototype.sIdentity(), + scale + ) + JOLT.destroy(scale) + + const vertices = new Float32Array( + JOLT.HEAP32.buffer, + triangleContext.GetVerticesData(), + triangleContext.GetVerticesSize() / Float32Array.BYTES_PER_ELEMENT + ) + const buffer = new THREE.BufferAttribute(vertices, 3).clone() + JOLT.destroy(triangleContext) + + const geometry = new THREE.BufferGeometry() + geometry.setAttribute("position", buffer) + geometry.computeVertexNormals() + + const material = new THREE.MeshStandardMaterial({ + color: 0x33ff33, + wireframe: true, + }) + const mesh = new THREE.Mesh(geometry, material) + mesh.castShadow = true + + return mesh } } -export async function CreateMirabufFromUrl(path: string): Promise { - const miraAssembly = await LoadMirabufRemote(path) - .catch(console.error); +export async function CreateMirabufFromUrl( + path: string +): Promise { + const miraAssembly = await LoadMirabufRemote(path).catch(console.error) if (!miraAssembly || !(miraAssembly instanceof mirabuf.Assembly)) { - return; + return } - const parser = new MirabufParser(miraAssembly); + const parser = new MirabufParser(miraAssembly) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { - console.error(`Assembly Parser produced significant errors for '${miraAssembly.info!.name!}'`); - return; + console.error( + `Assembly Parser produced significant errors for '${miraAssembly.info!.name!}'` + ) + return } - - return new MirabufSceneObject(new MirabufInstance(parser)); + + return new MirabufSceneObject(new MirabufInstance(parser)) } -export default MirabufSceneObject; \ No newline at end of file +export default MirabufSceneObject + diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index d6d8256385..9607847e79 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -1,26 +1,41 @@ -import { MirabufFloatArr_JoltVec3, MirabufVector3_JoltVec3, ThreeMatrix4_JoltMat44, ThreeVector3_JoltVec3, _JoltQuat } from "../../util/TypeConversions"; -import JOLT from "../../util/loading/JoltSyncLoader"; -import Jolt from "@barclah/jolt-physics"; -import * as THREE from 'three'; -import { mirabuf } from '../../proto/mirabuf'; -import MirabufParser, { GAMEPIECE_SUFFIX, GROUNDED_JOINT_ID, RigidNodeReadOnly } from "../../mirabuf/MirabufParser"; -import WorldSystem from "../WorldSystem"; -import Mechanism from "./Mechanism"; +import { + MirabufFloatArr_JoltVec3, + MirabufVector3_JoltVec3, + ThreeMatrix4_JoltMat44, + ThreeVector3_JoltVec3, + _JoltQuat, +} from "../../util/TypeConversions" +import JOLT from "../../util/loading/JoltSyncLoader" +import Jolt from "@barclah/jolt-physics" +import * as THREE from "three" +import { mirabuf } from "../../proto/mirabuf" +import MirabufParser, { + GAMEPIECE_SUFFIX, + GROUNDED_JOINT_ID, + RigidNodeReadOnly, +} from "../../mirabuf/MirabufParser" +import WorldSystem from "../WorldSystem" +import Mechanism from "./Mechanism" /** * Layers used for determining enabled/disabled collisions. */ -const LAYER_FIELD = 0; // Used for grounded rigid node of a field as well as any rigid nodes jointed to it. -const LAYER_GENERAL_DYNAMIC = 1; // Used for game pieces or any general dynamic objects that can collide with anything and everything. -const RobotLayers: number[] = [ // Reserved layers for robots. Robot layers have no collision with themselves but have collision with everything else. - 2, 3, 4, 5, 6, 7, 8, 9 -]; +const LAYER_FIELD = 0 // Used for grounded rigid node of a field as well as any rigid nodes jointed to it. +const LAYER_GENERAL_DYNAMIC = 1 // Used for game pieces or any general dynamic objects that can collide with anything and everything. +const RobotLayers: number[] = [ + // Reserved layers for robots. Robot layers have no collision with themselves but have collision with everything else. + 2, + 3, 4, 5, 6, 7, 8, 9, +] + +// Layer for ghost object in god mode, interacts with nothing +const GHOST_LAYER = 10 // Please update this accordingly. -const COUNT_OBJECT_LAYERS = 10; +const COUNT_OBJECT_LAYERS = 11 -export const SIMULATION_PERIOD = 1.0 / 120.0; -const STANDARD_SUB_STEPS = 3; +export const SIMULATION_PERIOD = 1.0 / 120.0 +const STANDARD_SUB_STEPS = 3 /** * The PhysicsSystem handles all Jolt Phyiscs interactions within Synthesis. @@ -28,41 +43,48 @@ const STANDARD_SUB_STEPS = 3; * Fields, and Game pieces, and simulate them. */ class PhysicsSystem extends WorldSystem { - - private _joltInterface: Jolt.JoltInterface; - private _joltPhysSystem: Jolt.PhysicsSystem; - private _joltBodyInterface: Jolt.BodyInterface; - private _bodies: Array; + private _joltInterface: Jolt.JoltInterface + private _joltPhysSystem: Jolt.PhysicsSystem + private _joltBodyInterface: Jolt.BodyInterface + private _bodies: Array private _constraints: Array /** * Creates a PhysicsSystem object. */ constructor() { - super(); + super() - this._bodies = []; - this._constraints = []; + this._bodies = [] + this._constraints = [] - const joltSettings = new JOLT.JoltSettings(); - SetupCollisionFiltering(joltSettings); + const joltSettings = new JOLT.JoltSettings() + SetupCollisionFiltering(joltSettings) - this._joltInterface = new JOLT.JoltInterface(joltSettings); - JOLT.destroy(joltSettings); + this._joltInterface = new JOLT.JoltInterface(joltSettings) + JOLT.destroy(joltSettings) - this._joltPhysSystem = this._joltInterface.GetPhysicsSystem(); - this._joltBodyInterface = this._joltPhysSystem.GetBodyInterface(); + this._joltPhysSystem = this._joltInterface.GetPhysicsSystem() + this._joltBodyInterface = this._joltPhysSystem.GetBodyInterface() - this._joltPhysSystem.SetGravity(new JOLT.Vec3(0, -9.8, 0)); + this._joltPhysSystem.SetGravity(new JOLT.Vec3(0, -9.8, 0)) - const ground = this.CreateBox(new THREE.Vector3(5.0, 0.5, 5.0), undefined, new THREE.Vector3(0.0, -2.0, 0.0), undefined); - this._joltBodyInterface.AddBody(ground.GetID(), JOLT.EActivation_Activate); + const ground = this.CreateBox( + new THREE.Vector3(5.0, 0.5, 5.0), + undefined, + new THREE.Vector3(0.0, -2.0, 0.0), + undefined + ) + this._joltBodyInterface.AddBody( + ground.GetID(), + JOLT.EActivation_Activate + ) } /** * TEMPORARY * Create a box. - * + * * @param halfExtents The half extents of the Box. * @param mass Mass of the Box. Leave undefined to make Box static. * @param position Posiition of the Box (default: 0, 0, 0) @@ -70,38 +92,41 @@ class PhysicsSystem extends WorldSystem { * @returns Reference to Jolt Body */ public CreateBox( - halfExtents: THREE.Vector3, - mass: number | undefined, - position: THREE.Vector3 | undefined, - rotation: THREE.Euler | THREE.Quaternion | undefined) { - const size = ThreeVector3_JoltVec3(halfExtents); - const shape = new JOLT.BoxShape(size, 0.1); - JOLT.destroy(size); - - const pos = position ? ThreeVector3_JoltVec3(position) : new JOLT.Vec3(0.0, 0.0, 0.0); - const rot = _JoltQuat(rotation); + halfExtents: THREE.Vector3, + mass: number | undefined, + position: THREE.Vector3 | undefined, + rotation: THREE.Euler | THREE.Quaternion | undefined + ) { + const size = ThreeVector3_JoltVec3(halfExtents) + const shape = new JOLT.BoxShape(size, 0.1) + JOLT.destroy(size) + + const pos = position + ? ThreeVector3_JoltVec3(position) + : new JOLT.Vec3(0.0, 0.0, 0.0) + const rot = _JoltQuat(rotation) const creationSettings = new JOLT.BodyCreationSettings( shape, pos, rot, mass ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, mass ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD - ); + ) if (mass) { - creationSettings.mMassPropertiesOverride.mMass = mass; + creationSettings.mMassPropertiesOverride.mMass = mass } - const body = this._joltBodyInterface.CreateBody(creationSettings); - JOLT.destroy(pos); - JOLT.destroy(rot); - JOLT.destroy(creationSettings); + const body = this._joltBodyInterface.CreateBody(creationSettings) + JOLT.destroy(pos) + JOLT.destroy(rot) + JOLT.destroy(creationSettings) - this._bodies.push(body.GetID()); - return body; + this._bodies.push(body.GetID()) + return body } /** * This creates a body in Jolt. Mostly used for Unit test validation. - * + * * @param shape Shape to impart on the body. * @param mass Mass of the body. * @param position Position of the body. @@ -109,127 +134,175 @@ class PhysicsSystem extends WorldSystem { * @returns Resulting Body object. */ public CreateBody( - shape: Jolt.Shape, - mass: number | undefined, - position: THREE.Vector3 | undefined, - rotation: THREE.Euler | THREE.Quaternion | undefined) { - const pos = position ? ThreeVector3_JoltVec3(position) : new JOLT.Vec3(0.0, 0.0, 0.0); - const rot = _JoltQuat(rotation); + shape: Jolt.Shape, + mass: number | undefined, + position: THREE.Vector3 | undefined, + rotation: THREE.Euler | THREE.Quaternion | undefined + ) { + const pos = position + ? ThreeVector3_JoltVec3(position) + : new JOLT.Vec3(0.0, 0.0, 0.0) + const rot = _JoltQuat(rotation) const creationSettings = new JOLT.BodyCreationSettings( shape, pos, rot, mass ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, mass ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD - ); + ) if (mass) { - creationSettings.mMassPropertiesOverride.mMass = mass; + creationSettings.mMassPropertiesOverride.mMass = mass } - const body = this._joltBodyInterface.CreateBody(creationSettings); - JOLT.destroy(pos); - JOLT.destroy(rot); - JOLT.destroy(creationSettings); + const body = this._joltBodyInterface.CreateBody(creationSettings) + JOLT.destroy(pos) + JOLT.destroy(rot) + JOLT.destroy(creationSettings) - this._bodies.push(body.GetID()); - return body; + this._bodies.push(body.GetID()) + return body } /** * Utility function for creating convex hulls. Mostly used for Unit test validation. - * + * * @param points Flat pack array of vector 3 components. * @param density Density of the convex hull. * @returns Resulting shape. */ - public CreateConvexHull(points: Float32Array, density: number = 1.0): Jolt.ShapeResult { + public CreateConvexHull( + points: Float32Array, + density: number = 1.0 + ): Jolt.ShapeResult { if (points.length % 3) { - throw new Error(`Invalid size of points: ${points.length}`); + throw new Error(`Invalid size of points: ${points.length}`) } - const settings = new JOLT.ConvexHullShapeSettings(); - settings.mPoints.clear(); - settings.mPoints.reserve(points.length / 3.0); + const settings = new JOLT.ConvexHullShapeSettings() + settings.mPoints.clear() + settings.mPoints.reserve(points.length / 3.0) for (let i = 0; i < points.length; i += 3) { - settings.mPoints.push_back(new JOLT.Vec3(points[i], points[i + 1], points[i + 2])); + settings.mPoints.push_back( + new JOLT.Vec3(points[i], points[i + 1], points[i + 2]) + ) } - settings.mDensity = density; - return settings.Create(); + settings.mDensity = density + return settings.Create() } public CreateMechanismFromParser(parser: MirabufParser): Mechanism { - const layer = parser.assembly.dynamic ? new LayerReserve() : undefined; + const layer = parser.assembly.dynamic ? new LayerReserve() : undefined // const layer = undefined; console.log(`Using layer ${layer?.layer}`) - const bodyMap = this.CreateBodiesFromParser(parser, layer); - const rootBody = parser.rootNode; - const mechanism = new Mechanism(rootBody, bodyMap, layer); - this.CreateJointsFromParser(parser, mechanism); - return mechanism; + const bodyMap = this.CreateBodiesFromParser(parser, layer) + const rootBody = parser.rootNode + const mechanism = new Mechanism(rootBody, bodyMap, layer) + this.CreateJointsFromParser(parser, mechanism) + return mechanism } /** * Creates all the joints for a mirabuf assembly given an already compiled mapping of rigid nodes to bodies. - * + * * @param parser Mirabuf parser with complete set of rigid nodes and assembly data. * @param mechainsm Mapping of the name of rigid groups to Jolt bodies. Retrieved from CreateBodiesFromParser. */ public CreateJointsFromParser(parser: MirabufParser, mechanism: Mechanism) { - const jointData = parser.assembly.data!.joints!; - for (const [jGuid, jInst] of (Object.entries(jointData.jointInstances!) as [string, mirabuf.joint.JointInstance][])) { - if (jGuid == GROUNDED_JOINT_ID) - continue; + const jointData = parser.assembly.data!.joints! + for (const [jGuid, jInst] of Object.entries( + jointData.jointInstances! + ) as [string, mirabuf.joint.JointInstance][]) { + if (jGuid == GROUNDED_JOINT_ID) continue - const rnA = parser.partToNodeMap.get(jInst.parentPart!); - const rnB = parser.partToNodeMap.get(jInst.childPart!); + const rnA = parser.partToNodeMap.get(jInst.parentPart!) + const rnB = parser.partToNodeMap.get(jInst.childPart!) if (!rnA || !rnB) { - console.warn(`Skipping joint '${jInst.info!.name!}'. Couldn't find associated rigid nodes.`); - continue; + console.warn( + `Skipping joint '${jInst.info!.name!}'. Couldn't find associated rigid nodes.` + ) + continue } else if (rnA.id == rnB.id) { - console.warn(`Skipping joint '${jInst.info!.name!}'. Jointing the same parts. Likely in issue with Fusion Design structure.`); - continue; + console.warn( + `Skipping joint '${jInst.info!.name!}'. Jointing the same parts. Likely in issue with Fusion Design structure.` + ) + continue } - const jDef = parser.assembly.data!.joints!.jointDefinitions![jInst.jointReference!]! as mirabuf.joint.Joint; - const bodyIdA = mechanism.GetBodyByNodeId(rnA.id); - const bodyIdB = mechanism.GetBodyByNodeId(rnB.id); + const jDef = parser.assembly.data!.joints!.jointDefinitions![ + jInst.jointReference! + ]! as mirabuf.joint.Joint + const bodyIdA = mechanism.GetBodyByNodeId(rnA.id) + const bodyIdB = mechanism.GetBodyByNodeId(rnB.id) if (!bodyIdA || !bodyIdB) { - console.warn(`Skipping joint '${jInst.info!.name!}'. Failed to find rigid nodes' associated bodies.`); - continue; + console.warn( + `Skipping joint '${jInst.info!.name!}'. Failed to find rigid nodes' associated bodies.` + ) + continue } - const bodyA = this.GetBody(bodyIdA); - const bodyB = this.GetBody(bodyIdB); + const bodyA = this.GetBody(bodyIdA) + const bodyB = this.GetBody(bodyIdB) const constraints: Jolt.Constraint[] = [] - let listener: Jolt.PhysicsStepListener | undefined = undefined; + let listener: Jolt.PhysicsStepListener | undefined = undefined switch (jDef.jointMotionType!) { case mirabuf.joint.JointMotion.REVOLUTE: if (this.IsWheel(jDef)) { - if (parser.directedGraph.GetAdjacencyList(rnA.id).length > 0) { - const res = this.CreateWheelConstraint(jInst, jDef, bodyA, bodyB, parser.assembly.info!.version!); + if ( + parser.directedGraph.GetAdjacencyList(rnA.id) + .length > 0 + ) { + const res = this.CreateWheelConstraint( + jInst, + jDef, + bodyA, + bodyB, + parser.assembly.info!.version! + ) constraints.push(res[0]) constraints.push(res[1]) listener = res[2] } else { - const res = this.CreateWheelConstraint(jInst, jDef, bodyB, bodyA, parser.assembly.info!.version!); + const res = this.CreateWheelConstraint( + jInst, + jDef, + bodyB, + bodyA, + parser.assembly.info!.version! + ) constraints.push(res[0]) constraints.push(res[1]) listener = res[2] } } else { - constraints.push(this.CreateHingeConstraint(jInst, jDef, bodyA, bodyB, parser.assembly.info!.version!)); + constraints.push( + this.CreateHingeConstraint( + jInst, + jDef, + bodyA, + bodyB, + parser.assembly.info!.version! + ) + ) } - break; + break case mirabuf.joint.JointMotion.SLIDER: - constraints.push(this.CreateSliderConstraint(jInst, jDef, bodyA, bodyB)); - break; + constraints.push( + this.CreateSliderConstraint(jInst, jDef, bodyA, bodyB) + ) + break default: - console.debug('Unsupported joint detected. Skipping...'); - break; + console.debug("Unsupported joint detected. Skipping...") + break } if (constraints.length > 0) { - constraints.forEach(x => mechanism.AddConstraint({ parentBody: bodyIdA, childBody: bodyIdB, constraint: x })) + constraints.forEach(x => + mechanism.AddConstraint({ + parentBody: bodyIdA, + childBody: bodyIdB, + constraint: x, + }) + ) } if (listener) { mechanism.AddStepListener(listener) @@ -239,7 +312,7 @@ class PhysicsSystem extends WorldSystem { /** * Creates a Hinge constraint. - * + * * @param jointInstance Joint instance. * @param jointDefinition Joint definition. * @param bodyA Parent body to connect. @@ -248,383 +321,511 @@ class PhysicsSystem extends WorldSystem { * @returns Resulting Jolt Hinge Constraint. */ private CreateHingeConstraint( - jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, - bodyA: Jolt.Body, bodyB: Jolt.Body, versionNum: number): Jolt.Constraint { + jointInstance: mirabuf.joint.JointInstance, + jointDefinition: mirabuf.joint.Joint, + bodyA: Jolt.Body, + bodyB: Jolt.Body, + versionNum: number + ): Jolt.Constraint { // HINGE CONSTRAINT - const hingeConstraintSettings = new JOLT.HingeConstraintSettings(); - + const hingeConstraintSettings = new JOLT.HingeConstraintSettings() + const jointOrigin = jointDefinition.origin ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) - : new JOLT.Vec3(0, 0, 0); + : new JOLT.Vec3(0, 0, 0) // TODO: Offset transformation for robot builder. const jointOriginOffset = jointInstance.offset ? MirabufVector3_JoltVec3(jointInstance.offset as mirabuf.Vector3) - : new JOLT.Vec3(0, 0, 0); + : new JOLT.Vec3(0, 0, 0) - const anchorPoint = jointOrigin.Add(jointOriginOffset); - hingeConstraintSettings.mPoint1 = hingeConstraintSettings.mPoint2 = anchorPoint; + const anchorPoint = jointOrigin.Add(jointOriginOffset) + hingeConstraintSettings.mPoint1 = hingeConstraintSettings.mPoint2 = + anchorPoint - const rotationalFreedom = jointDefinition.rotational!.rotationalFreedom!; + const rotationalFreedom = jointDefinition.rotational!.rotationalFreedom! - const miraAxis = rotationalFreedom.axis! as mirabuf.Vector3; - let axis: Jolt.Vec3; + const miraAxis = rotationalFreedom.axis! as mirabuf.Vector3 + let axis: Jolt.Vec3 // No scaling, these are unit vectors if (versionNum < 5) { - axis = new JOLT.Vec3(-miraAxis.x ?? 0, miraAxis.y ?? 0, miraAxis.z! ?? 0); + axis = new JOLT.Vec3( + -miraAxis.x ?? 0, + miraAxis.y ?? 0, + miraAxis.z! ?? 0 + ) } else { - axis = new JOLT.Vec3(miraAxis.x! ?? 0, miraAxis.y! ?? 0, miraAxis.z! ?? 0); + axis = new JOLT.Vec3( + miraAxis.x! ?? 0, + miraAxis.y! ?? 0, + miraAxis.z! ?? 0 + ) } - hingeConstraintSettings.mHingeAxis1 = hingeConstraintSettings.mHingeAxis2 - = axis.Normalized(); - hingeConstraintSettings.mNormalAxis1 = hingeConstraintSettings.mNormalAxis2 - = getPerpendicular(hingeConstraintSettings.mHingeAxis1); + hingeConstraintSettings.mHingeAxis1 = + hingeConstraintSettings.mHingeAxis2 = axis.Normalized() + hingeConstraintSettings.mNormalAxis1 = + hingeConstraintSettings.mNormalAxis2 = getPerpendicular( + hingeConstraintSettings.mHingeAxis1 + ) // Some values that are meant to be exactly PI are perceived as being past it, causing unexpected beavior. // This safety check caps the values to be within [-PI, PI] wth minimal difference in precision. - const piSafetyCheck = (v: number) => Math.min(3.14158, Math.max(-3.14158, v)); - - if (rotationalFreedom.limits && Math.abs((rotationalFreedom.limits.upper ?? 0) - (rotationalFreedom.limits.lower ?? 0)) > 0.001) { - const currentPos = piSafetyCheck(rotationalFreedom.value ?? 0); - const upper = piSafetyCheck(rotationalFreedom.limits.upper ?? 0) - currentPos; - const lower = piSafetyCheck(rotationalFreedom.limits.lower ?? 0) - currentPos; - - hingeConstraintSettings.mLimitsMin = -upper; - hingeConstraintSettings.mLimitsMax = -lower; + const piSafetyCheck = (v: number) => + Math.min(3.14158, Math.max(-3.14158, v)) + + if ( + rotationalFreedom.limits && + Math.abs( + (rotationalFreedom.limits.upper ?? 0) - + (rotationalFreedom.limits.lower ?? 0) + ) > 0.001 + ) { + const currentPos = piSafetyCheck(rotationalFreedom.value ?? 0) + const upper = + piSafetyCheck(rotationalFreedom.limits.upper ?? 0) - currentPos + const lower = + piSafetyCheck(rotationalFreedom.limits.lower ?? 0) - currentPos + + hingeConstraintSettings.mLimitsMin = -upper + hingeConstraintSettings.mLimitsMax = -lower } - - const constraint = hingeConstraintSettings.Create(bodyA, bodyB); - this._joltPhysSystem.AddConstraint(constraint); - return constraint; + const constraint = hingeConstraintSettings.Create(bodyA, bodyB) + this._joltPhysSystem.AddConstraint(constraint) + + return constraint } /** * Creates a new slider constraint. - * + * * @param jointInstance Joint instance. * @param jointDefinition Joint definition. * @param bodyA Parent body to connect. * @param bodyB Child body to connect. - * + * * @returns Resulting Jolt constraint. */ private CreateSliderConstraint( - jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, - bodyA: Jolt.Body, bodyB: Jolt.Body): Jolt.Constraint { + jointInstance: mirabuf.joint.JointInstance, + jointDefinition: mirabuf.joint.Joint, + bodyA: Jolt.Body, + bodyB: Jolt.Body + ): Jolt.Constraint { + const sliderConstraintSettings = new JOLT.SliderConstraintSettings() - const sliderConstraintSettings = new JOLT.SliderConstraintSettings(); - const jointOrigin = jointDefinition.origin ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) - : new JOLT.Vec3(0, 0, 0); + : new JOLT.Vec3(0, 0, 0) // TODO: Offset transformation for robot builder. const jointOriginOffset = jointInstance.offset ? MirabufVector3_JoltVec3(jointInstance.offset as mirabuf.Vector3) - : new JOLT.Vec3(0, 0, 0); - - const anchorPoint = jointOrigin.Add(jointOriginOffset); - sliderConstraintSettings.mPoint1 = sliderConstraintSettings.mPoint2 = anchorPoint; - - const prismaticFreedom = jointDefinition.prismatic!.prismaticFreedom!; - - const miraAxis = prismaticFreedom.axis! as mirabuf.Vector3; - const axis = new JOLT.Vec3(miraAxis.x! ?? 0, miraAxis.y! ?? 0, miraAxis.z! ?? 0); - - sliderConstraintSettings.mSliderAxis1 = sliderConstraintSettings.mSliderAxis2 - = axis.Normalized(); - sliderConstraintSettings.mNormalAxis1 = sliderConstraintSettings.mNormalAxis2 - = getPerpendicular(sliderConstraintSettings.mSliderAxis1); - - if (prismaticFreedom.limits && Math.abs((prismaticFreedom.limits.upper ?? 0) - (prismaticFreedom.limits.lower ?? 0)) > 0.001) { - - const currentPos = (prismaticFreedom.value ?? 0) * 0.01; - const upper = ((prismaticFreedom.limits.upper ?? 0) * 0.01) - currentPos; - const lower = ((prismaticFreedom.limits.lower ?? 0) * 0.01) - currentPos; + : new JOLT.Vec3(0, 0, 0) + + const anchorPoint = jointOrigin.Add(jointOriginOffset) + sliderConstraintSettings.mPoint1 = sliderConstraintSettings.mPoint2 = + anchorPoint + + const prismaticFreedom = jointDefinition.prismatic!.prismaticFreedom! + + const miraAxis = prismaticFreedom.axis! as mirabuf.Vector3 + const axis = new JOLT.Vec3( + miraAxis.x! ?? 0, + miraAxis.y! ?? 0, + miraAxis.z! ?? 0 + ) + + sliderConstraintSettings.mSliderAxis1 = + sliderConstraintSettings.mSliderAxis2 = axis.Normalized() + sliderConstraintSettings.mNormalAxis1 = + sliderConstraintSettings.mNormalAxis2 = getPerpendicular( + sliderConstraintSettings.mSliderAxis1 + ) + + if ( + prismaticFreedom.limits && + Math.abs( + (prismaticFreedom.limits.upper ?? 0) - + (prismaticFreedom.limits.lower ?? 0) + ) > 0.001 + ) { + const currentPos = (prismaticFreedom.value ?? 0) * 0.01 + const upper = + (prismaticFreedom.limits.upper ?? 0) * 0.01 - currentPos + const lower = + (prismaticFreedom.limits.lower ?? 0) * 0.01 - currentPos // Calculate mid point - const midPoint = (upper + lower) / 2.0; - const halfRange = Math.abs((upper - lower) / 2.0); - + const midPoint = (upper + lower) / 2.0 + const halfRange = Math.abs((upper - lower) / 2.0) + // Move the anchor points - sliderConstraintSettings.mPoint2 - = anchorPoint.Add(axis.Normalized().Mul(midPoint)); + sliderConstraintSettings.mPoint2 = anchorPoint.Add( + axis.Normalized().Mul(midPoint) + ) - sliderConstraintSettings.mLimitsMax = halfRange; - sliderConstraintSettings.mLimitsMin = -halfRange; + sliderConstraintSettings.mLimitsMax = halfRange + sliderConstraintSettings.mLimitsMin = -halfRange } - - const constraint = sliderConstraintSettings.Create(bodyA, bodyB); - - this._constraints.push(constraint); - this._joltPhysSystem.AddConstraint(constraint); - return constraint; - } + const constraint = sliderConstraintSettings.Create(bodyA, bodyB) + + this._constraints.push(constraint) + this._joltPhysSystem.AddConstraint(constraint) + return constraint + } public CreateWheelConstraint( - jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, - bodyMain: Jolt.Body, bodyWheel: Jolt.Body, versionNum: number): [Jolt.Constraint, Jolt.VehicleConstraint, Jolt.PhysicsStepListener] { + jointInstance: mirabuf.joint.JointInstance, + jointDefinition: mirabuf.joint.Joint, + bodyMain: Jolt.Body, + bodyWheel: Jolt.Body, + versionNum: number + ): [Jolt.Constraint, Jolt.VehicleConstraint, Jolt.PhysicsStepListener] { // HINGE CONSTRAINT - const fixedSettings = new JOLT.FixedConstraintSettings(); - + const fixedSettings = new JOLT.FixedConstraintSettings() + const jointOrigin = jointDefinition.origin ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) - : new JOLT.Vec3(0, 0, 0); + : new JOLT.Vec3(0, 0, 0) const jointOriginOffset = jointInstance.offset ? MirabufVector3_JoltVec3(jointInstance.offset as mirabuf.Vector3) - : new JOLT.Vec3(0, 0, 0); + : new JOLT.Vec3(0, 0, 0) - const anchorPoint = jointOrigin.Add(jointOriginOffset); - fixedSettings.mPoint1 = fixedSettings.mPoint2 = anchorPoint; + const anchorPoint = jointOrigin.Add(jointOriginOffset) + fixedSettings.mPoint1 = fixedSettings.mPoint2 = anchorPoint - const rotationalFreedom = jointDefinition.rotational!.rotationalFreedom!; + const rotationalFreedom = jointDefinition.rotational!.rotationalFreedom! - const miraAxis = rotationalFreedom.axis! as mirabuf.Vector3; - let axis: Jolt.Vec3; + const miraAxis = rotationalFreedom.axis! as mirabuf.Vector3 + let axis: Jolt.Vec3 // No scaling, these are unit vectors if (versionNum < 5) { - axis = new JOLT.Vec3(-miraAxis.x ?? 0, miraAxis.y ?? 0, miraAxis.z ?? 0); + axis = new JOLT.Vec3( + -miraAxis.x ?? 0, + miraAxis.y ?? 0, + miraAxis.z ?? 0 + ) } else { - axis = new JOLT.Vec3(miraAxis.x ?? 0, miraAxis.y ?? 0, miraAxis.z ?? 0); + axis = new JOLT.Vec3( + miraAxis.x ?? 0, + miraAxis.y ?? 0, + miraAxis.z ?? 0 + ) } - const bounds = bodyWheel.GetShape().GetLocalBounds(); - const radius = (bounds.mMax.GetY() - bounds.mMin.GetY()) / 2.0; - - const wheelSettings = new JOLT.WheelSettingsWV(); - wheelSettings.mPosition = anchorPoint.Add(axis.Mul(0.1)); - wheelSettings.mMaxSteerAngle = 0.0; - wheelSettings.mMaxHandBrakeTorque = 0.0; - wheelSettings.mRadius = radius * 1.05; - wheelSettings.mWidth = 0.1; - wheelSettings.mSuspensionMinLength = 0.0000003; - wheelSettings.mSuspensionMaxLength = 0.0000006; - wheelSettings.mInertia = 1; - - const friction = new JOLT.LinearCurve(); - friction.Clear(); - friction.AddPoint(1,1); - friction.AddPoint(0,1); - wheelSettings.mLongitudinalFriction = friction; - - const vehicleSettings = new JOLT.VehicleConstraintSettings(); - - vehicleSettings.mWheels.clear(); - vehicleSettings.mWheels.push_back(wheelSettings); - - const controllerSettings = new JOLT.WheeledVehicleControllerSettings(); - controllerSettings.mEngine.mMaxTorque = 1500.0; - controllerSettings.mTransmission.mClutchStrength = 10.0; - controllerSettings.mTransmission.mGearRatios.clear(); - controllerSettings.mTransmission.mGearRatios.push_back(2); - controllerSettings.mTransmission.mMode = JOLT.ETransmissionMode_Auto; - vehicleSettings.mController = controllerSettings; - - vehicleSettings.mAntiRollBars.clear(); - - const vehicleConstraint = new JOLT.VehicleConstraint(bodyMain, vehicleSettings); - const fixedConstraint = JOLT.castObject(fixedSettings.Create(bodyMain, bodyWheel), JOLT.TwoBodyConstraint); + const bounds = bodyWheel.GetShape().GetLocalBounds() + const radius = (bounds.mMax.GetY() - bounds.mMin.GetY()) / 2.0 + + const wheelSettings = new JOLT.WheelSettingsWV() + wheelSettings.mPosition = anchorPoint.Add(axis.Mul(0.1)) + wheelSettings.mMaxSteerAngle = 0.0 + wheelSettings.mMaxHandBrakeTorque = 0.0 + wheelSettings.mRadius = radius * 1.05 + wheelSettings.mWidth = 0.1 + wheelSettings.mSuspensionMinLength = 0.0000003 + wheelSettings.mSuspensionMaxLength = 0.0000006 + wheelSettings.mInertia = 1 + + const friction = new JOLT.LinearCurve() + friction.Clear() + friction.AddPoint(1, 1) + friction.AddPoint(0, 1) + wheelSettings.mLongitudinalFriction = friction + + const vehicleSettings = new JOLT.VehicleConstraintSettings() + + vehicleSettings.mWheels.clear() + vehicleSettings.mWheels.push_back(wheelSettings) + + const controllerSettings = new JOLT.WheeledVehicleControllerSettings() + controllerSettings.mEngine.mMaxTorque = 1500.0 + controllerSettings.mTransmission.mClutchStrength = 10.0 + controllerSettings.mTransmission.mGearRatios.clear() + controllerSettings.mTransmission.mGearRatios.push_back(2) + controllerSettings.mTransmission.mMode = JOLT.ETransmissionMode_Auto + vehicleSettings.mController = controllerSettings + + vehicleSettings.mAntiRollBars.clear() + + const vehicleConstraint = new JOLT.VehicleConstraint( + bodyMain, + vehicleSettings + ) + const fixedConstraint = JOLT.castObject( + fixedSettings.Create(bodyMain, bodyWheel), + JOLT.TwoBodyConstraint + ) // Wheel Collision Tester - const tester = new JOLT.VehicleCollisionTesterCastCylinder(bodyWheel.GetObjectLayer(), 0.05); - vehicleConstraint.SetVehicleCollisionTester(tester); - const listener = new JOLT.VehicleConstraintStepListener(vehicleConstraint); - this._joltPhysSystem.AddStepListener(listener); - - this._joltPhysSystem.AddConstraint(vehicleConstraint); - this._joltPhysSystem.AddConstraint(fixedConstraint); - - this._constraints.push(fixedConstraint, vehicleConstraint); - return [fixedConstraint, vehicleConstraint, listener]; + const tester = new JOLT.VehicleCollisionTesterCastCylinder( + bodyWheel.GetObjectLayer(), + 0.05 + ) + vehicleConstraint.SetVehicleCollisionTester(tester) + const listener = new JOLT.VehicleConstraintStepListener( + vehicleConstraint + ) + this._joltPhysSystem.AddStepListener(listener) + + this._joltPhysSystem.AddConstraint(vehicleConstraint) + this._joltPhysSystem.AddConstraint(fixedConstraint) + + this._constraints.push(fixedConstraint, vehicleConstraint) + return [fixedConstraint, vehicleConstraint, listener] } private IsWheel(jDef: mirabuf.joint.Joint) { - return (jDef.info!.name! != 'grounded') && (jDef.userData) && ((new Map(Object.entries(jDef.userData.data!)).get('wheel') ?? 'false') == 'true') + return ( + jDef.info!.name! != "grounded" && + jDef.userData && + (new Map(Object.entries(jDef.userData.data!)).get("wheel") ?? + "false") == "true" + ) } /** * Creates a map, mapping the name of RigidNodes to Jolt BodyIDs - * + * * @param parser MirabufParser containing properly parsed RigidNodes * @returns Mapping of Jolt BodyIDs */ - public CreateBodiesFromParser(parser: MirabufParser, layerReserve?: LayerReserve): Map { - const rnToBodies = new Map(); - - if ((parser.assembly.dynamic && !layerReserve) || layerReserve?.isReleased) { - throw new Error('No layer reserve for dynamic assembly'); + public CreateBodiesFromParser( + parser: MirabufParser, + layerReserve?: LayerReserve + ): Map { + const rnToBodies = new Map() + + if ( + (parser.assembly.dynamic && !layerReserve) || + layerReserve?.isReleased + ) { + throw new Error("No layer reserve for dynamic assembly") } - const reservedLayer: number | undefined = layerReserve?.layer; - - filterNonPhysicsNodes(parser.rigidNodes, parser.assembly).forEach(rn => { - - const compoundShapeSettings = new JOLT.StaticCompoundShapeSettings(); - let shapesAdded = 0; - - let totalMass = 0; - const comAccum = new mirabuf.Vector3(); - - const minBounds = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0); - const maxBounds = new JOLT.Vec3(-1000000.0, -1000000.0, -1000000.0); - - const rnLayer: number = reservedLayer - ? reservedLayer - : (rn.id.endsWith(GAMEPIECE_SUFFIX) ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD); - - rn.parts.forEach(partId => { - const partInstance = parser.assembly.data!.parts!.partInstances![partId]!; - if (partInstance.skipCollider == null || partInstance == undefined || partInstance.skipCollider == false) { - const partDefinition = parser.assembly.data!.parts!.partDefinitions![partInstance.partDefinitionReference!]!; - - const partShapeResult = this.CreateShapeSettingsFromPart(partDefinition); - - if (partShapeResult) { - - const [shapeSettings, partMin, partMax] = partShapeResult; - - const transform = ThreeMatrix4_JoltMat44(parser.globalTransforms.get(partId)!); - const translation = transform.GetTranslation(); - const rotation = transform.GetQuaternion(); - compoundShapeSettings.AddShape( - translation, - rotation, - shapeSettings, - 0 - ); - shapesAdded++; - - this.UpdateMinMaxBounds(transform.Multiply3x3(partMin), minBounds, maxBounds); - this.UpdateMinMaxBounds(transform.Multiply3x3(partMax), minBounds, maxBounds); - - JOLT.destroy(partMin); - JOLT.destroy(partMax); - JOLT.destroy(transform); - - if (partDefinition.physicalData && partDefinition.physicalData.com && partDefinition.physicalData.mass) { - const mass = partDefinition.massOverride ? partDefinition.massOverride! : partDefinition.physicalData.mass!; - totalMass += mass; - comAccum.x += partDefinition.physicalData.com.x! * mass / 100.0; - comAccum.y += partDefinition.physicalData.com.y! * mass / 100.0; - comAccum.z += partDefinition.physicalData.com.z! * mass / 100.0; + const reservedLayer: number | undefined = layerReserve?.layer + + filterNonPhysicsNodes(parser.rigidNodes, parser.assembly).forEach( + rn => { + const compoundShapeSettings = + new JOLT.StaticCompoundShapeSettings() + let shapesAdded = 0 + + let totalMass = 0 + const comAccum = new mirabuf.Vector3() + + const minBounds = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0) + const maxBounds = new JOLT.Vec3( + -1000000.0, + -1000000.0, + -1000000.0 + ) + + const rnLayer: number = reservedLayer + ? reservedLayer + : rn.id.endsWith(GAMEPIECE_SUFFIX) + ? LAYER_GENERAL_DYNAMIC + : LAYER_FIELD + + rn.parts.forEach(partId => { + const partInstance = + parser.assembly.data!.parts!.partInstances![partId]! + if ( + partInstance == undefined || + partInstance.skipCollider == null || + partInstance.skipCollider == false + ) { + const partDefinition = + parser.assembly.data!.parts!.partDefinitions![ + partInstance.partDefinitionReference! + ]! + + const partShapeResult = + this.CreateShapeSettingsFromPart(partDefinition) + + if (partShapeResult) { + const [shapeSettings, partMin, partMax] = + partShapeResult + + const transform = ThreeMatrix4_JoltMat44( + parser.globalTransforms.get(partId)! + ) + const translation = transform.GetTranslation() + const rotation = transform.GetQuaternion() + compoundShapeSettings.AddShape( + translation, + rotation, + shapeSettings, + 0 + ) + shapesAdded++ + + this.UpdateMinMaxBounds( + transform.Multiply3x3(partMin), + minBounds, + maxBounds + ) + this.UpdateMinMaxBounds( + transform.Multiply3x3(partMax), + minBounds, + maxBounds + ) + + JOLT.destroy(partMin) + JOLT.destroy(partMax) + JOLT.destroy(transform) + + if ( + partDefinition.physicalData && + partDefinition.physicalData.com && + partDefinition.physicalData.mass + ) { + const mass = partDefinition.massOverride + ? partDefinition.massOverride! + : partDefinition.physicalData.mass! + totalMass += mass + comAccum.x += + (partDefinition.physicalData.com.x! * + mass) / + 100.0 + comAccum.y += + (partDefinition.physicalData.com.y! * + mass) / + 100.0 + comAccum.z += + (partDefinition.physicalData.com.z! * + mass) / + 100.0 + } } } - } - }); + }) - if (shapesAdded > 0) { - - const shapeResult = compoundShapeSettings.Create(); + if (shapesAdded > 0) { + const shapeResult = compoundShapeSettings.Create() - if (!shapeResult.IsValid || shapeResult.HasError()) { - console.error(`Failed to create shape for RigidNode ${rn.id}\n${shapeResult.GetError().c_str()}`); - } + if (!shapeResult.IsValid || shapeResult.HasError()) { + console.error( + `Failed to create shape for RigidNode ${rn.id}\n${shapeResult.GetError().c_str()}` + ) + } - const shape = shapeResult.Get(); + const shape = shapeResult.Get() - if (rn.isDynamic) { - shape.GetMassProperties().mMass = totalMass == 0.0 ? 1 : totalMass; + if (rn.isDynamic) { + shape.GetMassProperties().mMass = + totalMass == 0.0 ? 1 : totalMass + } + + const bodySettings = new JOLT.BodyCreationSettings( + shape, + new JOLT.Vec3(0.0, 0.0, 0.0), + new JOLT.Quat(0, 0, 0, 1), + rn.isDynamic + ? JOLT.EMotionType_Dynamic + : JOLT.EMotionType_Static, + rnLayer + ) + const body = + this._joltBodyInterface.CreateBody(bodySettings) + this._joltBodyInterface.AddBody( + body.GetID(), + JOLT.EActivation_Activate + ) + body.SetAllowSleeping(false) + rnToBodies.set(rn.id, body.GetID()) + + // Little testing components + body.SetRestitution(0.4) } - const bodySettings = new JOLT.BodyCreationSettings( - shape, - new JOLT.Vec3(0.0, 0.0, 0.0), - new JOLT.Quat(0, 0, 0, 1), - rn.isDynamic ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, - rnLayer - ); - const body = this._joltBodyInterface.CreateBody(bodySettings); - this._joltBodyInterface.AddBody(body.GetID(), JOLT.EActivation_Activate); - body.SetAllowSleeping(false); - rnToBodies.set(rn.id, body.GetID()); - - // Little testing components - body.SetRestitution(0.4); + // Cleanup + JOLT.destroy(compoundShapeSettings) } + ) - // Cleanup - JOLT.destroy(compoundShapeSettings); - }); - - return rnToBodies; + return rnToBodies } /** * Creates the Jolt ShapeSettings for a given part using the Part Definition of said part. - * + * * @param partDefinition Definition of the part to create. * @returns If successful, the created convex hull shape settings from the given Part Definition. */ - private CreateShapeSettingsFromPart(partDefinition: mirabuf.IPartDefinition): [Jolt.ShapeSettings, Jolt.Vec3, Jolt.Vec3] | undefined | null { - const settings = new JOLT.ConvexHullShapeSettings(); + private CreateShapeSettingsFromPart( + partDefinition: mirabuf.IPartDefinition + ): [Jolt.ShapeSettings, Jolt.Vec3, Jolt.Vec3] | undefined | null { + const settings = new JOLT.ConvexHullShapeSettings() - const min = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0); - const max = new JOLT.Vec3(-1000000.0, -1000000.0, -1000000.0); + const min = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0) + const max = new JOLT.Vec3(-1000000.0, -1000000.0, -1000000.0) - const points = settings.mPoints; + const points = settings.mPoints partDefinition.bodies!.forEach(body => { - if (body.triangleMesh && body.triangleMesh.mesh && body.triangleMesh.mesh.verts) { - const vertArr = body.triangleMesh.mesh.verts; - for (let i = 0; i < body.triangleMesh.mesh.verts.length; i += 3) { - const vert = MirabufFloatArr_JoltVec3(vertArr, i); - points.push_back(vert); - this.UpdateMinMaxBounds(vert, min, max); - JOLT.destroy(vert); + if ( + body.triangleMesh && + body.triangleMesh.mesh && + body.triangleMesh.mesh.verts + ) { + const vertArr = body.triangleMesh.mesh.verts + for ( + let i = 0; + i < body.triangleMesh.mesh.verts.length; + i += 3 + ) { + const vert = MirabufFloatArr_JoltVec3(vertArr, i) + points.push_back(vert) + this.UpdateMinMaxBounds(vert, min, max) + JOLT.destroy(vert) } } - }); + }) if (points.size() < 4) { - JOLT.destroy(settings); - JOLT.destroy(min); - JOLT.destroy(max); - return; + JOLT.destroy(settings) + JOLT.destroy(min) + JOLT.destroy(max) + return } else { - return [settings, min, max]; + return [settings, min, max] } } /** * Helper function to update min and max vector bounds. - * + * * @param v Vector to add to min, max, bounds. * @param min Minimum vector of the bounds. * @param max Maximum vector of the bounds. */ private UpdateMinMaxBounds(v: Jolt.Vec3, min: Jolt.Vec3, max: Jolt.Vec3) { - if (v.GetX() < min.GetX()) - min.SetX(v.GetX()); - if (v.GetY() < min.GetY()) - min.SetY(v.GetY()); - if (v.GetZ() < min.GetZ()) - min.SetZ(v.GetZ()); - - if (v.GetX() > max.GetX()) - max.SetX(v.GetX()); - if (v.GetY() > max.GetY()) - max.SetY(v.GetY()); - if (v.GetZ() > max.GetZ()) - max.SetZ(v.GetZ()); + if (v.GetX() < min.GetX()) min.SetX(v.GetX()) + if (v.GetY() < min.GetY()) min.SetY(v.GetY()) + if (v.GetZ() < min.GetZ()) min.SetZ(v.GetZ()) + + if (v.GetX() > max.GetX()) max.SetX(v.GetX()) + if (v.GetY() > max.GetY()) max.SetY(v.GetY()) + if (v.GetZ() > max.GetZ()) max.SetZ(v.GetZ()) } /** * Destroys bodies. - * + * * @param bodies Bodies to destroy. */ public DestroyBodies(...bodies: Jolt.Body[]) { bodies.forEach(x => { - this._joltBodyInterface.RemoveBody(x.GetID()); - this._joltBodyInterface.DestroyBody(x.GetID()); - }); + this._joltBodyInterface.RemoveBody(x.GetID()) + this._joltBodyInterface.DestroyBody(x.GetID()) + }) } public DestroyBodyIds(...bodies: Jolt.BodyID[]) { bodies.forEach(x => { - this._joltBodyInterface.RemoveBody(x); - this._joltBodyInterface.DestroyBody(x); - }); + this._joltBodyInterface.RemoveBody(x) + this._joltBodyInterface.DestroyBody(x) + }) } public DestroyMechanism(mech: Mechanism) { @@ -633,44 +834,84 @@ class PhysicsSystem extends WorldSystem { }) mech.constraints.forEach(x => { this._joltPhysSystem.RemoveConstraint(x.constraint) - }); + }) mech.nodeToBody.forEach(x => { - this._joltBodyInterface.RemoveBody(x); + this._joltBodyInterface.RemoveBody(x) // this._joltBodyInterface.DestroyBody(x); - }); - console.log('Mechanism destroyed') + }) + console.log("Mechanism destroyed") } - public GetBody(bodyId: Jolt.BodyID) { - return this._joltPhysSystem.GetBodyLockInterface().TryGetBody(bodyId); + public GetBody(bodyId: Jolt.BodyID): Jolt.Body { + return this._joltPhysSystem.GetBodyLockInterface().TryGetBody(bodyId) } public Update(_: number): void { - this._joltInterface.Step(SIMULATION_PERIOD, STANDARD_SUB_STEPS); + this._joltInterface.Step(SIMULATION_PERIOD, STANDARD_SUB_STEPS) } public Destroy(): void { this._constraints.forEach(x => { - this._joltPhysSystem.RemoveConstraint(x); + this._joltPhysSystem.RemoveConstraint(x) // JOLT.destroy(x); - }); - this._constraints = []; + }) + this._constraints = [] // Destroy Jolt Bodies. - this.DestroyBodyIds(...this._bodies); - this._bodies = []; + this.DestroyBodyIds(...this._bodies) + this._bodies = [] - JOLT.destroy(this._joltBodyInterface); - JOLT.destroy(this._joltInterface); + JOLT.destroy(this._joltBodyInterface) + JOLT.destroy(this._joltInterface) + } + + /** + * Creates a ghost object and a distance constraint that connects it to the given body + * The ghost body is part of the GHOST_LAYER which doesn't interact with any other layer + * The caller is responsible for cleaning up the ghost body and the constraint + * + * @param body The body to be attatched to and moved + * @returns The ghost body and the constraint + */ + + public CreateGodModeBody(body: Jolt.Body): [Jolt.Body, Jolt.Constraint] { + const bodyPosition = body.GetPosition() + const ghostPosition = new THREE.Vector3( + bodyPosition.GetX(), + bodyPosition.GetY() + 0.05, + bodyPosition.GetZ() + ) + const ghostBody = this.CreateBox( + new THREE.Vector3(0.05, 0.05, 0.05), + undefined, + ghostPosition, + undefined + ) + + const ghostBodyId = ghostBody.GetID() + // TODO: Verify that layers don't interact by default + this._joltBodyInterface.SetObjectLayer(ghostBodyId, GHOST_LAYER) + this._joltBodyInterface.AddBody(ghostBodyId, JOLT.EActivation_Activate) + this._bodies.push(ghostBodyId) + + const constraintSettings = new JOLT.SwingTwistConstraintSettings() + const constraint = constraintSettings.Create(ghostBody, body) + this._joltPhysSystem.AddConstraint(constraint) + + return [ghostBody, constraint] } } export class LayerReserve { - private _layer: number; - private _isReleased: boolean; + private _layer: number + private _isReleased: boolean - public get layer() { return this._layer } - public get isReleased() { return this._isReleased } + public get layer() { + return this._layer + } + public get isReleased() { + return this._isReleased + } public constructor() { this._layer = RobotLayers.shift()! @@ -679,92 +920,114 @@ export class LayerReserve { public Release() { if (!this._isReleased) { - RobotLayers.push(this._layer); - this._isReleased = true; + RobotLayers.push(this._layer) + this._isReleased = true } } } /** * Initialize collision groups and filtering for Jolt. - * + * * @param settings Jolt object used for applying filters. */ -function SetupCollisionFiltering(settings: Jolt.JoltSettings) { - const objectFilter = new JOLT.ObjectLayerPairFilterTable(COUNT_OBJECT_LAYERS); - +function SetupCollisionFiltering(settings: Jolt.JoltSettings): void { + const objectFilter = new JOLT.ObjectLayerPairFilterTable( + COUNT_OBJECT_LAYERS + ) + // Enable Field layer collisions - objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, LAYER_GENERAL_DYNAMIC); - objectFilter.EnableCollision(LAYER_FIELD, LAYER_GENERAL_DYNAMIC); + objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, LAYER_GENERAL_DYNAMIC) + objectFilter.EnableCollision(LAYER_FIELD, LAYER_GENERAL_DYNAMIC) for (let i = 0; i < RobotLayers.length; i++) { - objectFilter.EnableCollision(LAYER_FIELD, RobotLayers[i]); - objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, RobotLayers[i]); + objectFilter.EnableCollision(LAYER_FIELD, RobotLayers[i]) + objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, RobotLayers[i]) } // Enable Collisions between other robots for (let i = 0; i < RobotLayers.length - 1; i++) { for (let j = i + 1; j < RobotLayers.length; j++) { - objectFilter.EnableCollision(RobotLayers[i], RobotLayers[j]); + objectFilter.EnableCollision(RobotLayers[i], RobotLayers[j]) } } - const BP_LAYER_FIELD = new JOLT.BroadPhaseLayer(LAYER_FIELD); - const BP_LAYER_GENERAL_DYNAMIC = new JOLT.BroadPhaseLayer(LAYER_GENERAL_DYNAMIC); + const BP_LAYER_FIELD = new JOLT.BroadPhaseLayer(LAYER_FIELD) + const BP_LAYER_GENERAL_DYNAMIC = new JOLT.BroadPhaseLayer( + LAYER_GENERAL_DYNAMIC + ) - const bpRobotLayers = new Array(RobotLayers.length); + const bpRobotLayers = new Array(RobotLayers.length) for (let i = 0; i < bpRobotLayers.length; i++) { - bpRobotLayers[i] = new JOLT.BroadPhaseLayer(RobotLayers[i]); + bpRobotLayers[i] = new JOLT.BroadPhaseLayer(RobotLayers[i]) } - const COUNT_BROAD_PHASE_LAYERS = 2 + RobotLayers.length; + const COUNT_BROAD_PHASE_LAYERS = 2 + RobotLayers.length - const bpInterface = new JOLT.BroadPhaseLayerInterfaceTable(COUNT_OBJECT_LAYERS, COUNT_BROAD_PHASE_LAYERS); - - bpInterface.MapObjectToBroadPhaseLayer(LAYER_FIELD, BP_LAYER_FIELD); - bpInterface.MapObjectToBroadPhaseLayer(LAYER_GENERAL_DYNAMIC, BP_LAYER_GENERAL_DYNAMIC); + const bpInterface = new JOLT.BroadPhaseLayerInterfaceTable( + COUNT_OBJECT_LAYERS, + COUNT_BROAD_PHASE_LAYERS + ) + + bpInterface.MapObjectToBroadPhaseLayer(LAYER_FIELD, BP_LAYER_FIELD) + bpInterface.MapObjectToBroadPhaseLayer( + LAYER_GENERAL_DYNAMIC, + BP_LAYER_GENERAL_DYNAMIC + ) for (let i = 0; i < bpRobotLayers.length; i++) { - bpInterface.MapObjectToBroadPhaseLayer(RobotLayers[i], bpRobotLayers[i]); + bpInterface.MapObjectToBroadPhaseLayer(RobotLayers[i], bpRobotLayers[i]) } - settings.mObjectLayerPairFilter = objectFilter; - settings.mBroadPhaseLayerInterface = bpInterface; - settings.mObjectVsBroadPhaseLayerFilter = new JOLT.ObjectVsBroadPhaseLayerFilterTable( - settings.mBroadPhaseLayerInterface, - COUNT_BROAD_PHASE_LAYERS, - settings.mObjectLayerPairFilter, - COUNT_OBJECT_LAYERS - ); + settings.mObjectLayerPairFilter = objectFilter + settings.mBroadPhaseLayerInterface = bpInterface + settings.mObjectVsBroadPhaseLayerFilter = + new JOLT.ObjectVsBroadPhaseLayerFilterTable( + settings.mBroadPhaseLayerInterface, + COUNT_BROAD_PHASE_LAYERS, + settings.mObjectLayerPairFilter, + COUNT_OBJECT_LAYERS + ) } -function filterNonPhysicsNodes(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembly): RigidNodeReadOnly[] { +function filterNonPhysicsNodes( + nodes: RigidNodeReadOnly[], + mira: mirabuf.Assembly +): RigidNodeReadOnly[] { return nodes.filter(x => { for (const part of x.parts) { - const inst = mira.data!.parts!.partInstances![part]!; - const def = mira.data!.parts!.partDefinitions![inst.partDefinitionReference!]!; + const inst = mira.data!.parts!.partInstances![part]! + const def = + mira.data!.parts!.partDefinitions![ + inst.partDefinitionReference! + ]! if (def.bodies && def.bodies.length > 0) { - return true; + return true } } - return false; - }); + return false + }) } function getPerpendicular(vec: Jolt.Vec3): Jolt.Vec3 { - return tryGetPerpendicular(vec, new JOLT.Vec3(0, 1, 0)) - ?? tryGetPerpendicular(vec, new JOLT.Vec3(0, 0, 1))!; + return ( + tryGetPerpendicular(vec, new JOLT.Vec3(0, 1, 0)) ?? + tryGetPerpendicular(vec, new JOLT.Vec3(0, 0, 1))! + ) } -function tryGetPerpendicular(vec: Jolt.Vec3, toCheck: Jolt.Vec3): Jolt.Vec3 | undefined { +function tryGetPerpendicular( + vec: Jolt.Vec3, + toCheck: Jolt.Vec3 +): Jolt.Vec3 | undefined { if (Math.abs(Math.abs(vec.Dot(toCheck)) - 1.0) < 0.0001) { - return undefined; + return undefined } - const a = vec.Dot(toCheck); + const a = vec.Dot(toCheck) return new JOLT.Vec3( toCheck.GetX() - vec.GetX() * a, toCheck.GetY() - vec.GetY() * a, toCheck.GetZ() - vec.GetZ() * a - ).Normalized(); + ).Normalized() } -export default PhysicsSystem; \ No newline at end of file +export default PhysicsSystem diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index 71246ece9b..e9946fe998 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -1,85 +1,109 @@ -import { test, expect, describe, assert } from 'vitest'; -import PhysicsSystem, { LayerReserve } from '../systems/physics/PhysicsSystem'; -import { LoadMirabufLocal } from '@/mirabuf/MirabufLoader'; -import MirabufParser from '@/mirabuf/MirabufParser'; - -describe('Physics Sansity Checks', () => { - test('Convex Hull Shape (Cube)', () => { - const points: Float32Array = new Float32Array( - [ - 0.5, -0.5, 0.5, - -0.5, -0.5, 0.5, - -0.5, -0.5, -0.5, - 0.5, -0.5, -0.5, - 0.5, 0.5, 0.5, - -0.5, 0.5, 0.5, - -0.5, 0.5, -0.5, - 0.5, 0.5, -0.5, - ] - ); - - const system = new PhysicsSystem(); - const shapeResult = system.CreateConvexHull(points); - - assert(shapeResult.HasError() == false, shapeResult.GetError().c_str()); - expect(shapeResult.IsValid()).toBe(true); - - const shape = shapeResult.Get(); - - expect(shape.GetVolume() - 1.0).toBeLessThan(0.001); - expect(shape.GetCenterOfMass().Length()).toBe(0.0); - - shape.Release(); - system.Destroy(); - - }); - test('Convex Hull Shape (Tetrahedron)', () => { - const points: Float32Array = new Float32Array( - [ - 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0 - ] - ); - - const system = new PhysicsSystem(); - const shapeResult = system.CreateConvexHull(points); - - assert(shapeResult.HasError() == false, shapeResult.GetError().c_str()); - expect(shapeResult.IsValid()).toBe(true); - - const shape = shapeResult.Get(); - const bounds = shape.GetLocalBounds(); - const boxSize = bounds.mMax.Sub(bounds.mMin); - - expect(boxSize.GetX() - 1.0).toBeLessThan(0.001); - expect(boxSize.GetY() - 1.0).toBeLessThan(0.001); - expect(boxSize.GetZ() - 1.0).toBeLessThan(0.001); - expect(shape.GetVolume() - (1.0 / 6.0)).toBeLessThan(0.001); - expect(shape.GetMassProperties().mMass - 6.0).toBeLessThan(0.001); - - shape.Release(); - system.Destroy(); - }); -}); - -describe('Mirabuf Physics Loading', () => { - test('Body Loading (Dozer)', () => { - const assembly = LoadMirabufLocal('./public/Downloadables/Mira/Robots/Dozer_v9.mira'); - const parser = new MirabufParser(assembly); - const physSystem = new PhysicsSystem(); - const mapping = physSystem.CreateBodiesFromParser(parser, new LayerReserve()); - - expect(mapping.size).toBe(7); - }); - - test('Body Loading (Team_2471_(2018)_v7.mira)', () => { - const assembly = LoadMirabufLocal('./public/Downloadables/Mira/Robots/Team 2471 (2018)_v7.mira'); - const parser = new MirabufParser(assembly); - const physSystem = new PhysicsSystem(); - const mapping = physSystem.CreateBodiesFromParser(parser, new LayerReserve()); - - expect(mapping.size).toBe(10); - }); -}); \ No newline at end of file +import { test, expect, describe, assert } from "vitest" +import PhysicsSystem, { LayerReserve } from "../systems/physics/PhysicsSystem" +import { LoadMirabufLocal } from "@/mirabuf/MirabufLoader" +import MirabufParser from "@/mirabuf/MirabufParser" +import * as THREE from "three" + +describe("Physics Sansity Checks", () => { + test("Convex Hull Shape (Cube)", () => { + const points: Float32Array = new Float32Array([ + 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, + 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, + ]) + + const system = new PhysicsSystem() + const shapeResult = system.CreateConvexHull(points) + + assert(shapeResult.HasError() == false, shapeResult.GetError().c_str()) + expect(shapeResult.IsValid()).toBe(true) + + const shape = shapeResult.Get() + + expect(shape.GetVolume() - 1.0).toBeLessThan(0.001) + expect(shape.GetCenterOfMass().Length()).toBe(0.0) + + shape.Release() + system.Destroy() + }) + test("Convex Hull Shape (Tetrahedron)", () => { + const points: Float32Array = new Float32Array([ + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, + ]) + + const system = new PhysicsSystem() + const shapeResult = system.CreateConvexHull(points) + + assert(shapeResult.HasError() == false, shapeResult.GetError().c_str()) + expect(shapeResult.IsValid()).toBe(true) + + const shape = shapeResult.Get() + const bounds = shape.GetLocalBounds() + const boxSize = bounds.mMax.Sub(bounds.mMin) + + expect(boxSize.GetX() - 1.0).toBeLessThan(0.001) + expect(boxSize.GetY() - 1.0).toBeLessThan(0.001) + expect(boxSize.GetZ() - 1.0).toBeLessThan(0.001) + expect(shape.GetVolume() - 1.0 / 6.0).toBeLessThan(0.001) + expect(shape.GetMassProperties().mMass - 6.0).toBeLessThan(0.001) + + shape.Release() + system.Destroy() + }) +}) + +describe("GodMode", () => { + test("Basic", () => { + const system = new PhysicsSystem() + const box = system.CreateBox( + new THREE.Vector3(1, 1, 1), + 1, + new THREE.Vector3(0, 0, 0), + undefined + ) + const [ghostObject, ghostConstraint] = system.CreateGodModeBody(box) + + assert(system.GetBody(ghostObject.GetID()) != undefined) + assert(system.GetBody(box.GetID()) != undefined) + assert(ghostConstraint != undefined) + // Check constraint after running for a few seconds + // TODO: Make sure this is the correct way to do this + // TODO: Figure out how to make it use substeps to check instead + for (let i = 0; i < 30; i++) { + // TODO: Change this once this function actually uses deltaT + system.Update(0) + } + assert(system.GetBody(ghostObject.GetID()) != undefined) + assert(system.GetBody(box.GetID()) != undefined) + assert(ghostConstraint != undefined) + }) +}) + +describe("Mirabuf Physics Loading", () => { + test("Body Loading (Dozer)", () => { + const assembly = LoadMirabufLocal( + "./public/Downloadables/Mira/Robots/Dozer_v9.mira" + ) + const parser = new MirabufParser(assembly) + const physSystem = new PhysicsSystem() + const mapping = physSystem.CreateBodiesFromParser( + parser, + new LayerReserve() + ) + + expect(mapping.size).toBe(7) + }) + + test("Body Loading (Team_2471_(2018)_v7.mira)", () => { + const assembly = LoadMirabufLocal( + "./public/Downloadables/Mira/Robots/Team 2471 (2018)_v7.mira" + ) + const parser = new MirabufParser(assembly) + const physSystem = new PhysicsSystem() + const mapping = physSystem.CreateBodiesFromParser( + parser, + new LayerReserve() + ) + + expect(mapping.size).toBe(10) + }) +}) From 7009552e88974a27ab4a54cb03e0d78e164fcc4c Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 24 Jun 2024 09:46:26 -0700 Subject: [PATCH 02/14] expose SetPosition method on _joltBodyInterface with SetBodyPosition method on PhysicsSystem --- fission/src/mirabuf/MirabufSceneObject.ts | 6 +++-- fission/src/systems/physics/PhysicsSystem.ts | 16 +++++++++++- fission/src/test/PhysicsSystem.test.ts | 27 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 5e12a0bcca..3d01684589 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -69,7 +69,10 @@ class MirabufSceneObject extends SceneObject { console.error("Failed to anchor to mechanism") } const bodyId = World.PhysicsSystem.GetBody(anchorBody!) - World.PhysicsSystem.CreateGodModeBody(bodyId) + const [godModeBody, _godModeConstraint] = + World.PhysicsSystem.CreateGodModeBody(bodyId) + godModeBody.AddForce(new JOLT.Vec3(0, -10, 0)) + godModeBody.AddTorque(new JOLT.Vec3(1, 1, 1)) // Simulation World.SimulationSystem.RegisterMechanism(this._mechanism) @@ -191,4 +194,3 @@ export async function CreateMirabufFromUrl( } export default MirabufSceneObject - diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 9607847e79..5010b90460 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -71,7 +71,7 @@ class PhysicsSystem extends WorldSystem { const ground = this.CreateBox( new THREE.Vector3(5.0, 0.5, 5.0), - undefined, + 1, new THREE.Vector3(0.0, -2.0, 0.0), undefined ) @@ -900,6 +900,20 @@ class PhysicsSystem extends WorldSystem { return [ghostBody, constraint] } + + /** + * Exposes the SetPosition method on the _joltBodyInterface + * + * @param id The bodyID + * @param position The desired position for the body + */ + public SetBodyPosition(id: Jolt.BodyID, position: Jolt.Vec3): void { + this._joltBodyInterface.SetPosition( + id, + position, + JOLT.EActivation_Activate + ) + } } export class LayerReserve { diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index e9946fe998..9dfbf136af 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -3,6 +3,7 @@ import PhysicsSystem, { LayerReserve } from "../systems/physics/PhysicsSystem" import { LoadMirabufLocal } from "@/mirabuf/MirabufLoader" import MirabufParser from "@/mirabuf/MirabufParser" import * as THREE from "three" +import JOLT from "@/util/loading/JoltSyncLoader" describe("Physics Sansity Checks", () => { test("Convex Hull Shape (Cube)", () => { @@ -76,6 +77,32 @@ describe("GodMode", () => { assert(system.GetBody(box.GetID()) != undefined) assert(ghostConstraint != undefined) }) + + test("Position", () => { + const system = new PhysicsSystem() + const box = system.CreateBox( + new THREE.Vector3(1, 1, 1), + 1, + new THREE.Vector3(0, 0, 0), + undefined + ) + const [ghostObject, _ghostConstraint] = system.CreateGodModeBody(box) + const origPosition = ghostObject.GetPosition() + ghostObject.AddTorque(new JOLT.Vec3(1, 1, 1)) + ghostObject.AddForce(new JOLT.Vec3(1, 1, 1)) + + for (let i = 0; i < 30; i++) { + system.Update(i) + } + + const currPosition = ghostObject.GetPosition() + + assert( + currPosition.GetX() != origPosition.GetX() || + currPosition.GetY() != origPosition.GetY() || + currPosition.GetZ() != origPosition.GetZ() + ) + }) }) describe("Mirabuf Physics Loading", () => { From ba1a5184e12425a5bcfa14a1891e6887b16a44a8 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 24 Jun 2024 14:54:03 -0700 Subject: [PATCH 03/14] added godmode test in HUD --- fission/src/mirabuf/MirabufSceneObject.ts | 17 +++++-- fission/src/systems/physics/PhysicsSystem.ts | 22 ++++----- fission/src/test/PhysicsSystem.test.ts | 18 +++++--- fission/src/ui/components/MainHUD.tsx | 45 ++++++++++++++----- .../modals/spawning/ManageAssembliesModal.tsx | 14 +++--- 5 files changed, 78 insertions(+), 38 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 3d01684589..50c4e761a5 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -62,17 +62,22 @@ class MirabufSceneObject extends SceneObject { }) } + /* + // TODO: Comment out before PR, tranfer to test // God mode testing: add ghost object to all spawned mirabuf files const nodeId = this._mechanism.rootBody const anchorBody = this._mechanism.GetBodyByNodeId(nodeId) if (anchorBody == undefined) { console.error("Failed to anchor to mechanism") } - const bodyId = World.PhysicsSystem.GetBody(anchorBody!) const [godModeBody, _godModeConstraint] = - World.PhysicsSystem.CreateGodModeBody(bodyId) - godModeBody.AddForce(new JOLT.Vec3(0, -10, 0)) - godModeBody.AddTorque(new JOLT.Vec3(1, 1, 1)) + World.PhysicsSystem.CreateGodModeBody(anchorBody!) + + World.PhysicsSystem.SetBodyPosition( + godModeBody.GetID(), + new JOLT.Vec3(1, 1, 1) + ) + */ // Simulation World.SimulationSystem.RegisterMechanism(this._mechanism) @@ -139,6 +144,10 @@ class MirabufSceneObject extends SceneObject { this._physicsLayerReserve?.Release() } + public GetRootNodeId(): Jolt.BodyID | undefined { + return this._mechanism.nodeToBody.get(this._mechanism.rootBody) + } + private CreateMeshForShape(shape: Jolt.Shape): THREE.Mesh { const scale = new JOLT.Vec3(1, 1, 1) const triangleContext = new JOLT.ShapeGetTriangles( diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 5010b90460..0d06b4df64 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -29,7 +29,7 @@ const RobotLayers: number[] = [ ] // Layer for ghost object in god mode, interacts with nothing -const GHOST_LAYER = 10 +const LAYER_GHOST = 10 // Please update this accordingly. const COUNT_OBJECT_LAYERS = 11 @@ -71,7 +71,7 @@ class PhysicsSystem extends WorldSystem { const ground = this.CreateBox( new THREE.Vector3(5.0, 0.5, 5.0), - 1, + undefined, new THREE.Vector3(0.0, -2.0, 0.0), undefined ) @@ -867,14 +867,15 @@ class PhysicsSystem extends WorldSystem { /** * Creates a ghost object and a distance constraint that connects it to the given body - * The ghost body is part of the GHOST_LAYER which doesn't interact with any other layer + * The ghost body is part of the LAYER_GHOST which doesn't interact with any other layer * The caller is responsible for cleaning up the ghost body and the constraint * - * @param body The body to be attatched to and moved + * @param id The id of the body to be attatched to and moved * @returns The ghost body and the constraint */ - public CreateGodModeBody(body: Jolt.Body): [Jolt.Body, Jolt.Constraint] { + public CreateGodModeBody(id: Jolt.BodyID): [Jolt.Body, Jolt.Constraint] { + const body = this.GetBody(id) const bodyPosition = body.GetPosition() const ghostPosition = new THREE.Vector3( bodyPosition.GetX(), @@ -889,23 +890,24 @@ class PhysicsSystem extends WorldSystem { ) const ghostBodyId = ghostBody.GetID() - // TODO: Verify that layers don't interact by default - this._joltBodyInterface.SetObjectLayer(ghostBodyId, GHOST_LAYER) + this._joltBodyInterface.SetObjectLayer(ghostBodyId, LAYER_GHOST) this._joltBodyInterface.AddBody(ghostBodyId, JOLT.EActivation_Activate) this._bodies.push(ghostBodyId) const constraintSettings = new JOLT.SwingTwistConstraintSettings() const constraint = constraintSettings.Create(ghostBody, body) this._joltPhysSystem.AddConstraint(constraint) + this._constraints.push(constraint) return [ghostBody, constraint] } /** * Exposes the SetPosition method on the _joltBodyInterface + * Sets the position of the body * - * @param id The bodyID - * @param position The desired position for the body + * @param id The id of the body + * @param position The new position of the body */ public SetBodyPosition(id: Jolt.BodyID, position: Jolt.Vec3): void { this._joltBodyInterface.SetPosition( @@ -932,7 +934,7 @@ export class LayerReserve { this._isReleased = false } - public Release() { + public Release(): void { if (!this._isReleased) { RobotLayers.push(this._layer) this._isReleased = true diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index 9dfbf136af..1069221131 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -61,7 +61,9 @@ describe("GodMode", () => { new THREE.Vector3(0, 0, 0), undefined ) - const [ghostObject, ghostConstraint] = system.CreateGodModeBody(box) + const [ghostObject, ghostConstraint] = system.CreateGodModeBody( + box.GetID() + ) assert(system.GetBody(ghostObject.GetID()) != undefined) assert(system.GetBody(box.GetID()) != undefined) @@ -71,11 +73,14 @@ describe("GodMode", () => { // TODO: Figure out how to make it use substeps to check instead for (let i = 0; i < 30; i++) { // TODO: Change this once this function actually uses deltaT - system.Update(0) + system.Update(i) } + assert(system.GetBody(ghostObject.GetID()) != undefined) assert(system.GetBody(box.GetID()) != undefined) assert(ghostConstraint != undefined) + + system.Destroy() }) test("Position", () => { @@ -86,10 +91,11 @@ describe("GodMode", () => { new THREE.Vector3(0, 0, 0), undefined ) - const [ghostObject, _ghostConstraint] = system.CreateGodModeBody(box) + const [ghostObject, _ghostConstraint] = system.CreateGodModeBody( + box.GetID() + ) const origPosition = ghostObject.GetPosition() - ghostObject.AddTorque(new JOLT.Vec3(1, 1, 1)) - ghostObject.AddForce(new JOLT.Vec3(1, 1, 1)) + system.SetBodyPosition(ghostObject.GetID(), new JOLT.Vec3(2, 2, 2)) for (let i = 0; i < 30; i++) { system.Update(i) @@ -102,6 +108,8 @@ describe("GodMode", () => { currPosition.GetY() != origPosition.GetY() || currPosition.GetZ() != origPosition.GetZ() ) + + system.Destroy() }) }) diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index baacd7dc84..d197538f21 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -14,6 +14,11 @@ import { ToastType, useToastContext } from "@/ui/ToastContext" import { Random } from "@/util/Random" import APS, { APS_USER_INFO_UPDATE_EVENT } from "@/aps/APS" import { UserIcon } from "./UserIcon" +import World from "@/systems/World" +import JOLT from "@/util/loading/JoltSyncLoader" +import * as THREE from "three" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import SceneObject from "@/systems/scene/SceneObject" type ButtonProps = { value: string @@ -147,6 +152,11 @@ const MainHUD: React.FC = () => { icon={} onClick={() => openModal("import-mirabuf")} /> + } + onClick={TestGodMode} + />

{userInfo ? - } - larger={true} - onClick={() => APS.logout()} - /> + } + larger={true} + onClick={() => APS.logout()} + /> : - } - larger={true} - onClick={() => APS.requestAuthCode()} - /> + } + larger={true} + onClick={() => APS.requestAuthCode()} + /> } ) } +function TestGodMode() { + const robot: MirabufSceneObject = [...World.SceneRenderer.sceneObjects.entries()].filter(x => { const y = (x[1] instanceof MirabufSceneObject); return y }).map(x => x[1])[0] as MirabufSceneObject + const rootNodeId = robot.GetRootNodeId() + if (rootNodeId == undefined) { + console.error("Robot root node not found for god mode") + return; + } + const [ghostBody, _ghostConstraint] = World.PhysicsSystem.CreateGodModeBody(rootNodeId) + World.PhysicsSystem.SetBodyPosition(ghostBody.GetID(), new JOLT.Vec3(2, 2, 2)) +} + export default MainHUD diff --git a/fission/src/ui/modals/spawning/ManageAssembliesModal.tsx b/fission/src/ui/modals/spawning/ManageAssembliesModal.tsx index 7ae40b40c2..d69a11f46f 100644 --- a/fission/src/ui/modals/spawning/ManageAssembliesModal.tsx +++ b/fission/src/ui/modals/spawning/ManageAssembliesModal.tsx @@ -37,18 +37,18 @@ const ManageAssembliesModal: React.FC = ({ modalId }) => { icon={} modalId={modalId} onAccept={() => { - // showTooltip("controls", [ - // { control: "WASD", description: "Drive" }, - // { control: "E", description: "Intake" }, - // { control: "Q", description: "Dispense" }, - // ]); - } + // showTooltip("controls", [ + // { control: "WASD", description: "Drive" }, + // { control: "E", description: "Intake" }, + // { control: "Q", description: "Dispense" }, + // ]); + } } >
{ - assemblies.map(x => AssemblyCard({id: x, update: update})) + assemblies.map(x => AssemblyCard({ id: x, update: update })) }
From 35114dbe8693063c14b35d60bbcba8d857d56e2d Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 24 Jun 2024 14:57:32 -0700 Subject: [PATCH 04/14] removed test code from mirabuf scene object class --- fission/src/mirabuf/MirabufSceneObject.ts | 17 ----------------- fission/src/test/PhysicsSystem.test.ts | 1 - 2 files changed, 18 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 50c4e761a5..04d231fc12 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -62,23 +62,6 @@ class MirabufSceneObject extends SceneObject { }) } - /* - // TODO: Comment out before PR, tranfer to test - // God mode testing: add ghost object to all spawned mirabuf files - const nodeId = this._mechanism.rootBody - const anchorBody = this._mechanism.GetBodyByNodeId(nodeId) - if (anchorBody == undefined) { - console.error("Failed to anchor to mechanism") - } - const [godModeBody, _godModeConstraint] = - World.PhysicsSystem.CreateGodModeBody(anchorBody!) - - World.PhysicsSystem.SetBodyPosition( - godModeBody.GetID(), - new JOLT.Vec3(1, 1, 1) - ) - */ - // Simulation World.SimulationSystem.RegisterMechanism(this._mechanism) const simLayer = World.SimulationSystem.GetSimulationLayer( diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index 1069221131..1f0af4c08b 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -72,7 +72,6 @@ describe("GodMode", () => { // TODO: Make sure this is the correct way to do this // TODO: Figure out how to make it use substeps to check instead for (let i = 0; i < 30; i++) { - // TODO: Change this once this function actually uses deltaT system.Update(i) } From 05cf6fbd531cf99297d1e6bce95380435fd2c3d5 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 24 Jun 2024 15:19:16 -0700 Subject: [PATCH 05/14] removed unnecessary test --- fission/src/mirabuf/MirabufSceneObject.ts | 17 ++++++++++++ fission/src/test/PhysicsSystem.test.ts | 32 ++--------------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 04d231fc12..50c4e761a5 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -62,6 +62,23 @@ class MirabufSceneObject extends SceneObject { }) } + /* + // TODO: Comment out before PR, tranfer to test + // God mode testing: add ghost object to all spawned mirabuf files + const nodeId = this._mechanism.rootBody + const anchorBody = this._mechanism.GetBodyByNodeId(nodeId) + if (anchorBody == undefined) { + console.error("Failed to anchor to mechanism") + } + const [godModeBody, _godModeConstraint] = + World.PhysicsSystem.CreateGodModeBody(anchorBody!) + + World.PhysicsSystem.SetBodyPosition( + godModeBody.GetID(), + new JOLT.Vec3(1, 1, 1) + ) + */ + // Simulation World.SimulationSystem.RegisterMechanism(this._mechanism) const simLayer = World.SimulationSystem.GetSimulationLayer( diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index 1f0af4c08b..e16d8559ac 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -72,6 +72,7 @@ describe("GodMode", () => { // TODO: Make sure this is the correct way to do this // TODO: Figure out how to make it use substeps to check instead for (let i = 0; i < 30; i++) { + // TODO: Change this once this function actually uses deltaT system.Update(i) } @@ -79,36 +80,7 @@ describe("GodMode", () => { assert(system.GetBody(box.GetID()) != undefined) assert(ghostConstraint != undefined) - system.Destroy() - }) - - test("Position", () => { - const system = new PhysicsSystem() - const box = system.CreateBox( - new THREE.Vector3(1, 1, 1), - 1, - new THREE.Vector3(0, 0, 0), - undefined - ) - const [ghostObject, _ghostConstraint] = system.CreateGodModeBody( - box.GetID() - ) - const origPosition = ghostObject.GetPosition() - system.SetBodyPosition(ghostObject.GetID(), new JOLT.Vec3(2, 2, 2)) - - for (let i = 0; i < 30; i++) { - system.Update(i) - } - - const currPosition = ghostObject.GetPosition() - - assert( - currPosition.GetX() != origPosition.GetX() || - currPosition.GetY() != origPosition.GetY() || - currPosition.GetZ() != origPosition.GetZ() - ) - - system.Destroy() + //system.Destroy() }) }) From 2dd9b791e63aef5035aa9f25344811bcffeda54d Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 24 Jun 2024 15:22:30 -0700 Subject: [PATCH 06/14] removed god mode testing in mirabuf --- fission/src/mirabuf/MirabufSceneObject.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 50c4e761a5..04d231fc12 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -62,23 +62,6 @@ class MirabufSceneObject extends SceneObject { }) } - /* - // TODO: Comment out before PR, tranfer to test - // God mode testing: add ghost object to all spawned mirabuf files - const nodeId = this._mechanism.rootBody - const anchorBody = this._mechanism.GetBodyByNodeId(nodeId) - if (anchorBody == undefined) { - console.error("Failed to anchor to mechanism") - } - const [godModeBody, _godModeConstraint] = - World.PhysicsSystem.CreateGodModeBody(anchorBody!) - - World.PhysicsSystem.SetBodyPosition( - godModeBody.GetID(), - new JOLT.Vec3(1, 1, 1) - ) - */ - // Simulation World.SimulationSystem.RegisterMechanism(this._mechanism) const simLayer = World.SimulationSystem.GetSimulationLayer( From 62d6585717185a5b7f72734af823f4b903648636 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 24 Jun 2024 15:41:06 -0700 Subject: [PATCH 07/14] renamed god mode button in HUD --- fission/src/ui/components/MainHUD.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index d197538f21..140fb6f6f0 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -153,7 +153,7 @@ const MainHUD: React.FC = () => { onClick={() => openModal("import-mirabuf")} /> } onClick={TestGodMode} /> From 773d058db63c268743cbae44393c3d845f209725 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 24 Jun 2024 17:04:58 -0700 Subject: [PATCH 08/14] formated fission --- fission/src/Synthesis.tsx | 20 +-- fission/src/mirabuf/MirabufParser.ts | 2 +- fission/src/systems/World.ts | 34 ++--- fission/src/systems/input/InputSystem.ts | 98 ++++++------- .../systems/simulation/SimulationSystem.ts | 22 +-- .../behavior/ArcadeDriveBehavior.ts | 49 ++++--- .../systems/simulation/behavior/Behavior.ts | 2 +- .../simulation/behavior/GenericArmBehavior.ts | 47 ++++--- .../behavior/GenericElevatorBehavior.ts | 45 +++--- .../systems/simulation/driver/SliderDriver.ts | 15 +- .../systems/simulation/driver/WheelDriver.ts | 12 +- .../simulation/stimulus/WheelStimulus.ts | 2 +- .../synthesis_brain/SynthesisBrain.ts | 130 ++++++++++-------- fission/src/test/PhysicsSystem.test.ts | 53 +------ fission/src/ui/ToastContext.tsx | 8 +- fission/src/ui/components/Dropdown.tsx | 15 +- fission/src/ui/components/MainHUD.tsx | 18 +-- fission/src/ui/components/NumberInput.tsx | 2 +- fission/src/ui/components/Scene.tsx | 14 +- .../modals/configuring/ChangeInputsModal.tsx | 45 +++--- .../theme-editor/ThemeEditorModal.tsx | 6 +- 21 files changed, 316 insertions(+), 323 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 261efc1615..c6d0fb9a39 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -1,9 +1,9 @@ -import Scene from '@/components/Scene.tsx'; -import MirabufSceneObject from './mirabuf/MirabufSceneObject.ts'; -import { LoadMirabufRemote } from './mirabuf/MirabufLoader.ts'; -import { mirabuf } from './proto/mirabuf'; -import MirabufParser, { ParseErrorSeverity } from './mirabuf/MirabufParser.ts'; -import MirabufInstance from './mirabuf/MirabufInstance.ts'; +import Scene from "@/components/Scene.tsx" +import MirabufSceneObject from "./mirabuf/MirabufSceneObject.ts" +import { LoadMirabufRemote } from "./mirabuf/MirabufLoader.ts" +import { mirabuf } from "./proto/mirabuf" +import MirabufParser, { ParseErrorSeverity } from "./mirabuf/MirabufParser.ts" +import MirabufInstance from "./mirabuf/MirabufInstance.ts" import { AnimatePresence } from "framer-motion" import { ReactElement, useEffect } from "react" import { ModalControlProvider, useModalManager } from "@/ui/ModalContext" @@ -49,10 +49,10 @@ import ScoringZonesPanel from "@/panels/configuring/scoring/ScoringZonesPanel" import ZoneConfigPanel from "@/panels/configuring/scoring/ZoneConfigPanel" import ScoreboardPanel from "@/panels/information/ScoreboardPanel" import DriverStationPanel from "@/panels/simulation/DriverStationPanel" -import ManageAssembliesModal from '@/modals/spawning/ManageAssembliesModal.tsx'; -import World from '@/systems/World.ts'; -import { AddRobotsModal, AddFieldsModal, SpawningModal } from '@/modals/spawning/SpawningModals.tsx'; -import ImportMirabufModal from '@/modals/mirabuf/ImportMirabufModal.tsx'; +import ManageAssembliesModal from "@/modals/spawning/ManageAssembliesModal.tsx" +import World from "@/systems/World.ts" +import { AddRobotsModal, AddFieldsModal, SpawningModal } from "@/modals/spawning/SpawningModals.tsx" +import ImportMirabufModal from "@/modals/mirabuf/ImportMirabufModal.tsx" const DEFAULT_MIRA_PATH = "/api/mira/Robots/Team 2471 (2018)_v7.mira" diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index 7936f8ca44..1d503f5623 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -191,7 +191,7 @@ class MirabufParser { // Build undirected graph const graph = new Graph() graph.AddNode(rootNode ? rootNode.id : this._rigidNodes[0].id) - const jointInstances = (Object.values(assembly.data!.joints!.jointInstances!) as mirabuf.joint.JointInstance[]) + const jointInstances = Object.values(assembly.data!.joints!.jointInstances!) as mirabuf.joint.JointInstance[] jointInstances.forEach((x: mirabuf.joint.JointInstance) => { const rA = this._partToNodeMap.get(x.parentPart) const rB = this._partToNodeMap.get(x.childPart) diff --git a/fission/src/systems/World.ts b/fission/src/systems/World.ts index e38fac6813..7470618dc8 100644 --- a/fission/src/systems/World.ts +++ b/fission/src/systems/World.ts @@ -1,9 +1,9 @@ import * as THREE from "three" -import PhysicsSystem from "./physics/PhysicsSystem"; -import SceneRenderer from "./scene/SceneRenderer"; -import SimulationSystem from "./simulation/SimulationSystem"; -import InputSystem from "./input/InputSystem"; +import PhysicsSystem from "./physics/PhysicsSystem" +import SceneRenderer from "./scene/SceneRenderer" +import SimulationSystem from "./simulation/SimulationSystem" +import InputSystem from "./input/InputSystem" class World { private static _isAlive: boolean = false @@ -37,10 +37,10 @@ class World { World._clock = new THREE.Clock() World._isAlive = true - World._sceneRenderer = new SceneRenderer(); - World._physicsSystem = new PhysicsSystem(); - World._simulationSystem = new SimulationSystem(); - World._inputSystem = new InputSystem(); + World._sceneRenderer = new SceneRenderer() + World._physicsSystem = new PhysicsSystem() + World._simulationSystem = new SimulationSystem() + World._inputSystem = new InputSystem() } public static DestroyWorld() { @@ -48,18 +48,18 @@ class World { World._isAlive = false - World._physicsSystem.Destroy(); - World._sceneRenderer.Destroy(); - World._simulationSystem.Destroy(); - World._inputSystem.Destroy(); + World._physicsSystem.Destroy() + World._sceneRenderer.Destroy() + World._simulationSystem.Destroy() + World._inputSystem.Destroy() } public static UpdateWorld() { - const deltaT = World._clock.getDelta(); - World._simulationSystem.Update(deltaT); - World._physicsSystem.Update(deltaT); - World._inputSystem.Update(deltaT); - World._sceneRenderer.Update(deltaT); + const deltaT = World._clock.getDelta() + World._simulationSystem.Update(deltaT) + World._physicsSystem.Update(deltaT) + World._inputSystem.Update(deltaT) + World._sceneRenderer.Update(deltaT) } } diff --git a/fission/src/systems/input/InputSystem.ts b/fission/src/systems/input/InputSystem.ts index cd7f4610e6..21aca82ee7 100644 --- a/fission/src/systems/input/InputSystem.ts +++ b/fission/src/systems/input/InputSystem.ts @@ -1,4 +1,4 @@ -import WorldSystem from "../WorldSystem"; +import WorldSystem from "../WorldSystem" declare global { type ModifierState = { @@ -16,115 +16,119 @@ declare global { } } -export const emptyModifierState: ModifierState = { ctrl: false, alt: false, shift: false, meta: false }; +export const emptyModifierState: ModifierState = { ctrl: false, alt: false, shift: false, meta: false } // When a robot is loaded, default inputs replace any unassigned inputs const defaultInputs: { [key: string]: Input } = { - "intake": { name: "intake", keyCode: "KeyE", isGlobal: true, modifiers: emptyModifierState }, - "shootGamepiece": { name: "shootGamepiece", keyCode: "KeyQ", isGlobal: true, modifiers: emptyModifierState }, - "enableGodMode": { name: "enableGodMode", keyCode: "KeyG", isGlobal: true, modifiers: emptyModifierState }, - - "arcadeForward": { name: "arcadeForward", keyCode: "KeyW", isGlobal: false, modifiers: emptyModifierState }, - "arcadeBackward": { name: "arcadeBackward", keyCode: "KeyS", isGlobal: false, modifiers: emptyModifierState }, - "arcadeLeft": { name: "arcadeLeft", keyCode: "KeyA", isGlobal: false, modifiers: emptyModifierState }, - "arcadeRight": { name: "arcadeRight", keyCode: "KeyD", isGlobal: false, modifiers: emptyModifierState }, + intake: { name: "intake", keyCode: "KeyE", isGlobal: true, modifiers: emptyModifierState }, + shootGamepiece: { name: "shootGamepiece", keyCode: "KeyQ", isGlobal: true, modifiers: emptyModifierState }, + enableGodMode: { name: "enableGodMode", keyCode: "KeyG", isGlobal: true, modifiers: emptyModifierState }, + + arcadeForward: { name: "arcadeForward", keyCode: "KeyW", isGlobal: false, modifiers: emptyModifierState }, + arcadeBackward: { name: "arcadeBackward", keyCode: "KeyS", isGlobal: false, modifiers: emptyModifierState }, + arcadeLeft: { name: "arcadeLeft", keyCode: "KeyA", isGlobal: false, modifiers: emptyModifierState }, + arcadeRight: { name: "arcadeRight", keyCode: "KeyD", isGlobal: false, modifiers: emptyModifierState }, } class InputSystem extends WorldSystem { - public static allInputs: { [key: string]: Input } = { } - private static _currentModifierState: ModifierState; + public static allInputs: { [key: string]: Input } = {} + private static _currentModifierState: ModifierState // Inputs global to all of synthesis like camera controls public static get globalInputs(): { [key: string]: Input } { - return Object.fromEntries( - Object.entries(InputSystem.allInputs) - .filter(([_, input]) => input.isGlobal)); + return Object.fromEntries(Object.entries(InputSystem.allInputs).filter(([_, input]) => input.isGlobal)) } // Robot specific controls like driving public static get robotInputs(): { [key: string]: Input } { - return Object.fromEntries( - Object.entries(InputSystem.allInputs) - .filter(([_, input]) => !input.isGlobal)); + return Object.fromEntries(Object.entries(InputSystem.allInputs).filter(([_, input]) => !input.isGlobal)) } // A list of keys currently being pressed - private static _keysPressed: { [key: string]: boolean } = {}; + private static _keysPressed: { [key: string]: boolean } = {} constructor() { - super(); + super() - this.HandleKeyDown = this.HandleKeyDown.bind(this); - document.addEventListener('keydown', this.HandleKeyDown); + this.HandleKeyDown = this.HandleKeyDown.bind(this) + document.addEventListener("keydown", this.HandleKeyDown) + + this.HandleKeyUp = this.HandleKeyUp.bind(this) + document.addEventListener("keyup", this.HandleKeyUp) - this.HandleKeyUp = this.HandleKeyUp.bind(this); - document.addEventListener('keyup', this.HandleKeyUp); - // TODO: Load saved inputs from mira (robot specific) & global inputs for (const key in defaultInputs) { if (Object.prototype.hasOwnProperty.call(defaultInputs, key)) { - InputSystem.allInputs[key] = defaultInputs[key]; + InputSystem.allInputs[key] = defaultInputs[key] } } } public Update(_: number): void { if (!document.hasFocus()) { - for (const keyCode in InputSystem._keysPressed) - delete InputSystem._keysPressed[keyCode]; - return; + for (const keyCode in InputSystem._keysPressed) delete InputSystem._keysPressed[keyCode] + return } - InputSystem._currentModifierState = { ctrl: InputSystem.isKeyPressed("ControlLeft") || InputSystem.isKeyPressed("ControlRight"), alt: InputSystem.isKeyPressed("AltLeft") || InputSystem.isKeyPressed("AltRight"), shift: InputSystem.isKeyPressed("ShiftLeft") || InputSystem.isKeyPressed("ShiftRight"), meta: InputSystem.isKeyPressed("MetaLeft") || InputSystem.isKeyPressed("MetaRight") } + InputSystem._currentModifierState = { + ctrl: InputSystem.isKeyPressed("ControlLeft") || InputSystem.isKeyPressed("ControlRight"), + alt: InputSystem.isKeyPressed("AltLeft") || InputSystem.isKeyPressed("AltRight"), + shift: InputSystem.isKeyPressed("ShiftLeft") || InputSystem.isKeyPressed("ShiftRight"), + meta: InputSystem.isKeyPressed("MetaLeft") || InputSystem.isKeyPressed("MetaRight"), + } } - public Destroy(): void { - document.removeEventListener('keydown', this.HandleKeyDown); - document.removeEventListener('keyup', this.HandleKeyUp); - } + public Destroy(): void { + document.removeEventListener("keydown", this.HandleKeyDown) + document.removeEventListener("keyup", this.HandleKeyUp) + } // Called when any key is pressed private HandleKeyDown(event: KeyboardEvent) { - InputSystem._keysPressed[event.code] = true; + InputSystem._keysPressed[event.code] = true } // Called when any key is released private HandleKeyUp(event: KeyboardEvent) { - InputSystem._keysPressed[event.code] = false; + InputSystem._keysPressed[event.code] = false } // Returns true if the given key is currently down private static isKeyPressed(key: string): boolean { - return !!InputSystem._keysPressed[key]; + return !!InputSystem._keysPressed[key] } // If an input exists, return true if it is pressed - public static getInput(inputName: string) : boolean { + public static getInput(inputName: string): boolean { // Checks if there is an input assigned to this action if (inputName in this.allInputs) { - const targetInput = this.allInputs[inputName]; + const targetInput = this.allInputs[inputName] // Check for input modifiers - if (!this.CompareModifiers(InputSystem._currentModifierState, targetInput.modifiers)) - return false; + if (!this.CompareModifiers(InputSystem._currentModifierState, targetInput.modifiers)) return false - return this.isKeyPressed(targetInput.keyCode); + return this.isKeyPressed(targetInput.keyCode) } // If the input does not exist, returns false - return false; + return false } // Combines two inputs into a positive/negative axis public static GetAxis(positive: string, negative: string) { - return (this.getInput(positive) ? 1 : 0) - (this.getInput(negative) ? 1 : 0); + return (this.getInput(positive) ? 1 : 0) - (this.getInput(negative) ? 1 : 0) } // Returns true if two modifier states are identical - private static CompareModifiers(state1: ModifierState, state2: ModifierState) : boolean { - return state1.alt == state2.alt && state1.ctrl == state2.ctrl && state1.meta == state2.meta && state1.shift == state2.shift; + private static CompareModifiers(state1: ModifierState, state2: ModifierState): boolean { + return ( + state1.alt == state2.alt && + state1.ctrl == state2.ctrl && + state1.meta == state2.meta && + state1.shift == state2.shift + ) } } -export default InputSystem; \ No newline at end of file +export default InputSystem diff --git a/fission/src/systems/simulation/SimulationSystem.ts b/fission/src/systems/simulation/SimulationSystem.ts index 0bc789040e..331739f8a7 100644 --- a/fission/src/systems/simulation/SimulationSystem.ts +++ b/fission/src/systems/simulation/SimulationSystem.ts @@ -58,9 +58,15 @@ class SimulationLayer { private _drivers: Driver[] private _stimuli: Stimulus[] - public get brain() { return this._brain; } - public get drivers() { return this._drivers; } - public get stimuli() { return this._stimuli; } + public get brain() { + return this._brain + } + public get drivers() { + return this._drivers + } + public get stimuli() { + return this._stimuli + } constructor(mechanism: Mechanism) { this._mechanism = mechanism @@ -101,13 +107,13 @@ class SimulationLayer { public SetBrain(brain: T | undefined) { if (this._brain) this._brain.Disable() - this._brain = brain; - + this._brain = brain + if (this._brain) { - this._brain.Enable(); + this._brain.Enable() } } } -export default SimulationSystem; -export {SimulationLayer}; \ No newline at end of file +export default SimulationSystem +export { SimulationLayer } diff --git a/fission/src/systems/simulation/behavior/ArcadeDriveBehavior.ts b/fission/src/systems/simulation/behavior/ArcadeDriveBehavior.ts index 4d5ed5497c..c9bb881a85 100644 --- a/fission/src/systems/simulation/behavior/ArcadeDriveBehavior.ts +++ b/fission/src/systems/simulation/behavior/ArcadeDriveBehavior.ts @@ -1,35 +1,42 @@ -import WheelDriver from "../driver/WheelDriver"; -import WheelRotationStimulus from "../stimulus/WheelStimulus"; -import Behavior from "./Behavior"; -import InputSystem from "@/systems/input/InputSystem"; +import WheelDriver from "../driver/WheelDriver" +import WheelRotationStimulus from "../stimulus/WheelStimulus" +import Behavior from "./Behavior" +import InputSystem from "@/systems/input/InputSystem" class ArcadeDriveBehavior extends Behavior { - leftWheels: WheelDriver[]; - rightWheels: WheelDriver[]; + leftWheels: WheelDriver[] + rightWheels: WheelDriver[] - private _driveSpeed = 30; - private _turnSpeed = 30; + private _driveSpeed = 30 + private _turnSpeed = 30 - constructor(leftWheels: WheelDriver[], rightWheels: WheelDriver[], leftStimuli: WheelRotationStimulus[], rightStimuli: WheelRotationStimulus[]) { - super(leftWheels.concat(rightWheels), leftStimuli.concat(rightStimuli)); - - this.leftWheels = leftWheels; - this.rightWheels = rightWheels; + constructor( + leftWheels: WheelDriver[], + rightWheels: WheelDriver[], + leftStimuli: WheelRotationStimulus[], + rightStimuli: WheelRotationStimulus[] + ) { + super(leftWheels.concat(rightWheels), leftStimuli.concat(rightStimuli)) + + this.leftWheels = leftWheels + this.rightWheels = rightWheels } // Sets the drivetrains target linear and rotational velocity private DriveSpeeds(linearVelocity: number, rotationVelocity: number) { - const leftSpeed = linearVelocity + rotationVelocity; - const rightSpeed = linearVelocity - rotationVelocity; - - this.leftWheels.forEach((wheel) => wheel.targetWheelSpeed = leftSpeed); - this.rightWheels.forEach((wheel) => wheel.targetWheelSpeed = rightSpeed); + const leftSpeed = linearVelocity + rotationVelocity + const rightSpeed = linearVelocity - rotationVelocity + + this.leftWheels.forEach(wheel => (wheel.targetWheelSpeed = leftSpeed)) + this.rightWheels.forEach(wheel => (wheel.targetWheelSpeed = rightSpeed)) } public Update(_: number): void { - this.DriveSpeeds(InputSystem.GetAxis("arcadeForward", "arcadeBackward")*this._driveSpeed, - InputSystem.GetAxis("arcadeRight", "arcadeLeft")*this._turnSpeed); + this.DriveSpeeds( + InputSystem.GetAxis("arcadeForward", "arcadeBackward") * this._driveSpeed, + InputSystem.GetAxis("arcadeRight", "arcadeLeft") * this._turnSpeed + ) } } -export default ArcadeDriveBehavior; \ No newline at end of file +export default ArcadeDriveBehavior diff --git a/fission/src/systems/simulation/behavior/Behavior.ts b/fission/src/systems/simulation/behavior/Behavior.ts index fcfc54cca3..2dd0fcb190 100644 --- a/fission/src/systems/simulation/behavior/Behavior.ts +++ b/fission/src/systems/simulation/behavior/Behavior.ts @@ -17,7 +17,7 @@ abstract class Behavior { this._stimuli = stimuli } - public abstract Update(deltaT: number): void; + public abstract Update(deltaT: number): void } export default Behavior diff --git a/fission/src/systems/simulation/behavior/GenericArmBehavior.ts b/fission/src/systems/simulation/behavior/GenericArmBehavior.ts index 6e5a0b7c0c..b2d50d8064 100644 --- a/fission/src/systems/simulation/behavior/GenericArmBehavior.ts +++ b/fission/src/systems/simulation/behavior/GenericArmBehavior.ts @@ -1,37 +1,46 @@ -import HingeDriver from "../driver/HingeDriver"; -import HingeStimulus from "../stimulus/HingeStimulus"; -import Behavior from "./Behavior"; -import InputSystem, { emptyModifierState } from "@/systems/input/InputSystem"; +import HingeDriver from "../driver/HingeDriver" +import HingeStimulus from "../stimulus/HingeStimulus" +import Behavior from "./Behavior" +import InputSystem, { emptyModifierState } from "@/systems/input/InputSystem" class GenericArmBehavior extends Behavior { - private _hingeDriver: HingeDriver; + private _hingeDriver: HingeDriver - private _positiveInput: string; - private _negativeInput: string; - - private _rotationalSpeed = 30; + private _positiveInput: string + private _negativeInput: string + + private _rotationalSpeed = 30 constructor(hingeDriver: HingeDriver, hingeStimulus: HingeStimulus, jointIndex: number) { - super([hingeDriver], [hingeStimulus]); - this._hingeDriver = hingeDriver; + super([hingeDriver], [hingeStimulus]) + this._hingeDriver = hingeDriver - this._positiveInput = "joint " + jointIndex + " Positive"; - this._negativeInput = "joint " + jointIndex + " Negative"; + this._positiveInput = "joint " + jointIndex + " Positive" + this._negativeInput = "joint " + jointIndex + " Negative" // TODO: load inputs from mira - InputSystem.allInputs[this._positiveInput] = { name: this._positiveInput, keyCode: "Digit" + jointIndex.toString(), isGlobal: false, modifiers: emptyModifierState }; - InputSystem.allInputs[this._negativeInput] = { name: this._negativeInput, keyCode: "Digit" + jointIndex.toString(), isGlobal: false, - modifiers: { ctrl: false, alt: false, shift: true, meta: false } }; + InputSystem.allInputs[this._positiveInput] = { + name: this._positiveInput, + keyCode: "Digit" + jointIndex.toString(), + isGlobal: false, + modifiers: emptyModifierState, + } + InputSystem.allInputs[this._negativeInput] = { + name: this._negativeInput, + keyCode: "Digit" + jointIndex.toString(), + isGlobal: false, + modifiers: { ctrl: false, alt: false, shift: true, meta: false }, + } } // Sets the arms target rotational velocity rotateArm(rotationalVelocity: number) { - this._hingeDriver.targetVelocity = rotationalVelocity; + this._hingeDriver.targetVelocity = rotationalVelocity } public Update(_: number): void { - this.rotateArm(InputSystem.GetAxis(this._positiveInput, this._negativeInput)*this._rotationalSpeed); + this.rotateArm(InputSystem.GetAxis(this._positiveInput, this._negativeInput) * this._rotationalSpeed) } } -export default GenericArmBehavior; \ No newline at end of file +export default GenericArmBehavior diff --git a/fission/src/systems/simulation/behavior/GenericElevatorBehavior.ts b/fission/src/systems/simulation/behavior/GenericElevatorBehavior.ts index 1190cff16a..0d9ce8de79 100644 --- a/fission/src/systems/simulation/behavior/GenericElevatorBehavior.ts +++ b/fission/src/systems/simulation/behavior/GenericElevatorBehavior.ts @@ -1,37 +1,46 @@ -import SliderDriver from "../driver/SliderDriver"; -import SliderStimulus from "../stimulus/SliderStimulus"; -import Behavior from "./Behavior"; -import InputSystem, { emptyModifierState } from "@/systems/input/InputSystem"; +import SliderDriver from "../driver/SliderDriver" +import SliderStimulus from "../stimulus/SliderStimulus" +import Behavior from "./Behavior" +import InputSystem, { emptyModifierState } from "@/systems/input/InputSystem" class GenericElevatorBehavior extends Behavior { - private _sliderDriver: SliderDriver; + private _sliderDriver: SliderDriver - private _positiveInput: string; - private _negativeInput: string; + private _positiveInput: string + private _negativeInput: string - private _linearSpeed = 1; + private _linearSpeed = 1 constructor(sliderDriver: SliderDriver, sliderStimulus: SliderStimulus, jointIndex: number) { - super([sliderDriver], [sliderStimulus]); - this._sliderDriver = sliderDriver; + super([sliderDriver], [sliderStimulus]) + this._sliderDriver = sliderDriver - this._positiveInput = "joint " + jointIndex + " Positive"; - this._negativeInput = "joint " + jointIndex + " Negative"; + this._positiveInput = "joint " + jointIndex + " Positive" + this._negativeInput = "joint " + jointIndex + " Negative" // TODO: load inputs from mira - InputSystem.allInputs[this._positiveInput] = { name: this._positiveInput, keyCode: "Digit" + jointIndex.toString(), isGlobal: false, modifiers: emptyModifierState }; - InputSystem.allInputs[this._negativeInput] = { name: this._negativeInput, keyCode: "Digit" + jointIndex.toString(), isGlobal: false, - modifiers: { ctrl: false, alt: false, shift: true, meta: false } }; + InputSystem.allInputs[this._positiveInput] = { + name: this._positiveInput, + keyCode: "Digit" + jointIndex.toString(), + isGlobal: false, + modifiers: emptyModifierState, + } + InputSystem.allInputs[this._negativeInput] = { + name: this._negativeInput, + keyCode: "Digit" + jointIndex.toString(), + isGlobal: false, + modifiers: { ctrl: false, alt: false, shift: true, meta: false }, + } } // Changes the elevators target position moveElevator(positionDelta: number) { - this._sliderDriver.targetPosition += positionDelta; + this._sliderDriver.targetPosition += positionDelta } public Update(deltaT: number): void { - this.moveElevator(InputSystem.GetAxis(this._positiveInput, this._negativeInput)*this._linearSpeed*deltaT); + this.moveElevator(InputSystem.GetAxis(this._positiveInput, this._negativeInput) * this._linearSpeed * deltaT) } } -export default GenericElevatorBehavior; \ No newline at end of file +export default GenericElevatorBehavior diff --git a/fission/src/systems/simulation/driver/SliderDriver.ts b/fission/src/systems/simulation/driver/SliderDriver.ts index 5cd0282524..050a41c925 100644 --- a/fission/src/systems/simulation/driver/SliderDriver.ts +++ b/fission/src/systems/simulation/driver/SliderDriver.ts @@ -1,8 +1,8 @@ -import Jolt from "@barclah/jolt-physics"; -import Driver from "./Driver"; -import { SIMULATION_PERIOD } from "@/systems/physics/PhysicsSystem"; -import JOLT from "@/util/loading/JoltSyncLoader"; -import InputSystem from "@/systems/input/InputSystem"; +import Jolt from "@barclah/jolt-physics" +import Driver from "./Driver" +import { SIMULATION_PERIOD } from "@/systems/physics/PhysicsSystem" +import JOLT from "@/util/loading/JoltSyncLoader" +import InputSystem from "@/systems/input/InputSystem" class SliderDriver extends Driver { private _constraint: Jolt.SliderConstraint @@ -47,8 +47,9 @@ class SliderDriver extends Driver { } public Update(_: number): void { - this._targetPosition += ((InputSystem.getInput("sliderUp") ? 1 : 0) - (InputSystem.getInput("sliderDown") ? 1 : 0))*3; - this._constraint.SetTargetPosition(this._targetPosition); + this._targetPosition += + ((InputSystem.getInput("sliderUp") ? 1 : 0) - (InputSystem.getInput("sliderDown") ? 1 : 0)) * 3 + this._constraint.SetTargetPosition(this._targetPosition) } } diff --git a/fission/src/systems/simulation/driver/WheelDriver.ts b/fission/src/systems/simulation/driver/WheelDriver.ts index 5758e357ba..8de5244adf 100644 --- a/fission/src/systems/simulation/driver/WheelDriver.ts +++ b/fission/src/systems/simulation/driver/WheelDriver.ts @@ -15,17 +15,19 @@ class WheelDriver extends Driver { this._targetWheelSpeed = radsPerSec } - public get constraint(): Jolt.VehicleConstraint { return this._constraint } + public get constraint(): Jolt.VehicleConstraint { + return this._constraint + } public constructor(constraint: Jolt.VehicleConstraint) { super() - this._constraint = constraint; - this._wheel = JOLT.castObject(this._constraint.GetWheel(0), JOLT.WheelWV); + this._constraint = constraint + this._wheel = JOLT.castObject(this._constraint.GetWheel(0), JOLT.WheelWV) } - public Update(_: number): void { - this._wheel.SetAngularVelocity(this._targetWheelSpeed); + public Update(_: number): void { + this._wheel.SetAngularVelocity(this._targetWheelSpeed) } } diff --git a/fission/src/systems/simulation/stimulus/WheelStimulus.ts b/fission/src/systems/simulation/stimulus/WheelStimulus.ts index 27426933bd..cfb6206085 100644 --- a/fission/src/systems/simulation/stimulus/WheelStimulus.ts +++ b/fission/src/systems/simulation/stimulus/WheelStimulus.ts @@ -36,7 +36,7 @@ class WheelRotationStimulus extends EncoderStimulus { public Update(deltaT: number): void { if (this._accum) { - this._wheelRotationAccum += this._wheel.GetAngularVelocity() * deltaT; + this._wheelRotationAccum += this._wheel.GetAngularVelocity() * deltaT } } diff --git a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts index 7ce89ec572..cbce7c4b61 100644 --- a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts +++ b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts @@ -1,49 +1,48 @@ -import Mechanism from "@/systems/physics/Mechanism"; -import Brain from "../Brain"; -import Behavior from "../behavior/Behavior"; -import World from "@/systems/World"; -import WheelDriver from "../driver/WheelDriver"; -import WheelRotationStimulus from "../stimulus/WheelStimulus"; -import ArcadeDriveBehavior from "../behavior/ArcadeDriveBehavior"; -import { SimulationLayer } from "../SimulationSystem"; -import Jolt from "@barclah/jolt-physics"; -import JOLT from "@/util/loading/JoltSyncLoader"; -import HingeDriver from "../driver/HingeDriver"; -import HingeStimulus from "../stimulus/HingeStimulus"; -import GenericArmBehavior from "../behavior/GenericArmBehavior"; -import SliderDriver from "../driver/SliderDriver"; -import SliderStimulus from "../stimulus/SliderStimulus"; -import GenericElevatorBehavior from "../behavior/GenericElevatorBehavior"; - +import Mechanism from "@/systems/physics/Mechanism" +import Brain from "../Brain" +import Behavior from "../behavior/Behavior" +import World from "@/systems/World" +import WheelDriver from "../driver/WheelDriver" +import WheelRotationStimulus from "../stimulus/WheelStimulus" +import ArcadeDriveBehavior from "../behavior/ArcadeDriveBehavior" +import { SimulationLayer } from "../SimulationSystem" +import Jolt from "@barclah/jolt-physics" +import JOLT from "@/util/loading/JoltSyncLoader" +import HingeDriver from "../driver/HingeDriver" +import HingeStimulus from "../stimulus/HingeStimulus" +import GenericArmBehavior from "../behavior/GenericArmBehavior" +import SliderDriver from "../driver/SliderDriver" +import SliderStimulus from "../stimulus/SliderStimulus" +import GenericElevatorBehavior from "../behavior/GenericElevatorBehavior" class SynthesisBrain extends Brain { - private _behaviors: Behavior[] = []; - private _simLayer: SimulationLayer; + private _behaviors: Behavior[] = [] + private _simLayer: SimulationLayer - _leftWheelIndices: number[] = []; + _leftWheelIndices: number[] = [] // Tracks how many joins have been made for unique controls - _currentJointIndex = 1; + _currentJointIndex = 1 public constructor(mechanism: Mechanism) { - super(mechanism); + super(mechanism) - this._simLayer = World.SimulationSystem.GetSimulationLayer(mechanism)!; + this._simLayer = World.SimulationSystem.GetSimulationLayer(mechanism)! - if (!this._simLayer) { - console.log("SimulationLayer is undefined"); - return; + if (!this._simLayer) { + console.log("SimulationLayer is undefined") + return } - this.ConfigureArcadeDriveBehavior(); - this.ConfigureArmBehaviors(); - this.ConfigureElevatorBehaviors(); + this.ConfigureArcadeDriveBehavior() + this.ConfigureArmBehaviors() + this.ConfigureElevatorBehaviors() } - public Enable(): void { } + public Enable(): void {} - public Update(deltaT: number): void { - this._behaviors.forEach((b) => b.Update(deltaT)); + public Update(deltaT: number): void { + this._behaviors.forEach(b => b.Update(deltaT)) } public Disable(): void { @@ -52,59 +51,76 @@ class SynthesisBrain extends Brain { // Creates an instance of ArcadeDriveBehavior and automatically configures it public ConfigureArcadeDriveBehavior() { - const wheelDrivers: WheelDriver[] = this._simLayer.drivers.filter((driver) => driver instanceof WheelDriver) as WheelDriver[]; - const wheelStimuli: WheelRotationStimulus[] = this._simLayer.stimuli.filter((stimulus) => stimulus instanceof WheelRotationStimulus) as WheelRotationStimulus[]; + const wheelDrivers: WheelDriver[] = this._simLayer.drivers.filter( + driver => driver instanceof WheelDriver + ) as WheelDriver[] + const wheelStimuli: WheelRotationStimulus[] = this._simLayer.stimuli.filter( + stimulus => stimulus instanceof WheelRotationStimulus + ) as WheelRotationStimulus[] // Two body constraints are part of wheels and are used to determine which way a wheel is facing - const fixedConstraints: Jolt.TwoBodyConstraint[] = this._mechanism.constraints.filter((mechConstraint) => mechConstraint.constraint instanceof JOLT.TwoBodyConstraint).map((mechConstraint) => mechConstraint.constraint as Jolt.TwoBodyConstraint); + const fixedConstraints: Jolt.TwoBodyConstraint[] = this._mechanism.constraints + .filter(mechConstraint => mechConstraint.constraint instanceof JOLT.TwoBodyConstraint) + .map(mechConstraint => mechConstraint.constraint as Jolt.TwoBodyConstraint) - const leftWheels: WheelDriver[] = []; - const leftStimuli: WheelRotationStimulus[] = []; + const leftWheels: WheelDriver[] = [] + const leftStimuli: WheelRotationStimulus[] = [] - const rightWheels: WheelDriver[] = []; - const rightStimuli: WheelRotationStimulus[] = []; + const rightWheels: WheelDriver[] = [] + const rightStimuli: WheelRotationStimulus[] = [] // Determines which wheels and stimuli belong to which side of the robot for (let i = 0; i < wheelDrivers.length; i++) { - const wheelPos = fixedConstraints[i].GetConstraintToBody1Matrix().GetTranslation(); + const wheelPos = fixedConstraints[i].GetConstraintToBody1Matrix().GetTranslation() - const robotCOM = World.PhysicsSystem.GetBody(this._mechanism.constraints[0].childBody).GetCenterOfMassPosition() as Jolt.Vec3; - const rightVector = new JOLT.Vec3(1, 0, 0); + const robotCOM = World.PhysicsSystem.GetBody( + this._mechanism.constraints[0].childBody + ).GetCenterOfMassPosition() as Jolt.Vec3 + const rightVector = new JOLT.Vec3(1, 0, 0) const dotProduct = rightVector.Dot(wheelPos.Sub(robotCOM)) if (dotProduct < 0) { - rightWheels.push(wheelDrivers[i]); - rightStimuli.push(wheelStimuli[i]); - } - else { - leftWheels.push(wheelDrivers[i]); - leftStimuli.push(wheelStimuli[i]); + rightWheels.push(wheelDrivers[i]) + rightStimuli.push(wheelStimuli[i]) + } else { + leftWheels.push(wheelDrivers[i]) + leftStimuli.push(wheelStimuli[i]) } } - this._behaviors.push(new ArcadeDriveBehavior(leftWheels, rightWheels, leftStimuli, rightStimuli)); + this._behaviors.push(new ArcadeDriveBehavior(leftWheels, rightWheels, leftStimuli, rightStimuli)) } // Creates instances of ArmBehavior and automatically configures them public ConfigureArmBehaviors() { - const hingeDrivers: HingeDriver[] = this._simLayer.drivers.filter((driver) => driver instanceof HingeDriver) as HingeDriver[]; - const hingeStimuli: HingeStimulus[] = this._simLayer.stimuli.filter((stimulus) => stimulus instanceof HingeStimulus) as HingeStimulus[]; + const hingeDrivers: HingeDriver[] = this._simLayer.drivers.filter( + driver => driver instanceof HingeDriver + ) as HingeDriver[] + const hingeStimuli: HingeStimulus[] = this._simLayer.stimuli.filter( + stimulus => stimulus instanceof HingeStimulus + ) as HingeStimulus[] for (let i = 0; i < hingeDrivers.length; i++) { - this._behaviors.push(new GenericArmBehavior(hingeDrivers[i], hingeStimuli[i], this._currentJointIndex)); - this._currentJointIndex++; + this._behaviors.push(new GenericArmBehavior(hingeDrivers[i], hingeStimuli[i], this._currentJointIndex)) + this._currentJointIndex++ } } // Creates instances of ElevatorBehavior and automatically configures them public ConfigureElevatorBehaviors() { - const sliderDrivers: SliderDriver[] = this._simLayer.drivers.filter((driver) => driver instanceof SliderDriver) as SliderDriver[]; - const sliderStimuli: SliderStimulus[] = this._simLayer.stimuli.filter((stimulus) => stimulus instanceof SliderStimulus) as SliderStimulus[]; + const sliderDrivers: SliderDriver[] = this._simLayer.drivers.filter( + driver => driver instanceof SliderDriver + ) as SliderDriver[] + const sliderStimuli: SliderStimulus[] = this._simLayer.stimuli.filter( + stimulus => stimulus instanceof SliderStimulus + ) as SliderStimulus[] for (let i = 0; i < sliderDrivers.length; i++) { - this._behaviors.push(new GenericElevatorBehavior(sliderDrivers[i], sliderStimuli[i], this._currentJointIndex)); - this._currentJointIndex++; + this._behaviors.push( + new GenericElevatorBehavior(sliderDrivers[i], sliderStimuli[i], this._currentJointIndex) + ) + this._currentJointIndex++ } } } diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index c34f809502..7c5f278e34 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -2,22 +2,13 @@ import { test, expect, describe, assert } from "vitest" import PhysicsSystem, { LayerReserve } from "../systems/physics/PhysicsSystem" import { LoadMirabufLocal } from "@/mirabuf/MirabufLoader" import MirabufParser from "@/mirabuf/MirabufParser" -<<<<<<< HEAD import * as THREE from "three" -import JOLT from "@/util/loading/JoltSyncLoader" -======= ->>>>>>> b6da23e59d9c08bf187158486f6b7b2a9bd59bb6 describe("Physics Sansity Checks", () => { test("Convex Hull Shape (Cube)", () => { const points: Float32Array = new Float32Array([ -<<<<<<< HEAD - 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, - 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -======= 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, ->>>>>>> b6da23e59d9c08bf187158486f6b7b2a9bd59bb6 ]) const system = new PhysicsSystem() @@ -35,13 +26,7 @@ describe("Physics Sansity Checks", () => { system.Destroy() }) test("Convex Hull Shape (Tetrahedron)", () => { -<<<<<<< HEAD - const points: Float32Array = new Float32Array([ - 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ]) -======= const points: Float32Array = new Float32Array([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]) ->>>>>>> b6da23e59d9c08bf187158486f6b7b2a9bd59bb6 const system = new PhysicsSystem() const shapeResult = system.CreateConvexHull(points) @@ -64,19 +49,11 @@ describe("Physics Sansity Checks", () => { }) }) -<<<<<<< HEAD describe("GodMode", () => { test("Basic", () => { const system = new PhysicsSystem() - const box = system.CreateBox( - new THREE.Vector3(1, 1, 1), - 1, - new THREE.Vector3(0, 0, 0), - undefined - ) - const [ghostObject, ghostConstraint] = system.CreateGodModeBody( - box.GetID() - ) + const box = system.CreateBox(new THREE.Vector3(1, 1, 1), 1, new THREE.Vector3(0, 0, 0), undefined) + const [ghostObject, ghostConstraint] = system.CreateGodModeBody(box.GetID()) assert(system.GetBody(ghostObject.GetID()) != undefined) assert(system.GetBody(box.GetID()) != undefined) @@ -97,19 +74,6 @@ describe("GodMode", () => { }) }) -describe("Mirabuf Physics Loading", () => { - test("Body Loading (Dozer)", () => { - const assembly = LoadMirabufLocal( - "./public/Downloadables/Mira/Robots/Dozer_v9.mira" - ) - const parser = new MirabufParser(assembly) - const physSystem = new PhysicsSystem() - const mapping = physSystem.CreateBodiesFromParser( - parser, - new LayerReserve() - ) - -======= describe("Mirabuf Physics Loading", () => { test("Body Loading (Dozer)", () => { const assembly = LoadMirabufLocal("./public/Downloadables/Mira/Robots/Dozer_v9.mira") @@ -117,27 +81,14 @@ describe("Mirabuf Physics Loading", () => { const physSystem = new PhysicsSystem() const mapping = physSystem.CreateBodiesFromParser(parser, new LayerReserve()) ->>>>>>> b6da23e59d9c08bf187158486f6b7b2a9bd59bb6 expect(mapping.size).toBe(7) }) test("Body Loading (Team_2471_(2018)_v7.mira)", () => { -<<<<<<< HEAD - const assembly = LoadMirabufLocal( - "./public/Downloadables/Mira/Robots/Team 2471 (2018)_v7.mira" - ) - const parser = new MirabufParser(assembly) - const physSystem = new PhysicsSystem() - const mapping = physSystem.CreateBodiesFromParser( - parser, - new LayerReserve() - ) -======= const assembly = LoadMirabufLocal("./public/Downloadables/Mira/Robots/Team 2471 (2018)_v7.mira") const parser = new MirabufParser(assembly) const physSystem = new PhysicsSystem() const mapping = physSystem.CreateBodiesFromParser(parser, new LayerReserve()) ->>>>>>> b6da23e59d9c08bf187158486f6b7b2a9bd59bb6 expect(mapping.size).toBe(10) }) diff --git a/fission/src/ui/ToastContext.tsx b/fission/src/ui/ToastContext.tsx index 2730f7001f..a6ee866b88 100644 --- a/fission/src/ui/ToastContext.tsx +++ b/fission/src/ui/ToastContext.tsx @@ -1,10 +1,4 @@ -import React, { - createContext, - useState, - useContext, - useCallback, - ReactNode, -} from "react" +import React, { createContext, useState, useContext, useCallback, ReactNode } from "react" import Toast from "@/components/Toast" import { AnimatePresence, motion } from "framer-motion" diff --git a/fission/src/ui/components/Dropdown.tsx b/fission/src/ui/components/Dropdown.tsx index ab26a92236..05b684d4a0 100644 --- a/fission/src/ui/components/Dropdown.tsx +++ b/fission/src/ui/components/Dropdown.tsx @@ -39,9 +39,10 @@ const Dropdown: React.FC = ({ label, options, onSelect }) => {