Separate runtime door animation and add dual-side handles
This commit is contained in:
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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?.()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user