diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 634c005e9d..1650e75380 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -67,7 +67,7 @@ def readFromDesign(self) -> "ExporterOptions": for field in fields(self): attribute = designAttributes.itemByName(INTERNAL_ID, field.name) if attribute: - attrJsonData = makeObjectFromJson(field.type, json.loads(attribute.value)) + attrJsonData = makeObjectFromJson(type(field.type), json.loads(attribute.value)) setattr(self, field.name, attrJsonData) return self diff --git a/exporter/SynthesisFusionAddin/src/Types.py b/exporter/SynthesisFusionAddin/src/Types.py index 166bc43976..4219bf1814 100644 --- a/exporter/SynthesisFusionAddin/src/Types.py +++ b/exporter/SynthesisFusionAddin/src/Types.py @@ -109,7 +109,7 @@ def makeObjectFromJson(objType: type, data: Any) -> Any: assert is_dataclass(obj) and isinstance(data, dict), "Found unsupported type to decode." for field in fields(obj): if field.name in data: - setattr(obj, field.name, makeObjectFromJson(field.type, data[field.name])) + setattr(obj, field.name, makeObjectFromJson(type(field.type), data[field.name])) else: setattr(obj, field.name, field.default_factory if field.default_factory is not MISSING else field.default) diff --git a/fission/package.json b/fission/package.json index 114741b076..b35a21e462 100644 --- a/fission/package.json +++ b/fission/package.json @@ -65,6 +65,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^8.8.0", "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-import": "^2.30.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "jsdom": "^24.1.0", @@ -73,10 +74,11 @@ "prettier": "3.3.2", "protobufjs": "^7.2.6", "protobufjs-cli": "^1.1.2", + "rollup": "^4.22.4", "tailwindcss": "^3.3.3", "tsconfig-paths": "^4.2.0", "typescript": "^5.4.5", - "vite": "^5.1.4", + "vite": "5.2.14", "vite-plugin-glsl": "^1.3.0", "vite-plugin-singlefile": "^0.13.5", "vitest": "^1.5.3" diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 6f976395a8..ec18ba6bc8 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -47,9 +47,7 @@ import ChooseInputSchemePanel from "./ui/panels/configuring/ChooseInputSchemePan import ProgressNotifications from "./ui/components/ProgressNotification.tsx" import SceneOverlay from "./ui/components/SceneOverlay.tsx" -import WPILibWSWorker from "@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker" import WSViewPanel from "./ui/panels/WSViewPanel.tsx" -import Lazy from "./util/Lazy.ts" import RCConfigPWMGroupModal from "@/modals/configuring/rio-config/RCConfigPWMGroupModal.tsx" import RCConfigCANGroupModal from "@/modals/configuring/rio-config/RCConfigCANGroupModal.tsx" @@ -62,8 +60,8 @@ import APSManagementModal from "./ui/modals/APSManagementModal.tsx" import ConfigurePanel from "./ui/panels/configuring/assembly-config/ConfigurePanel.tsx" import CameraSelectionPanel from "./ui/panels/configuring/CameraSelectionPanel.tsx" import ContextMenu from "./ui/components/ContextMenu.tsx" - -const worker = new Lazy(() => new WPILibWSWorker()) +import GlobalUIComponent from "./ui/components/GlobalUIComponent.tsx" +import InitialConfigPanel from "./ui/panels/configuring/initial-config/InitialConfigPanel.tsx" function Synthesis() { const { openModal, closeModal, getActiveModalElement } = useModalManager(initialModals) @@ -95,8 +93,6 @@ function Synthesis() { setConsentPopupDisable(false) } - worker.getValue() - let mainLoopHandle = 0 const mainLoop = () => { mainLoopHandle = requestAnimationFrame(mainLoop) @@ -164,6 +160,7 @@ function Synthesis() { closeAllPanels={closeAllPanels} > + @@ -232,6 +229,7 @@ const initialPanels: ReactElement[] = [ , , , + , ] export default Synthesis diff --git a/fission/src/aps/APS.ts b/fission/src/aps/APS.ts index 4873d92136..d9ae0500e3 100644 --- a/fission/src/aps/APS.ts +++ b/fission/src/aps/APS.ts @@ -1,5 +1,5 @@ import World from "@/systems/World" -import { MainHUD_AddToast } from "@/ui/components/MainHUD" +import { Global_AddToast } from "@/ui/components/GlobalUIControls" import { Mutex } from "async-mutex" const APS_AUTH_KEY = "aps_auth" @@ -14,7 +14,7 @@ export const ENDPOINT_SYNTHESIS_CHALLENGE = `/api/aps/challenge` const ENDPOINT_AUTODESK_AUTHENTICATION_AUTHORIZE = "https://developer.api.autodesk.com/authentication/v2/authorize" const ENDPOINT_AUTODESK_AUTHENTICATION_TOKEN = "https://developer.api.autodesk.com/authentication/v2/token" -const ENDPOINT_AUTODESK_REVOKE_TOKEN = "https://developer.api.autodesk.com/authentication/v2/revoke" +const ENDPOINT_AUTODESK_AUTHENTICATION_REVOKE = "https://developer.api.autodesk.com/authentication/v2/revoke" const ENDPOINT_AUTODESK_USERINFO = "https://api.userprofile.autodesk.com/userinfo" export interface APSAuth { @@ -163,7 +163,7 @@ class APS { ["client_id", CLIENT_ID], ] as string[][]), } - const res = await fetch(ENDPOINT_AUTODESK_REVOKE_TOKEN, opts) + const res = await fetch(ENDPOINT_AUTODESK_AUTHENTICATION_REVOKE, opts) if (!res.ok) { console.log("Failed to revoke auth token:\n") return false @@ -205,7 +205,7 @@ class APS { } catch (e) { console.error(e) World.AnalyticsSystem?.Exception("APS Login Failure") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") } }) } @@ -236,7 +236,7 @@ class APS { const json = await res.json() if (!res.ok) { if (shouldRelog) { - MainHUD_AddToast("warning", "Must Re-signin.", json.userMessage) + Global_AddToast?.("warning", "Must Re-signin.", json.userMessage) this.auth = undefined await this.requestAuthCode() return false @@ -249,13 +249,13 @@ class APS { if (this.auth) { await this.loadUserInfo(this.auth) if (APS.userInfo) { - MainHUD_AddToast("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) + Global_AddToast?.("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) } } return true } catch (e) { World.AnalyticsSystem?.Exception("APS Login Failure") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") this.auth = undefined await this.requestAuthCode() return false @@ -281,7 +281,7 @@ class APS { const json = await res.json() if (!res.ok) { World.AnalyticsSystem?.Exception("APS Login Failure") - MainHUD_AddToast("error", "Error signing in.", json.userMessage) + Global_AddToast?.("error", "Error signing in.", json.userMessage) this.auth = undefined return } @@ -293,7 +293,7 @@ class APS { if (auth) { await this.loadUserInfo(auth) if (APS.userInfo) { - MainHUD_AddToast("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) + Global_AddToast?.("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) } } else { console.error("Couldn't get auth data.") @@ -306,7 +306,7 @@ class APS { if (retry_login) { this.auth = undefined World.AnalyticsSystem?.Exception("APS Login Failure") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") } } @@ -327,7 +327,7 @@ class APS { const json = await res.json() if (!res.ok) { World.AnalyticsSystem?.Exception("APS Failure: User Info") - MainHUD_AddToast("error", "Error fetching user data.", json.userMessage) + Global_AddToast?.("error", "Error fetching user data.", json.userMessage) this.auth = undefined await this.requestAuthCode() return @@ -347,7 +347,7 @@ class APS { } catch (e) { console.error(e) World.AnalyticsSystem?.Exception("APS Login Failure: User Info") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") this.auth = undefined } } @@ -363,7 +363,7 @@ class APS { } catch (e) { console.error(e) World.AnalyticsSystem?.Exception("APS Login Failure: Code Challenge") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") } } } diff --git a/fission/src/aps/APSDataManagement.ts b/fission/src/aps/APSDataManagement.ts index 6deac410ea..b19e583eda 100644 --- a/fission/src/aps/APSDataManagement.ts +++ b/fission/src/aps/APSDataManagement.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { MainHUD_AddToast } from "@/ui/components/MainHUD" +import { Global_AddToast } from "@/ui/components/GlobalUIControls" import APS from "./APS" import TaskStatus from "@/util/TaskStatus" import { Mutex } from "async-mutex" @@ -140,9 +140,9 @@ export async function getHubs(): Promise { console.log(auth) console.log(APS.userInfo) if (e instanceof APSDataError) { - MainHUD_AddToast("error", e.title, e.detail) + Global_AddToast?.("error", e.title, e.detail) } else if (e instanceof Error) { - MainHUD_AddToast("error", "Failed to get hubs.", e.message) + Global_AddToast?.("error", "Failed to get hubs.", e.message) } return undefined } @@ -179,7 +179,7 @@ export async function getProjects(hub: Hub): Promise { } catch (e) { console.error("Failed to get hubs") if (e instanceof Error) { - MainHUD_AddToast("error", "Failed to get hubs.", e.message) + Global_AddToast?.("error", "Failed to get hubs.", e.message) } return undefined } @@ -224,7 +224,7 @@ export async function getFolderData(project: Project, folder: Folder): Promise { const count = countMap.get(material)! diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 4bb0f4769b..d2a234f8ed 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -4,13 +4,12 @@ import MirabufInstance from "./MirabufInstance" import MirabufParser, { ParseErrorSeverity, RigidNodeId, RigidNodeReadOnly } from "./MirabufParser" import World from "@/systems/World" import Jolt from "@barclah/jolt-physics" -import { JoltMat44_ThreeMatrix4 } from "@/util/TypeConversions" +import { JoltMat44_ThreeMatrix4, JoltVec3_ThreeVector3 } from "@/util/TypeConversions" import * as THREE from "three" import JOLT from "@/util/loading/JoltSyncLoader" import { BodyAssociate, LayerReserve } from "@/systems/physics/PhysicsSystem" import Mechanism from "@/systems/physics/Mechanism" import InputSystem from "@/systems/input/InputSystem" -import TransformGizmos from "@/ui/components/TransformGizmos" import { EjectorPreferences, FieldPreferences, IntakePreferences } from "@/systems/preferences/PreferenceTypes" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import { MiraType } from "./MirabufLoader" @@ -23,8 +22,9 @@ import { ProgressHandle } from "@/ui/components/ProgressNotificationData" import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" import { ContextData, ContextSupplier } from "@/ui/components/ContextMenuData" import { CustomOrbitControls } from "@/systems/scene/CameraControls" -import { setSelectedBrainIndexGlobal } from "@/ui/panels/configuring/ChooseInputSchemePanel" -import { MainHUD_OpenPanel } from "@/ui/components/MainHUD" +import GizmoSceneObject from "@/systems/scene/GizmoSceneObject" +import { ConfigMode, setNextConfigurePanelSettings } from "@/ui/panels/configuring/assembly-config/ConfigurePanelControls" +import { Global_OpenPanel } from "@/ui/components/GlobalUIControls" const DEBUG_BODIES = false @@ -33,6 +33,23 @@ interface RnDebugMeshes { comMesh: THREE.Mesh } +/** + * The goal with the spotlight assembly is to provide a contextual target assembly + * the user would like to modifiy. Generally this will be which even assembly was + * last spawned in, however, systems (such as the configuration UI) can elect + * assemblies to be in the spotlight when moving from interface to interface. + */ +let spotlightAssembly: number | undefined + +export function setSpotlightAssembly(assembly: MirabufSceneObject) { + spotlightAssembly = assembly.id +} + +// TODO: If nothing is in the spotlight, select last entry before defaulting to undefined +export function getSpotlightAssembly(): MirabufSceneObject | undefined { + return World.SceneRenderer.sceneObjects.get(spotlightAssembly ?? 0) as MirabufSceneObject +} + class MirabufSceneObject extends SceneObject implements ContextSupplier { private _assemblyName: string private _mirabufInstance: MirabufInstance @@ -42,8 +59,6 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { private _debugBodies: Map | null private _physicsLayerReserve: LayerReserve | undefined - private _transformGizmos: TransformGizmos | undefined - private _intakePreferences: IntakePreferences | undefined private _ejectorPreferences: EjectorPreferences | undefined @@ -110,8 +125,6 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { this._debugBodies = null - this.EnableTransformControls() // adding transform gizmo to mirabuf object on its creation - this.getPreferences() // creating nametag for robots @@ -154,96 +167,53 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { }) // Simulation - World.SimulationSystem.RegisterMechanism(this._mechanism) - const simLayer = World.SimulationSystem.GetSimulationLayer(this._mechanism)! - this._brain = new SynthesisBrain(this._mechanism, this._assemblyName) - simLayer.SetBrain(this._brain) + if (this.miraType == MiraType.ROBOT) { + World.SimulationSystem.RegisterMechanism(this._mechanism) + const simLayer = World.SimulationSystem.GetSimulationLayer(this._mechanism)! + this._brain = new SynthesisBrain(this._mechanism, this._assemblyName) + simLayer.SetBrain(this._brain) + } // Intake this.UpdateIntakeSensor() - this.UpdateScoringZones() - } - - public Update(): void { - const brainIndex = this._brain instanceof SynthesisBrain ? this._brain.brainIndex ?? -1 : -1 - if (InputSystem.getInput("eject", brainIndex)) { - this.Eject() - } - this._mirabufInstance.parser.rigidNodes.forEach(rn => { - if (!this._mirabufInstance.meshes.size) return // if this.dispose() has been ran then return - 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 meshes = this._mirabufInstance.meshes.get(part) ?? [] - meshes.forEach(([batch, id]) => batch.setMatrixAt(id, partTransform)) - }) + setSpotlightAssembly(this) - /** - * Update the position and rotation of the body to match the position of the transform gizmo. - * - * This block of code should only be executed if the transform gizmo exists. - */ - if (this._transformGizmos) { - if (InputSystem.isKeyPressed("Enter")) { - // confirming placement of the mirabuf object - this.DisableTransformControls() - return - } else if (InputSystem.isKeyPressed("Escape")) { - // cancelling the creation of the mirabuf scene object - World.SceneRenderer.RemoveSceneObject(this.id) - return - } + this.UpdateBatches() - // if the gizmo is being dragged, copy the mesh position and rotation to the Mirabuf body - if (this._transformGizmos.isBeingDragged()) { - this._transformGizmos.UpdateMirabufPositioning(this, rn) - World.PhysicsSystem.DisablePhysicsForBody(this._mechanism.GetBodyByNodeId(rn.id)!) - } - } + const bounds = this.ComputeBoundingBox() + if (!Number.isFinite(bounds.min.y)) return - 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()}` - ) - } - // console.debug(`POSITION: ${body.GetPosition().GetX()}, ${body.GetPosition().GetY()}, ${body.GetPosition().GetZ()}`) + const offset = new JOLT.Vec3( + -(bounds.min.x + bounds.max.x) / 2.0, + 0.1 + (bounds.max.y - bounds.min.y) / 2.0 - (bounds.min.y + bounds.max.y) / 2.0, + -(bounds.min.z + bounds.max.z) / 2.0 + ) - if (this._debugBodies) { - const { colliderMesh, comMesh } = this._debugBodies.get(rn.id)! - colliderMesh.position.setFromMatrixPosition(transform) - colliderMesh.rotation.setFromRotationMatrix(transform) + this._mirabufInstance.parser.rigidNodes.forEach(rn => { + const jBodyId = this._mechanism.GetBodyByNodeId(rn.id) + if (!jBodyId) return - const comTransform = JoltMat44_ThreeMatrix4(body.GetCenterOfMassTransform()) + const newPos = World.PhysicsSystem.GetBody(jBodyId).GetPosition().Add(offset) + World.PhysicsSystem.SetBodyPosition(jBodyId, newPos) - comMesh.position.setFromMatrixPosition(comTransform) - comMesh.rotation.setFromRotationMatrix(comTransform) - } + JOLT.destroy(newPos) }) - this._mirabufInstance.batches.forEach(x => { - x.computeBoundingBox() - x.computeBoundingSphere() - }) + this.UpdateMeshTransforms() + } - /* Updating the position of the name tag according to the robots position on screen */ - if (this._nameTag && PreferencesSystem.getGlobalPreference("RenderSceneTags")) { - const boundingBox = this.ComputeBoundingBox() - this._nameTag.position = World.SceneRenderer.WorldToPixelSpace( - new THREE.Vector3( - (boundingBox.max.x + boundingBox.min.x) / 2, - boundingBox.max.y + 0.1, - (boundingBox.max.z + boundingBox.min.z) / 2 - ) - ) + public Update(): void { + const brainIndex = this._brain instanceof SynthesisBrain ? this._brain.brainIndex ?? -1 : -1 + if (InputSystem.getInput("eject", brainIndex)) { + this.Eject() } + + this.UpdateMeshTransforms() + + this.UpdateBatches() + this.UpdateNameTag() } public Dispose(): void { @@ -265,7 +235,6 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { }) this._nameTag?.Dispose() - this.DisableTransformControls() World.SimulationSystem.UnregisterMechanism(this._mechanism) World.PhysicsSystem.DestroyMechanism(this._mechanism) this._mirabufInstance.Dispose(World.SceneRenderer.scene) @@ -279,7 +248,9 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { this._debugBodies?.clear() this._physicsLayerReserve?.Release() - if (this._brain && this._brain instanceof SynthesisBrain) this._brain?.clearControls() + if (this._brain && this._brain instanceof SynthesisBrain) { + this._brain.clearControls() + } } public Eject() { @@ -325,6 +296,70 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { return mesh } + /** + * Matches mesh transforms to their Jolt counterparts. + */ + public UpdateMeshTransforms() { + this._mirabufInstance.parser.rigidNodes.forEach(rn => { + if (!this._mirabufInstance.meshes.size) return // if this.dispose() has been ran then return + const body = World.PhysicsSystem.GetBody(this._mechanism.GetBodyByNodeId(rn.id)!) + const transform = JoltMat44_ThreeMatrix4(body.GetWorldTransform()) + this.UpdateNodeParts(rn, transform) + + 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()}` + ) + } + + 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) + } + }) + } + + public UpdateNodeParts(rn: RigidNodeReadOnly, transform: THREE.Matrix4) { + rn.parts.forEach(part => { + const partTransform = this._mirabufInstance.parser.globalTransforms + .get(part)! + .clone() + .premultiply(transform) + const meshes = this._mirabufInstance.meshes.get(part) ?? [] + meshes.forEach(([batch, id]) => batch.setMatrixAt(id, partTransform)) + }) + } + + /** Updates the batch computations */ + private UpdateBatches() { + this._mirabufInstance.batches.forEach(x => { + x.computeBoundingBox() + x.computeBoundingSphere() + }) + } + + /** Updates the position of the nametag relative to the robots position */ + private UpdateNameTag() { + if (this._nameTag && PreferencesSystem.getGlobalPreference("RenderSceneTags")) { + const boundingBox = this.ComputeBoundingBox() + this._nameTag.position = World.SceneRenderer.WorldToPixelSpace( + new THREE.Vector3( + (boundingBox.max.x + boundingBox.min.x) / 2, + boundingBox.max.y + 0.1, + (boundingBox.max.z + boundingBox.min.z) / 2 + ) + ) + } + } + public UpdateIntakeSensor() { if (this._intakeSensor) { World.SceneRenderer.RemoveSceneObject(this._intakeSensor.id) @@ -376,34 +411,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { } /** - * Changes the mode of the mirabuf object from being interacted with to being placed. - */ - public EnableTransformControls(): void { - if (this._transformGizmos) return - - this._transformGizmos = new TransformGizmos( - new THREE.Mesh( - new THREE.SphereGeometry(3.0), - new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0 }) - ) - ) - this._transformGizmos.AddMeshToScene() - this._transformGizmos.CreateGizmo("translate", 5.0) - - this.DisablePhysics() - } - - /** - * Changes the mode of the mirabuf object from being placed to being interacted with. - */ - public DisableTransformControls(): void { - if (!this._transformGizmos) return - this._transformGizmos?.RemoveGizmos() - this._transformGizmos = undefined - this.EnablePhysics() - } - - /** + * Calculates the bounding box of the mirabuf object. * * @returns The bounding box of the mirabuf object. */ @@ -416,6 +424,30 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { return box } + /** + * Once a gizmo is created and attached to this mirabuf object, this will be executed to align the gizmo correctly. + * + * @param gizmo Gizmo attached to the mirabuf object + */ + public PostGizmoCreation(gizmo: GizmoSceneObject) { + const jRootId = this.GetRootNodeId() + if (!jRootId) { + console.error("No root node found.") + return + } + + const jBody = World.PhysicsSystem.GetBody(jRootId) + if (jBody.IsStatic()) { + const aaBox = jBody.GetWorldSpaceBounds() + const mat = new THREE.Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) + const center = aaBox.mMin.Add(aaBox.mMax).Div(2.0) + mat.compose(JoltVec3_ThreeVector3(center), new THREE.Quaternion(0, 0, 0, 1), new THREE.Vector3(1, 1, 1)) + gizmo.SetTransform(mat) + } else { + gizmo.SetTransform(JoltMat44_ThreeMatrix4(jBody.GetCenterOfMassTransform())) + } + } + private getPreferences(): void { this._intakePreferences = PreferencesSystem.getRobotPreferences(this.assemblyName)?.intake this._ejectorPreferences = PreferencesSystem.getRobotPreferences(this.assemblyName)?.ejector @@ -423,13 +455,13 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { this._fieldPreferences = PreferencesSystem.getFieldPreferences(this.assemblyName) } - private EnablePhysics() { + public EnablePhysics() { this._mirabufInstance.parser.rigidNodes.forEach(rn => { World.PhysicsSystem.EnablePhysicsForBody(this._mechanism.GetBodyByNodeId(rn.id)!) }) } - private DisablePhysics() { + public DisablePhysics() { this._mirabufInstance.parser.rigidNodes.forEach(rn => { World.PhysicsSystem.DisablePhysicsForBody(this._mechanism.GetBodyByNodeId(rn.id)!) }) @@ -452,7 +484,10 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { data.items.push({ name: "Move", func: () => { - this.EnableTransformControls() + setNextConfigurePanelSettings({ + configMode: ConfigMode.MOVE, + selectedAssembly: this + }) }, }) @@ -462,8 +497,8 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { data.items.push({ name: "Set Scheme", func: () => { - setSelectedBrainIndexGlobal(brainIndex) - MainHUD_OpenPanel("choose-scheme") + setSpotlightAssembly(this) + Global_OpenPanel?.("choose-scheme") }, }) } diff --git a/fission/src/mirabuf/ScoringZoneSceneObject.ts b/fission/src/mirabuf/ScoringZoneSceneObject.ts index 469655aa12..d6984bac73 100644 --- a/fission/src/mirabuf/ScoringZoneSceneObject.ts +++ b/fission/src/mirabuf/ScoringZoneSceneObject.ts @@ -58,8 +58,6 @@ class ScoringZoneSceneObject extends SceneObject { public constructor(parentAssembly: MirabufSceneObject, index: number, render?: boolean) { super() - console.debug("Trying to create scoring zone...") - this._parentAssembly = parentAssembly this._prefs = this._parentAssembly.fieldPreferences?.scoringZones[index] this._toRender = render ?? PreferencesSystem.getGlobalPreference("RenderScoringZones") @@ -138,8 +136,6 @@ class ScoringZoneSceneObject extends SceneObject { } OnContactRemovedEvent.AddListener(this._collisionRemoved) } - - console.debug("Scoring zone created successfully") } } } @@ -195,8 +191,6 @@ class ScoringZoneSceneObject extends SceneObject { } public Dispose(): void { - console.debug("Destroying scoring zone") - if (this._joltBodyId) { World.PhysicsSystem.DestroyBodyIds(this._joltBodyId) if (this._mesh) { diff --git a/fission/src/systems/analytics/AnalyticsSystem.ts b/fission/src/systems/analytics/AnalyticsSystem.ts index 4eb0b9fa27..a7dc4da255 100644 --- a/fission/src/systems/analytics/AnalyticsSystem.ts +++ b/fission/src/systems/analytics/AnalyticsSystem.ts @@ -78,8 +78,6 @@ class AnalyticsSystem extends WorldSystem { betaCode = betaCode.substring(betaCode.indexOf("=") + 1, betaCode.indexOf(";")) this.SetUserProperty("Beta Code", betaCode) - } else { - console.debug("No code match") } if (MOBILE_USER_AGENT_REGEX.test(navigator.userAgent)) { diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index eaeb5f3dcc..5635ac6721 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -27,6 +27,10 @@ import PreferencesSystem from "../preferences/PreferencesSystem" export type JoltBodyIndexAndSequence = number +export const PAUSE_REF_ASSEMBLY_SPAWNING = "assembly-spawning" +export const PAUSE_REF_ASSEMBLY_CONFIG = "assembly-config" +export const PAUSE_REF_ASSEMBLY_MOVE = "assembly-move" + /** * Layers used for determining enabled/disabled collisions. */ @@ -79,12 +83,12 @@ class PhysicsSystem extends WorldSystem { private _physicsEventQueue: PhysicsEvent[] = [] - private _pauseCounter = 0 + private _pauseSet = new Set() private _bodyAssociations: Map public get isPaused(): boolean { - return this._pauseCounter > 0 + return this._pauseSet.size > 0 } /** @@ -111,7 +115,7 @@ class PhysicsSystem extends WorldSystem { const ground = this.CreateBox( new THREE.Vector3(5.0, 0.5, 5.0), undefined, - new THREE.Vector3(0.0, -2.0, 0.0), + new THREE.Vector3(0.0, -0.5, 0.0), undefined ) ground.SetFriction(FLOOR_FRICTION) @@ -146,28 +150,28 @@ class PhysicsSystem extends WorldSystem { /** * Holds a pause. * - * The pause works off of a request counter. + * @param ref String to reference your hold. */ - public HoldPause() { - this._pauseCounter++ + public HoldPause(ref: string) { + this._pauseSet.add(ref) } /** * Forces all holds on the pause to be released. */ public ForceUnpause() { - this._pauseCounter = 0 + this._pauseSet.clear() } /** * Releases a pause. * - * The pause works off of a request counter. + * @param ref String to reference your hold. + * + * @returns Whether or not your hold was successfully removed. */ - public ReleasePause() { - if (this._pauseCounter > 0) { - this._pauseCounter-- - } + public ReleasePause(ref: string): boolean { + return this._pauseSet.delete(ref) } /** @@ -965,7 +969,7 @@ class PhysicsSystem extends WorldSystem { } public Update(deltaT: number): void { - if (this._pauseCounter > 0) { + if (this._pauseSet.size > 0) { return } @@ -1052,7 +1056,7 @@ class PhysicsSystem extends WorldSystem { * @param id The id of the body * @param position The new position of the body */ - public SetBodyPosition(id: Jolt.BodyID, position: Jolt.Vec3, activate: boolean = true): void { + public SetBodyPosition(id: Jolt.BodyID, position: Jolt.RVec3, activate: boolean = true): void { if (!this.IsBodyAdded(id)) { return } @@ -1076,6 +1080,24 @@ class PhysicsSystem extends WorldSystem { ) } + public SetBodyPositionAndRotation( + id: Jolt.BodyID, + position: Jolt.RVec3, + rotation: Jolt.Quat, + activate: boolean = true + ): void { + if (!this.IsBodyAdded(id)) { + return + } + + this._joltBodyInterface.SetPositionAndRotation( + id, + position, + rotation, + activate ? JOLT.EActivation_Activate : JOLT.EActivation_DontActivate + ) + } + /** * Exposes SetShape method on the _joltBodyInterface * Sets the shape of the body diff --git a/fission/src/systems/scene/GizmoSceneObject.ts b/fission/src/systems/scene/GizmoSceneObject.ts new file mode 100644 index 0000000000..81137b2452 --- /dev/null +++ b/fission/src/systems/scene/GizmoSceneObject.ts @@ -0,0 +1,254 @@ +import * as THREE from "three" +import SceneObject from "./SceneObject" +import { TransformControls } from "three/examples/jsm/controls/TransformControls.js" +import InputSystem from "../input/InputSystem" +import World from "../World" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import { Object3D, PerspectiveCamera } from "three" +import { ThreeQuaternion_JoltQuat, JoltMat44_ThreeMatrix4, ThreeVector3_JoltVec3 } from "@/util/TypeConversions" +import { RigidNodeId } from "@/mirabuf/MirabufParser" + +export type GizmoMode = "translate" | "rotate" | "scale" + +class GizmoSceneObject extends SceneObject { + private _gizmo: TransformControls + private _obj: Object3D + private _forceUpdate: boolean = false + + private _parentObject?: MirabufSceneObject + private _relativeTransformations?: Map + + private _mainCamera: PerspectiveCamera + + private _size: number + + /** @returns the instance of the transform gizmo itself */ + public get gizmo() { + return this._gizmo + } + + /** @returns Object3D that is attached to transform gizmo */ + public get obj() { + return this._obj + } + + /** @returns true if gizmo is currently being dragged */ + public get isDragging() { + return this._gizmo.dragging + } + + /** @returns the id of the parent scene object */ + public get parentObjectId() { + return this._parentObject?.id + } + + public constructor( + mode: GizmoMode, + size: number, + obj?: THREE.Mesh, + parentObject?: MirabufSceneObject, + postGizmoCreation?: (gizmo: GizmoSceneObject) => void + ) { + super() + + this._obj = obj ?? new THREE.Mesh() + this._parentObject = parentObject + this._mainCamera = World.SceneRenderer.mainCamera + + this._size = size + + this._gizmo = new TransformControls(World.SceneRenderer.mainCamera, World.SceneRenderer.renderer.domElement) + this._gizmo.setMode(mode) + + World.SceneRenderer.RegisterGizmoSceneObject(this) + + postGizmoCreation?.(this) + + if (this._parentObject) { + this._relativeTransformations = new Map() + const gizmoTransformInv = this._obj.matrix.clone().invert() + + /** Due to the limited math functionality exposed to JS for Jolt, we need everything in ThreeJS. */ + this._parentObject.mirabufInstance.parser.rigidNodes.forEach(rn => { + const jBodyId = this._parentObject!.mechanism.GetBodyByNodeId(rn.id) + if (!jBodyId) return + + const worldTransform = JoltMat44_ThreeMatrix4(World.PhysicsSystem.GetBody(jBodyId).GetWorldTransform()) + const relativeTransform = worldTransform.premultiply(gizmoTransformInv) + this._relativeTransformations!.set(rn.id, relativeTransform) + }) + } + } + + public Setup(): void { + // adding the mesh and gizmo to the scene + World.SceneRenderer.AddObject(this._obj) + World.SceneRenderer.AddObject(this._gizmo) + + // forcing the gizmo to rotate and transform with the object + this._gizmo.setSpace("local") + this._gizmo.attach(this._obj) + + this._gizmo.addEventListener("dragging-changed", (event: { target: TransformControls; value: unknown }) => { + // disable orbit controls when dragging the transform gizmo + const gizmoDragging = World.SceneRenderer.IsAnyGizmoDragging() + World.SceneRenderer.orbitControls.enabled = !event.value && !gizmoDragging + + const isShift = InputSystem.isKeyPressed("ShiftRight") || InputSystem.isKeyPressed("ShiftLeft") + const isAlt = InputSystem.isKeyPressed("AltRight") || InputSystem.isKeyPressed("AltLeft") + + switch (event.target.mode) { + case "translate": { + // snap if alt is pressed + event.target.translationSnap = isAlt ? 0.1 : null + + // disable other gizmos when translating + const gizmos = [...World.SceneRenderer.gizmosOnMirabuf.values()] + gizmos.forEach(obj => { + if (obj.gizmo.object === event.target.object && obj.gizmo.mode !== "translate") { + obj.gizmo.dragging = false + obj.gizmo.enabled = !event.value + return + } + }) + break + } + case "rotate": { + // snap if alt is pressed + event.target.rotationSnap = isAlt ? Math.PI * (1.0 / 12.0) : null + + // disable scale gizmos added to the same object + const gizmos = [...World.SceneRenderer.gizmosOnMirabuf.values()] + gizmos.forEach(obj => { + if ( + obj.gizmo.mode === "scale" && + event.target !== obj.gizmo && + obj.gizmo.object === event.target.object + ) { + obj.gizmo.dragging = false + obj.gizmo.enabled = !event.value + return + } + }) + break + } + case "scale": { + // snap if alt is pressed + event.target.setScaleSnap(isAlt ? 0.1 : null) + + // scale uniformly if shift is pressed + event.target.axis = isShift ? "XYZE" : null + break + } + default: { + console.error("Invalid gizmo state") + break + } + } + }) + } + + public Update(): void { + this._gizmo.updateMatrixWorld() + + if (!this.gizmo.object) { + console.error("No object added to gizmo") + return + } + + // updating the size of the gizmo based on the distance from the camera + const mainCameraFovRadians = (Math.PI * (this._mainCamera.fov * 0.5)) / 180 + this._gizmo.setSize( + (this._size / this._mainCamera.position.distanceTo(this.gizmo.object!.position)) * + Math.tan(mainCameraFovRadians) * + 1.9 + ) + + /** Translating the obj changes to the mirabuf scene object */ + if (this._parentObject) { + this._parentObject.DisablePhysics() + if (this.isDragging || this._forceUpdate) { + this._forceUpdate = false + this._parentObject.mirabufInstance.parser.rigidNodes.forEach(rn => { + this.UpdateNodeTransform(rn.id) + }) + this._parentObject.UpdateMeshTransforms() + } + } + } + + public Dispose(): void { + this._gizmo.detach() + this._parentObject?.EnablePhysics() + World.SceneRenderer.RemoveObject(this._obj) + World.SceneRenderer.RemoveObject(this._gizmo) + + this._relativeTransformations?.clear() + } + + /** changes the mode of the gizmo */ + public SetMode(mode: GizmoMode) { + this._gizmo.setMode(mode) + } + + /** + * Updates a given node to follow the gizmo. + * + * @param rnId Target node to update. + */ + public UpdateNodeTransform(rnId: RigidNodeId) { + if (!this._parentObject || !this._relativeTransformations || !this._relativeTransformations.has(rnId)) return + + const jBodyId = this._parentObject.mechanism.GetBodyByNodeId(rnId) + if (!jBodyId) return + + const relativeTransform = this._relativeTransformations.get(rnId)! + const worldTransform = relativeTransform.clone().premultiply(this._obj.matrix) + const position = new THREE.Vector3(0, 0, 0) + const rotation = new THREE.Quaternion(0, 0, 0, 1) + worldTransform.decompose(position, rotation, new THREE.Vector3(1, 1, 1)) + + World.PhysicsSystem.SetBodyPositionAndRotation( + jBodyId, + ThreeVector3_JoltVec3(position), + ThreeQuaternion_JoltQuat(rotation) + ) + } + + /** + * Updates the gizmos location. + * + * @param gizmoTransformation Transform for the gizmo to take on. + */ + public SetTransform(gizmoTransformation: THREE.Matrix4) { + // Super hacky, prolly has something to do with how the transform controls update the attached object. + const position = new THREE.Vector3(0, 0, 0) + const rotation = new THREE.Quaternion(0, 0, 0, 1) + const scale = new THREE.Vector3(1, 1, 1) + gizmoTransformation.decompose(position, rotation, scale) + this._obj.matrix.compose(position, rotation, scale) + + this._obj.position.setFromMatrixPosition(gizmoTransformation) + this._obj.rotation.setFromRotationMatrix(gizmoTransformation) + + this._forceUpdate = true + } + + public SetRotation(rotation: THREE.Quaternion) { + const position = new THREE.Vector3(0, 0, 0) + const scale = new THREE.Vector3(1, 1, 1) + this._obj.matrix.decompose(position, new THREE.Quaternion(0, 0, 0, 1), scale) + this._obj.matrix.compose(position, rotation, scale) + + this._obj.rotation.setFromQuaternion(rotation) + + this._forceUpdate = true + } + + /** @return true if gizmo is attached to mirabufSceneObject */ + public HasParent(): boolean { + return this._parentObject !== undefined + } +} + +export default GizmoSceneObject diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts index 56c023c8dc..745545ce81 100644 --- a/fission/src/systems/scene/SceneRenderer.ts +++ b/fission/src/systems/scene/SceneRenderer.ts @@ -1,15 +1,12 @@ import * as THREE from "three" -import SceneObject from "@/systems/scene/SceneObject" -import WorldSystem from "@/systems/WorldSystem" - -import { TransformControls } from "three/examples/jsm/controls/TransformControls.js" +import WorldSystem from "../WorldSystem" +import SceneObject from "./SceneObject" +import GizmoSceneObject from "./GizmoSceneObject" import { EdgeDetectionMode, EffectComposer, EffectPass, RenderPass, SMAAEffect } from "postprocessing" - -import vertexShader from "@/shaders/vertex.glsl" import fragmentShader from "@/shaders/fragment.glsl" +import vertexShader from "@/shaders/vertex.glsl" import { Theme } from "@/ui/ThemeContext" import Jolt from "@barclah/jolt-physics" -import InputSystem from "@/systems/input/InputSystem" import { CameraControls, CameraControlsType, CustomOrbitControls } from "@/systems/scene/CameraControls" import ScreenInteractionHandler, { InteractionEnd } from "./ScreenInteractionHandler" @@ -20,7 +17,8 @@ import World from "../World" import { ThreeVector3_JoltVec3 } from "@/util/TypeConversions" import { RigidNodeAssociate } from "@/mirabuf/MirabufSceneObject" import { ContextData, ContextSupplierEvent } from "@/ui/components/ContextMenuData" -import { MainHUD_OpenPanel } from "@/ui/components/MainHUD" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import { Global_OpenPanel } from "@/ui/components/GlobalUIControls" const CLEAR_COLOR = 0x121212 const GROUND_COLOR = 0x4066c7 @@ -41,9 +39,9 @@ class SceneRenderer extends WorldSystem { private _antiAliasPass: EffectPass private _sceneObjects: Map + private _gizmosOnMirabuf: Map // maps of all the gizmos that are attached to a mirabuf scene object private _cameraControls: CameraControls - private _transformControls: Map // maps all rendered transform controls to their size private _light: THREE.DirectionalLight | CSM | undefined private _screenInteractionHandler: ScreenInteractionHandler @@ -68,11 +66,18 @@ class SceneRenderer extends WorldSystem { return this._cameraControls } + /** + * Collection that maps Mirabuf objects to active GizmoSceneObjects + */ + public get gizmosOnMirabuf() { + return this._gizmosOnMirabuf + } + public constructor() { super() this._sceneObjects = new Map() - this._transformControls = new Map() + this._gizmosOnMirabuf = new Map() const aspect = window.innerWidth / window.innerHeight this._mainCamera = new THREE.PerspectiveCamera(STANDARD_CAMERA_FOV_Y, aspect, 0.1, 1000) @@ -100,7 +105,7 @@ class SceneRenderer extends WorldSystem { this._scene.add(ambientLight) const ground = new THREE.Mesh(new THREE.BoxGeometry(10, 1, 10), this.CreateToonMaterial(GROUND_COLOR)) - ground.position.set(0.0, -2.0, 0.0) + ground.position.set(0.0, -0.5, 0.0) ground.receiveShadow = true ground.castShadow = true this._scene.add(ground) @@ -176,15 +181,6 @@ class SceneRenderer extends WorldSystem { this._skybox.position.copy(this._mainCamera.position) - const mainCameraFovRadians = (Math.PI * (this._mainCamera.fov * 0.5)) / 180 - this._transformControls.forEach((size, tc) => { - tc.setSize( - (size / this._mainCamera.position.distanceTo(tc.object!.position)) * - Math.tan(mainCameraFovRadians) * - 1.9 - ) - }) - // Update the tags each frame if they are enabled in preferences if (PreferencesSystem.getGlobalPreference("RenderSceneTags")) new SceneOverlayEvent(SceneOverlayEventKey.UPDATE) @@ -278,13 +274,29 @@ class SceneRenderer extends WorldSystem { return id } + /** Registers gizmos that are attached to a parent mirabufsceneobject */ + public RegisterGizmoSceneObject(obj: GizmoSceneObject): number { + if (obj.HasParent()) this._gizmosOnMirabuf.set(obj.parentObjectId!, obj) + return this.RegisterSceneObject(obj) + } + public RemoveAllSceneObjects() { this._sceneObjects.forEach(obj => obj.Dispose()) + this._gizmosOnMirabuf.clear() this._sceneObjects.clear() } public RemoveSceneObject(id: number) { const obj = this._sceneObjects.get(id) + + // If the object is a mirabuf object, remove the gizmo as well + if (obj instanceof MirabufSceneObject) { + const objGizmo = this._gizmosOnMirabuf.get(id) + if (this._gizmosOnMirabuf.delete(id)) objGizmo!.Dispose() + } else if (obj instanceof GizmoSceneObject && obj.HasParent()) { + this._gizmosOnMirabuf.delete(obj.parentObjectId!) + } + if (this._sceneObjects.delete(id)) { obj!.Dispose() } @@ -370,83 +382,9 @@ class SceneRenderer extends WorldSystem { } } - /** - * Attach new transform gizmo to Mesh - * - * @param obj Mesh to attach gizmo to - * @param mode Transform mode (translate, rotate, scale) - * @param size Size of the gizmo - * @returns void - */ - public AddTransformGizmo(obj: THREE.Object3D, mode: "translate" | "rotate" | "scale" = "translate", size: number) { - const transformControl = new TransformControls(this._mainCamera, this._renderer.domElement) - transformControl.setMode(mode) - transformControl.attach(obj) - - // allowing the transform gizmos to rotate with the object - transformControl.space = "local" - - transformControl.addEventListener( - "dragging-changed", - (event: { target: TransformControls; value: unknown }) => { - const isAnyGizmoDragging = Array.from(this._transformControls.keys()).some(gizmo => gizmo.dragging) - if (!event.value && !isAnyGizmoDragging) { - this._cameraControls.enabled = true // enable orbit controls when not dragging another transform gizmo - } else if (!event.value && isAnyGizmoDragging) { - this._cameraControls.enabled = false // disable orbit controls when dragging another transform gizmo - } else { - this._cameraControls.enabled = !event.value // disable orbit controls when dragging transform gizmo - } - - if (event.target.mode === "translate") { - this._transformControls.forEach((_size, tc) => { - // disable other transform gizmos when translating - if (tc.object === event.target.object && tc.mode !== "translate") { - tc.dragging = false - tc.enabled = !event.value - return - } - }) - } else if ( - event.target.mode === "scale" && - (InputSystem.isKeyPressed("ShiftRight") || InputSystem.isKeyPressed("ShiftLeft")) - ) { - // scale uniformly if shift is pressed - transformControl.axis = "XYZE" - } else if (event.target.mode === "rotate") { - // scale on all axes - this._transformControls.forEach((_size, tc) => { - // disable scale transform gizmo when scaling - if (tc.mode === "scale" && tc !== event.target && tc.object === event.target.object) { - tc.dragging = false - tc.enabled = !event.value - return - } - }) - } - } - ) - - this._transformControls.set(transformControl, size) - this._scene.add(transformControl) - - return transformControl - } - - /** - * Remove transform gizmos from Mesh - * - * @param obj Mesh to remove gizmo from - * @returns void - */ - public RemoveTransformGizmos(obj: THREE.Object3D) { - this._transformControls.forEach((_, tc) => { - if (tc.object === obj) { - tc.detach() - this._scene.remove(tc) - this._transformControls.delete(tc) - } - }) + /** returns whether any gizmos are being currently dragged */ + public IsAnyGizmoDragging(): boolean { + return [...this._gizmosOnMirabuf.values()].some(obj => obj.gizmo.dragging) } /** @@ -505,7 +443,7 @@ class SceneRenderer extends WorldSystem { miraSupplierData.items.push({ name: "Add", func: () => { - MainHUD_OpenPanel("import-mirabuf") + Global_OpenPanel?.("import-mirabuf") }, }) } diff --git a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts index a58c1bdb3a..c98225cf58 100644 --- a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts +++ b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts @@ -17,6 +17,7 @@ import GenericElevatorBehavior from "../behavior/synthesis/GenericElevatorBehavi import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import { DefaultSequentialConfig } from "@/systems/preferences/PreferenceTypes" import InputSystem from "@/systems/input/InputSystem" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" class SynthesisBrain extends Brain { public static brainIndexMap = new Map() @@ -63,6 +64,7 @@ class SynthesisBrain extends Brain { this._simLayer = World.SimulationSystem.GetSimulationLayer(mechanism)! this._assemblyName = assemblyName + // I'm not fixing this right now, but this is going to become an issue... this._brainIndex = SynthesisBrain.brainIndexMap.size SynthesisBrain.brainIndexMap.set(this._brainIndex, this) @@ -220,6 +222,10 @@ class SynthesisBrain extends Brain { /** Put any field configuration here */ } + + public static GetBrainIndex(assembly: MirabufSceneObject | undefined): number | undefined { + return (assembly?.brain as SynthesisBrain)?.brainIndex + } } export default SynthesisBrain diff --git a/fission/src/systems/simulation/wpilib_brain/SimInput.ts b/fission/src/systems/simulation/wpilib_brain/SimInput.ts index 973f6b9c70..b4776f03a3 100644 --- a/fission/src/systems/simulation/wpilib_brain/SimInput.ts +++ b/fission/src/systems/simulation/wpilib_brain/SimInput.ts @@ -1,20 +1,27 @@ import World from "@/systems/World" import EncoderStimulus from "../stimulus/EncoderStimulus" -import { SimCANEncoder, SimGyro } from "./WPILibBrain" +import { SimCANEncoder, SimGyro, SimAccel, SimDIO, SimAI } from "./WPILibBrain" import Mechanism from "@/systems/physics/Mechanism" import Jolt from "@barclah/jolt-physics" import JOLT from "@/util/loading/JoltSyncLoader" +import { JoltQuat_ThreeQuaternion, JoltVec3_ThreeVector3 } from "@/util/TypeConversions" +import * as THREE from "three" -export interface SimInput { - Update: (deltaT: number) => void +export abstract class SimInput { + constructor(protected _device: string) {} + + public abstract Update(deltaT: number): void + + public get device(): string { + return this._device + } } -export class SimEncoderInput implements SimInput { - private _device: string +export class SimEncoderInput extends SimInput { private _stimulus: EncoderStimulus constructor(device: string, stimulus: EncoderStimulus) { - this._device = device + super(device) this._stimulus = stimulus } @@ -24,8 +31,7 @@ export class SimEncoderInput implements SimInput { } } -export class SimGyroInput implements SimInput { - private _device: string +export class SimGyroInput extends SimInput { private _robot: Mechanism private _joltID?: Jolt.BodyID private _joltBody?: Jolt.Body @@ -35,7 +41,7 @@ export class SimGyroInput implements SimInput { private static AXIS_Z: Jolt.Vec3 = new JOLT.Vec3(0, 0, 1) constructor(device: string, robot: Mechanism) { - this._device = device + super(device) this._robot = robot this._joltID = this._robot.nodeToBody.get(this._robot.rootBody) @@ -58,12 +64,102 @@ export class SimGyroInput implements SimInput { return this.GetAxis(SimGyroInput.AXIS_Z) } + private GetAxisVelocity(axis: "x" | "y" | "z"): number { + const axes = this._joltBody?.GetAngularVelocity() + if (!axes) return 0 + + switch (axis) { + case "x": + return axes.GetX() + case "y": + return axes.GetY() + case "z": + return axes.GetZ() + } + } + public Update(_deltaT: number) { const x = this.GetX() const y = this.GetY() const z = this.GetZ() + SimGyro.SetAngleX(this._device, x) SimGyro.SetAngleY(this._device, y) SimGyro.SetAngleZ(this._device, z) + SimGyro.SetRateX(this._device, this.GetAxisVelocity("x")) + SimGyro.SetRateY(this._device, this.GetAxisVelocity("y")) + SimGyro.SetRateZ(this._device, this.GetAxisVelocity("z")) + } +} + +export class SimAccelInput extends SimInput { + private _robot: Mechanism + private _joltID?: Jolt.BodyID + private _prevVel: THREE.Vector3 + + constructor(device: string, robot: Mechanism) { + super(device) + this._robot = robot + this._joltID = this._robot.nodeToBody.get(this._robot.rootBody) + this._prevVel = new THREE.Vector3(0, 0, 0) + } + + public Update(deltaT: number) { + if (!this._joltID) return + const body = World.PhysicsSystem.GetBody(this._joltID) + + const rot = JoltQuat_ThreeQuaternion(body.GetRotation()) + const mat = new THREE.Matrix4().makeRotationFromQuaternion(rot).transpose() + const newVel = JoltVec3_ThreeVector3(body.GetLinearVelocity()).applyMatrix4(mat) + + const x = (newVel.x - this._prevVel.x) / deltaT + const y = (newVel.y - this._prevVel.y) / deltaT + const z = (newVel.y - this._prevVel.y) / deltaT + + SimAccel.SetX(this._device, x) + SimAccel.SetY(this._device, y) + SimAccel.SetZ(this._device, z) + + this._prevVel = newVel + } +} + +export class SimDigitalInput extends SimInput { + private _valueSupplier: () => boolean + + /** + * Creates a Simulation Digital Input object. + * + * @param device Device ID + * @param valueSupplier Called each frame and returns what the value should be set to + */ + constructor(device: string, valueSupplier: () => boolean) { + super(device) + this._valueSupplier = valueSupplier + } + + private SetValue(value: boolean) { + SimDIO.SetValue(this._device, value) + } + + public GetValue(): boolean { + return SimDIO.GetValue(this._device) + } + + public Update(_deltaT: number) { + if (this._valueSupplier) this.SetValue(this._valueSupplier()) + } +} + +export class SimAnalogInput extends SimInput { + private _valueSupplier: () => number + + constructor(device: string, valueSupplier: () => number) { + super(device) + this._valueSupplier = valueSupplier + } + + public Update(_deltaT: number) { + SimAI.SetValue(this._device, this._valueSupplier()) } } diff --git a/fission/src/systems/simulation/wpilib_brain/SimOutput.ts b/fission/src/systems/simulation/wpilib_brain/SimOutput.ts index 40fe5494e9..04e9205c7c 100644 --- a/fission/src/systems/simulation/wpilib_brain/SimOutput.ts +++ b/fission/src/systems/simulation/wpilib_brain/SimOutput.ts @@ -2,22 +2,25 @@ import Driver from "../driver/Driver" import HingeDriver from "../driver/HingeDriver" import SliderDriver from "../driver/SliderDriver" import WheelDriver from "../driver/WheelDriver" -import { SimCAN, SimPWM, SimType } from "./WPILibBrain" - -// TODO: Averaging is probably not the right solution (if we want large output groups) -// We can keep averaging, but we need a better ui for creating one to one (or just small) output groups -// The issue is that if a drivetrain is one output group, then each driver is given the average of all the motors -// We instead want a system where every driver gets (a) unique motor(s) that control it -// That way a single driver might get the average of two motors or something, if it has two motors to control it -// A system where motors a drivers are visually "linked" with "threads" in the UI would work well in my opinion -export abstract class SimOutputGroup { - public name: string +import { SimAO, SimCAN, SimDIO, SimPWM, SimType } from "./WPILibBrain" + +export abstract class SimOutput { + constructor(protected _name: string) {} + + public abstract Update(deltaT: number): void + + public get name(): string { + return this._name + } +} + +export abstract class SimOutputGroup extends SimOutput { public ports: number[] public drivers: Driver[] public type: SimType public constructor(name: string, ports: number[], drivers: Driver[], type: SimType) { - this.name = name + super(name) this.ports = ports this.drivers = drivers this.type = type @@ -35,7 +38,6 @@ export class PWMOutputGroup extends SimOutputGroup { const average = this.ports.reduce((sum, port) => { const speed = SimPWM.GetSpeed(`${port}`) ?? 0 - console.debug(port, speed) return sum + speed }, 0) / this.ports.length @@ -59,7 +61,7 @@ export class CANOutputGroup extends SimOutputGroup { const average = this.ports.reduce((sum, port) => { const device = SimCAN.GetDeviceWithID(port, SimType.CANMotor) - return sum + (device?.get(" { @@ -72,3 +74,36 @@ export class CANOutputGroup extends SimOutputGroup { }) } } + +export class SimDigitalOutput extends SimOutput { + /** + * Creates a Simulation Digital Input/Output object. + * + * @param device Device ID + */ + constructor(name: string) { + super(name) + } + + public SetValue(value: boolean) { + SimDIO.SetValue(this._name, value) + } + + public GetValue(): boolean { + return SimDIO.GetValue(this._name) + } + + public Update(_deltaT: number) {} +} + +export class SimAnalogOutput extends SimOutput { + public constructor(name: string) { + super(name) + } + + public GetVoltage(): number { + return SimAO.GetVoltage(this._name) + } + + public Update(_deltaT: number) {} +} diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index a00034d1a4..7fa35eddaf 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -1,14 +1,17 @@ import Mechanism from "@/systems/physics/Mechanism" import Brain from "../Brain" +import Lazy from "@/util/Lazy.ts" import WPILibWSWorker from "./WPILibWSWorker?worker" import { SimulationLayer } from "../SimulationSystem" import World from "@/systems/World" -import { SimOutputGroup } from "./SimOutput" -import { SimInput } from "./SimInput" +import { SimAnalogOutput, SimDigitalOutput, SimOutput } from "./SimOutput" +import { SimAccelInput, SimAnalogInput, SimDigitalInput, SimGyroInput, SimInput } from "./SimInput" +import { Random } from "@/util/Random" + +const worker: Lazy = new Lazy(() => new WPILibWSWorker()) -const worker = new WPILibWSWorker() const PWM_SPEED = " +type DeviceData = Map export const simMap = new Map>() export class SimGeneric { private constructor() {} + public static Get(simType: SimType, device: string, field: string): T | undefined + public static Get(simType: SimType, device: string, field: string, defaultValue: T): T public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { const fieldType = GetFieldType(field) if (fieldType != FieldType.Read && fieldType != FieldType.Both) { @@ -83,7 +93,7 @@ export class SimGeneric { return (data.get(field) as T | undefined) ?? defaultValue } - public static Set(simType: SimType, device: string, field: string, value: T): boolean { + public static Set(simType: SimType, device: string, field: string, value: T): boolean { const fieldType = GetFieldType(field) if (fieldType != FieldType.Write && fieldType != FieldType.Both) { console.warn(`Field '${field}' is not a write or both field type`) @@ -102,11 +112,11 @@ export class SimGeneric { return false } - const selectedData: { [key: string]: number } = {} + const selectedData: { [key: string]: number | boolean } = {} selectedData[field] = value data.set(field, value) - worker.postMessage({ + worker.getValue().postMessage({ command: "update", data: { type: simType, @@ -136,7 +146,7 @@ export class SimCAN { private constructor() {} public static GetDeviceWithID(id: number, type: SimType): DeviceData | undefined { - const id_exp = /.*\[(\d+)\]/g + const id_exp = /SYN.*\[(\d+)\]/g const entries = [...simMap.entries()].filter(([simType, _data]) => simType == type) for (const [_simType, data] of entries) { for (const key of data.keys()) { @@ -204,6 +214,111 @@ export class SimGyro { public static SetAngleZ(device: string, angle: number): boolean { return SimGeneric.Set(SimType.Gyro, device, ">angle_z", angle) } + + public static SetRateX(device: string, rate: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">rate_x", rate) + } + + public static SetRateY(device: string, rate: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">rate_y", rate) + } + + public static SetRateZ(device: string, rate: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">rate_z", rate) + } +} + +export class SimAccel { + private constructor() {} + + public static SetX(device: string, accel: number): boolean { + return SimGeneric.Set(SimType.Accel, device, ">x", accel) + } + + public static SetY(device: string, accel: number): boolean { + return SimGeneric.Set(SimType.Accel, device, ">y", accel) + } + + public static SetZ(device: string, accel: number): boolean { + return SimGeneric.Set(SimType.Accel, device, ">z", accel) + } +} + +export class SimDIO { + private constructor() {} + + public static SetValue(device: string, value: boolean): boolean { + return SimGeneric.Set(SimType.DIO, device, "<>value", value) + } + + public static GetValue(device: string): boolean { + return SimGeneric.Get(SimType.DIO, device, "<>value", false) + } +} + +export class SimAI { + constructor() {} + + public static SetValue(device: string, value: number): boolean { + return SimGeneric.Set(SimType.AI, device, ">voltage", value) + } + + /** + * The number of averaging bits + */ + public static GetAvgBits(device: string) { + return SimGeneric.Get(SimType.AI, device, "voltage", voltage) + } + /** + * If the accumulator is initialized in the robot program + */ + public static GetAccumInit(device: string) { + return SimGeneric.Get(SimType.AI, device, "accum_value", accum_value) + } + /** + * The number of accumulated values + */ + public static SetAccumCount(device: string, accum_count: number) { + return SimGeneric.Set(SimType.AI, device, ">accum_count", accum_count) + } + /** + * The center value of the accumulator + */ + public static GetAccumCenter(device: string) { + return SimGeneric.Get(SimType.AI, device, "voltage", 0.0) + } } type WSMessage = { @@ -212,7 +327,7 @@ type WSMessage = { data: Map } -worker.addEventListener("message", (eventData: MessageEvent) => { +worker.getValue().addEventListener("message", (eventData: MessageEvent) => { let data: WSMessage | undefined if (typeof eventData.data == "object") { @@ -226,8 +341,7 @@ worker.addEventListener("message", (eventData: MessageEvent) => { } } - if (!data?.type || !(Object.values(SimType) as string[]).includes(data.type) || data.device.split(" ")[0] != "SYN") - return + if (!data?.type || !(Object.values(SimType) as string[]).includes(data.type)) return UpdateSimMap(data.type as SimType, data.device, data.data) }) @@ -253,7 +367,7 @@ function UpdateSimMap(type: SimType, device: string, updateData: DeviceData) { class WPILibBrain extends Brain { private _simLayer: SimulationLayer - private _simOutputs: SimOutputGroup[] = [] + private _simOutputs: SimOutput[] = [] private _simInputs: SimInput[] = [] constructor(mechanism: Mechanism) { @@ -265,9 +379,16 @@ class WPILibBrain extends Brain { console.warn("SimulationLayer is undefined") return } + + this.addSimInput(new SimGyroInput("Test Gyro[1]", mechanism)) + this.addSimInput(new SimAccelInput("ADXL362[4]", mechanism)) + this.addSimInput(new SimDigitalInput("SYN DI[0]", () => Random() > 0.5)) + this.addSimOutput(new SimDigitalOutput("SYN DO[1]")) + this.addSimInput(new SimAnalogInput("SYN AI[0]", () => Random() * 12)) + this.addSimOutput(new SimAnalogOutput("SYN AO[1]")) } - public addSimOutputGroup(device: SimOutputGroup) { + public addSimOutput(device: SimOutput) { this._simOutputs.push(device) } @@ -281,11 +402,11 @@ class WPILibBrain extends Brain { } public Enable(): void { - worker.postMessage({ command: "connect" }) + worker.getValue().postMessage({ command: "connect" }) } public Disable(): void { - worker.postMessage({ command: "disconnect" }) + worker.getValue().postMessage({ command: "disconnect" }) } } diff --git a/fission/src/ui/components/GlobalUIComponent.tsx b/fission/src/ui/components/GlobalUIComponent.tsx new file mode 100644 index 0000000000..6f38fb8723 --- /dev/null +++ b/fission/src/ui/components/GlobalUIComponent.tsx @@ -0,0 +1,48 @@ +import { useModalControlContext } from "@/ui/ModalContext" +import { usePanelControlContext } from "@/ui/PanelContext" +import { useToastContext } from "@/ui/ToastContext" +import { useEffect } from "react" +import { setAddToast, setOpenModal, setOpenPanel } from "./GlobalUIControls" + +/** + * So this is Hunter's kinda cursed approach to de-react-ifying some of our UI controls. + * Essentially, this component will expose context controls for our UI, which allows + * non-UI components (such as APSDataManagement) to use UI controls (such as addToast). + * + * Stored in a component to ensure a lifetime is followed with this handles. + * + * @returns Global UI Component + */ +function GlobalUIComponent() { + const { openModal } = useModalControlContext() + const { openPanel } = usePanelControlContext() + const { addToast } = useToastContext() + + useEffect(() => { + setOpenModal(openModal) + + return () => { + setOpenModal(undefined) + } + }, [openModal]) + + useEffect(() => { + setOpenPanel(openPanel) + + return () => { + setOpenPanel(undefined) + } + }, [openPanel]) + + useEffect(() => { + setAddToast(addToast) + + return () => { + setAddToast(undefined) + } + }, [addToast]) + + return <> +} + +export default GlobalUIComponent diff --git a/fission/src/ui/components/GlobalUIControls.ts b/fission/src/ui/components/GlobalUIControls.ts new file mode 100644 index 0000000000..e3a459ac9a --- /dev/null +++ b/fission/src/ui/components/GlobalUIControls.ts @@ -0,0 +1,20 @@ +/** + * This is where all the global references to the Global UI controls are located. + * See GlobalUIComponent.tsx for explanation of this madness. + */ + +import { ToastType } from "@/ui/ToastContext" + +export let Global_AddToast: ((type: ToastType, title: string, description: string) => void) | undefined = undefined +export let Global_OpenPanel: ((panelId: string) => void) | undefined = undefined +export let Global_OpenModal: ((modalId: string) => void) | undefined = undefined + +export function setAddToast(func: typeof Global_AddToast) { + Global_AddToast = func +} +export function setOpenPanel(func: typeof Global_OpenPanel) { + Global_OpenPanel = func +} +export function setOpenModal(func: typeof Global_OpenModal) { + Global_OpenModal = func +} diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index 6fe6ccf888..eb8740f852 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -4,12 +4,13 @@ import { useModalControlContext } from "@/ui/ModalContext" import { usePanelControlContext } from "@/ui/PanelContext" import { motion } from "framer-motion" import logo from "@/assets/autodesk_logo.png" -import { ToastType, useToastContext } from "@/ui/ToastContext" +import { useToastContext } from "@/ui/ToastContext" import APS, { APS_USER_INFO_UPDATE_EVENT } from "@/aps/APS" import { UserIcon } from "./UserIcon" import { ButtonIcon, SynthesisIcons } from "./StyledComponents" import { Button } from "@mui/base" import { Box } from "@mui/material" +import { setAddToast } from "./GlobalUIControls" type ButtonProps = { value: string @@ -44,10 +45,6 @@ const MainHUDButton: React.FC = ({ value, icon, onClick, larger }) ) } -// Future Synthesis Interns: Please, never, ever, ever do this. -export let MainHUD_AddToast: (type: ToastType, title: string, description: string) => void = (_a, _b, _c) => {} -export let MainHUD_OpenPanel: (panelId: string) => void = _id => {} - const variants = { open: { opacity: 1, y: "-50%", x: 0 }, closed: { opacity: 0, y: "-50%", x: "-100%" }, @@ -59,8 +56,7 @@ const MainHUD: React.FC = () => { const { addToast } = useToastContext() const [isOpen, setIsOpen] = useState(false) - MainHUD_AddToast = addToast - MainHUD_OpenPanel = openPanel + setAddToast(addToast) const [userInfo, setUserInfo] = useState(APS.userInfo) diff --git a/fission/src/ui/components/Scene.tsx b/fission/src/ui/components/Scene.tsx index 6ba35ddb6f..034d865503 100644 --- a/fission/src/ui/components/Scene.tsx +++ b/fission/src/ui/components/Scene.tsx @@ -17,8 +17,6 @@ function Scene({ useStats }: SceneProps) { World.InitWorld() if (refContainer.current) { - console.debug("Adding ThreeJs to DOM") - const sr = World.SceneRenderer sr.renderer.domElement.style.width = "100%" sr.renderer.domElement.style.height = "100%" @@ -30,7 +28,6 @@ function Scene({ useStats }: SceneProps) { }) if (useStats && !stats) { - console.log("Adding stat") stats = new Stats() stats.dom.style.position = "absolute" stats.dom.style.top = "0px" diff --git a/fission/src/ui/components/TransformGizmoControl.tsx b/fission/src/ui/components/TransformGizmoControl.tsx new file mode 100644 index 0000000000..3dd5a2d9f7 --- /dev/null +++ b/fission/src/ui/components/TransformGizmoControl.tsx @@ -0,0 +1,138 @@ +import { useEffect, useState } from "react" +import TransformGizmoControlProps from "./TransformGizmoControlProps" +import GizmoSceneObject, { GizmoMode } from "@/systems/scene/GizmoSceneObject" +import { ToggleButton, ToggleButtonGroup } from "./ToggleButtonGroup" +import World from "@/systems/World" +import Button, { ButtonSize } from "./Button" +import InputSystem from "@/systems/input/InputSystem" +import * as THREE from "three" + +/** + * Creates GizmoSceneObject and gives you a toggle button group to control the modes of the gizmo. + * The lifetime of the gizmo is entirely handles within this component and will be recreated depending + * on the updates made to the parameters provided. You can setup initial properties of the gizmo with + * the `postGizmoCreation` handle. + * + * @param param0 Transform Gizmo Controls. + * @returns TransformGizmoControl component. + */ +function TransformGizmoControl({ + defaultMesh, + gizmoRef, + size, + parent, + defaultMode, + translateDisabled, + rotateDisabled, + scaleDisabled, + sx, + postGizmoCreation, + onAccept, + onCancel, +}: TransformGizmoControlProps) { + const [mode, setMode] = useState(defaultMode) + const [gizmo, setGizmo] = useState(undefined) + + useEffect(() => { + const gizmo = new GizmoSceneObject("translate", size, defaultMesh, parent, (gizmo: GizmoSceneObject) => { + parent?.PostGizmoCreation(gizmo) + postGizmoCreation?.(gizmo) + }) + + if (gizmoRef) gizmoRef.current = gizmo + + setGizmo(gizmo) + + return () => { + World.SceneRenderer.RemoveSceneObject(gizmo.id) + } + }, [gizmoRef, defaultMesh, size, parent, postGizmoCreation]) + + useEffect(() => { + return () => { + if (gizmoRef) gizmoRef.current = undefined + } + }, [gizmoRef]) + + const disableOptions = 2 <= (translateDisabled ? 1 : 0) + (rotateDisabled ? 1 : 0) + (scaleDisabled ? 1 : 0) + + const buttons = [] + if (!translateDisabled) + buttons.push( + + Move + + ) + if (!rotateDisabled) + buttons.push( + + Rotate + + ) + if (!scaleDisabled) + buttons.push( + + Scale + + ) + + useEffect(() => { + const func = () => { + // creating enter key and escape key event listeners + if (InputSystem.isKeyPressed("Enter")) { + onAccept?.(gizmo) + } else if (InputSystem.isKeyPressed("Escape")) { + onCancel?.(gizmo) + } + + cancelAnimationFrame(animHandle) + animHandle = requestAnimationFrame(func) + } + let animHandle = requestAnimationFrame(func) + + return () => { + cancelAnimationFrame(animHandle) + } + }, [gizmo, onAccept, onCancel]) + + // If there are no modes enabled, consider the UI pointless. + return disableOptions ? ( + <> + ) : ( + <> + { + if (v == undefined) return + + setMode(v) + gizmo?.SetMode(v) + }} + sx={{ + ...(sx ?? {}), + alignSelf: "center", + }} + > + {/* { translateDisabled ? <> : Move } + { rotateDisabled ? <> : Rotate } + { scaleDisabled ? <> : Scale } */} + {buttons} + + {rotateDisabled ? ( + <> + ) : ( +