diff --git a/packages/editor/src/components/editor/first-person-controls.tsx b/packages/editor/src/components/editor/first-person-controls.tsx index de4297a3..cbf1d07e 100644 --- a/packages/editor/src/components/editor/first-person-controls.tsx +++ b/packages/editor/src/components/editor/first-person-controls.tsx @@ -1,7 +1,7 @@ 'use client' import '../../three-types' -import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core' +import { type AnyNodeId, sceneRegistry, useInteractive, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { KeyboardControls } from '@react-three/drei' import { useFrame, useThree } from '@react-three/fiber' @@ -195,22 +195,26 @@ export const FirstPersonControls = () => { if (node?.type !== 'door' || node.openingKind === 'opening') return if (isOperationDoorType(node.doorType)) { - const currentOpenAmount = node.operationState ?? 0 + const currentOpenAmount = + useInteractive.getState().doors[doorId]?.operationState ?? node.operationState ?? 0 animateDoorOpenState( doorId, 'operationState', currentOpenAmount, currentOpenAmount >= 0.5 ? 0 : 1, rebuildColliderWorld, + { persist: false }, ) } else { - const currentSwingAngle = node.swingAngle ?? 0 + const currentSwingAngle = + useInteractive.getState().doors[doorId]?.swingAngle ?? node.swingAngle ?? 0 animateDoorOpenState( doorId, 'swingAngle', currentSwingAngle, currentSwingAngle >= DOOR_SWING_OPEN_ANGLE / 2 ? 0 : DOOR_SWING_OPEN_ANGLE, rebuildColliderWorld, + { persist: false }, ) } }, [rebuildColliderWorld, resolveInteractableDoorId]) diff --git a/packages/editor/src/components/editor/first-person/build-collider-world.ts b/packages/editor/src/components/editor/first-person/build-collider-world.ts index 9fa57b91..a908d3e0 100644 --- a/packages/editor/src/components/editor/first-person/build-collider-world.ts +++ b/packages/editor/src/components/editor/first-person/build-collider-world.ts @@ -4,6 +4,7 @@ import { type DoorNode, isOperationDoorType, sceneRegistry, + useInteractive, useScene, } from '@pascal-app/core' import * as THREE from 'three' @@ -112,11 +113,14 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) { if (leafW <= 0 || leafH <= 0) return null const leafCenterY = -node.frameThickness / 2 + const runtimeDoorState = useInteractive.getState().doors[node.id] + const operationState = runtimeDoorState?.operationState ?? node.operationState + const swingAngle = runtimeDoorState?.swingAngle ?? node.swingAngle root.updateWorldMatrix(true, false) if (node.doorType === 'garage-sectional' || node.doorType === 'garage-rollup') { - const openAmount = getGarageVisibleOpeningRatio(node.doorType, node.operationState) + const openAmount = getGarageVisibleOpeningRatio(node.doorType, operationState) const visibleHeight = leafH * (1 - openAmount) if (visibleHeight <= 0.12) return null @@ -138,7 +142,7 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) { if ( isOperationDoorType(node.doorType) && - (node.operationState ?? 0) >= OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD + (operationState ?? 0) >= OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD ) { return null } @@ -146,7 +150,7 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) { const hingeX = node.hingesSide === 'right' ? leafW / 2 : -leafW / 2 const swingDirectionSign = node.swingDirection === 'inward' ? 1 : -1 const hingeDirectionSign = node.hingesSide === 'right' ? 1 : -1 - const clampedSwingAngle = Math.max(0, Math.min(Math.PI / 2, node.swingAngle ?? 0)) + const clampedSwingAngle = Math.max(0, Math.min(Math.PI / 2, swingAngle ?? 0)) const leafSwingRotation = clampedSwingAngle * swingDirectionSign * hingeDirectionSign const sourceGeometry = new THREE.BoxGeometry( diff --git a/packages/editor/src/components/ui/panels/door-panel.tsx b/packages/editor/src/components/ui/panels/door-panel.tsx index 77b009d7..891ff9bd 100755 --- a/packages/editor/src/components/ui/panels/door-panel.tsx +++ b/packages/editor/src/components/ui/panels/door-panel.tsx @@ -5,6 +5,7 @@ import { type AnyNodeId, DoorNode, emitter, + useInteractive, useScene, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' @@ -132,6 +133,9 @@ export function DoorPanel() { }) if (!hasChange) return + if ('operationState' in updates || 'swingAngle' in updates || 'doorType' in updates) { + useInteractive.getState().removeDoorOpenState(selectedId as AnyNodeId) + } updateNode(selectedId as AnyNode['id'], updates) useScene.getState().dirtyNodes.add(selectedId as AnyNodeId) }, diff --git a/packages/editor/src/lib/door-interaction.ts b/packages/editor/src/lib/door-interaction.ts index 00c162e7..a37026bb 100644 --- a/packages/editor/src/lib/door-interaction.ts +++ b/packages/editor/src/lib/door-interaction.ts @@ -7,6 +7,10 @@ const activeDoorAnimations = new Map() export { isOperationDoorType } +type DoorOpenAnimationOptions = { + persist?: boolean +} + export function updateDoorOpenState( doorId: AnyNodeId, data: { operationState?: number; swingAngle?: number }, @@ -43,6 +47,7 @@ export function animateDoorOpenState( from: number, to: number, onComplete?: () => void, + options?: DoorOpenAnimationOptions, ) { const existingFrame = activeDoorAnimations.get(doorId) if (existingFrame !== undefined) { @@ -61,8 +66,12 @@ export function animateDoorOpenState( activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick)) } else { activeDoorAnimations.delete(doorId) - updateDoorOpenState(doorId, { [field]: to }) - clearRuntimeDoorOpenState(doorId) + if (options?.persist ?? true) { + updateDoorOpenState(doorId, { [field]: to }) + clearRuntimeDoorOpenState(doorId) + } else { + setRuntimeDoorOpenState(doorId, { [field]: to }) + } onComplete?.() } } diff --git a/packages/viewer/src/systems/door/door-system.tsx b/packages/viewer/src/systems/door/door-system.tsx index cba46835..4ca304e8 100644 --- a/packages/viewer/src/systems/door/door-system.tsx +++ b/packages/viewer/src/systems/door/door-system.tsx @@ -250,6 +250,7 @@ function addDoorLeaf( segments, contentPadding, handle, + handleBothSides = false, handleHeight, handleSide, doorCloser, @@ -268,6 +269,7 @@ function addDoorLeaf( segments: DoorNode['segments'] contentPadding: DoorNode['contentPadding'] handle: boolean + handleBothSides?: boolean handleHeight: number handleSide: DoorNode['handleSide'] doorCloser: boolean @@ -313,6 +315,11 @@ function addDoorLeaf( addLeafBox(baseMaterial, 0.028, 0.14, 0.01, handleX, handleY, faceZ + 0.005) addLeafBox(baseMaterial, 0.022, 0.1, 0.035, handleX, handleY, faceZ + 0.025) + + if (handleBothSides) { + addLeafBox(baseMaterial, 0.028, 0.14, 0.01, handleX, handleY, -faceZ - 0.005) + addLeafBox(baseMaterial, 0.022, 0.1, 0.035, handleX, handleY, -faceZ - 0.025) + } } if (hasLeafContent && doorCloser) { @@ -477,6 +484,16 @@ function addFoldingDoor( handleY, handlePoint.z + 0.045, ) + addBox( + mesh, + baseMaterial, + 0.035, + 0.16, + leafDepth + 0.035, + handlePoint.x - 0.035, + handleY, + handlePoint.z - 0.045, + ) } function addPocketDoor( @@ -570,6 +587,7 @@ function addPocketDoor( contentPadding, }) addBox(mesh, baseMaterial, 0.03, 0.18, leafDepth + 0.03, handleX, handleY, leafDepth / 2 + 0.02) + addBox(mesh, baseMaterial, 0.03, 0.18, leafDepth + 0.03, handleX, handleY, -leafDepth / 2 - 0.02) } function addBarnDoor( @@ -687,6 +705,16 @@ function addBarnDoor( handleY, faceZ + leafDepth / 2 + 0.02, ) + addBox( + mesh, + baseMaterial, + 0.032, + 0.22, + leafDepth + 0.034, + handleX, + handleY, + faceZ - leafDepth / 2 - 0.02, + ) } function addSlidingDoor( @@ -732,7 +760,7 @@ function addSlidingDoor( const backZ = -leafDepth / 2 - 0.006 const railY = leafCenterY + panelHeight / 2 - Math.min(frameThickness * 0.35, 0.02) const handleY = handleHeight - doorHeight / 2 - const handleX = activeX - activeSign * (panelWidth / 2 - 0.06) + const handleX = activeX + activeSign * (panelWidth / 2 - 0.06) addBox(mesh, revealMaterial, insideWidth, 0.024, Math.max(frameDepth * 0.32, 0.026), 0, railY, 0) addBox( @@ -788,16 +816,8 @@ function addSlidingDoor( contentPadding, keepFrameWhenEmpty: true, }) - addBox( - mesh, - baseMaterial, - 0.028, - 0.18, - leafDepth + 0.03, - handleX, - handleY, - frontZ + leafDepth / 2 + 0.018, - ) + addBox(mesh, baseMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ + leafDepth / 2 + 0.01) + addBox(mesh, baseMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ - leafDepth / 2 - 0.01) } function addGarageSectionalDoor( @@ -1299,6 +1319,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) { segments, contentPadding, handle, + handleBothSides: doorType === 'double' || doorType === 'french', handleHeight, handleSide: 'right', doorCloser, @@ -1318,6 +1339,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) { segments, contentPadding, handle, + handleBothSides: doorType === 'double' || doorType === 'french', handleHeight, handleSide: 'left', doorCloser: false, @@ -1340,6 +1362,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) { segments, contentPadding, handle, + handleBothSides: doorType === 'hinged', handleHeight, handleSide, doorCloser,