diff --git a/fission/public/assetpack.zip b/fission/public/assetpack.zip index 57bd655496..fa23c2badf 100644 --- a/fission/public/assetpack.zip +++ b/fission/public/assetpack.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23bfaf7899d84b00331c12bf1ada20d27d3c50856b14bb5733821b44848812ba -size 193147860 +oid sha256:b6e9ac2e654bcf75728a0c9abd38027db28df2596dbc5fea04e8e0c5f600eef8 +size 294 diff --git a/fission/src/mirabuf/IntakeSensorSceneObject.ts b/fission/src/mirabuf/IntakeSensorSceneObject.ts index 8e4ee29374..febfb2f140 100644 --- a/fission/src/mirabuf/IntakeSensorSceneObject.ts +++ b/fission/src/mirabuf/IntakeSensorSceneObject.ts @@ -1,6 +1,7 @@ import type Jolt from "@azaleacolburn/jolt-physics" import * as THREE from "three" import { OnContactPersistedEvent } from "@/systems/physics/ContactEvents" +import { LAYER_GENERAL_DYNAMIC } from "@/systems/physics/PhysicsSystem" import SceneObject from "@/systems/scene/SceneObject" import World from "@/systems/World" import JOLT from "@/util/loading/JoltSyncLoader" @@ -131,7 +132,8 @@ class IntakeSensorSceneObject extends SceneObject { private intakeCollision(gpID: Jolt.BodyID) { const associate = World.physicsSystem.getBodyAssociation(gpID) - if (associate?.isGamePiece) { + const inGPLayer = World.physicsSystem.getBody(gpID).GetObjectLayer() === LAYER_GENERAL_DYNAMIC + if (associate?.isGamePiece || inGPLayer) { associate.robotLastInContactWith = this._parentAssembly this._parentAssembly.setEjectable(gpID) } diff --git a/fission/src/mirabuf/MirabufInstance.ts b/fission/src/mirabuf/MirabufInstance.ts index 3fb6ed7f47..80bd9a564a 100644 --- a/fission/src/mirabuf/MirabufInstance.ts +++ b/fission/src/mirabuf/MirabufInstance.ts @@ -304,9 +304,8 @@ class MirabufInstance { batchedMesh.castShadow = true batchedMesh.receiveShadow = true - materialBodyMap.forEach(instances => { - const body = instances[0] - instances[1].forEach(instance => { + materialBodyMap.forEach(([body, instances]) => { + instances.forEach(instance => { const mat = this._mirabufParser.globalTransforms.get(instance.info!.GUID!)! const geometry = new THREE.BufferGeometry() diff --git a/fission/src/mirabuf/MirabufLoader.ts b/fission/src/mirabuf/MirabufLoader.ts index 475d6b3b39..5dfa632830 100644 --- a/fission/src/mirabuf/MirabufLoader.ts +++ b/fission/src/mirabuf/MirabufLoader.ts @@ -7,6 +7,12 @@ import World from "@/systems/World" const MIRABUF_LOCALSTORAGE_GENERATION_KEY = "Synthesis Nonce Key" const MIRABUF_LOCALSTORAGE_GENERATION = "4543246" +export enum MiraType { + ROBOT = 1, + FIELD, + PIECE, +} + export type MirabufCacheID = string export interface MirabufCacheInfo { @@ -25,20 +31,32 @@ export interface MirabufRemoteInfo { type MapCache = { [id: MirabufCacheID]: MirabufCacheInfo } -const robotsDirName = "Robots" -const fieldsDirName = "Fields" +const robotsDirName = "robot" +const fieldsDirName = "field" +const piecesDirName = "piece" const root = await navigator.storage.getDirectory() -const robotFolderHandle = await root.getDirectoryHandle(robotsDirName, { - create: true, -}) -const fieldFolderHandle = await root.getDirectoryHandle(fieldsDirName, { - create: true, -}) +const getDirectoryHandle = (dirName: string) => root.getDirectoryHandle(dirName, { create: true }) + +const dirNameMap: Record = { + [MiraType.ROBOT]: robotsDirName, + [MiraType.FIELD]: fieldsDirName, + [MiraType.PIECE]: piecesDirName, +} + +const dirHandleMap: Record = { + [MiraType.ROBOT]: await getDirectoryHandle(robotsDirName), + [MiraType.FIELD]: await getDirectoryHandle(fieldsDirName), + [MiraType.PIECE]: await getDirectoryHandle(piecesDirName), +} -export let backUpRobots: MapCache = {} -export let backUpFields: MapCache = {} +export const backUpMap: Record = { + [MiraType.ROBOT]: {}, + [MiraType.FIELD]: {}, + [MiraType.PIECE]: {}, +} export const canOPFS = await (async () => { + const robotFolderHandle = dirHandleMap[MiraType.ROBOT] try { if (robotFolderHandle.name == robotsDirName) { robotFolderHandle.entries @@ -65,15 +83,24 @@ export const canOPFS = await (async () => { for await (const key of robotFolderHandle.keys()) { robotFolderHandle.removeEntry(key) } + + const fieldFolderHandle = dirHandleMap[MiraType.FIELD] for await (const key of fieldFolderHandle.keys()) { fieldFolderHandle.removeEntry(key) } + const pieceFolderHandle = dirHandleMap[MiraType.PIECE] + for await (const key of pieceFolderHandle.keys()) { + pieceFolderHandle.removeEntry(key) + } + window.localStorage.setItem(robotsDirName, "{}") window.localStorage.setItem(fieldsDirName, "{}") + window.localStorage.setItem(piecesDirName, "{}") - backUpRobots = {} - backUpFields = {} + backUpMap[MiraType.FIELD] = {} + backUpMap[MiraType.PIECE] = {} + backUpMap[MiraType.ROBOT] = {} return false } @@ -105,7 +132,7 @@ class MirabufCachingService { return {} } - const key = miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName + const key = dirNameMap[miraType] const map = window.localStorage.getItem(key) if (map) { @@ -141,23 +168,28 @@ class MirabufCachingService { if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`) const miraBuff = await resp.arrayBuffer() + // It's impossible to know if a dynamic assembly is a piece without previously parsing it out of a larger assembly + miraType ??= this.assemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD World.analyticsSystem?.event("Remote Download", { + type: dirNameMap[miraType], assemblyName: name ?? fetchLocation, - type: miraType === MiraType.ROBOT ? "robot" : "field", fileSize: miraBuff.byteLength, }) const cached = await MirabufCachingService.storeInCache(fetchLocation, miraBuff, miraType, name) - if (cached) return cached + if (cached) { + return cached + } globalAddToast("error", "Cache Fallback", `Unable to cache “${fetchLocation}”. Using raw buffer instead.`) // fallback: return raw buffer wrapped in MirabufCacheInfo return { id: Date.now().toString(), - miraType: miraType ?? (this.assemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD), + // There isn't a way to know set this to game piece correctly, since you must parse the assembly to know + miraType, cacheKey: fetchLocation, buffer: new Uint8Array(miraBuff), name: name, @@ -188,7 +220,7 @@ class MirabufCachingService { } World.analyticsSystem?.event("APS Download", { - type: miraType == MiraType.ROBOT ? "robot" : "field", + type: dirNameMap[miraType], fileSize: miraBuff.byteLength, }) @@ -233,9 +265,11 @@ class MirabufCachingService { try { const map: MapCache = this.getCacheMap(miraType) const id = map[key].id - const buffer = miraType == MiraType.ROBOT ? backUpRobots[id].buffer : backUpFields[id].buffer + const buffer = backUpMap[miraType][id].buffer + const defaultName = map[key].name const defaultStorageID = map[key].thumbnailStorageID + const info: MirabufCacheInfo = { id: id, cacheKey: key, @@ -245,12 +279,10 @@ class MirabufCachingService { thumbnailStorageID: thumbnailStorageID ?? defaultStorageID, } map[key] = info - if (miraType == MiraType.ROBOT) { - backUpRobots[id] = info - } else { - backUpFields[id] = info - } - window.localStorage.setItem(miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName, JSON.stringify(map)) + const backUp = backUpMap[miraType] + backUp[id] = info + + window.localStorage.setItem(dirNameMap[miraType], JSON.stringify(map)) return true } catch (e) { console.error(`Failed to cache info\n${e}`) @@ -350,37 +382,34 @@ class MirabufCachingService { * @returns {Promise} Promise with the result of the promise. Assembly of the mirabuf file if successful, undefined if not. */ public static async get(id: MirabufCacheID, miraType: MiraType): Promise { + const cache = backUpMap[miraType] + try { // Get buffer from hashMap. If not in hashMap, check OPFS. Otherwise, buff is undefined - const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields - const buff = - cache[id]?.buffer ?? - (await (async () => { - const fileHandle = canOPFS - ? await (miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle).getFileHandle(id, { - create: false, - }) - : undefined - return fileHandle - ? new Uint8Array( - (await fileHandle.getFile().then(async x => await x.arrayBuffer())) as ArrayBuffer - ) - : undefined - })()) - - // If we have buffer, get assembly - if (buff) { - const assembly = this.assemblyFromBuffer(buff.buffer as ArrayBuffer) - World.analyticsSystem?.event("Cache Get", { - key: id, - type: miraType == MiraType.ROBOT ? "robot" : "field", - assemblyName: assembly.info!.name!, - fileSize: buff.byteLength, + const getOPFSBuffer = async (): Promise => { + const dirHandle = dirHandleMap[miraType] + if (!canOPFS) return + + const fileHandle = await dirHandle.getFileHandle(id, { + create: false, }) - return assembly - } else { + return await fileHandle.getFile().then(x => x.arrayBuffer()) + } + + const buff = (cache[id]?.buffer?.buffer as ArrayBuffer | undefined) ?? (await getOPFSBuffer()) + if (!buff) { console.error(`Failed to find arrayBuffer for id: ${id}`) + return undefined } + + const assembly = this.assemblyFromBuffer(buff) + World.analyticsSystem?.event("Cache Get", { + key: id, + type: dirNameMap[miraType], + assemblyName: assembly.info!.name!, + fileSize: buff.byteLength, + }) + return assembly } catch (e) { console.error(`Failed to find file\n${e}`) return undefined @@ -401,25 +430,22 @@ class MirabufCachingService { const map = this.getCacheMap(miraType) if (map) { delete map[key] - window.localStorage.setItem( - miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName, - JSON.stringify(map) - ) + window.localStorage.setItem(dirNameMap[miraType], JSON.stringify(map)) } if (canOPFS) { - const dir = miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle + const dir = dirHandleMap[miraType] await dir.removeEntry(id) } - const backUpCache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields + const backUpCache = backUpMap[miraType] if (backUpCache) { delete backUpCache[id] } World.analyticsSystem?.event("Cache Remove", { key: key, - type: miraType == MiraType.ROBOT ? "robot" : "field", + type: dirNameMap[miraType], }) return true } catch (e) { @@ -434,19 +460,20 @@ class MirabufCachingService { */ public static async removeAll() { if (canOPFS) { - for await (const key of robotFolderHandle.keys()) { - robotFolderHandle.removeEntry(key) - } - for await (const key of fieldFolderHandle.keys()) { - fieldFolderHandle.removeEntry(key) - } + Object.values(dirHandleMap).forEach(async dirHandle => { + for await (const key of dirHandle.keys()) { + dirHandle.removeEntry(key) + } + }) } window.localStorage.setItem(robotsDirName, "{}") window.localStorage.setItem(fieldsDirName, "{}") + window.localStorage.setItem(piecesDirName, "{}") - backUpRobots = {} - backUpFields = {} + backUpMap[MiraType.FIELD] = {} + backUpMap[MiraType.PIECE] = {} + backUpMap[MiraType.ROBOT] = {} } /** @@ -468,17 +495,16 @@ class MirabufCachingService { const updatedBuffer = mirabuf.Assembly.encode(assembly).finish() // Update the cached buffer - const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields + const cache = backUpMap[miraType] if (cache[id]) { cache[id].buffer = updatedBuffer } // Update OPFS if available if (canOPFS) { - const fileHandle = await (miraType == MiraType.ROBOT - ? robotFolderHandle - : fieldFolderHandle - ).getFileHandle(id, { create: false }) + const fileHandle = await dirHandleMap[miraType].getFileHandle(id, { + create: false, + }) const writable = await fileHandle.createWritable() await writable.write(updatedBuffer.buffer as ArrayBuffer) await writable.close() @@ -486,7 +512,7 @@ class MirabufCachingService { World.analyticsSystem?.event("Devtool Cache Persist", { key: id, - type: miraType == MiraType.ROBOT ? "robot" : "field", + type: dirNameMap[miraType], assemblyName: assembly.info?.name ?? "unknown", fileSize: updatedBuffer.byteLength, }) @@ -504,12 +530,14 @@ class MirabufCachingService { key: string, miraBuff: ArrayBuffer, miraType?: MiraType, + // Optional name for when assembly is being decoded anyway like in CacheAndGetLocal() name?: string ): Promise { try { - const backupID = Date.now().toString() + const backupID = crypto.randomUUID() if (!miraType) { console.debug("Double loading") + // Piece can't be known without parsing miraType = this.assemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD } @@ -522,29 +550,26 @@ class MirabufCachingService { name: name, } map[key] = info - window.localStorage.setItem(miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName, JSON.stringify(map)) + window.localStorage.setItem(dirNameMap[miraType], JSON.stringify(map)) World.analyticsSystem?.event("Cache Store", { assemblyName: name ?? "-", key: key, - type: miraType == MiraType.ROBOT ? "robot" : "field", + type: dirNameMap[miraType], fileSize: miraBuff.byteLength, }) // Store buffer if (canOPFS) { // Store in OPFS - const fileHandle = await (miraType == MiraType.ROBOT - ? robotFolderHandle - : fieldFolderHandle - ).getFileHandle(backupID, { create: true }) + const fileHandle = await dirHandleMap[miraType].getFileHandle(backupID, { create: true }) const writable = await fileHandle.createWritable() await writable.write(miraBuff) await writable.close() } // Store in hash - const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields + const cache = backUpMap[miraType] const mapInfo: MirabufCacheInfo = { id: backupID, miraType: miraType, @@ -564,10 +589,7 @@ class MirabufCachingService { private static async hashBuffer(buffer: ArrayBuffer): Promise { const hashBuffer = await crypto.subtle.digest("SHA-256", buffer) - let hash = "" - new Uint8Array(hashBuffer).forEach(x => { - hash = hash + String.fromCharCode(x) - }) + const hash: string = String.fromCharCode(...new Uint8Array(hashBuffer)) return btoa(hash).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") } @@ -576,9 +598,4 @@ class MirabufCachingService { } } -export enum MiraType { - ROBOT = 1, - FIELD, -} - export default MirabufCachingService diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index a0e1cf09ae..523080c977 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -39,44 +39,61 @@ class MirabufParser { private _groundedNode: RigidNode | undefined + private _gamePieces?: MirabufParser[] + private _isGamePiece: boolean + private _gamePieceTransform?: mirabuf.ITransform + public get errors() { - return [...this._errors] + return this._errors } - public get maxErrorSeverity() { + + public get maxErrorSeverity(): number { return Math.max(...this._errors.map(x => x[0])) } - public get assembly() { + public get assembly(): mirabuf.Assembly { return this._assembly } - public get partTreeValues() { + public get partTreeValues(): Map { return this._partTreeValues } - public get designHierarchyRoot() { + public get designHierarchyRoot(): mirabuf.INode { return this._designHierarchyRoot } - public get partToNodeMap() { + public get partToNodeMap(): Map { return this._partToNodeMap } - public get globalTransforms() { + public get globalTransforms(): Map { return this._globalTransforms } - public get groundedNode() { + public get groundedNode(): RigidNodeReadOnly | undefined { return this._groundedNode ? new RigidNodeReadOnly(this._groundedNode) : undefined } public get rigidNodes(): Map { return new Map(this._rigidNodes.map(x => [x.id, new RigidNodeReadOnly(x)])) } - public get directedGraph() { + public get directedGraph(): Graph { return this._directedGraph } - public get rootNode() { + public get rootNode(): string { return this._rootNode } + public get gamePieces(): MirabufParser[] | undefined { + return this._gamePieces + } + public get isGamePiece(): boolean { + return this._isGamePiece + } + public get gamePieceTransform(): mirabuf.ITransform | undefined { + return this._gamePieceTransform + } - public constructor(assembly: mirabuf.Assembly, progressHandle?: ProgressHandle) { + public constructor(assembly: mirabuf.Assembly, isGamePiece: boolean = false, progressHandle?: ProgressHandle) { this._assembly = assembly this._errors = [] this._globalTransforms = new Map() + this._gamePieces = undefined + this._isGamePiece = isGamePiece + if (isGamePiece && assembly.transform) this._gamePieceTransform = assembly.transform progressHandle?.update("Parsing assembly...", 0.3) @@ -86,15 +103,16 @@ class MirabufParser { this.initializeRigidGroups() // 1: from ancestral breaks in joints // Fields Only: Assign Game Piece rigid nodes - if (!assembly.dynamic) this.assignGamePieceRigidNodes() + if (!assembly.dynamic) { + progressHandle?.update("Wrangling Gamepieces...", 0.4) + this._gamePieces = this.pruneGamePieceNodes().map(gp => new MirabufParser(gp, true)) + } // 2: Grounded joint const gInst = assembly.data!.joints!.jointInstances![GROUNDED_JOINT_ID] const gNode = this.newRigidNode() this.movePartToRigidNode(gInst.parts!.nodes!.at(0)!.value!, gNode) - // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); - // 3: Traverse and round up const traverseNodeRoundup = (node: mirabuf.INode, parentNode: RigidNode) => { const currentNode = this._partToNodeMap.get(node.value!) @@ -105,10 +123,7 @@ class MirabufParser { } this._designHierarchyRoot.children?.forEach(x => traverseNodeRoundup(x, gNode)) - // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); - this.bandageRigidNodes(assembly) // 4: Bandage via RigidGroups - // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); // 5. Remove Empty RNs this._rigidNodes = this._rigidNodes.filter(x => x.parts.size > 0) @@ -123,21 +138,20 @@ class MirabufParser { // 8. Retrieve Masses this._rigidNodes.forEach(rn => { - rn.mass = 0 - rn.parts.forEach(part => { - const inst = assembly.data?.parts?.partInstances?.[part] - if (!inst?.partDefinitionReference) return - const def = assembly.data?.parts?.partDefinitions?.[inst.partDefinitionReference!] - rn.mass += def?.massOverride ? def.massOverride : (def?.physicalData?.mass ?? 0) - }) + rn.mass = [...rn.parts] + .map(part => assembly.data?.parts?.partInstances?.[part]) + .reduce((acc, inst) => { + // The if statement satisfies the type guard while the filter function doesn't + if (inst?.partDefinitionReference == undefined) return acc + + const def = assembly.data?.parts?.partDefinitions?.[inst?.partDefinitionReference] + return acc + (def?.massOverride ?? def?.physicalData?.mass ?? 0) + }, 0) }) this._directedGraph = this.generateRigidNodeGraph(assembly, rootNodeId) - if (!this.assembly.data?.parts?.partDefinitions) { - console.warn("Failed to get part definitions") - return - } + if (!this.assembly.data?.parts?.partDefinitions) console.warn("Failed to get part definitions") } private traverseTree(nodes: mirabuf.INode[], op: (node: mirabuf.INode) => void) { @@ -149,56 +163,148 @@ class MirabufParser { private initializeRigidGroups() { const jointInstanceKeys = Object.keys(this._assembly.data!.joints!.jointInstances!) as string[] - jointInstanceKeys.forEach(key => { - if (key === GROUNDED_JOINT_ID) return - - const jInst = this._assembly.data!.joints!.jointInstances![key] - const [ancestorA, ancestorB] = this.findAncestralBreak(jInst.parentPart!, jInst.childPart!) - const parentRN = this.newRigidNode() - - this.movePartToRigidNode(ancestorA, parentRN) - this.movePartToRigidNode(ancestorB, this.newRigidNode()) - - if (jInst.parts && jInst.parts.nodes) - this.traverseTree(jInst.parts.nodes, x => this.movePartToRigidNode(x.value!, parentRN)) - }) + jointInstanceKeys + .filter(key => key !== GROUNDED_JOINT_ID) + .forEach(key => { + const jInst = this._assembly.data!.joints!.jointInstances![key] + const [ancestorA, ancestorB] = this.findAncestralBreak(jInst.parentPart!, jInst.childPart!) + const parentRN = this.newRigidNode() + + this.movePartToRigidNode(ancestorA, parentRN) + this.movePartToRigidNode(ancestorB, this.newRigidNode()) + + if (jInst.parts && jInst.parts.nodes) + this.traverseTree(jInst.parts.nodes, x => this.movePartToRigidNode(x.value!, parentRN)) + }) } - private assignGamePieceRigidNodes() { + /* + * Separates and returns the sub-assemblies (partInstances) of each game piece + */ + private pruneGamePieceNodes(): mirabuf.Assembly[] { // Collect all definitions labeled as gamepieces (dynamic = true) const gamepieceDefinitions: Set = new Set( Object.values(this._assembly.data!.parts!.partDefinitions!) .filter(def => def.dynamic) - .map((def: mirabuf.IPartDefinition) => { - return def.info!.GUID! - }) + .map((def: mirabuf.IPartDefinition) => def.info!.GUID!) ) // Create gamepiece rigid nodes from PartInstances with corresponding definitions - Object.values(this._assembly.data!.parts!.partInstances!).forEach((inst: mirabuf.IPartInstance) => { - if (!gamepieceDefinitions.has(inst.partDefinitionReference!)) return + const gamePieces = Object.values(this._assembly.data!.parts!.partInstances!) + .filter(inst => gamepieceDefinitions.has(inst.partDefinitionReference!)) + .map(inst => { + const instNode = this.binarySearchDesignTree(inst.info!.GUID!) + if (instNode == null) { + this.NewError(ParseErrorSeverity.LIKELY_ISSUES, "Failed to find game piece in Design Tree") + return + } + // Trick to capture and delete references to gamePiece + // Removing this yields a null function runtime error + const gpRn = this.newRigidNode(GAMEPIECE_SUFFIX) + gpRn.isGamePiece = true + this.movePartToRigidNode(instNode!.value!, gpRn) + if (instNode.children) + this.traverseTree(instNode.children, x => this.movePartToRigidNode(x.value!, gpRn)) + this.deleteRigidNode(gpRn) + + // Delete partInstances + Object.entries(this._assembly.data?.parts?.partInstances ?? {}) + .filter(([_key, subInst]) => inst === subInst) + .forEach(([key, _subInst]) => delete this._assembly.data?.parts?.partInstances?.[key]) + + // Assumes that the game piece is composed of one instance + return this.convertPartInstanceToAssembly(inst, instNode) + }) + .filter(asm => asm != undefined) - const instNode = this.binarySearchDesignTree(inst.info!.GUID!) - if (!instNode) { - this._errors.push([ParseErrorSeverity.LIKELY_ISSUES, "Failed to find Game piece in Design Tree"]) - return - } + return gamePieces + } + + /* + * Converts specfic part instances to entire assemblies. Designed and tested for gamePiece instances, but theoretically should generalize + * + * Assumptions: + * - The part is a single dynamic rigid body + * - One joint and one part exist on the instance + */ + private convertPartInstanceToAssembly( + inst: mirabuf.IPartInstance, + instNode: mirabuf.INode, + isEndEffector: boolean = false, + isDynamic: boolean = true + ): mirabuf.Assembly | undefined { + const jointDefinition = new mirabuf.joint.Joint({ + info: { + GUID: GROUNDED_JOINT_ID, + name: GROUNDED_JOINT_ID, + }, + jointMotionType: mirabuf.joint.JointMotion.RIGID, + // Affects the placement of the joints, cannot affect the placement of the assembly in absolute space + origin: new mirabuf.Vector3(), + }) + const jointInstance = new mirabuf.joint.JointInstance({ + isEndEffector, + parentPart: "", + jointReference: jointDefinition.info?.GUID, + parts: { nodes: [instNode] }, + }) + + const joints = new mirabuf.joint.Joints({ + jointDefinitions: { + [GROUNDED_JOINT_ID]: jointDefinition, + }, + jointInstances: { + [GROUNDED_JOINT_ID]: jointInstance, + }, + rigidGroups: [], + // This probably needs to be changed if this function gets generalized for mix-n-match or something + motorDefinitions: {}, + }) + + const partDefinitionReference = inst?.partDefinitionReference + if (partDefinitionReference == null) { + this.NewError(ParseErrorSeverity.UNIMPORTABLE, "partInstance does not reference a partDefinition") + return + } + const partDefinition = this.assembly.data?.parts?.partDefinitions?.[partDefinitionReference] ?? {} + + const parts = new mirabuf.Parts({ + info: inst.info, + partDefinitions: { + [partDefinitionReference]: partDefinition, + }, + partInstances: { + [inst.info?.GUID ?? ""]: inst, + }, + }) - const gpRn = this.newRigidNode(GAMEPIECE_SUFFIX) - gpRn.isGamePiece = true - this.movePartToRigidNode(instNode!.value!, gpRn) - if (instNode.children) this.traverseTree(instNode.children, x => this.movePartToRigidNode(x.value!, gpRn)) + const gamePieceAssembly = new mirabuf.Assembly({ + info: inst.info, + data: { + parts, + joints, + materials: this.assembly.data?.materials, + // This probably needs to be changed if this function gets generalized for mix-n-match or something + signals: {}, + }, + dynamic: isDynamic, + designHierarchy: { nodes: [instNode] }, + // This probably needs to be changed if this function gets generalized for mix-n-match or something + jointHierarchy: {}, + thumbnail: null, + transform: inst.transform, }) + + return gamePieceAssembly } private bandageRigidNodes(assembly: mirabuf.Assembly) { assembly.data!.joints!.rigidGroups!.forEach(rg => { - let rn: RigidNode | null = null - rg.occurrences!.forEach(y => { + rg.occurrences!.reduce((rn, y) => { const currentRn = this._partToNodeMap.get(y)! - rn = !rn ? currentRn : currentRn.id != rn.id ? this.mergeRigidNodes(currentRn, rn) : rn - }) + return !rn ? currentRn : currentRn.id != rn.id ? this.mergeRigidNodes(currentRn, rn) : rn + }, null) }) } @@ -247,6 +353,13 @@ class MirabufParser { return node } + private deleteRigidNode(node: RigidNode) { + const index = this._rigidNodes.indexOf(node) + if (index != -1 && index != null) { + this._rigidNodes.splice(index) + } + } + private mergeRigidNodes(rnA: RigidNode, rnB: RigidNode) { const newRn = this.newRigidNode("merged") const allParts = new Set([...rnA.parts, ...rnB.parts]) @@ -274,10 +387,11 @@ class MirabufParser { */ private loadGlobalTransforms() { const root = this._designHierarchyRoot - const partInstances = new Map( - Object.entries(this._assembly.data!.parts!.partInstances!) - ) - const partDefinitions = this._assembly.data!.parts!.partDefinitions! + const parts = this._assembly.data?.parts + if (!parts) return + + const partInstances = new Map(Object.entries(parts.partInstances!)) + const partDefinitions = parts.partDefinitions! this._globalTransforms.clear() @@ -288,8 +402,6 @@ class MirabufParser { if (!partInstance || this.globalTransforms.has(child.value!)) return const mat = convertMirabufTransformToThreeMatrix(partInstance.transform!)! - // console.log(`[${partInstance.info!.name!}] -> ${matToString(mat)}`); - this._globalTransforms.set(child.value!, mat.premultiply(parent)) getTransforms(child, mat) }) @@ -305,8 +417,6 @@ class MirabufParser { ? convertMirabufTransformToThreeMatrix(def.baseTransform) : new THREE.Matrix4().identity() - // console.log(`[${partInstance.info!.name!}] -> ${matToString(mat!)}`); - this._globalTransforms.set(partInstance.info!.GUID!, mat) getTransforms(child, mat) }) @@ -326,30 +436,28 @@ class MirabufParser { const valueA = ptv.get(partA)! const valueB = ptv.get(partB)! - while (pathA.value! == pathB.value! && pathA.value! != partA && pathB.value! != partB) { - const ancestorIndexA = this.binarySearchIndex(valueA, pathA.children!) - const ancestorValueA = ptv.get(pathA.children![ancestorIndexA].value!)! - pathA = pathA.children![ancestorIndexA + (ancestorValueA < valueA ? 1 : 0)] + const getNextChild = (value: number, children: mirabuf.INode[]) => { + const ancestorIndex = this.binarySearchIndex(value, children!) + const ancestorValue = ptv.get(children![ancestorIndex].value!)! + + return children![ancestorIndex + (ancestorValue < value ? 1 : 0)] + } - const ancestorIndexB = this.binarySearchIndex(valueB, pathB.children!) - const ancestorValueB = ptv.get(pathB.children![ancestorIndexB].value!)! - pathB = pathB.children![ancestorIndexB + (ancestorValueB < valueB ? 1 : 0)] + while (pathA.value! == pathB.value! && pathA.value! != partA && pathB.value! != partB) { + pathA = getNextChild(valueA, pathA.children!) + pathB = getNextChild(valueB, pathB.children!) } if (pathA.value! == partA && pathA.value! == pathB.value!) { - const ancestorIndexB = this.binarySearchIndex(valueB, pathB.children!) - const ancestorValueB = ptv.get(pathB.children![ancestorIndexB].value!)! - pathB = pathB.children![ancestorIndexB + (ancestorValueB < valueB ? 1 : 0)] + pathB = getNextChild(valueB, pathB.children!) } else if (pathB.value! == partB && pathA.value! == pathB.value!) { - const ancestorIndexA = this.binarySearchIndex(valueA, pathA.children!) - const ancestorValueA = ptv.get(pathA.children![ancestorIndexA].value!)! - pathA = pathA.children![ancestorIndexA + (ancestorValueA < valueA ? 1 : 0)] + pathA = getNextChild(valueA, pathA.children!) } return [pathA.value!, pathB.value!] } - private binarySearchIndex(target: number, children: mirabuf.INode[]): number { + public binarySearchIndex(target: number, children: mirabuf.INode[]): number { let l = 0 let h = children.length @@ -372,13 +480,13 @@ class MirabufParser { let node = this._designHierarchyRoot const targetValue = this._partTreeValues.get(target)! - while (node.value != target && node.children) { + while (node?.value != target && node?.children) { const i = this.binarySearchIndex(targetValue, node.children!) const iValue = this._partTreeValues.get(node.children![i].value!)! node = node.children![i + (iValue < targetValue ? 1 : 0)] } - return node.value! == target ? node : null + return node?.value === target ? node : null } private generateTreeValues() { @@ -394,11 +502,27 @@ class MirabufParser { this._designHierarchyRoot = new mirabuf.Node() this._designHierarchyRoot.value = "Importer Generated Root" this._designHierarchyRoot.children = [] - this._designHierarchyRoot.children.push(...this._assembly.designHierarchy!.nodes!) + if (this._assembly.designHierarchy == null) { + console.error(this._assembly) + this.NewError(ParseErrorSeverity.LIKELY_ISSUES, "Design hierarchy is null") + return + } + this._designHierarchyRoot.children.push(...this._assembly.designHierarchy.nodes!) recursive(this._designHierarchyRoot) this._partTreeValues = partTreeValues } + + private NewError(severity: ParseErrorSeverity, message: string) { + if (severity >= ParseErrorSeverity.LIKELY_ISSUES) { + console.error(message) + if (severity == ParseErrorSeverity.UNIMPORTABLE) + console.error(`Aborting Parse of assembly: ${this._assembly.info?.name}`) + } else { + console.warn(message) + } + this._errors.push([severity, message]) + } } /** diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index be780b0436..e9e575919d 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -3,7 +3,7 @@ import * as THREE from "three" import type { mirabuf } from "@/proto/mirabuf" import { OnContactAddedEvent } from "@/systems/physics/ContactEvents" import type Mechanism from "@/systems/physics/Mechanism" -import { BodyAssociate, type LayerReserve } from "@/systems/physics/PhysicsSystem" +import { BodyAssociate, LAYER_GENERAL_DYNAMIC, type LayerReserve } from "@/systems/physics/PhysicsSystem" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import { type Alliance, @@ -37,18 +37,20 @@ import { convertJoltRVec3ToJoltVec3, convertJoltVec3ToJoltRVec3, convertJoltVec3ToThreeVector3, + convertMirabufTransformToJoltPositionRVec3, + convertThreeVector3ToJoltRVec3, convertThreeVector3ToJoltVec3, } from "@/util/TypeConversions" -import { createMeshForShape } from "@/util/threejs/MeshCreation.ts" import SceneObject from "../systems/scene/SceneObject" import EjectableSceneObject from "./EjectableSceneObject" import FieldMiraEditor, { devtoolHandlers, devtoolKeys } from "./FieldMiraEditor" import IntakeSensorSceneObject from "./IntakeSensorSceneObject" import MirabufInstance from "./MirabufInstance" -import { MiraType } from "./MirabufLoader" +import MirabufCachingService, { type MirabufCacheID, MiraType } from "./MirabufLoader" import MirabufParser, { ParseErrorSeverity, type RigidNodeId, type RigidNodeReadOnly } from "./MirabufParser" import ProtectedZoneSceneObject from "./ProtectedZoneSceneObject" import ScoringZoneSceneObject from "./ScoringZoneSceneObject" +import { createMeshForShape } from "@/util/threejs/MeshCreation" const DEBUG_BODIES = false @@ -108,6 +110,8 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { private _collision?: (event: OnContactAddedEvent) => void private _cacheId?: string + private _miraType: MiraType = MiraType.ROBOT // Placeholder + public get intakeActive() { return this._intakeActive } @@ -161,7 +165,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { } public get miraType(): MiraType { - return this._mirabufInstance.parser.assembly.dynamic ? MiraType.ROBOT : MiraType.FIELD + return this._miraType } public get rootNodeId(): string { @@ -194,6 +198,10 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { this._station = station } + public set miraType(type: MiraType) { + this._miraType = type + } + public get cacheId() { return this._cacheId } @@ -201,14 +209,22 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { public constructor( mirabufInstance: MirabufInstance, assemblyName: string, - progressHandle?: ProgressHandle, - cacheId?: string + cacheId: string, + progressHandle?: ProgressHandle ) { super() this._mirabufInstance = mirabufInstance this._assemblyName = assemblyName this._cacheId = cacheId + this._miraType = this._mirabufInstance.parser.assembly.dynamic + ? // Game pieces imported with a field + this._mirabufInstance.parser.isGamePiece || + // Game pieces imported independently (this doesn't work ig) + MirabufCachingService.getCacheMap(MiraType.PIECE)[cacheId] != undefined + ? MiraType.PIECE + : MiraType.ROBOT + : MiraType.FIELD progressHandle?.update("Creating mechanism...", 0.9) @@ -219,41 +235,40 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { this.getPreferences() - if (this.miraType === MiraType.ROBOT) { - // creating nametag for robots - this._nameTag = new SceneOverlayTag(() => - this._brain instanceof SynthesisBrain - ? this._brain.inputSchemeName - : this._brain instanceof WPILibBrain - ? "Magic" - : "Not Configured" - ) + // Creating nametag for robots + if (this.miraType !== MiraType.ROBOT) return + this._nameTag = new SceneOverlayTag(() => + this._brain instanceof SynthesisBrain + ? this._brain.inputSchemeName + : this._brain instanceof WPILibBrain + ? "Magic" + : "Not Configured" + ) - // Detects when something collides with the robot - this._collision = (event: OnContactAddedEvent) => { - const body1 = event.message.body1 - const body2 = event.message.body2 + // Detects when something collides with the robot + this._collision = (event: OnContactAddedEvent) => { + const body1 = event.message.body1 + const body2 = event.message.body2 - if (body1.GetIndexAndSequenceNumber() === this.getRootNodeId()?.GetIndexAndSequenceNumber()) { - this.recordRobotCollision(body2) - } else if (body2.GetIndexAndSequenceNumber() === this.getRootNodeId()?.GetIndexAndSequenceNumber()) { - this.recordRobotCollision(body1) - } + if (body1.GetIndexAndSequenceNumber() === this.getRootNodeId()?.GetIndexAndSequenceNumber()) { + this.recordRobotCollision(body2) + } else if (body2.GetIndexAndSequenceNumber() === this.getRootNodeId()?.GetIndexAndSequenceNumber()) { + this.recordRobotCollision(body1) } - OnContactAddedEvent.addListener(this._collision) - - // Center of Mass Indicator - const material = new THREE.MeshBasicMaterial({ - color: 0xff00ff, // purple - transparent: true, - opacity: 0.1, - wireframe: true, - }) - material.depthTest = false - this._centerOfMassIndicator = new THREE.Mesh(new THREE.SphereGeometry(0.02), material) - this._centerOfMassIndicator.visible = false - World.sceneRenderer.scene.add(this._centerOfMassIndicator) } + OnContactAddedEvent.addListener(this._collision) + + // Center of Mass Indicator + const material = new THREE.MeshBasicMaterial({ + color: 0xff00ff, // purple + transparent: true, + opacity: 0.1, + wireframe: true, + }) + material.depthTest = false + this._centerOfMassIndicator = new THREE.Mesh(new THREE.SphereGeometry(0.02), material) + this._centerOfMassIndicator.visible = false + World.sceneRenderer.scene.add(this._centerOfMassIndicator) } public setup(): void { @@ -304,9 +319,28 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { this.updateBatches() + // // Only for game piece imported with a field + // if (this._mirabufInstance.parser.isGamePiece) { + // const jBodyId = this.mechanism.getBodyByNodeId(this.mechanism.rootBody) + // if (!jBodyId) { + // console.warn( + // `Jolt Body for SceneObject ${this.id} with rootBody ${this.mechanism.rootBody} as NodeId not found` + // ) + // return + // } + // const position = convertMirabufTransformToJoltPositionRVec3(this.mirabufInstance.parser.gamePieceTransform!) + // // position.SetZ(position.GetZ() - 0.25) + // World.physicsSystem.setBodyPosition(jBodyId, position) + // this.updateMeshTransforms() + // } + + // if (this.miraType === MiraType.ROBOT || this._mirabufInstance.parser.isGamePiece) { + const bounds = this.computeBoundingBox() + if (!Number.isFinite(bounds.min.y)) return this._basePositionTransform = this.getPositionTransform(new THREE.Vector3()) this.moveToSpawnLocation() + // } const cameraControls = World.sceneRenderer.currentCameraControls as CustomOrbitControls @@ -316,7 +350,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { } // Centered in xz plane, bottom surface of object - private getPositionTransform(vec: THREE.Vector3) { + private getPositionTransform(vec: THREE.Vector3): THREE.Vector3 { const box = this.computeBoundingBox() const transform = box.getCenter(vec) transform.setY(box.min.y) @@ -333,6 +367,15 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { const fieldLocations = field?.fieldPreferences?.spawnLocations if (this._alliance != null && this._station != null && fieldLocations != null) { pos = fieldLocations[this._alliance][this._station] + } else if (this._miraType === MiraType.PIECE) { + console.log("placing game piece") + const posVec = convertMirabufTransformToJoltPositionRVec3( + this._mirabufInstance.parser.gamePieceTransform! + ) + pos = { + pos: [posVec.GetX(), posVec.GetY(), posVec.GetZ()], + yaw: 0, + } } else { pos = fieldLocations?.default ?? pos } @@ -568,6 +611,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { } public setEjectable(bodyId?: Jolt.BodyID): boolean { + // 1) still require you’ve configured an ejector if (!bodyId) { return false } @@ -576,7 +620,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { console.log(bodyId) const now = Date.now() if (now - this._lastEjectableToastTime > MirabufSceneObject.EJECTABLE_TOAST_COOLDOWN_MS) { - console.log(`Configure an ejectable first.`) + console.log(`Configure an ejector first.`) globalAddToast("info", "Configure Ejectable", "Configure an ejectable first.") this._lastEjectableToastTime = now } @@ -694,7 +738,11 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { * * @returns the object containing the width (x), height (y), and depth (z) dimensions in meters. */ - public getDimensionsWithoutRotation(): { width: number; height: number; depth: number } { + public getDimensionsWithoutRotation(): { + width: number + height: number + depth: number + } { const rootNodeId = this.getRootNodeId() if (!rootNodeId) { console.warn("No root node found for robot, using regular dimensions") @@ -859,13 +907,19 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { public getSupplierData(): ContextData { const data: ContextData = { - title: this.miraType == MiraType.ROBOT ? "A Robot" : "A Field", + title: + this.miraType == MiraType.ROBOT + ? "A Robot" + : this.miraType == MiraType.PIECE + ? "A Game Piece" + : "A Field", items: [], } data.items.push( { name: "Move", + customProps: { configurationType: this.miraType === MiraType.ROBOT ? "ROBOTS" : "FIELDS", configMode: ConfigMode.MOVE, @@ -876,6 +930,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { }, { name: "Configure", + customProps: { configurationType: this.miraType === MiraType.ROBOT ? "ROBOTS" : "FIELDS", configMode: undefined, @@ -941,24 +996,47 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { private recordRobotCollision(collision: Jolt.BodyID) { const objectCollidedWith = World.physicsSystem.getBodyAssociation(collision) - if (objectCollidedWith && objectCollidedWith.isGamePiece) { + const inGPLayer = World.physicsSystem.getBody(collision).GetObjectLayer() === LAYER_GENERAL_DYNAMIC + if (objectCollidedWith && (objectCollidedWith.isGamePiece || inGPLayer)) { objectCollidedWith.robotLastInContactWith = this } } } -export async function createMirabuf( +export function createMirabuf( assembly: mirabuf.Assembly, - progressHandle?: ProgressHandle, - cacheId?: string -): Promise { - const parser = new MirabufParser(assembly, progressHandle) + id: MirabufCacheID, + type: MiraType, + progressHandle?: ProgressHandle +): + | { + mainSceneObject: MirabufSceneObject + gamePieces?: MirabufInstance[] + } + | undefined { + const parser = new MirabufParser(assembly, type == MiraType.PIECE, progressHandle) if (parser.maxErrorSeverity >= ParseErrorSeverity.UNIMPORTABLE) { console.error(`Assembly Parser produced significant errors for '${assembly.info!.name!}'`) return } - return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!, progressHandle, cacheId) + const mainSceneObject = new MirabufSceneObject( + new MirabufInstance(parser), + assembly.info!.name!, + id, + progressHandle + ) + if (parser.gamePieces == undefined || parser.gamePieces.length === 0) + return { + mainSceneObject, + } + + const gamePieces = parser.gamePieces.map(parser => new MirabufInstance(parser)) + + return { + mainSceneObject, + gamePieces, + } } /** diff --git a/fission/src/mirabuf/ScoringZoneSceneObject.ts b/fission/src/mirabuf/ScoringZoneSceneObject.ts index 1f97f43cac..e8a5283be2 100644 --- a/fission/src/mirabuf/ScoringZoneSceneObject.ts +++ b/fission/src/mirabuf/ScoringZoneSceneObject.ts @@ -1,6 +1,7 @@ import Jolt from "@azaleacolburn/jolt-physics" import * as THREE from "three" import { OnContactAddedEvent, OnContactRemovedEvent } from "@/systems/physics/ContactEvents" +import { LAYER_GENERAL_DYNAMIC } from "@/systems/physics/PhysicsSystem" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import type { ScoringZonePreferences } from "@/systems/preferences/PreferenceTypes" import SceneObject from "@/systems/scene/SceneObject" @@ -224,9 +225,10 @@ class ScoringZoneSceneObject extends SceneObject { if (this._collisionRemoved) OnContactRemovedEvent.removeListener(this._collisionRemoved) } - private zoneCollision(gpID: Jolt.BodyID) { + public zoneCollision(gpID: Jolt.BodyID) { const associate = World.physicsSystem.getBodyAssociation(gpID) - if (associate?.isGamePiece && this._prefs) { + const inGPLayer = World.physicsSystem.getBody(gpID).GetObjectLayer() === LAYER_GENERAL_DYNAMIC + if ((associate?.isGamePiece || inGPLayer) && this._prefs) { // If persistent, Update() will handle points if (this._prefs.persistentPoints) { this._gpContacted.push(gpID) diff --git a/fission/src/systems/analytics/AnalyticsSystem.ts b/fission/src/systems/analytics/AnalyticsSystem.ts index c5af51c3b6..b1ec097646 100644 --- a/fission/src/systems/analytics/AnalyticsSystem.ts +++ b/fission/src/systems/analytics/AnalyticsSystem.ts @@ -20,7 +20,7 @@ export interface AccumTimes { } type MiraEvent = { key?: string - type?: "robot" | "field" + type?: "robot" | "field" | "piece" assemblyName?: string /** * Size (in bytes) of the mirabuf file diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 8e49f062f2..944d100464 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -2,7 +2,7 @@ import type Jolt from "@azaleacolburn/jolt-physics" import * as THREE from "three" import JOLT from "@/util/loading/JoltSyncLoader" import type MirabufParser from "../../mirabuf/MirabufParser" -import { GAMEPIECE_SUFFIX, GROUNDED_JOINT_ID, type RigidNodeReadOnly } from "../../mirabuf/MirabufParser" +import { GROUNDED_JOINT_ID, type RigidNodeReadOnly } from "../../mirabuf/MirabufParser" import { mirabuf } from "../../proto/mirabuf" import { convertJoltRVec3ToJoltVec3, @@ -34,7 +34,7 @@ import type { JoltBodyIndexAndSequence } from "./PhysicsTypes" * 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. +export const LAYER_GENERAL_DYNAMIC = 1 // Used for game pieces or any general dynamic objects that can collide with anything and everything. const ROBOT_LAYERS: number[] = [ // Reserved layers for robots. Robot layers have no collision with themselves but have collision with everything else. 2, @@ -327,9 +327,9 @@ class PhysicsSystem extends WorldSystem { } public createMechanismFromParser(parser: MirabufParser): Mechanism { - const layer = parser.assembly.dynamic ? new LayerReserve() : undefined - const bodyMap = this.createBodiesFromParser(parser, layer) + const layer = parser.assembly.dynamic && !parser.isGamePiece ? new LayerReserve() : undefined const rootBody = parser.rootNode + const bodyMap = this.createBodiesFromParser(parser, layer) const mechanism = new Mechanism(rootBody, bodyMap, parser.assembly.dynamic, layer) this.createJointsFromParser(parser, mechanism) return mechanism @@ -664,15 +664,6 @@ class PhysicsSystem extends WorldSystem { const listener = new JOLT.VehicleConstraintStepListener(vehicleConstraint) this._joltPhysSystem.AddStepListener(listener) - // const callbacks = new JOLT.VehicleConstraintCallbacksJS() - // callbacks.GetCombinedFriction = (_wheelIndex, _tireFrictionDirection, tireFriction, _body2Ptr, _subShapeID2) => { - // return tireFriction - // } - // callbacks.OnPreStepCallback = (_vehicle, _stepContext) => { }; - // callbacks.OnPostCollideCallback = (_vehicle, _stepContext) => { }; - // callbacks.OnPostStepCallback = (_vehicle, _stepContext) => { }; - // callbacks.SetVehicleConstraint(vehicleConstraint) - this._joltPhysSystem.AddConstraint(vehicleConstraint) this._joltPhysSystem.AddConstraint(fixedConstraint) @@ -783,122 +774,6 @@ class PhysicsSystem extends WorldSystem { } } - // TODO: Ball socket joints should try to be reduced to the shoulder joint equivalent for Jolt (SwingTwistConstraint) - // private CreateBallBadAgainConstraint( - // jointInstance: mirabuf.joint.JointInstance, - // jointDefinition: mirabuf.joint.Joint, - // bodyA: Jolt.Body, - // bodyB: Jolt.Body, - // mechanism: Mechanism, - // ): void { - - // const jointOrigin = jointDefinition.origin - // ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) - // : 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) - - // const pitchDof = jointDefinition.custom!.dofs!.at(0) - // const yawDof = jointDefinition.custom!.dofs!.at(1) - // const rollDof = jointDefinition.custom!.dofs!.at(2) - // const pitchAxis = new JOLT.Vec3(pitchDof?.axis?.x ?? 0, pitchDof?.axis?.y ?? 0, pitchDof?.axis?.z ?? 0) - // const yawAxis = new JOLT.Vec3(yawDof?.axis?.x ?? 0, yawDof?.axis?.y ?? 0, yawDof?.axis?.z ?? 0) - // const rollAxis = new JOLT.Vec3(rollDof?.axis?.x ?? 0, rollDof?.axis?.y ?? 0, rollDof?.axis?.z ?? 0) - - // console.debug(`Anchor Point: ${joltVec3ToString(anchorPoint)}`) - // console.debug(`Pitch Axis: ${joltVec3ToString(pitchAxis)} ${pitchDof?.limits ? `[${pitchDof.limits.lower!.toFixed(3)}, ${pitchDof.limits.upper!.toFixed(3)}]` : ''}`) - // console.debug(`Yaw Axis: ${joltVec3ToString(yawAxis)} ${yawDof?.limits ? `[${yawDof.limits.lower!.toFixed(3)}, ${yawDof.limits.upper!.toFixed(3)}]` : ''}`) - // console.debug(`Roll Axis: ${joltVec3ToString(rollAxis)} ${rollDof?.limits ? `[${rollDof.limits.lower!.toFixed(3)}, ${rollDof.limits.upper!.toFixed(3)}]` : ''}`) - - // const constraints: { axis: Jolt.Vec3, friction: number, value: number, upper?: number, lower?: number }[] = [] - - // if (pitchDof?.limits && (pitchDof.limits.upper ?? 0) - (pitchDof.limits.lower ?? 0) < 0.001) { - // console.debug('Pitch Fixed') - // } else { - // constraints.push({ - // axis: pitchAxis, - // friction: 0.0, - // value: pitchDof?.value ?? 0, - // upper: pitchDof?.limits ? pitchDof.limits.upper ?? 0 : undefined, - // lower: pitchDof?.limits ? pitchDof.limits.lower ?? 0 : undefined - // }) - // } - - // if (yawDof?.limits && (yawDof.limits.upper ?? 0) - (yawDof.limits.lower ?? 0) < 0.001) { - // console.debug('Yaw Fixed') - // } else { - // constraints.push({ - // axis: yawAxis, - // friction: 0.0, - // value: yawDof?.value ?? 0, - // upper: yawDof?.limits ? yawDof.limits.upper ?? 0 : undefined, - // lower: yawDof?.limits ? yawDof.limits.lower ?? 0 : undefined - // }) - // } - - // if (rollDof?.limits && (rollDof.limits.upper ?? 0) - (rollDof.limits.lower ?? 0) < 0.001) { - // console.debug('Roll Fixed') - // } else { - // constraints.push({ - // axis: rollAxis, - // friction: 0.0, - // value: rollDof?.value ?? 0, - // upper: rollDof?.limits ? rollDof.limits.upper ?? 0 : undefined, - // lower: rollDof?.limits ? rollDof.limits.lower ?? 0 : undefined - // }) - // } - - // let bodyStart = bodyB - // let bodyNext = bodyA - // if (constraints.length > 1) { - // console.debug('Starting with Ghost Body') - // bodyNext = this.CreateGhostBody(anchorPoint) - // this._joltBodyInterface.AddBody(bodyNext.GetID(), JOLT.EActivation_Activate) - // mechanism.ghostBodies.push(bodyNext.GetID()) - // } - // for (let i = 0; i < constraints.length; ++i) { - // console.debug(`Constraint ${i}`) - // const c = constraints[i] - // const hingeSettings = new JOLT.HingeConstraintSettings() - // hingeSettings.mMaxFrictionTorque = c.friction; - // hingeSettings.mPoint1 = hingeSettings.mPoint2 = anchorPoint - // hingeSettings.mHingeAxis1 = hingeSettings.mHingeAxis2 = c.axis.Normalized() - // hingeSettings.mNormalAxis1 = hingeSettings.mNormalAxis2 = getPerpendicular( - // hingeSettings.mHingeAxis1 - // ) - // if (c.upper && c.lower) { - // // Some values that are meant to be exactly PI are perceived as being past it, causing unexpected behavior. - // // 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)) - - // const currentPos = piSafetyCheck(c.value) - // const upper = piSafetyCheck(c.upper) - currentPos - // const lower = piSafetyCheck(c.lower) - currentPos - - // hingeSettings.mLimitsMin = -upper - // hingeSettings.mLimitsMax = -lower - // } - - // const hingeConstraint = hingeSettings.Create(bodyStart, bodyNext) - // this._joltPhysSystem.AddConstraint(hingeConstraint) - // this._constraints.push(hingeConstraint) - // bodyStart = bodyNext - // if (i == constraints.length - 2) { - // bodyNext = bodyA - // console.debug('Finishing with Body A') - // } else { - // console.debug('New Ghost Body') - // bodyNext = this.CreateGhostBody(anchorPoint) - // this._joltBodyInterface.AddBody(bodyNext.GetID(), JOLT.EActivation_Activate) - // mechanism.ghostBodies.push(bodyNext.GetID()) - // } - // } - // } - private isWheel(jDef: mirabuf.joint.Joint): boolean { return (jDef.info?.name !== "grounded" && (jDef.userData?.data?.wheel ?? "false") === "true") ?? false } @@ -912,8 +787,8 @@ class PhysicsSystem extends WorldSystem { 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") + if ((parser.assembly.dynamic && !layerReserve && !parser.isGamePiece) || layerReserve?.isReleased) { + throw new Error("No layer reserve for non-game piece dynamic assembly") } const reservedLayer: number | undefined = layerReserve?.layer @@ -949,21 +824,22 @@ class PhysicsSystem extends WorldSystem { const rnLayer: number = reservedLayer ? reservedLayer - : rn.id.endsWith(GAMEPIECE_SUFFIX) + : parser.isGamePiece ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD rn.parts.forEach(partId => { const partInstance = parser.assembly.data!.parts!.partInstances![partId]! - if (partInstance.skipCollider) return + if (!partInstance?.partDefinitionReference || partInstance?.skipCollider) { + return + } const partDefinition = - parser.assembly.data!.parts!.partDefinitions![partInstance.partDefinitionReference!]! + parser.assembly.data!.parts!.partDefinitions![partInstance?.partDefinitionReference] const partShapeResult = rn.isDynamic ? this.createConvexShapeSettingsFromPart(partDefinition) : this.createConcaveShapeSettingsFromPart(partDefinition) - // const partShapeResult = this.CreateConvexShapeSettingsFromPart(partDefinition) if (!partShapeResult) return @@ -1018,7 +894,9 @@ class PhysicsSystem extends WorldSystem { frictionAccum.push(frictionPairing) } - if (!partDefinition.physicalData?.com || !partDefinition.physicalData.mass) return + if (!partDefinition.physicalData?.com || !partDefinition.physicalData.mass) { + return + } const mass = partDefinition.massOverride ? partDefinition.massOverride! @@ -1203,7 +1081,11 @@ class PhysicsSystem extends WorldSystem { if (!collector.HadHit()) return undefined const hitPoint = ray.GetPointOnRay(collector.mHit.mFraction) - return { data: collector.mHit, point: convertJoltRVec3ToJoltVec3(hitPoint), ray: ray } + return { + data: collector.mHit, + point: convertJoltRVec3ToJoltVec3(hitPoint), + ray: ray, + } } /** @@ -1572,9 +1454,13 @@ function setupCollisionFiltering(settings: Jolt.JoltSettings) { } function filterNonPhysicsNodes(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembly): RigidNodeReadOnly[] { + const instances = mira.data?.parts?.partInstances return nodes.filter(x => { for (const part of x.parts) { - const inst = mira.data!.parts!.partInstances![part]! + const inst = instances![part] ?? instances![mira.info?.GUID ?? ""] + if (!inst) { + return false + } const def = mira.data!.parts!.partDefinitions![inst.partDefinitionReference!]! if (def.bodies && def.bodies.length > 0) { return true diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts index 382e64a992..d8a009f5ca 100644 --- a/fission/src/systems/scene/SceneRenderer.ts +++ b/fission/src/systems/scene/SceneRenderer.ts @@ -63,8 +63,9 @@ class SceneRenderer extends WorldSystem { getAll: () => this.filterSceneObjects(obj => obj instanceof MirabufSceneObject), findWhere: (predicate: Parameters<(typeof Array)["prototype"]["find"]>[0]) => this.mirabufSceneObjects.getAll().find(predicate), - getField: () => this.mirabufSceneObjects.findWhere(obj => obj.miraType == MiraType.FIELD), - getRobots: () => this.mirabufSceneObjects.getAll().filter(obj => obj.miraType == MiraType.ROBOT), + getField: () => this.mirabufSceneObjects.findWhere(obj => obj.miraType === MiraType.FIELD), + getRobots: () => this.mirabufSceneObjects.getAll().filter(obj => obj.miraType === MiraType.ROBOT), + getPieces: () => this.mirabufSceneObjects.getAll().filter(obj => obj.miraType === MiraType.PIECE), } as const public get mainCamera() { @@ -393,6 +394,10 @@ class SceneRenderer extends WorldSystem { this._gizmosOnMirabuf.delete(obj.parentObjectId!) } + if (obj instanceof MirabufSceneObject && obj.miraType == MiraType.FIELD) { + this.removeAllGamePieces() + } + if (this._sceneObjects.delete(id)) { obj!.dispose() } @@ -406,6 +411,14 @@ class SceneRenderer extends WorldSystem { } } + public removeAllGamePieces() { + for (const [id, obj] of this._sceneObjects) { + if (obj instanceof MirabufSceneObject && obj.miraType == MiraType.PIECE) { + this.removeSceneObject(id) + } + } + } + public createSphere(radius: number, material?: THREE.Material | undefined): THREE.Mesh { const geo = new THREE.SphereGeometry(radius) if (material) { diff --git a/fission/src/test/TestSetup.server.ts b/fission/src/test/TestSetup.server.ts index 1f41f9b6c1..2b2be48786 100644 --- a/fission/src/test/TestSetup.server.ts +++ b/fission/src/test/TestSetup.server.ts @@ -12,12 +12,14 @@ export async function setup() { console.log("Starting static file server...") - const assets = sirv(serveDirectory) - + const assets = sirv(serveDirectory, { + dev: false, + }) server = http.createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*") res.setHeader("Access-Control-Allow-Methods", "GET") assets(req, res) + console.log(res.statusCode, res.statusMessage, req.url) }) await new Promise((resolve, reject) => { diff --git a/fission/src/test/World.test.ts b/fission/src/test/World.test.ts index dd57d25b4e..6c4544ccb9 100644 --- a/fission/src/test/World.test.ts +++ b/fission/src/test/World.test.ts @@ -8,6 +8,7 @@ vi.mock("@/systems/physics/PhysicsSystem", () => ({ })), getLastDeltaT: vi.fn(() => 0.016), BodyAssociate: vi.fn(), + LAYER_GENERAL_DYNAMIC: 1, })) vi.mock("@/systems/scene/SceneRenderer", () => ({ diff --git a/fission/src/test/mirabuf/FieldMiraEditor.test.ts b/fission/src/test/mirabuf/FieldMiraEditor.test.ts index 757f022133..afe1e6c465 100644 --- a/fission/src/test/mirabuf/FieldMiraEditor.test.ts +++ b/fission/src/test/mirabuf/FieldMiraEditor.test.ts @@ -1,3 +1,4 @@ +import FieldMiraEditor from "../../mirabuf/FieldMiraEditor" import { assert, describe, expect, test, vi } from "vitest" import MirabufCachingService, { MiraType } from "@/mirabuf/MirabufLoader.ts" import { createMirabuf } from "@/mirabuf/MirabufSceneObject.ts" @@ -6,7 +7,6 @@ import { defaultRobotSpawnLocation, type ScoringZonePreferences, } from "@/systems/preferences/PreferenceTypes.ts" -import FieldMiraEditor from "../../mirabuf/FieldMiraEditor.ts" import { mirabuf } from "../../proto/mirabuf" function mockParts(): mirabuf.IParts { @@ -125,7 +125,11 @@ describe("Devtool Scoring Zones Caching Tests", () => { editor.setUserData("devtool:scoring_zones", scoringZonePayload) const newPayload: ScoringZonePreferences[] = [ - { ...scoringZonePayload[0], name: "Blue Zone", alliance: "blue" as Alliance }, + { + ...scoringZonePayload[0], + name: "Blue Zone", + alliance: "blue" as Alliance, + }, ] editor.setUserData("devtool:scoring_zones", newPayload) expect(editor.getUserData("devtool:scoring_zones")).toEqual(newPayload) diff --git a/fission/src/test/mirabuf/MirabufLoader.test.ts b/fission/src/test/mirabuf/MirabufLoader.test.ts index a175a5498e..1c69c6d0b4 100644 --- a/fission/src/test/mirabuf/MirabufLoader.test.ts +++ b/fission/src/test/mirabuf/MirabufLoader.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, type MockedFunction, test, vi } from "vitest" -import MirabufLoader, { backUpRobots, MiraType } from "../../mirabuf/MirabufLoader" +import MirabufLoader, { backUpMap, MiraType } from "../../mirabuf/MirabufLoader" vi.mock("@/systems/World", () => ({ default: { @@ -75,7 +75,7 @@ describe("MirabufLoader", () => { Object.defineProperty(navigator.storage, "getDirectory", { value: vi.fn(async () => ({ getDirectoryHandle: vi.fn(async () => ({ - name: "Robots", + name: "robot", getFileHandle: vi.fn(async () => ({ createWritable: vi.fn(async () => ({ write: vi.fn(), @@ -156,8 +156,13 @@ describe("MirabufLoader", () => { localStorageMock["Synthesis Nonce Key"] = "4543246" const map = { [key]: { id, miraType, cacheKey: key } } - localStorageMock["Robots"] = JSON.stringify(map) - backUpRobots[id] = { id, miraType, cacheKey: key, buffer: new Uint8Array(new ArrayBuffer(1)) } + localStorageMock["robot"] = JSON.stringify(map) + backUpMap[miraType][id] = { + id, + miraType, + cacheKey: key, + buffer: new Uint8Array(new ArrayBuffer(1)), + } const name = "Test Robot" const thumbnailStorageID = "thumb123" @@ -165,7 +170,7 @@ describe("MirabufLoader", () => { expect(result).toBe(true) - const updatedMap = JSON.parse(localStorageMock["Robots"]) + const updatedMap = JSON.parse(localStorageMock["robot"]) expect(updatedMap[key].name).toBe(name) expect(updatedMap[key].thumbnailStorageID).toBe(thumbnailStorageID) expect(updatedMap[key].id).toBe(id) diff --git a/fission/src/test/mirabuf/MirabufParser.test.ts b/fission/src/test/mirabuf/MirabufParser.test.ts index f93224d944..396131624a 100644 --- a/fission/src/test/mirabuf/MirabufParser.test.ts +++ b/fission/src/test/mirabuf/MirabufParser.test.ts @@ -8,7 +8,10 @@ describe("Mirabuf Parser Tests", () => { const spikeMira = await MirabufCachingService.cacheRemote( "/api/mira/robots/Dozer_v9.mira", MiraType.ROBOT - ).then(x => MirabufCachingService.get(x!.id, MiraType.ROBOT)) + ).then(x => { + expect(x).toBeDefined() + return MirabufCachingService.get(x!.id, MiraType.ROBOT) + }) const t = new MirabufParser(spikeMira!) const rn = [...t.rigidNodes.values()] @@ -58,12 +61,12 @@ describe("Mirabuf Parser Tests", () => { test("Generate Rigid Nodes (FRC Field 2018_v13.mira)", async () => { const field = await MirabufCachingService.cacheRemote( - "/api/mira/Fields/FRC Field 2018_v13.mira", + "/api/mira/fields/FRC Field 2018_v13.mira", MiraType.FIELD ).then(x => MirabufCachingService.get(x!.id, MiraType.FIELD)) const t = new MirabufParser(field!) - expect(filterNonPhysicsNodes([...t.rigidNodes.values()], field!).length).toBe(34) + expect(filterNonPhysicsNodes([...t.rigidNodes.values()], field!).length).toBe(2) }) }) diff --git a/fission/src/test/mirabuf/MirabufSceneObject.test.ts b/fission/src/test/mirabuf/MirabufSceneObject.test.ts index 7cb10b31cc..d3b2f39691 100644 --- a/fission/src/test/mirabuf/MirabufSceneObject.test.ts +++ b/fission/src/test/mirabuf/MirabufSceneObject.test.ts @@ -27,8 +27,17 @@ const mockSceneRenderer = { scene: { add: vi.fn(), remove: vi.fn() }, registerSceneObject: vi.fn(), removeSceneObject: vi.fn(), - createSphere: vi.fn(() => ({ material: {}, geometry: {}, position: {}, rotation: {} })), - currentCameraControls: { focusProvider: undefined, controlsType: "Orbit", locked: false }, + createSphere: vi.fn(() => ({ + material: {}, + geometry: {}, + position: {}, + rotation: {}, + })), + currentCameraControls: { + focusProvider: undefined, + controlsType: "Orbit", + locked: false, + }, worldToPixelSpace: vi.fn(() => [0, 0]), createToonMaterial: vi.fn(() => ({ color: 0x123456 })), setupMaterial: vi.fn(), @@ -59,11 +68,25 @@ vi.mock("@/systems/World", () => ({ vi.mock("@/systems/preferences/PreferencesSystem", () => ({ default: { getRobotPreferences: vi.fn(() => ({ - intake: { deltaTransformation: [1], zoneDiameter: 1, parentNode: "n", showZoneAlways: false, maxPieces: 1 }, - ejector: { deltaTransformation: [1], ejectorVelocity: 1, parentNode: "n", ejectOrder: "FIFO" }, + intake: { + deltaTransformation: [1], + zoneDiameter: 1, + parentNode: "n", + showZoneAlways: false, + maxPieces: 1, + }, + ejector: { + deltaTransformation: [1], + ejectorVelocity: 1, + parentNode: "n", + ejectOrder: "FIFO", + }, simConfig: undefined, })), - getFieldPreferences: vi.fn(() => ({ defaultSpawnLocation: [0, 1, 0], scoringZones: [] })), + getFieldPreferences: vi.fn(() => ({ + defaultSpawnLocation: [0, 1, 0], + scoringZones: [], + })), getGlobalPreference: vi.fn(() => false), addPreferenceEventListener: vi.fn(() => () => {}), setRobotPreferences: vi.fn(), @@ -76,7 +99,10 @@ vi.mock("@/ui/components/SceneOverlayEvents", () => ({ })) vi.mock("@/systems/simulation/synthesis_brain/SynthesisBrain", () => ({ - default: vi.fn(() => ({ inputSchemeName: "TestScheme", clearControls: vi.fn() })), + default: vi.fn(() => ({ + inputSchemeName: "TestScheme", + clearControls: vi.fn(), + })), })) vi.mock("@/systems/simulation/wpilib_brain/WPILibBrain", () => ({ @@ -109,7 +135,16 @@ function mockMirabufInstance(): MirabufInstance { assembly: { dynamic: true, info: { name: "TestAssembly" } }, rootNode: "root", rigidNodes: new Map([ - ["root", { id: "root", parts: new Set(), isDynamic: true, isGamePiece: false, mass: 1 }], + [ + "root", + { + id: "root", + parts: new Set(), + isDynamic: true, + isGamePiece: false, + mass: 1, + }, + ], ]), globalTransforms: new Map(), }, @@ -139,7 +174,7 @@ describe("MirabufSceneObject", () => { vi.clearAllMocks() mirabufInstance = mockMirabufInstance() progressHandle = undefined - instance = new MirabufSceneObject(mirabufInstance, "TestAssembly", progressHandle) + instance = new MirabufSceneObject(mirabufInstance, "TestAssembly", "", progressHandle) console.log = vi.fn() console.error = vi.fn() @@ -170,7 +205,9 @@ describe("MirabufSceneObject", () => { test("Dispose cleans up scene objects and mechanism", () => { setPrivate(instance, "_ejectables", [{ id: 1, gamePieceBodyId: mockBodyId() }]) setPrivate(instance, "_scoringZones", [{ id: 2 }]) - setPrivate(instance, "_intakeSensor", { id: 3 } as unknown as IntakeSensorSceneObject) + setPrivate(instance, "_intakeSensor", { + id: 3, + } as unknown as IntakeSensorSceneObject) instance.dispose() expect(mockSceneRenderer.removeSceneObject).toHaveBeenCalled() expect(mockPhysicsSystem.destroyMechanism).toHaveBeenCalled() @@ -248,7 +285,7 @@ describe("MirabufSceneObject - Real Systems Integration", () => { batch.computeBoundingBox() }) - const dozerSceneObject = new MirabufSceneObject(mirabufInstance, "Dozer_v9", undefined) + const dozerSceneObject = new MirabufSceneObject(mirabufInstance, "Dozer_v9", cacheInfo!.id, undefined) const originalDimensions = dozerSceneObject.getDimensions() diff --git a/fission/src/test/mirabuf/ScoringZoneSceneObject.test.ts b/fission/src/test/mirabuf/ScoringZoneSceneObject.test.ts index 6278070263..43f3ec12c1 100644 --- a/fission/src/test/mirabuf/ScoringZoneSceneObject.test.ts +++ b/fission/src/test/mirabuf/ScoringZoneSceneObject.test.ts @@ -10,7 +10,8 @@ const mockPhysicsSystem = { destroyBodyIds: vi.fn(), setBodyPosition: vi.fn(), setBodyRotation: vi.fn(), - getBody: vi.fn((_bodyId: Jolt.BodyID) => createBodyMock() as unknown as Jolt.Body), + // This cast is fine as long as we regularly update createBodyMock() to include new methods we call on bodies in functions we're testing + getBody: vi.fn(() => createBodyMock() as unknown as Jolt.Body), getBodyAssociation: vi.fn(), disablePhysicsForBody: vi.fn(), enablePhysicsForBody: vi.fn(), @@ -77,10 +78,12 @@ describe("ScoringZoneSceneObject", () => { test("ZoneCollision updates score", () => { const instance = new ScoringZoneSceneObject({} as unknown as MirabufSceneObject, 0) Reflect.set(instance, "_prefs", { persistentPoints: false, alliance: "red", points: 10 }) - const gamePieceBody = {} as unknown as Jolt.BodyID + // This is *ok* as long as we update createBodyMock properly + // In the long run, we should have better ways of constructing arguments for testing + const gpID = {} as unknown as Jolt.BodyID mockPhysicsSystem.getBodyAssociation = vi.fn(() => ({ isGamePiece: true, associatedBody: 0 })) const dispatchSpy = vi.spyOn(OnScoreChangedEvent.prototype, "dispatch") - instance["zoneCollision"](gamePieceBody) + instance.zoneCollision(gpID) expect(SimulationSystem.redScore).toBe(10) expect(dispatchSpy).toHaveBeenCalled() }) diff --git a/fission/src/test/mocks/jolt.ts b/fission/src/test/mocks/jolt.ts index 92df25e72c..5cb94da948 100644 --- a/fission/src/test/mocks/jolt.ts +++ b/fission/src/test/mocks/jolt.ts @@ -83,5 +83,6 @@ export function createBodyMock() { SetLinearVelocity: vi.fn(), SetAngularVelocity: vi.fn(), GetAngularVelocity: vi.fn(() => createVec3Mock()), + GetObjectLayer: vi.fn(() => 1), // In the future, this would need to be amended } } diff --git a/fission/src/test/physics/Mechanism.test.ts b/fission/src/test/physics/Mechanism.test.ts index b300a747c0..13fc1bb131 100644 --- a/fission/src/test/physics/Mechanism.test.ts +++ b/fission/src/test/physics/Mechanism.test.ts @@ -2,8 +2,8 @@ import type Jolt from "@azaleacolburn/jolt-physics" import { beforeEach, describe, expect, test, vi } from "vitest" import MirabufCachingService, { MiraType } from "@/mirabuf/MirabufLoader" import MirabufParser from "@/mirabuf/MirabufParser" -import type { RigidNodeId } from "../../mirabuf/MirabufParser" -import type { mirabuf } from "../../proto/mirabuf" +import type { RigidNodeId } from "@/mirabuf/MirabufParser.ts" +import type { mirabuf } from "@/proto/mirabuf" import Mechanism, { type MechanismConstraint } from "../../systems/physics/Mechanism" import PhysicsSystem, { type LayerReserve } from "../../systems/physics/PhysicsSystem" @@ -290,7 +290,10 @@ describe("Mirabuf Mechanism Creation", () => { test("Body Loading (Dozer)", async () => { const assembly = await MirabufCachingService.cacheRemote("/api/mira/robots/Dozer_v9.mira", MiraType.ROBOT).then( - x => MirabufCachingService.get(x!.id, MiraType.ROBOT) + x => { + expect(x).toBeDefined() + return MirabufCachingService.get(x!.id, MiraType.ROBOT) + } ) const parser = new MirabufParser(assembly!) @@ -301,11 +304,14 @@ describe("Mirabuf Mechanism Creation", () => { expect(mechanism.constraints.length).toBe(12) }) - test("Body Loading (Mutli-Joint Robot)", async () => { + test("Body Loading (Multi-Joint Robot)", async () => { const assembly = await MirabufCachingService.cacheRemote( "/api/mira/private/Multi-Joint_Wheels_v0.mira", MiraType.ROBOT - ).then(x => MirabufCachingService.get(x!.id, MiraType.ROBOT)) + ).then(x => { + expect(x).toBeDefined() + return MirabufCachingService.get(x!.id, MiraType.ROBOT) + }) const parser = new MirabufParser(assembly!) const mechanism = physSystem.createMechanismFromParser(parser) diff --git a/fission/src/test/physics/PhysicsSystemRobotSpawning.test.ts b/fission/src/test/physics/PhysicsSystemRobotSpawning.test.ts index 4a2f9926eb..8b0d56450c 100644 --- a/fission/src/test/physics/PhysicsSystemRobotSpawning.test.ts +++ b/fission/src/test/physics/PhysicsSystemRobotSpawning.test.ts @@ -6,8 +6,12 @@ import PhysicsSystem, { LayerReserve } from "@/systems/physics/PhysicsSystem" describe("Mirabuf Physics Loading", () => { test("Body Loading (Dozer)", async () => { const assembly = await MirabufCachingService.cacheRemote("/api/mira/robots/Dozer_v9.mira", MiraType.ROBOT).then( - x => MirabufCachingService.get(x!.id, MiraType.ROBOT) + x => { + expect(x).toBeDefined() + return MirabufCachingService.get(x!.id, MiraType.ROBOT) + } ) + const parser = new MirabufParser(assembly!) const physSystem = new PhysicsSystem() const mapping = physSystem.createBodiesFromParser(parser, new LayerReserve()) @@ -26,7 +30,10 @@ describe("Mirabuf Physics Loading", () => { const assembly = await MirabufCachingService.cacheRemote( "/api/mira/private/Multi-Joint_Wheels_v0.mira", MiraType.ROBOT - ).then(x => MirabufCachingService.get(x!.id, MiraType.ROBOT)) + ).then(x => { + expect(x).toBeDefined() + return MirabufCachingService.get(x!.id, MiraType.ROBOT) + }) const parser = new MirabufParser(assembly!) const physSystem = new PhysicsSystem() const mapping = physSystem.createBodiesFromParser(parser, new LayerReserve()) diff --git a/fission/src/ui/UIProvider.tsx b/fission/src/ui/UIProvider.tsx index 5d5397a582..9241e5bd15 100644 --- a/fission/src/ui/UIProvider.tsx +++ b/fission/src/ui/UIProvider.tsx @@ -91,7 +91,7 @@ export const UIProvider: React.FC = ({ children }) => { setModal(newModal as Modal) return id }, - [modal] + [modal, DEFAULT_MODAL_PROPS, DEFAULT_PROPS] ) const openPanel: OpenPanelFn = useCallback( @@ -131,7 +131,7 @@ export const UIProvider: React.FC = ({ children }) => { setPanels([...panels, panel as Panel]) return id }, - [panels] + [panels, DEFAULT_PANEL_PROPS] ) const closeCallbacks = (elem: Panel | Modal, closeType: CloseType) => { @@ -155,16 +155,19 @@ export const UIProvider: React.FC = ({ children }) => { if (modal) closeCallbacks(modal as Modal, closeType) setModal(undefined) }, - [modal] + [modal, closeCallbacks] ) - const closePanel = useCallback((id: string, closeType: CloseType) => { - setPanels(p => { - const panel = p.find((p: Panel) => p.id === id) - if (panel) closeCallbacks(panel, closeType) - return p.filter((pnl: Panel) => pnl.id !== id) - }) - }, []) + const closePanel = useCallback( + (id: string, closeType: CloseType) => { + setPanels(p => { + const panel = p.find((p: Panel) => p.id === id) + if (panel) closeCallbacks(panel, closeType) + return p.filter((pnl: Panel) => pnl.id !== id) + }) + }, + [closeCallbacks] + ) // biome-ignore-end lint/suspicious/noExplicitAny: need to be able to extend const snackbarAction = useCallback( @@ -173,7 +176,7 @@ export const UIProvider: React.FC = ({ children }) => { ), - [] + [closeSnackbar] ) const addToast = useCallback( @@ -194,7 +197,7 @@ export const UIProvider: React.FC = ({ children }) => { { variant, action: snackbarAction } ) }, - [enqueueSnackbar] + [enqueueSnackbar, snackbarAction] ) const configureScreen: ConfigureScreenFn = useCallback((screen, props, callbacks) => { diff --git a/fission/src/ui/components/ProgressNotification.tsx b/fission/src/ui/components/ProgressNotification.tsx index ffcee99759..a10de827a8 100644 --- a/fission/src/ui/components/ProgressNotification.tsx +++ b/fission/src/ui/components/ProgressNotification.tsx @@ -63,7 +63,7 @@ const ProgressNotification: React.FC = ({ handle }) => { lastUpdate: Date.now(), }) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handle.progress]) + }, [handle.progress, progressData.currentValue]) return ( { OnScoreChangedEvent.removeListener(onScoreChange) UpdateTimeLeft.removeListener(onTimeLeftChange) } - }, []) + }, [onScoreChange, onTimeLeftChange]) return ( diff --git a/fission/src/ui/modals/APSManagementModal.tsx b/fission/src/ui/modals/APSManagementModal.tsx index 7689f1148f..8dc33e44cf 100644 --- a/fission/src/ui/modals/APSManagementModal.tsx +++ b/fission/src/ui/modals/APSManagementModal.tsx @@ -15,7 +15,7 @@ const APSManagementModal: React.FC> = ({ modal }) => } configureScreen(modal!, { title: userInfo?.name ?? "Not signed in", acceptText: "Logout" }, { onBeforeAccept }) - }, [modal, userInfo?.name]) + }, [modal, userInfo?.name, configureScreen]) return ( diff --git a/fission/src/ui/modals/MainMenuModal.tsx b/fission/src/ui/modals/MainMenuModal.tsx index 99eca94cf5..4cd04d67d8 100644 --- a/fission/src/ui/modals/MainMenuModal.tsx +++ b/fission/src/ui/modals/MainMenuModal.tsx @@ -25,7 +25,7 @@ const MainMenuModal: React.FC> = ({ mo return () => { setIsMainMenuOpen(false) } - }, []) + }, [configureScreen, modal, setIsMainMenuOpen]) return (