Fix first-person door toggles and colliders
This commit is contained in:
@@ -7,6 +7,11 @@ import { KeyboardControls } from '@react-three/drei'
|
|||||||
import { useFrame, useThree } from '@react-three/fiber'
|
import { useFrame, useThree } from '@react-three/fiber'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Box3, Euler, Matrix4, Ray, Raycaster, Vector2, Vector3 } from 'three'
|
import { Box3, Euler, Matrix4, Ray, Raycaster, Vector2, Vector3 } from 'three'
|
||||||
|
import {
|
||||||
|
animateDoorOpenState,
|
||||||
|
DOOR_SWING_OPEN_ANGLE,
|
||||||
|
isOperationDoorType,
|
||||||
|
} from '../../lib/door-interaction'
|
||||||
import useEditor from '../../store/use-editor'
|
import useEditor from '../../store/use-editor'
|
||||||
import {
|
import {
|
||||||
buildFirstPersonColliderWorldFromRegistry,
|
buildFirstPersonColliderWorldFromRegistry,
|
||||||
@@ -22,7 +27,6 @@ const CAMERA_EYE_OFFSET = 0.45
|
|||||||
const LOOK_SENSITIVITY = 0.002
|
const LOOK_SENSITIVITY = 0.002
|
||||||
const CONTROLLER_CENTER_FROM_EYE = 0.85
|
const CONTROLLER_CENTER_FROM_EYE = 0.85
|
||||||
const DOOR_INTERACTION_DISTANCE = 2.5
|
const DOOR_INTERACTION_DISTANCE = 2.5
|
||||||
const DOOR_SWING_OPEN_ANGLE = Math.PI / 2
|
|
||||||
const DOOR_LEAF_INTERACTION_DEPTH = 0.08
|
const DOOR_LEAF_INTERACTION_DEPTH = 0.08
|
||||||
const keyboardMap = [
|
const keyboardMap = [
|
||||||
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
|
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
|
||||||
@@ -43,6 +47,12 @@ const doorLeafLocalHit = new Vector3()
|
|||||||
const doorLeafLocalRay = new Ray()
|
const doorLeafLocalRay = new Ray()
|
||||||
const doorLeafMatrix = new Matrix4()
|
const doorLeafMatrix = new Matrix4()
|
||||||
const doorLeafWorldHit = new Vector3()
|
const doorLeafWorldHit = new Vector3()
|
||||||
|
const doorOpeningBox = new Box3()
|
||||||
|
const doorOpeningInverseMatrix = new Matrix4()
|
||||||
|
const doorOpeningLocalHit = new Vector3()
|
||||||
|
const doorOpeningLocalRay = new Ray()
|
||||||
|
const doorOpeningMatrix = new Matrix4()
|
||||||
|
const doorOpeningWorldHit = new Vector3()
|
||||||
const spawnWorldPosition = new Vector3()
|
const spawnWorldPosition = new Vector3()
|
||||||
const spawnWorldEuler = new Euler(0, 0, 0, 'YXZ')
|
const spawnWorldEuler = new Euler(0, 0, 0, 'YXZ')
|
||||||
|
|
||||||
@@ -56,17 +66,6 @@ const resolvePlacedSpawnNode = (
|
|||||||
return [...candidates].sort((a, b) => a.id.localeCompare(b.id))[0] ?? null
|
return [...candidates].sort((a, b) => a.id.localeCompare(b.id))[0] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDoorOpenState(
|
|
||||||
doorId: AnyNodeId,
|
|
||||||
data: { operationState?: number; swingAngle?: number },
|
|
||||||
) {
|
|
||||||
const scene = useScene.getState()
|
|
||||||
const node = scene.nodes[doorId]
|
|
||||||
scene.updateNode(doorId, data)
|
|
||||||
scene.dirtyNodes.add(doorId)
|
|
||||||
if (node?.parentId) scene.dirtyNodes.add(node.parentId as AnyNodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FirstPersonControls = () => {
|
export const FirstPersonControls = () => {
|
||||||
const { camera, gl } = useThree()
|
const { camera, gl } = useThree()
|
||||||
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
||||||
@@ -124,6 +123,39 @@ export const FirstPersonControls = () => {
|
|||||||
if (leafW <= 0 || leafH <= 0) continue
|
if (leafW <= 0 || leafH <= 0) continue
|
||||||
|
|
||||||
const leafCenterY = -node.frameThickness / 2
|
const leafCenterY = -node.frameThickness / 2
|
||||||
|
|
||||||
|
if (isOperationDoorType(node.doorType)) {
|
||||||
|
doorOpeningMatrix
|
||||||
|
.copy(object.matrixWorld)
|
||||||
|
.multiply(new Matrix4().makeTranslation(0, leafCenterY, 0))
|
||||||
|
doorOpeningInverseMatrix.copy(doorOpeningMatrix).invert()
|
||||||
|
doorOpeningBox.min.set(-leafW / 2, -leafH / 2, -DOOR_LEAF_INTERACTION_DEPTH / 2)
|
||||||
|
doorOpeningBox.max.set(leafW / 2, leafH / 2, DOOR_LEAF_INTERACTION_DEPTH / 2)
|
||||||
|
doorOpeningLocalRay
|
||||||
|
.copy(doorInteractionRaycaster.ray)
|
||||||
|
.applyMatrix4(doorOpeningInverseMatrix)
|
||||||
|
|
||||||
|
const localOpeningHit = doorOpeningLocalRay.intersectBox(
|
||||||
|
doorOpeningBox,
|
||||||
|
doorOpeningLocalHit,
|
||||||
|
)
|
||||||
|
if (!localOpeningHit) continue
|
||||||
|
|
||||||
|
doorOpeningWorldHit.copy(localOpeningHit).applyMatrix4(doorOpeningMatrix)
|
||||||
|
const openingHitDistance = doorOpeningWorldHit.distanceTo(
|
||||||
|
doorInteractionRaycaster.ray.origin,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
openingHitDistance <= DOOR_INTERACTION_DISTANCE &&
|
||||||
|
openingHitDistance < closestDistance
|
||||||
|
) {
|
||||||
|
closestDoorId = doorId as AnyNodeId
|
||||||
|
closestDistance = openingHitDistance
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -162,27 +194,25 @@ export const FirstPersonControls = () => {
|
|||||||
const node = useScene.getState().nodes[doorId]
|
const node = useScene.getState().nodes[doorId]
|
||||||
if (node?.type !== 'door' || node.openingKind === 'opening') return
|
if (node?.type !== 'door' || node.openingKind === 'opening') return
|
||||||
|
|
||||||
if (
|
if (isOperationDoorType(node.doorType)) {
|
||||||
node.doorType === 'folding' ||
|
|
||||||
node.doorType === 'pocket' ||
|
|
||||||
node.doorType === 'barn' ||
|
|
||||||
node.doorType === 'sliding' ||
|
|
||||||
node.doorType === 'garage-sectional' ||
|
|
||||||
node.doorType === 'garage-rollup' ||
|
|
||||||
node.doorType === 'garage-tiltup'
|
|
||||||
) {
|
|
||||||
const currentOpenAmount = node.operationState ?? 0
|
const currentOpenAmount = node.operationState ?? 0
|
||||||
updateDoorOpenState(doorId, {
|
animateDoorOpenState(
|
||||||
operationState: currentOpenAmount >= 0.5 ? 0 : 1,
|
doorId,
|
||||||
})
|
'operationState',
|
||||||
|
currentOpenAmount,
|
||||||
|
currentOpenAmount >= 0.5 ? 0 : 1,
|
||||||
|
rebuildColliderWorld,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const currentSwingAngle = node.swingAngle ?? 0
|
const currentSwingAngle = node.swingAngle ?? 0
|
||||||
updateDoorOpenState(doorId, {
|
animateDoorOpenState(
|
||||||
swingAngle: currentSwingAngle >= DOOR_SWING_OPEN_ANGLE / 2 ? 0 : DOOR_SWING_OPEN_ANGLE,
|
doorId,
|
||||||
})
|
'swingAngle',
|
||||||
|
currentSwingAngle,
|
||||||
|
currentSwingAngle >= DOOR_SWING_OPEN_ANGLE / 2 ? 0 : DOOR_SWING_OPEN_ANGLE,
|
||||||
|
rebuildColliderWorld,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(rebuildColliderWorld)
|
|
||||||
}, [rebuildColliderWorld, resolveInteractableDoorId])
|
}, [rebuildColliderWorld, resolveInteractableDoorId])
|
||||||
|
|
||||||
const placedSpawn = useMemo<FirstPersonSpawn | null>(() => {
|
const placedSpawn = useMemo<FirstPersonSpawn | null>(() => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const UP = new THREE.Vector3(0, 1, 0)
|
|||||||
const SPAWN_EYE_HEIGHT = 1.65
|
const SPAWN_EYE_HEIGHT = 1.65
|
||||||
const RAYCAST_CLEARANCE = 25
|
const RAYCAST_CLEARANCE = 25
|
||||||
const DOOR_LEAF_COLLIDER_DEPTH = 0.06
|
const DOOR_LEAF_COLLIDER_DEPTH = 0.06
|
||||||
|
const OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD = 0.85
|
||||||
|
|
||||||
export const FIRST_PERSON_SPAWN_EYE_HEIGHT = SPAWN_EYE_HEIGHT
|
export const FIRST_PERSON_SPAWN_EYE_HEIGHT = SPAWN_EYE_HEIGHT
|
||||||
|
|
||||||
@@ -99,19 +100,56 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
|
|||||||
const hasLeafContent = node.segments.some((segment) => segment.type !== 'empty')
|
const hasLeafContent = node.segments.some((segment) => segment.type !== 'empty')
|
||||||
if (!hasLeafContent) return null
|
if (!hasLeafContent) return null
|
||||||
|
|
||||||
|
const isOperationDoor =
|
||||||
|
node.doorType === 'folding' ||
|
||||||
|
node.doorType === 'pocket' ||
|
||||||
|
node.doorType === 'barn' ||
|
||||||
|
node.doorType === 'sliding' ||
|
||||||
|
node.doorType === 'garage-sectional' ||
|
||||||
|
node.doorType === 'garage-rollup' ||
|
||||||
|
node.doorType === 'garage-tiltup'
|
||||||
const leafW = node.width - 2 * node.frameThickness
|
const leafW = node.width - 2 * node.frameThickness
|
||||||
const leafH = node.height - node.frameThickness
|
const leafH = node.height - node.frameThickness
|
||||||
if (leafW <= 0 || leafH <= 0) return null
|
if (leafW <= 0 || leafH <= 0) return null
|
||||||
|
|
||||||
const leafCenterY = -node.frameThickness / 2
|
const leafCenterY = -node.frameThickness / 2
|
||||||
|
|
||||||
|
root.updateWorldMatrix(true, false)
|
||||||
|
|
||||||
|
if (node.doorType === 'garage-sectional' || node.doorType === 'garage-rollup') {
|
||||||
|
const openAmount =
|
||||||
|
node.doorType === 'garage-sectional'
|
||||||
|
? Math.min(1, (node.operationState ?? 0) / 0.88)
|
||||||
|
: Math.max(0, Math.min(1, node.operationState ?? 0))
|
||||||
|
const visibleHeight = leafH * (1 - openAmount)
|
||||||
|
if (visibleHeight <= 0.12) return null
|
||||||
|
|
||||||
|
const sourceGeometry = new THREE.BoxGeometry(
|
||||||
|
leafW,
|
||||||
|
visibleHeight,
|
||||||
|
DOOR_LEAF_COLLIDER_DEPTH,
|
||||||
|
).toNonIndexed()
|
||||||
|
const geometry = new THREE.BufferGeometry()
|
||||||
|
geometry.setAttribute('position', sourceGeometry.getAttribute('position').clone())
|
||||||
|
geometry.setAttribute('normal', sourceGeometry.getAttribute('normal').clone())
|
||||||
|
sourceGeometry.dispose()
|
||||||
|
const visibleCenterY = leafCenterY - leafH / 2 + visibleHeight / 2
|
||||||
|
geometry.applyMatrix4(
|
||||||
|
root.matrixWorld.clone().multiply(new THREE.Matrix4().makeTranslation(0, visibleCenterY, 0)),
|
||||||
|
)
|
||||||
|
return geometry
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOperationDoor && (node.operationState ?? 0) >= OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
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, node.swingAngle ?? 0))
|
||||||
const leafSwingRotation = clampedSwingAngle * swingDirectionSign * hingeDirectionSign
|
const leafSwingRotation = clampedSwingAngle * swingDirectionSign * hingeDirectionSign
|
||||||
|
|
||||||
root.updateWorldMatrix(true, false)
|
|
||||||
|
|
||||||
const sourceGeometry = new THREE.BoxGeometry(
|
const sourceGeometry = new THREE.BoxGeometry(
|
||||||
leafW,
|
leafW,
|
||||||
leafH,
|
leafH,
|
||||||
|
|||||||
@@ -98,6 +98,18 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|||||||
edgeMaterial.color.setHex(valid ? 0x22_c5_5e : 0xef_44_44)
|
edgeMaterial.color.setHex(valid ? 0x22_c5_5e : 0xef_44_44)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPlacementOrientation = (event: WallEvent) => {
|
||||||
|
const faceSide = getSideFromNormal(event.normal)
|
||||||
|
const side = movingDoorNode.side ?? faceSide
|
||||||
|
const rotationOffset = side !== faceSide ? Math.PI : 0
|
||||||
|
return {
|
||||||
|
side,
|
||||||
|
itemRotation: calculateItemRotation(event.normal) + rotationOffset,
|
||||||
|
cursorRotation:
|
||||||
|
calculateCursorRotation(event.normal, event.node.start, event.node.end) + rotationOffset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onWallEnter = (event: WallEvent) => {
|
const onWallEnter = (event: WallEvent) => {
|
||||||
if (!isValidWallSideFace(event.normal)) return
|
if (!isValidWallSideFace(event.normal)) return
|
||||||
if (isCurvedWall(event.node)) {
|
if (isCurvedWall(event.node)) {
|
||||||
@@ -106,9 +118,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|||||||
}
|
}
|
||||||
if (event.node.parentId !== getLevelId()) return
|
if (event.node.parentId !== getLevelId()) return
|
||||||
|
|
||||||
const side = getSideFromNormal(event.normal)
|
const { side, itemRotation, cursorRotation } = getPlacementOrientation(event)
|
||||||
const itemRotation = calculateItemRotation(event.normal)
|
|
||||||
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
|
|
||||||
|
|
||||||
const localX = snapToHalf(event.localPosition[0])
|
const localX = snapToHalf(event.localPosition[0])
|
||||||
const { clampedX, clampedY } = clampToWall(
|
const { clampedX, clampedY } = clampToWall(
|
||||||
@@ -167,9 +177,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|||||||
}
|
}
|
||||||
if (event.node.parentId !== getLevelId()) return
|
if (event.node.parentId !== getLevelId()) return
|
||||||
|
|
||||||
const side = getSideFromNormal(event.normal)
|
const { side, itemRotation, cursorRotation } = getPlacementOrientation(event)
|
||||||
const itemRotation = calculateItemRotation(event.normal)
|
|
||||||
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
|
|
||||||
|
|
||||||
const localX = snapToHalf(event.localPosition[0])
|
const localX = snapToHalf(event.localPosition[0])
|
||||||
const { clampedX, clampedY } = clampToWall(
|
const { clampedX, clampedY } = clampToWall(
|
||||||
@@ -234,8 +242,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|||||||
if (isCurvedWall(event.node)) return
|
if (isCurvedWall(event.node)) return
|
||||||
if (event.node.parentId !== getLevelId()) return
|
if (event.node.parentId !== getLevelId()) return
|
||||||
|
|
||||||
const side = getSideFromNormal(event.normal)
|
const { side, itemRotation } = getPlacementOrientation(event)
|
||||||
const itemRotation = calculateItemRotation(event.normal)
|
|
||||||
|
|
||||||
const localX = snapToHalf(event.localPosition[0])
|
const localX = snapToHalf(event.localPosition[0])
|
||||||
const { clampedX, clampedY } = clampToWall(
|
const { clampedX, clampedY } = clampToWall(
|
||||||
|
|||||||
@@ -1,55 +1,16 @@
|
|||||||
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
animateDoorOpenState,
|
||||||
|
DOOR_SWING_OPEN_ANGLE,
|
||||||
|
isOperationDoorType,
|
||||||
|
updateDoorOpenState,
|
||||||
|
} from '../lib/door-interaction'
|
||||||
import { runRedo, runUndo } from '../lib/history'
|
import { runRedo, runUndo } from '../lib/history'
|
||||||
import { sfxEmitter } from '../lib/sfx-bus'
|
import { sfxEmitter } from '../lib/sfx-bus'
|
||||||
import useEditor from '../store/use-editor'
|
import useEditor from '../store/use-editor'
|
||||||
|
|
||||||
const DOOR_SWING_OPEN_ANGLE = Math.PI / 2
|
|
||||||
const DOOR_TOGGLE_ANIMATION_MS = 520
|
|
||||||
const activeDoorAnimations = new Map<AnyNodeId, number>()
|
|
||||||
|
|
||||||
function updateDoorOpenState(
|
|
||||||
doorId: AnyNodeId,
|
|
||||||
data: { operationState?: number; swingAngle?: number },
|
|
||||||
) {
|
|
||||||
const scene = useScene.getState()
|
|
||||||
const node = scene.nodes[doorId]
|
|
||||||
scene.updateNode(doorId, data)
|
|
||||||
scene.dirtyNodes.add(doorId)
|
|
||||||
if (node?.parentId) scene.dirtyNodes.add(node.parentId as AnyNodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function animateDoorOpenState(
|
|
||||||
doorId: AnyNodeId,
|
|
||||||
field: 'operationState' | 'swingAngle',
|
|
||||||
from: number,
|
|
||||||
to: number,
|
|
||||||
) {
|
|
||||||
const existingFrame = activeDoorAnimations.get(doorId)
|
|
||||||
if (existingFrame !== undefined) {
|
|
||||||
window.cancelAnimationFrame(existingFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
const startedAt = performance.now()
|
|
||||||
const ease = (value: number) => value * value * (3 - 2 * value)
|
|
||||||
|
|
||||||
const tick = (now: number) => {
|
|
||||||
const progress = Math.min(1, (now - startedAt) / DOOR_TOGGLE_ANIMATION_MS)
|
|
||||||
const value = from + (to - from) * ease(progress)
|
|
||||||
updateDoorOpenState(doorId, { [field]: value })
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick))
|
|
||||||
} else {
|
|
||||||
activeDoorAnimations.delete(doorId)
|
|
||||||
updateDoorOpenState(doorId, { [field]: to })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools call this in their onCancel handler when they have an active mid-action to cancel,
|
// Tools call this in their onCancel handler when they have an active mid-action to cancel,
|
||||||
// so that the global Escape handler knows not to also switch to select mode.
|
// so that the global Escape handler knows not to also switch to select mode.
|
||||||
let _toolCancelConsumed = false
|
let _toolCancelConsumed = false
|
||||||
@@ -197,15 +158,7 @@ export const useKeyboard = ({
|
|||||||
if (node?.type === 'door') {
|
if (node?.type === 'door') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (node.openingKind !== 'opening') {
|
if (node.openingKind !== 'opening') {
|
||||||
if (
|
if (isOperationDoorType(node.doorType)) {
|
||||||
node.doorType === 'folding' ||
|
|
||||||
node.doorType === 'pocket' ||
|
|
||||||
node.doorType === 'barn' ||
|
|
||||||
node.doorType === 'sliding' ||
|
|
||||||
node.doorType === 'garage-sectional' ||
|
|
||||||
node.doorType === 'garage-rollup' ||
|
|
||||||
node.doorType === 'garage-tiltup'
|
|
||||||
) {
|
|
||||||
const currentOpenAmount = node.operationState ?? 0
|
const currentOpenAmount = node.operationState ?? 0
|
||||||
animateDoorOpenState(
|
animateDoorOpenState(
|
||||||
node.id,
|
node.id,
|
||||||
@@ -249,15 +202,7 @@ export const useKeyboard = ({
|
|||||||
if (node.openingKind !== 'opening') {
|
if (node.openingKind !== 'opening') {
|
||||||
updateDoorOpenState(
|
updateDoorOpenState(
|
||||||
node.id,
|
node.id,
|
||||||
node.doorType === 'folding' ||
|
isOperationDoorType(node.doorType) ? { operationState: 0 } : { swingAngle: 0 },
|
||||||
node.doorType === 'pocket' ||
|
|
||||||
node.doorType === 'barn' ||
|
|
||||||
node.doorType === 'sliding' ||
|
|
||||||
node.doorType === 'garage-sectional' ||
|
|
||||||
node.doorType === 'garage-rollup' ||
|
|
||||||
node.doorType === 'garage-tiltup'
|
|
||||||
? { operationState: 0 }
|
|
||||||
: { swingAngle: 0 },
|
|
||||||
)
|
)
|
||||||
sfxEmitter.emit('sfx:item-rotate')
|
sfxEmitter.emit('sfx:item-rotate')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
||||||
|
|
||||||
|
export const DOOR_SWING_OPEN_ANGLE = Math.PI / 2
|
||||||
|
|
||||||
|
const DOOR_TOGGLE_ANIMATION_MS = 520
|
||||||
|
const activeDoorAnimations = new Map<AnyNodeId, number>()
|
||||||
|
|
||||||
|
export function isOperationDoorType(doorType: string | undefined) {
|
||||||
|
return (
|
||||||
|
doorType === 'folding' ||
|
||||||
|
doorType === 'pocket' ||
|
||||||
|
doorType === 'barn' ||
|
||||||
|
doorType === 'sliding' ||
|
||||||
|
doorType === 'garage-sectional' ||
|
||||||
|
doorType === 'garage-rollup' ||
|
||||||
|
doorType === 'garage-tiltup'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDoorOpenState(
|
||||||
|
doorId: AnyNodeId,
|
||||||
|
data: { operationState?: number; swingAngle?: number },
|
||||||
|
) {
|
||||||
|
const scene = useScene.getState()
|
||||||
|
const node = scene.nodes[doorId]
|
||||||
|
scene.updateNode(doorId, data)
|
||||||
|
scene.dirtyNodes.add(doorId)
|
||||||
|
if (node?.parentId) scene.dirtyNodes.add(node.parentId as AnyNodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function animateDoorOpenState(
|
||||||
|
doorId: AnyNodeId,
|
||||||
|
field: 'operationState' | 'swingAngle',
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
onComplete?: () => void,
|
||||||
|
) {
|
||||||
|
const existingFrame = activeDoorAnimations.get(doorId)
|
||||||
|
if (existingFrame !== undefined) {
|
||||||
|
window.cancelAnimationFrame(existingFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedAt = performance.now()
|
||||||
|
const ease = (value: number) => value * value * (3 - 2 * value)
|
||||||
|
|
||||||
|
const tick = (now: number) => {
|
||||||
|
const progress = Math.min(1, (now - startedAt) / DOOR_TOGGLE_ANIMATION_MS)
|
||||||
|
const value = from + (to - from) * ease(progress)
|
||||||
|
updateDoorOpenState(doorId, { [field]: value })
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick))
|
||||||
|
} else {
|
||||||
|
activeDoorAnimations.delete(doorId)
|
||||||
|
updateDoorOpenState(doorId, { [field]: to })
|
||||||
|
onComplete?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user