Separate runtime door animation and add dual-side handles
This commit is contained in:
@@ -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)
|
||||||
updateDoorOpenState(doorId, { [field]: to })
|
if (options?.persist ?? true) {
|
||||||
clearRuntimeDoorOpenState(doorId)
|
updateDoorOpenState(doorId, { [field]: to })
|
||||||
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user