Separate runtime door animation and add dual-side handles

This commit is contained in:
sudhir
2026-05-06 11:22:50 +05:30
parent f5f80d0f3f
commit 0de07d6130
5 changed files with 63 additions and 19 deletions
@@ -1,7 +1,7 @@
'use client' 'use client'
import '../../three-types' 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 { useViewer } from '@pascal-app/viewer'
import { KeyboardControls } from '@react-three/drei' import { KeyboardControls } from '@react-three/drei'
import { useFrame, useThree } from '@react-three/fiber' import { useFrame, useThree } from '@react-three/fiber'
@@ -195,22 +195,26 @@ export const FirstPersonControls = () => {
if (node?.type !== 'door' || node.openingKind === 'opening') return if (node?.type !== 'door' || node.openingKind === 'opening') return
if (isOperationDoorType(node.doorType)) { if (isOperationDoorType(node.doorType)) {
const currentOpenAmount = node.operationState ?? 0 const currentOpenAmount =
useInteractive.getState().doors[doorId]?.operationState ?? node.operationState ?? 0
animateDoorOpenState( animateDoorOpenState(
doorId, doorId,
'operationState', 'operationState',
currentOpenAmount, currentOpenAmount,
currentOpenAmount >= 0.5 ? 0 : 1, currentOpenAmount >= 0.5 ? 0 : 1,
rebuildColliderWorld, rebuildColliderWorld,
{ persist: false },
) )
} else { } else {
const currentSwingAngle = node.swingAngle ?? 0 const currentSwingAngle =
useInteractive.getState().doors[doorId]?.swingAngle ?? node.swingAngle ?? 0
animateDoorOpenState( animateDoorOpenState(
doorId, doorId,
'swingAngle', 'swingAngle',
currentSwingAngle, currentSwingAngle,
currentSwingAngle >= DOOR_SWING_OPEN_ANGLE / 2 ? 0 : DOOR_SWING_OPEN_ANGLE, currentSwingAngle >= DOOR_SWING_OPEN_ANGLE / 2 ? 0 : DOOR_SWING_OPEN_ANGLE,
rebuildColliderWorld, rebuildColliderWorld,
{ persist: false },
) )
} }
}, [rebuildColliderWorld, resolveInteractableDoorId]) }, [rebuildColliderWorld, resolveInteractableDoorId])
@@ -4,6 +4,7 @@ import {
type DoorNode, type DoorNode,
isOperationDoorType, isOperationDoorType,
sceneRegistry, sceneRegistry,
useInteractive,
useScene, useScene,
} from '@pascal-app/core' } from '@pascal-app/core'
import * as THREE from 'three' import * as THREE from 'three'
@@ -112,11 +113,14 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
if (leafW <= 0 || leafH <= 0) return null if (leafW <= 0 || leafH <= 0) return null
const leafCenterY = -node.frameThickness / 2 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) root.updateWorldMatrix(true, false)
if (node.doorType === 'garage-sectional' || node.doorType === 'garage-rollup') { 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) const visibleHeight = leafH * (1 - openAmount)
if (visibleHeight <= 0.12) return null if (visibleHeight <= 0.12) return null
@@ -138,7 +142,7 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
if ( if (
isOperationDoorType(node.doorType) && isOperationDoorType(node.doorType) &&
(node.operationState ?? 0) >= OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD (operationState ?? 0) >= OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD
) { ) {
return null return null
} }
@@ -146,7 +150,7 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
const hingeX = node.hingesSide === 'right' ? leafW / 2 : -leafW / 2 const hingeX = node.hingesSide === 'right' ? leafW / 2 : -leafW / 2
const swingDirectionSign = node.swingDirection === 'inward' ? 1 : -1 const swingDirectionSign = node.swingDirection === 'inward' ? 1 : -1
const hingeDirectionSign = node.hingesSide === 'right' ? 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 leafSwingRotation = clampedSwingAngle * swingDirectionSign * hingeDirectionSign
const sourceGeometry = new THREE.BoxGeometry( const sourceGeometry = new THREE.BoxGeometry(
@@ -5,6 +5,7 @@ import {
type AnyNodeId, type AnyNodeId,
DoorNode, DoorNode,
emitter, emitter,
useInteractive,
useScene, useScene,
} from '@pascal-app/core' } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer' import { useViewer } from '@pascal-app/viewer'
@@ -132,6 +133,9 @@ export function DoorPanel() {
}) })
if (!hasChange) return 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) updateNode(selectedId as AnyNode['id'], updates)
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId) useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
}, },
@@ -7,6 +7,10 @@ const activeDoorAnimations = new Map<AnyNodeId, number>()
export { isOperationDoorType } export { isOperationDoorType }
type DoorOpenAnimationOptions = {
persist?: boolean
}
export function updateDoorOpenState( export function updateDoorOpenState(
doorId: AnyNodeId, doorId: AnyNodeId,
data: { operationState?: number; swingAngle?: number }, data: { operationState?: number; swingAngle?: number },
@@ -43,6 +47,7 @@ export function animateDoorOpenState(
from: number, from: number,
to: number, to: number,
onComplete?: () => void, onComplete?: () => void,
options?: DoorOpenAnimationOptions,
) { ) {
const existingFrame = activeDoorAnimations.get(doorId) const existingFrame = activeDoorAnimations.get(doorId)
if (existingFrame !== undefined) { if (existingFrame !== undefined) {
@@ -61,8 +66,12 @@ export function animateDoorOpenState(
activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick)) activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick))
} else { } else {
activeDoorAnimations.delete(doorId) activeDoorAnimations.delete(doorId)
if (options?.persist ?? true) {
updateDoorOpenState(doorId, { [field]: to }) updateDoorOpenState(doorId, { [field]: to })
clearRuntimeDoorOpenState(doorId) clearRuntimeDoorOpenState(doorId)
} else {
setRuntimeDoorOpenState(doorId, { [field]: to })
}
onComplete?.() onComplete?.()
} }
} }
@@ -250,6 +250,7 @@ function addDoorLeaf(
segments, segments,
contentPadding, contentPadding,
handle, handle,
handleBothSides = false,
handleHeight, handleHeight,
handleSide, handleSide,
doorCloser, doorCloser,
@@ -268,6 +269,7 @@ function addDoorLeaf(
segments: DoorNode['segments'] segments: DoorNode['segments']
contentPadding: DoorNode['contentPadding'] contentPadding: DoorNode['contentPadding']
handle: boolean handle: boolean
handleBothSides?: boolean
handleHeight: number handleHeight: number
handleSide: DoorNode['handleSide'] handleSide: DoorNode['handleSide']
doorCloser: boolean doorCloser: boolean
@@ -313,6 +315,11 @@ function addDoorLeaf(
addLeafBox(baseMaterial, 0.028, 0.14, 0.01, handleX, handleY, faceZ + 0.005) 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) 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) { if (hasLeafContent && doorCloser) {
@@ -477,6 +484,16 @@ function addFoldingDoor(
handleY, handleY,
handlePoint.z + 0.045, 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( function addPocketDoor(
@@ -570,6 +587,7 @@ function addPocketDoor(
contentPadding, 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)
addBox(mesh, baseMaterial, 0.03, 0.18, leafDepth + 0.03, handleX, handleY, -leafDepth / 2 - 0.02)
} }
function addBarnDoor( function addBarnDoor(
@@ -687,6 +705,16 @@ function addBarnDoor(
handleY, handleY,
faceZ + leafDepth / 2 + 0.02, faceZ + leafDepth / 2 + 0.02,
) )
addBox(
mesh,
baseMaterial,
0.032,
0.22,
leafDepth + 0.034,
handleX,
handleY,
faceZ - leafDepth / 2 - 0.02,
)
} }
function addSlidingDoor( function addSlidingDoor(
@@ -732,7 +760,7 @@ function addSlidingDoor(
const backZ = -leafDepth / 2 - 0.006 const backZ = -leafDepth / 2 - 0.006
const railY = leafCenterY + panelHeight / 2 - Math.min(frameThickness * 0.35, 0.02) const railY = leafCenterY + panelHeight / 2 - Math.min(frameThickness * 0.35, 0.02)
const handleY = handleHeight - doorHeight / 2 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(mesh, revealMaterial, insideWidth, 0.024, Math.max(frameDepth * 0.32, 0.026), 0, railY, 0)
addBox( addBox(
@@ -788,16 +816,8 @@ function addSlidingDoor(
contentPadding, contentPadding,
keepFrameWhenEmpty: true, keepFrameWhenEmpty: true,
}) })
addBox( addBox(mesh, baseMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ + leafDepth / 2 + 0.01)
mesh, addBox(mesh, baseMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ - leafDepth / 2 - 0.01)
baseMaterial,
0.028,
0.18,
leafDepth + 0.03,
handleX,
handleY,
frontZ + leafDepth / 2 + 0.018,
)
} }
function addGarageSectionalDoor( function addGarageSectionalDoor(
@@ -1299,6 +1319,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
segments, segments,
contentPadding, contentPadding,
handle, handle,
handleBothSides: doorType === 'double' || doorType === 'french',
handleHeight, handleHeight,
handleSide: 'right', handleSide: 'right',
doorCloser, doorCloser,
@@ -1318,6 +1339,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
segments, segments,
contentPadding, contentPadding,
handle, handle,
handleBothSides: doorType === 'double' || doorType === 'french',
handleHeight, handleHeight,
handleSide: 'left', handleSide: 'left',
doorCloser: false, doorCloser: false,
@@ -1340,6 +1362,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
segments, segments,
contentPadding, contentPadding,
handle, handle,
handleBothSides: doorType === 'hinged',
handleHeight, handleHeight,
handleSide, handleSide,
doorCloser, doorCloser,