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'
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])
@@ -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(
@@ -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)
},
+11 -2
View File
@@ -7,6 +7,10 @@ const activeDoorAnimations = new Map<AnyNodeId, number>()
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?.()
}
}