diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx
index 22942f34e..b07fa0043 100644
--- a/fission/src/Synthesis.tsx
+++ b/fission/src/Synthesis.tsx
@@ -66,6 +66,7 @@ import ContextMenu from "./ui/components/ContextMenu.tsx"
import GlobalUIComponent from "./ui/components/GlobalUIComponent.tsx"
import InitialConfigPanel from "./ui/panels/configuring/initial-config/InitialConfigPanel.tsx"
import WPILibConnectionStatus from "./ui/components/WPILibConnectionStatus.tsx"
+import DragModeIndicator from "./ui/components/DragModeIndicator.tsx"
import AutoTestPanel from "./ui/panels/simulation/AutoTestPanel.tsx"
import TouchControls from "./ui/components/TouchControls.tsx"
import GraphicsSettings from "./ui/panels/GraphicsSettingsPanel.tsx"
@@ -195,6 +196,7 @@ function Synthesis() {
+
{!consentPopupDisable ? (
diff --git a/fission/src/systems/World.ts b/fission/src/systems/World.ts
index a1389e80c..29c57f188 100644
--- a/fission/src/systems/World.ts
+++ b/fission/src/systems/World.ts
@@ -5,6 +5,7 @@ import SceneRenderer from "./scene/SceneRenderer"
import SimulationSystem from "./simulation/SimulationSystem"
import InputSystem from "./input/InputSystem"
import AnalyticsSystem, { AccumTimes } from "./analytics/AnalyticsSystem"
+import DragModeSystem from "./scene/DragModeSystem"
class World {
private static _isAlive: boolean = false
@@ -16,6 +17,7 @@ class World {
private static _simulationSystem: SimulationSystem
private static _inputSystem: InputSystem
private static _analyticsSystem: AnalyticsSystem | undefined = undefined
+ private static _dragModeSystem: DragModeSystem
private static _accumTimes: AccumTimes = {
frames: 0,
@@ -49,6 +51,9 @@ class World {
public static get AnalyticsSystem() {
return World._analyticsSystem
}
+ public static get DragModeSystem() {
+ return World._dragModeSystem
+ }
public static resetAccumTimes() {
this._accumTimes = {
@@ -71,6 +76,7 @@ class World {
World._physicsSystem = new PhysicsSystem()
World._simulationSystem = new SimulationSystem()
World._inputSystem = new InputSystem()
+ World._dragModeSystem = new DragModeSystem()
try {
World._analyticsSystem = new AnalyticsSystem()
} catch (_) {
@@ -87,6 +93,7 @@ class World {
World._sceneRenderer.Destroy()
World._simulationSystem.Destroy()
World._inputSystem.Destroy()
+ World._dragModeSystem.Destroy()
World._analyticsSystem?.Destroy()
}
@@ -101,6 +108,7 @@ class World {
this._accumTimes.physicsTime += this.time(() => World._physicsSystem.Update(this._currentDeltaT))
this._accumTimes.inputTime += this.time(() => World._inputSystem.Update(this._currentDeltaT))
this._accumTimes.sceneTime += this.time(() => World._sceneRenderer.Update(this._currentDeltaT))
+ World._dragModeSystem.Update(this._currentDeltaT)
})
World._analyticsSystem?.Update(this._currentDeltaT)
diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts
index a3e8c552d..1988708c0 100644
--- a/fission/src/systems/physics/PhysicsSystem.ts
+++ b/fission/src/systems/physics/PhysicsSystem.ts
@@ -1,7 +1,6 @@
import {
JoltRVec3_JoltVec3,
JoltVec3_JoltRVec3,
- JoltVec3_ThreeVector3,
MirabufFloatArr_JoltFloat3,
MirabufFloatArr_JoltVec3,
MirabufVector3_JoltRVec3,
@@ -46,7 +45,7 @@ const RobotLayers: number[] = [
3, 4, 5, 6, 7, 8, 9,
]
-// Layer for ghost object in god mode, interacts with nothing
+// Layer for ghost objects used in constraint systems, interacts with nothing
const LAYER_GHOST = 10
// Please update this accordingly.
@@ -1306,38 +1305,6 @@ class PhysicsSystem extends WorldSystem {
return body
}
- /**
- * Creates a ghost object and a distance constraint that connects it to the given body
- * The ghost body is part of the LAYER_GHOST which doesn't interact with any other layer
- * The caller is responsible for cleaning up the ghost body and the constraint
- *
- * @param id The id of the body to be attatched to and moved
- * @returns The ghost body and the constraint
- */
-
- public CreateGodModeBody(id: Jolt.BodyID, anchorPoint: Jolt.Vec3): [Jolt.Body, Jolt.Constraint] {
- const body = this.GetBody(id)
- const ghostBody = this.CreateBox(
- new THREE.Vector3(0.05, 0.05, 0.05),
- undefined,
- JoltVec3_ThreeVector3(anchorPoint),
- undefined
- )
-
- const ghostBodyId = ghostBody.GetID()
- this._joltBodyInterface.SetObjectLayer(ghostBodyId, LAYER_GHOST)
- this._joltBodyInterface.AddBody(ghostBodyId, JOLT.EActivation_Activate)
- this._bodies.push(ghostBodyId)
-
- const constraintSettings = new JOLT.PointConstraintSettings()
- constraintSettings.mPoint1 = constraintSettings.mPoint2 = JoltVec3_JoltRVec3(anchorPoint)
- const constraint = constraintSettings.Create(ghostBody, body)
- this._joltPhysSystem.AddConstraint(constraint)
- this._constraints.push(constraint)
-
- return [ghostBody, constraint]
- }
-
public CreateSensor(shapeSettings: Jolt.ShapeSettings): Jolt.BodyID | undefined {
const shape = shapeSettings.Create()
if (shape.HasError()) {
diff --git a/fission/src/systems/scene/CameraControls.ts b/fission/src/systems/scene/CameraControls.ts
index 6a4772a62..e57da5ca7 100644
--- a/fission/src/systems/scene/CameraControls.ts
+++ b/fission/src/systems/scene/CameraControls.ts
@@ -29,7 +29,7 @@ export abstract class CameraControls {
public abstract dispose(): void
}
-interface SphericalCoords {
+export interface SphericalCoords {
theta: number
phi: number
r: number
@@ -112,6 +112,18 @@ export class CustomOrbitControls extends CameraControls {
return this._focusProvider
}
+ public get coords(): SphericalCoords {
+ return this._coords
+ }
+
+ public get focus(): THREE.Matrix4 {
+ return this._focus
+ }
+
+ public set focus(matrix: THREE.Matrix4) {
+ this._focus.copy(matrix)
+ }
+
public constructor(mainCamera: THREE.Camera, interactionHandler: ScreenInteractionHandler) {
super("Orbit")
diff --git a/fission/src/systems/scene/DragModeSystem.ts b/fission/src/systems/scene/DragModeSystem.ts
new file mode 100644
index 000000000..28133415a
--- /dev/null
+++ b/fission/src/systems/scene/DragModeSystem.ts
@@ -0,0 +1,633 @@
+import * as THREE from "three"
+import WorldSystem from "../WorldSystem"
+import World from "../World"
+import JOLT from "@/util/loading/JoltSyncLoader"
+import { ThreeVector3_JoltVec3, JoltVec3_ThreeVector3 } from "@/util/TypeConversions"
+import MirabufSceneObject, { RigidNodeAssociate } from "@/mirabuf/MirabufSceneObject"
+import {
+ InteractionStart,
+ InteractionMove,
+ InteractionEnd,
+ PRIMARY_MOUSE_INTERACTION,
+} from "./ScreenInteractionHandler"
+import { CustomOrbitControls, SphericalCoords } from "./CameraControls"
+import Jolt from "@azaleacolburn/jolt-physics"
+import { MiraType } from "@/mirabuf/MirabufLoader"
+
+interface DragTarget {
+ bodyId: Jolt.BodyID
+ initialPosition: THREE.Vector3
+ localOffset: THREE.Vector3 // Offset in body's local coordinate system
+ mass: number
+ dragDepth: number
+ physicsDisabled: boolean
+}
+
+interface CameraTransition {
+ isTransitioning: boolean
+ transitionProgress: number
+ transitionDuration: number
+ startCoords: SphericalCoords
+ targetCoords: SphericalCoords
+ startFocus: THREE.Matrix4
+ targetSceneObject: MirabufSceneObject | undefined
+}
+
+class DragModeSystem extends WorldSystem {
+ // Drag force constants - tune these to reduce wobble and improve stability
+ private static readonly DRAG_FORCE_CONSTANTS = {
+ // Linear motion control
+ MAX_DRAG_SPEED: 15.0, // Maximum speed when dragging (lower = more stable, higher = more responsive)
+ DAMPING_ZONE: 2, // Distance where speed starts to ramp down (larger = smoother approach)
+ FORCE_MULTIPLIER_BASE: 15.0, // Base force multiplier per unit mass (lower = less aggressive)
+ FORCE_MULTIPLIER_MAX: 500.0, // Maximum force regardless of mass (lower = more stable)
+
+ // Angular damping control
+ ANGULAR_DAMPING_BASE: 3.0, // Base angular damping per unit mass (higher = less rotation wobble)
+ ANGULAR_DAMPING_MAX: 500.0, // Maximum angular damping (higher = more rotation stability)
+
+ // Braking when stationary
+ LINEAR_BRAKING_BASE: 5.0, // Linear braking force per unit mass (higher = stops faster)
+ LINEAR_BRAKING_MAX: 200.0, // Maximum linear braking force
+ ANGULAR_BRAKING_BASE: 2.0, // Angular braking force per unit mass (higher = stops rotation faster)
+ ANGULAR_BRAKING_MAX: 5.0, // Maximum angular braking force
+
+ // Gravity compensation
+ GRAVITY_MAGNITUDE: 11, // Gravity acceleration (m/s/s)
+ GRAVITY_COMPENSATION: true, // Whether to compensate for gravity during drag
+
+ // Precision and sensitivity
+ MINIMUM_DISTANCE_THRESHOLD: 0.02, // Minimum distance to apply forces (smaller = more precision)
+ WHEEL_SCROLL_SENSITIVITY: -0.01, // Mouse wheel scroll sensitivity for Z-axis
+ } as const
+
+ private _enabled: boolean = false
+ private _dragTarget: DragTarget | undefined
+ private _isDragging: boolean = false
+ private _lastMousePosition: [number, number] = [0, 0]
+
+ // Debug visualization
+ private _debugSphere: THREE.Mesh | undefined
+
+ // Wheel event handling for Z-axis dragging
+ private _wheelEventHandler: ((event: WheelEvent) => void) | undefined
+
+ private _originalInteractionStart: ((i: InteractionStart) => void) | undefined
+ private _originalInteractionMove: ((i: InteractionMove) => void) | undefined
+ private _originalInteractionEnd: ((i: InteractionEnd) => void) | undefined
+
+ private _cameraTransition: CameraTransition = {
+ isTransitioning: false,
+ transitionProgress: 0,
+ transitionDuration: 1.0,
+ startCoords: { theta: 0, phi: 0, r: 0 },
+ targetCoords: { theta: 0, phi: 0, r: 0 },
+ startFocus: new THREE.Matrix4(),
+ targetSceneObject: undefined,
+ }
+
+ private _handleDisableDragMode: () => void
+
+ public constructor() {
+ super()
+
+ this._handleDisableDragMode = () => {
+ this.enabled = false
+ }
+
+ // Create wheel event handler for Z-axis dragging
+ this._wheelEventHandler = (event: WheelEvent) => {
+ if (this._isDragging && this._dragTarget) {
+ event.preventDefault()
+ this.handleWheelDuringDrag(event)
+ }
+ }
+
+ window.addEventListener("disableDragMode", this._handleDisableDragMode)
+ }
+
+ public get enabled(): boolean {
+ return this._enabled
+ }
+
+ public set enabled(enabled: boolean) {
+ if (this._enabled === enabled) return
+
+ this._enabled = enabled
+
+ if (enabled) {
+ this.hookInteractionHandlers()
+ } else {
+ this.unhookInteractionHandlers()
+ this.stopDragging()
+
+ if (this._cameraTransition.isTransitioning) {
+ this._cameraTransition.isTransitioning = false
+ World.SceneRenderer.currentCameraControls.enabled = true
+ }
+ }
+
+ window.dispatchEvent(new CustomEvent("dragModeToggled", { detail: { enabled } }))
+ }
+
+ public Update(deltaT: number): void {
+ if (!this._enabled) return
+
+ if (this._isDragging && this._dragTarget) {
+ this.updateDragForce()
+ }
+
+ if (this._cameraTransition.isTransitioning) {
+ this.updateCameraTransition(deltaT)
+ }
+ }
+
+ public Destroy(): void {
+ this.enabled = false
+
+ if (this._cameraTransition.isTransitioning) {
+ this._cameraTransition.isTransitioning = false
+ World.SceneRenderer.currentCameraControls.enabled = true
+ }
+
+ // Clean up debug sphere
+ this.removeDebugSphere()
+
+ window.removeEventListener("disableDragMode", this._handleDisableDragMode)
+ }
+
+ private createDebugSphere(position: THREE.Vector3): void {
+ // Remove existing debug sphere if any
+ this.removeDebugSphere()
+
+ // Create a small red sphere to visualize the raycast hit point
+ const geometry = new THREE.SphereGeometry(0.05, 16, 16)
+ const material = new THREE.MeshBasicMaterial({
+ color: 0xff0000,
+ transparent: true,
+ opacity: 0.8,
+ })
+ this._debugSphere = new THREE.Mesh(geometry, material)
+ this._debugSphere.position.copy(position)
+
+ // Add to the scene
+ World.SceneRenderer.scene.add(this._debugSphere)
+ }
+
+ private updateDebugSphere(position: THREE.Vector3): void {
+ if (this._debugSphere) {
+ this._debugSphere.position.copy(position)
+ }
+ }
+
+ private removeDebugSphere(): void {
+ if (this._debugSphere) {
+ World.SceneRenderer.scene.remove(this._debugSphere)
+ this._debugSphere.geometry.dispose()
+ if (this._debugSphere.material instanceof THREE.Material) {
+ this._debugSphere.material.dispose()
+ }
+ this._debugSphere = undefined
+ }
+ }
+
+ private hookInteractionHandlers(): void {
+ const handler = World.SceneRenderer.renderer.domElement.parentElement?.querySelector("canvas")
+ if (!handler) return
+
+ const screenHandler = World.SceneRenderer.screenInteractionHandler
+ this._originalInteractionStart = screenHandler.interactionStart
+ this._originalInteractionMove = screenHandler.interactionMove
+ this._originalInteractionEnd = screenHandler.interactionEnd
+
+ screenHandler.interactionStart = (interaction: InteractionStart) => this.onInteractionStart(interaction)
+ screenHandler.interactionMove = (interaction: InteractionMove) => this.onInteractionMove(interaction)
+ screenHandler.interactionEnd = (interaction: InteractionEnd) => this.onInteractionEnd(interaction)
+
+ // Add wheel event listener for Z-axis dragging
+ if (this._wheelEventHandler) {
+ handler.addEventListener("wheel", this._wheelEventHandler, { passive: false })
+ }
+ }
+
+ private unhookInteractionHandlers(): void {
+ const handler = World.SceneRenderer.renderer.domElement.parentElement?.querySelector("canvas")
+ const screenHandler = World.SceneRenderer.screenInteractionHandler
+ if (!screenHandler) return
+
+ if (this._originalInteractionStart) screenHandler.interactionStart = this._originalInteractionStart
+ if (this._originalInteractionMove) screenHandler.interactionMove = this._originalInteractionMove
+ if (this._originalInteractionEnd) screenHandler.interactionEnd = this._originalInteractionEnd
+
+ // Remove wheel event listener
+ if (handler && this._wheelEventHandler) {
+ handler.removeEventListener("wheel", this._wheelEventHandler)
+ }
+ }
+
+ private onInteractionStart(interaction: InteractionStart): void {
+ if (interaction.interactionType !== PRIMARY_MOUSE_INTERACTION) {
+ this._originalInteractionStart?.(interaction)
+ return
+ }
+
+ this._lastMousePosition = interaction.position
+
+ const hitResult = this.raycastFromMouse(interaction.position)
+ if (hitResult) {
+ const association = World.PhysicsSystem.GetBodyAssociation(hitResult.data.mBodyID) as RigidNodeAssociate
+ if (association?.sceneObject && association.sceneObject instanceof MirabufSceneObject) {
+ const body = World.PhysicsSystem.GetBody(hitResult.data.mBodyID)
+ if (body) {
+ const isStatic = body.GetMotionType() === JOLT.EMotionType_Static
+ const isFieldStructure =
+ association.sceneObject.miraType === MiraType.FIELD && !association.isGamePiece
+
+ if (!isStatic && !isFieldStructure) {
+ const hitPointVec = JoltVec3_ThreeVector3(hitResult.point)
+ this.startDragging(hitResult.data.mBodyID, interaction.position, hitPointVec)
+ return
+ }
+ }
+ }
+ }
+
+ this._originalInteractionStart?.(interaction)
+ }
+
+ private onInteractionMove(interaction: InteractionMove): void {
+ if (this._isDragging && interaction.movement) {
+ // Use absolute position instead of accumulating movement to prevent drift
+ this._lastMousePosition[0] += interaction.movement[0]
+ this._lastMousePosition[1] += interaction.movement[1]
+
+ // Clamp to screen bounds to prevent issues with cursor going off-screen
+ this._lastMousePosition[0] = Math.max(0, Math.min(window.innerWidth, this._lastMousePosition[0]))
+ this._lastMousePosition[1] = Math.max(0, Math.min(window.innerHeight, this._lastMousePosition[1]))
+ } else {
+ this._originalInteractionMove?.(interaction)
+ }
+ }
+
+ private onInteractionEnd(interaction: InteractionEnd): void {
+ if (interaction.interactionType === PRIMARY_MOUSE_INTERACTION && this._isDragging) {
+ this.stopDragging()
+ } else {
+ this._originalInteractionEnd?.(interaction)
+ }
+ }
+
+ private raycastFromMouse(mousePos: [number, number]) {
+ const camera = World.SceneRenderer.mainCamera
+ const origin = camera.position
+ const worldSpace = World.SceneRenderer.PixelToWorldSpace(mousePos[0], mousePos[1])
+ const direction = worldSpace.sub(origin).normalize().multiplyScalar(40.0)
+
+ return World.PhysicsSystem.RayCast(ThreeVector3_JoltVec3(origin), ThreeVector3_JoltVec3(direction))
+ }
+
+ private startDragging(bodyId: Jolt.BodyID, mousePos: [number, number], hitPoint: THREE.Vector3): void {
+ const body = World.PhysicsSystem.GetBody(bodyId)
+ if (!body) return
+
+ const bodyPos = body.GetPosition()
+ const bodyPosition = new THREE.Vector3(bodyPos.GetX(), bodyPos.GetY(), bodyPos.GetZ())
+ const bodyRotation = body.GetRotation()
+ const bodyQuaternion = new THREE.Quaternion(
+ bodyRotation.GetX(),
+ bodyRotation.GetY(),
+ bodyRotation.GetZ(),
+ bodyRotation.GetW()
+ )
+
+ const motionProperties = body.GetMotionProperties()
+ const mass = 1.0 / motionProperties.GetInverseMass()
+
+ const camera = World.SceneRenderer.mainCamera
+ const cameraToHit = hitPoint.clone().sub(camera.position)
+ const cameraDirection = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion)
+ const dragDepth = cameraToHit.dot(cameraDirection)
+
+ // Convert the hit point offset to the body's local coordinate system
+ const worldOffset = hitPoint.clone().sub(bodyPosition)
+ const localOffset = worldOffset.clone().applyQuaternion(bodyQuaternion.clone().invert())
+
+ const association = World.PhysicsSystem.GetBodyAssociation(bodyId) as RigidNodeAssociate
+ const isRobot = association?.sceneObject?.miraType === MiraType.ROBOT
+
+ this._dragTarget = {
+ bodyId: bodyId,
+ initialPosition: bodyPosition.clone(),
+ localOffset: localOffset,
+ mass: mass,
+ dragDepth: dragDepth,
+ physicsDisabled: isRobot,
+ }
+
+ this._isDragging = true
+ this._lastMousePosition = mousePos
+
+ // Create debug sphere at the exact hit point
+ this.createDebugSphere(hitPoint)
+
+ if (isRobot) {
+ World.PhysicsSystem.DisablePhysicsForBody(bodyId)
+ }
+
+ World.SceneRenderer.currentCameraControls.enabled = false
+ }
+
+ private stopDragging(): void {
+ if (!this._isDragging) return
+
+ if (this._dragTarget?.physicsDisabled) {
+ World.PhysicsSystem.EnablePhysicsForBody(this._dragTarget.bodyId)
+ } else if (this._dragTarget) {
+ const body = World.PhysicsSystem.GetBody(this._dragTarget.bodyId)
+ if (body) {
+ const currentVel = body.GetLinearVelocity()
+ const mass = this._dragTarget.mass
+ const stopBrakingStrength = Math.min(mass * 10.0, 300.0)
+ const stopBrakingForce = new JOLT.Vec3(
+ -currentVel.GetX() * stopBrakingStrength,
+ -currentVel.GetY() * stopBrakingStrength,
+ -currentVel.GetZ() * stopBrakingStrength
+ )
+ body.AddForce(stopBrakingForce)
+
+ const angularVel = body.GetAngularVelocity()
+ const angularStopBraking = Math.min(mass * 8.0, 200.0)
+ const angularStopTorque = new JOLT.Vec3(
+ -angularVel.GetX() * angularStopBraking,
+ -angularVel.GetY() * angularStopBraking,
+ -angularVel.GetZ() * angularStopBraking
+ )
+ body.AddTorque(angularStopTorque)
+ }
+ }
+
+ let targetSceneObject: MirabufSceneObject | undefined
+ let shouldTransition = true
+
+ if (this._dragTarget) {
+ const association = World.PhysicsSystem.GetBodyAssociation(this._dragTarget.bodyId) as RigidNodeAssociate
+ targetSceneObject = association?.sceneObject
+ if (association?.isGamePiece) {
+ shouldTransition = false
+ }
+ }
+
+ this._isDragging = false
+ this._dragTarget = undefined
+
+ // Remove debug sphere when dragging stops
+ this.removeDebugSphere()
+
+ if (shouldTransition) {
+ this.startCameraTransition(targetSceneObject)
+ } else {
+ World.SceneRenderer.currentCameraControls.enabled = true
+ }
+ }
+
+ private startCameraTransition(targetSceneObject: MirabufSceneObject | undefined): void {
+ const cameraControls = World.SceneRenderer.currentCameraControls as CustomOrbitControls
+
+ this._cameraTransition.startCoords = {
+ theta: cameraControls.coords.theta,
+ phi: cameraControls.coords.phi,
+ r: cameraControls.coords.r,
+ }
+ this._cameraTransition.startFocus.copy(cameraControls.focus)
+
+ this._cameraTransition.targetCoords = {
+ theta: this._cameraTransition.startCoords.theta,
+ phi: this._cameraTransition.startCoords.phi,
+ r: this._cameraTransition.startCoords.r,
+ }
+
+ this._cameraTransition.targetSceneObject = targetSceneObject
+
+ this._cameraTransition.isTransitioning = true
+ this._cameraTransition.transitionProgress = 0
+
+ cameraControls.enabled = true
+ cameraControls.focusProvider = undefined
+ }
+
+ private updateCameraTransition(deltaT: number): void {
+ if (!this._cameraTransition.isTransitioning) return
+
+ this._cameraTransition.transitionProgress += deltaT / this._cameraTransition.transitionDuration
+
+ if (this._cameraTransition.transitionProgress >= 1.0) {
+ this._cameraTransition.isTransitioning = false
+ this._cameraTransition.transitionProgress = 1.0
+
+ const cameraControls = World.SceneRenderer.currentCameraControls as CustomOrbitControls
+
+ if (this._cameraTransition.targetSceneObject) {
+ cameraControls.focusProvider = this._cameraTransition.targetSceneObject
+ }
+ return
+ }
+
+ const t = this.easeInOutCubic(this._cameraTransition.transitionProgress)
+
+ const cameraControls = World.SceneRenderer.currentCameraControls as CustomOrbitControls
+
+ const currentFocus = new THREE.Matrix4()
+ if (this._cameraTransition.targetSceneObject) {
+ const targetFocus = new THREE.Matrix4()
+ this._cameraTransition.targetSceneObject.LoadFocusTransform(targetFocus)
+
+ const startPos = new THREE.Vector3().setFromMatrixPosition(this._cameraTransition.startFocus)
+ const targetPos = new THREE.Vector3().setFromMatrixPosition(targetFocus)
+ const currentPos = new THREE.Vector3().lerpVectors(startPos, targetPos, t)
+
+ currentFocus.makeTranslation(currentPos.x, currentPos.y, currentPos.z)
+ } else {
+ currentFocus.copy(this._cameraTransition.startFocus)
+ }
+
+ cameraControls.focus = currentFocus
+ }
+
+ private easeInOutCubic(t: number): number {
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
+ }
+
+ private updateDragForce(): void {
+ if (!this._dragTarget) return
+
+ const body = World.PhysicsSystem.GetBody(this._dragTarget.bodyId)
+ if (!body) {
+ this.stopDragging()
+ return
+ }
+
+ const currentPos = body.GetPosition()
+ const currentPosition = new THREE.Vector3(currentPos.GetX(), currentPos.GetY(), currentPos.GetZ())
+ const currentRotation = body.GetRotation()
+ const currentQuaternion = new THREE.Quaternion(
+ currentRotation.GetX(),
+ currentRotation.GetY(),
+ currentRotation.GetZ(),
+ currentRotation.GetW()
+ )
+
+ // Convert the local offset back to world coordinates based on current body rotation
+ const currentWorldOffset = this._dragTarget.localOffset.clone().applyQuaternion(currentQuaternion)
+ const currentDragPointWorld = currentPosition.clone().add(currentWorldOffset)
+
+ const camera = World.SceneRenderer.mainCamera
+
+ // Create a ray from the camera through the current mouse position
+ const mouseNDC = new THREE.Vector2(
+ (this._lastMousePosition[0] / window.innerWidth) * 2 - 1,
+ -(this._lastMousePosition[1] / window.innerHeight) * 2 + 1
+ )
+
+ const raycaster = new THREE.Raycaster()
+ raycaster.setFromCamera(mouseNDC, camera)
+
+ // Create a dynamic drag plane perpendicular to the camera at the original drag depth
+ const cameraDirection = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion)
+ const dragPlanePosition = camera.position
+ .clone()
+ .add(cameraDirection.clone().multiplyScalar(this._dragTarget.dragDepth))
+ const dragPlane = new THREE.Plane()
+ dragPlane.setFromNormalAndCoplanarPoint(cameraDirection, dragPlanePosition)
+
+ const intersectionPoint = new THREE.Vector3()
+ const intersected = raycaster.ray.intersectPlane(dragPlane, intersectionPoint)
+
+ if (!intersected) {
+ // Fallback: project mouse position onto a sphere around the object
+ const fallbackDistance = Math.max(this._dragTarget.dragDepth * 0.5, 1.0)
+ const direction = raycaster.ray.direction.clone().normalize()
+ intersectionPoint.copy(camera.position).add(direction.multiplyScalar(fallbackDistance))
+ }
+
+ // The target is where we want the drag point (on the robot) to be
+ const targetDragPointWorld = intersectionPoint
+
+ // Update debug sphere to show where the drag point currently is on the robot
+ this.updateDebugSphere(currentDragPointWorld)
+
+ // Calculate the displacement needed to move the current drag point to the target
+ const displacement = targetDragPointWorld.clone().sub(currentDragPointWorld)
+ const distance = displacement.length()
+
+ if (distance > DragModeSystem.DRAG_FORCE_CONSTANTS.MINIMUM_DISTANCE_THRESHOLD) {
+ const maxSpeed = DragModeSystem.DRAG_FORCE_CONSTANTS.MAX_DRAG_SPEED
+ const dampingZone = DragModeSystem.DRAG_FORCE_CONSTANTS.DAMPING_ZONE
+
+ let targetSpeed: number
+ if (distance > dampingZone) {
+ targetSpeed = maxSpeed
+ } else {
+ targetSpeed = maxSpeed * (distance / dampingZone)
+ }
+
+ const direction = displacement.normalize()
+ const desiredVelocity = direction.multiplyScalar(targetSpeed)
+
+ const currentVel = body.GetLinearVelocity()
+ const currentVelocity = new THREE.Vector3(currentVel.GetX(), currentVel.GetY(), currentVel.GetZ())
+
+ const velocityError = desiredVelocity.sub(currentVelocity)
+
+ const mass = this._dragTarget.mass
+
+ const forceMultiplier = Math.min(
+ mass * DragModeSystem.DRAG_FORCE_CONSTANTS.FORCE_MULTIPLIER_BASE,
+ DragModeSystem.DRAG_FORCE_CONSTANTS.FORCE_MULTIPLIER_MAX
+ )
+ const forceNeeded = velocityError.multiplyScalar(forceMultiplier)
+
+ // Add gravity compensation to counteract downward pull
+ if (DragModeSystem.DRAG_FORCE_CONSTANTS.GRAVITY_COMPENSATION) {
+ const gravityCompensation = new THREE.Vector3(
+ 0,
+ mass * DragModeSystem.DRAG_FORCE_CONSTANTS.GRAVITY_MAGNITUDE,
+ 0
+ )
+ forceNeeded.add(gravityCompensation)
+ }
+
+ // Apply force at the center of mass and calculate the torque manually
+ // to simulate applying force at the drag point
+ const joltForce = ThreeVector3_JoltVec3(forceNeeded)
+ body.AddForce(joltForce)
+
+ // Calculate torque to simulate force applied at the drag point
+ // Use the current world offset (which rotates with the body)
+ const leverArm = currentWorldOffset // vector from COM to drag point in world coordinates
+ const torque = leverArm.cross(forceNeeded) // Cross product gives us the torque
+ const joltTorque = ThreeVector3_JoltVec3(torque)
+ body.AddTorque(joltTorque)
+
+ // Reduce angular damping since we want the natural rotation from the applied force
+ const angularVel = body.GetAngularVelocity()
+ const angularDampingStrength = Math.min(
+ mass * DragModeSystem.DRAG_FORCE_CONSTANTS.ANGULAR_DAMPING_BASE,
+ DragModeSystem.DRAG_FORCE_CONSTANTS.ANGULAR_DAMPING_MAX
+ )
+ const angularDampingTorque = new JOLT.Vec3(
+ -angularVel.GetX() * angularDampingStrength,
+ -angularVel.GetY() * angularDampingStrength,
+ -angularVel.GetZ() * angularDampingStrength
+ )
+ body.AddTorque(angularDampingTorque)
+ } else {
+ // When close to target, apply braking forces and gravity compensation
+ const currentVel = body.GetLinearVelocity()
+ const mass = this._dragTarget.mass
+ const brakingStrength = Math.min(
+ mass * DragModeSystem.DRAG_FORCE_CONSTANTS.LINEAR_BRAKING_BASE,
+ DragModeSystem.DRAG_FORCE_CONSTANTS.LINEAR_BRAKING_MAX
+ )
+ const brakingForce = new JOLT.Vec3(
+ -currentVel.GetX() * brakingStrength,
+ -currentVel.GetY() * brakingStrength,
+ -currentVel.GetZ() * brakingStrength
+ )
+
+ // Add gravity compensation to prevent falling when stationary
+ if (DragModeSystem.DRAG_FORCE_CONSTANTS.GRAVITY_COMPENSATION) {
+ const gravityCompensationY = mass * DragModeSystem.DRAG_FORCE_CONSTANTS.GRAVITY_MAGNITUDE
+ brakingForce.SetY(brakingForce.GetY() + gravityCompensationY)
+ }
+
+ body.AddForce(brakingForce)
+
+ const angularVel = body.GetAngularVelocity()
+ const angularBrakingStrength = Math.min(
+ mass * DragModeSystem.DRAG_FORCE_CONSTANTS.ANGULAR_BRAKING_BASE,
+ DragModeSystem.DRAG_FORCE_CONSTANTS.ANGULAR_BRAKING_MAX
+ )
+ const angularBrakingTorque = new JOLT.Vec3(
+ -angularVel.GetX() * angularBrakingStrength,
+ -angularVel.GetY() * angularBrakingStrength,
+ -angularVel.GetZ() * angularBrakingStrength
+ )
+ body.AddTorque(angularBrakingTorque)
+ }
+ }
+
+ private handleWheelDuringDrag(event: WheelEvent): void {
+ if (!this._dragTarget || !this._isDragging) return
+
+ // Adjust drag depth based on wheel delta
+ // Positive deltaY = wheel scroll down = move away from camera (increase depth)
+ // Negative deltaY = wheel scroll up = move toward camera (decrease depth)
+ const depthChange = event.deltaY * DragModeSystem.DRAG_FORCE_CONSTANTS.WHEEL_SCROLL_SENSITIVITY
+ this._dragTarget.dragDepth += depthChange
+
+ // Clamp drag depth to reasonable bounds
+ this._dragTarget.dragDepth = Math.max(0.5, Math.min(100.0, this._dragTarget.dragDepth))
+ }
+}
+
+export default DragModeSystem
diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts
index f8e0c4eb3..1a350eb51 100644
--- a/fission/src/systems/scene/SceneRenderer.ts
+++ b/fission/src/systems/scene/SceneRenderer.ts
@@ -81,6 +81,10 @@ class SceneRenderer extends WorldSystem {
return this._cameraControls
}
+ public get screenInteractionHandler(): ScreenInteractionHandler {
+ return this._screenInteractionHandler
+ }
+
/**
* Collection that maps Mirabuf objects to active GizmoSceneObjects
*/
diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts
index 424334d48..3b3e5d4a8 100644
--- a/fission/src/test/PhysicsSystem.test.ts
+++ b/fission/src/test/PhysicsSystem.test.ts
@@ -1,9 +1,7 @@
import { test, expect, describe, assert } from "vitest"
import PhysicsSystem, { LayerReserve } from "../systems/physics/PhysicsSystem"
import MirabufParser from "@/mirabuf/MirabufParser"
-import * as THREE from "three"
import MirabufCachingService, { MiraType } from "@/mirabuf/MirabufLoader"
-import { JoltRVec3_JoltVec3 } from "@/util/TypeConversions"
describe("Physics Sansity Checks", () => {
test("Convex Hull Shape (Cube)", () => {
@@ -50,34 +48,6 @@ describe("Physics Sansity Checks", () => {
})
})
-describe("GodMode", () => {
- test("Basic", () => {
- const system = new PhysicsSystem()
- const box = system.CreateBox(new THREE.Vector3(1, 1, 1), 1, new THREE.Vector3(0, 0, 0), undefined)
- const [ghostObject, ghostConstraint] = system.CreateGodModeBody(
- box.GetID(),
- JoltRVec3_JoltVec3(box.GetPosition())
- )
-
- assert(system.GetBody(ghostObject.GetID()) != undefined)
- assert(system.GetBody(box.GetID()) != undefined)
- assert(ghostConstraint != undefined)
- // Check constraint after running for a few seconds
- // TODO: Make sure this is the correct way to do this
- // TODO: Figure out how to make it use substeps to check instead
- for (let i = 0; i < 30; i++) {
- // TODO: Change this once this function actually uses deltaT
- system.Update(i)
- }
-
- assert(system.GetBody(ghostObject.GetID()) != undefined)
- assert(system.GetBody(box.GetID()) != undefined)
- assert(ghostConstraint != undefined)
-
- //system.Destroy()
- })
-})
-
describe("Mirabuf Physics Loading", () => {
test("Body Loading (Dozer)", async () => {
const assembly = await MirabufCachingService.CacheRemote("/api/mira/robots/Dozer_v9.mira", MiraType.ROBOT).then(
diff --git a/fission/src/ui/components/DragModeIndicator.tsx b/fission/src/ui/components/DragModeIndicator.tsx
new file mode 100644
index 000000000..acb60c7fe
--- /dev/null
+++ b/fission/src/ui/components/DragModeIndicator.tsx
@@ -0,0 +1,37 @@
+import { useEffect, useState } from "react"
+import Label, { LabelSize } from "./Label"
+import { FaHandPaper } from "react-icons/fa"
+import { Global_AddToast } from "./GlobalUIControls"
+
+export default function DragModeIndicator() {
+ const [enabled, setEnabled] = useState(false)
+
+ useEffect(() => {
+ const handleDragModeToggle = (event: CustomEvent) => {
+ setEnabled(event.detail.enabled)
+ }
+
+ window.addEventListener("dragModeToggled", handleDragModeToggle as EventListener)
+
+ return () => {
+ window.removeEventListener("dragModeToggled", handleDragModeToggle as EventListener)
+ }
+ }, [])
+
+ const handleClick = () => {
+ window.dispatchEvent(new CustomEvent("disableDragMode"))
+ Global_AddToast?.("info", "Drag Mode", "Drag mode has been disabled")
+ }
+
+ return enabled ? (
+
+
+
+
+ ) : (
+ <>>
+ )
+}
diff --git a/fission/src/ui/panels/DebugPanel.tsx b/fission/src/ui/panels/DebugPanel.tsx
index 6bf821e74..32034dccd 100644
--- a/fission/src/ui/panels/DebugPanel.tsx
+++ b/fission/src/ui/panels/DebugPanel.tsx
@@ -1,7 +1,6 @@
import Panel, { PanelPropsImpl } from "../components/Panel"
import Button from "../components/Button"
import World from "@/systems/World"
-import MirabufSceneObject from "@/mirabuf/MirabufSceneObject"
import { ToastType } from "../ToastContext"
import { Random } from "@/util/Random"
import MirabufCachingService, {
@@ -13,12 +12,10 @@ import { Box, styled } from "@mui/material"
import { usePanelControlContext } from "../helpers/UsePanelManager"
import APS from "@/aps/APS"
import PreferencesSystem from "@/systems/preferences/PreferencesSystem"
-import JOLT from "@/util/loading/JoltSyncLoader"
import Label from "../components/Label"
import { colorNameToVar } from "../helpers/UseThemeHelpers"
import { SynthesisIcons } from "../components/StyledComponents"
import { Global_AddToast } from "../components/GlobalUIControls"
-import { JoltRVec3_JoltVec3 } from "@/util/TypeConversions"
const LabelStyled = styled(Label)({
fontWeight: 700,
@@ -26,32 +23,13 @@ const LabelStyled = styled(Label)({
marginTop: "0.5rem",
})
-async function TestGodMode() {
- const robot: MirabufSceneObject = [...World.SceneRenderer.sceneObjects.entries()]
- .filter(x => {
- const y = x[1] instanceof MirabufSceneObject
- return y
- })
- .map(x => x[1])[0] as MirabufSceneObject
- const rootNodeId = robot.GetRootNodeId()
- if (rootNodeId == undefined) {
- console.error("Robot root node not found for god mode")
- return
+function ToggleDragMode() {
+ const dragSystem = World.DragModeSystem
+ if (dragSystem) {
+ dragSystem.enabled = !dragSystem.enabled
+ const status = dragSystem.enabled ? "enabled" : "disabled"
+ Global_AddToast?.("info", "Drag Mode", `Drag mode has been ${status}`)
}
- const robotPosition = World.PhysicsSystem.GetBody(rootNodeId).GetPosition()
- const [ghostBody, _ghostConstraint] = World.PhysicsSystem.CreateGodModeBody(
- rootNodeId,
- JoltRVec3_JoltVec3(robotPosition)
- )
-
- // Move ghostBody to demonstrate godMode movement
- await new Promise(f => setTimeout(f, 1000))
- World.PhysicsSystem.SetBodyPosition(
- ghostBody.GetID(),
- new JOLT.RVec3(robotPosition.GetX(), robotPosition.GetY() + 2, robotPosition.GetZ())
- )
- await new Promise(f => setTimeout(f, 1000))
- World.PhysicsSystem.SetBodyPosition(ghostBody.GetID(), new JOLT.RVec3(2, 2, 2))
}
const DebugPanel: React.FC = ({ panelId }) => {
@@ -96,7 +74,7 @@ const DebugPanel: React.FC = ({ panelId }) => {
}}
className="w-full"
/>
-
+