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" /> -