Move door animations into viewer system
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import '../../three-types'
|
||||
import { type AnyNodeId, sceneRegistry, useInteractive, useScene } from '@pascal-app/core'
|
||||
import { type AnyNodeId, emitter, 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'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Box3, Euler, Matrix4, Ray, Raycaster, Vector2, Vector3 } from 'three'
|
||||
import {
|
||||
animateDoorOpenState,
|
||||
DOOR_SWING_OPEN_ANGLE,
|
||||
isOperationDoorType,
|
||||
toggleDoorOpenState,
|
||||
} from '../../lib/door-interaction'
|
||||
import useEditor from '../../store/use-editor'
|
||||
import {
|
||||
@@ -159,7 +159,9 @@ export const FirstPersonControls = () => {
|
||||
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(DOOR_SWING_OPEN_ANGLE, node.swingAngle ?? 0))
|
||||
const currentSwingAngle =
|
||||
useInteractive.getState().doors[doorId as AnyNodeId]?.swingAngle ?? node.swingAngle ?? 0
|
||||
const clampedSwingAngle = Math.max(0, Math.min(DOOR_SWING_OPEN_ANGLE, currentSwingAngle))
|
||||
const leafSwingRotation = clampedSwingAngle * swingDirectionSign * hingeDirectionSign
|
||||
|
||||
doorLeafMatrix
|
||||
@@ -194,30 +196,8 @@ export const FirstPersonControls = () => {
|
||||
const node = useScene.getState().nodes[doorId]
|
||||
if (node?.type !== 'door' || node.openingKind === 'opening') return
|
||||
|
||||
if (isOperationDoorType(node.doorType)) {
|
||||
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 =
|
||||
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])
|
||||
toggleDoorOpenState(doorId, { persist: false })
|
||||
}, [resolveInteractableDoorId])
|
||||
|
||||
const placedSpawn = useMemo<FirstPersonSpawn | null>(() => {
|
||||
if (!(placedSpawnNode && placedSpawnNode.type === 'spawn')) return null
|
||||
@@ -258,6 +238,11 @@ export const FirstPersonControls = () => {
|
||||
}
|
||||
}, [rebuildColliderWorld])
|
||||
|
||||
useEffect(() => {
|
||||
emitter.on('door:animation-completed', rebuildColliderWorld)
|
||||
return () => emitter.off('door:animation-completed', rebuildColliderWorld)
|
||||
}, [rebuildColliderWorld])
|
||||
|
||||
useEffect(() => {
|
||||
if (!world) return
|
||||
if (controllerStart) return
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
animateDoorOpenState,
|
||||
DOOR_SWING_OPEN_ANGLE,
|
||||
isOperationDoorType,
|
||||
updateDoorOpenState,
|
||||
} from '../lib/door-interaction'
|
||||
import { closeDoorOpenState, toggleDoorOpenState } from '../lib/door-interaction'
|
||||
import { runRedo, runUndo } from '../lib/history'
|
||||
import { sfxEmitter } from '../lib/sfx-bus'
|
||||
import useEditor from '../store/use-editor'
|
||||
@@ -158,23 +153,7 @@ export const useKeyboard = ({
|
||||
if (node?.type === 'door') {
|
||||
e.preventDefault()
|
||||
if (node.openingKind !== 'opening') {
|
||||
if (isOperationDoorType(node.doorType)) {
|
||||
const currentOpenAmount = node.operationState ?? 0
|
||||
animateDoorOpenState(
|
||||
node.id,
|
||||
'operationState',
|
||||
currentOpenAmount,
|
||||
currentOpenAmount >= 0.5 ? 0 : 1,
|
||||
)
|
||||
} else {
|
||||
const currentSwingAngle = node.swingAngle ?? 0
|
||||
animateDoorOpenState(
|
||||
node.id,
|
||||
'swingAngle',
|
||||
currentSwingAngle,
|
||||
currentSwingAngle >= DOOR_SWING_OPEN_ANGLE / 2 ? 0 : DOOR_SWING_OPEN_ANGLE,
|
||||
)
|
||||
}
|
||||
toggleDoorOpenState(node.id)
|
||||
sfxEmitter.emit('sfx:item-rotate')
|
||||
}
|
||||
} else if (node && 'rotation' in node) {
|
||||
@@ -200,10 +179,7 @@ export const useKeyboard = ({
|
||||
if (node?.type === 'door') {
|
||||
e.preventDefault()
|
||||
if (node.openingKind !== 'opening') {
|
||||
updateDoorOpenState(
|
||||
node.id,
|
||||
isOperationDoorType(node.doorType) ? { operationState: 0 } : { swingAngle: 0 },
|
||||
)
|
||||
closeDoorOpenState(node.id)
|
||||
sfxEmitter.emit('sfx:item-rotate')
|
||||
}
|
||||
} else if (node && 'rotation' in node) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { type AnyNodeId, isOperationDoorType, useInteractive, useScene } from '@pascal-app/core'
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type DoorInteractiveState,
|
||||
isOperationDoorType,
|
||||
useInteractive,
|
||||
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 const DOOR_TOGGLE_ANIMATION_MS = 520
|
||||
|
||||
export { isOperationDoorType }
|
||||
|
||||
@@ -11,70 +15,74 @@ type DoorOpenAnimationOptions = {
|
||||
persist?: boolean
|
||||
}
|
||||
|
||||
export function updateDoorOpenState(
|
||||
function getDisplayedDoorValue(
|
||||
doorId: AnyNodeId,
|
||||
data: { operationState?: number; swingAngle?: number },
|
||||
field: keyof DoorInteractiveState,
|
||||
nodeValue: number | undefined,
|
||||
) {
|
||||
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)
|
||||
const interactive = useInteractive.getState()
|
||||
const runtimeValue = interactive.doors[doorId]?.[field]
|
||||
if (runtimeValue !== undefined) return runtimeValue
|
||||
|
||||
const queuedValue = interactive.doorAnimations[doorId]?.from
|
||||
if (queuedValue !== undefined) return queuedValue
|
||||
|
||||
return nodeValue ?? 0
|
||||
}
|
||||
|
||||
function setRuntimeDoorOpenState(
|
||||
function startDoorOpenAnimation(
|
||||
doorId: AnyNodeId,
|
||||
data: { operationState?: number; swingAngle?: number },
|
||||
) {
|
||||
const scene = useScene.getState()
|
||||
const node = scene.nodes[doorId]
|
||||
useInteractive.getState().setDoorOpenState(doorId, data)
|
||||
scene.dirtyNodes.add(doorId)
|
||||
if (node?.parentId) scene.dirtyNodes.add(node.parentId as AnyNodeId)
|
||||
}
|
||||
|
||||
function clearRuntimeDoorOpenState(doorId: AnyNodeId) {
|
||||
const scene = useScene.getState()
|
||||
const node = scene.nodes[doorId]
|
||||
useInteractive.getState().removeDoorOpenState(doorId)
|
||||
scene.dirtyNodes.add(doorId)
|
||||
if (node?.parentId) scene.dirtyNodes.add(node.parentId as AnyNodeId)
|
||||
}
|
||||
|
||||
export function animateDoorOpenState(
|
||||
doorId: AnyNodeId,
|
||||
field: 'operationState' | 'swingAngle',
|
||||
field: keyof DoorInteractiveState,
|
||||
from: number,
|
||||
to: number,
|
||||
onComplete?: () => void,
|
||||
options?: DoorOpenAnimationOptions,
|
||||
) {
|
||||
const existingFrame = activeDoorAnimations.get(doorId)
|
||||
if (existingFrame !== undefined) {
|
||||
window.cancelAnimationFrame(existingFrame)
|
||||
useInteractive.getState().startDoorAnimation(doorId, {
|
||||
field,
|
||||
from,
|
||||
to,
|
||||
startedAt: null,
|
||||
durationMs: DOOR_TOGGLE_ANIMATION_MS,
|
||||
persist: options?.persist ?? true,
|
||||
})
|
||||
}
|
||||
|
||||
export function toggleDoorOpenState(doorId: AnyNodeId, options?: DoorOpenAnimationOptions) {
|
||||
const node = useScene.getState().nodes[doorId]
|
||||
if (node?.type !== 'door' || node.openingKind === 'opening') return
|
||||
|
||||
if (isOperationDoorType(node.doorType)) {
|
||||
const currentOpenAmount = getDisplayedDoorValue(doorId, 'operationState', node.operationState)
|
||||
startDoorOpenAnimation(
|
||||
doorId,
|
||||
'operationState',
|
||||
currentOpenAmount,
|
||||
currentOpenAmount >= 0.5 ? 0 : 1,
|
||||
options,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const startedAt = performance.now()
|
||||
const ease = (value: number) => value * value * (3 - 2 * value)
|
||||
const currentSwingAngle = getDisplayedDoorValue(doorId, 'swingAngle', node.swingAngle)
|
||||
startDoorOpenAnimation(
|
||||
doorId,
|
||||
'swingAngle',
|
||||
currentSwingAngle,
|
||||
currentSwingAngle >= DOOR_SWING_OPEN_ANGLE / 2 ? 0 : DOOR_SWING_OPEN_ANGLE,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
const tick = (now: number) => {
|
||||
const progress = Math.min(1, (now - startedAt) / DOOR_TOGGLE_ANIMATION_MS)
|
||||
const value = from + (to - from) * ease(progress)
|
||||
setRuntimeDoorOpenState(doorId, { [field]: value })
|
||||
export function closeDoorOpenState(doorId: AnyNodeId, options?: DoorOpenAnimationOptions) {
|
||||
const node = useScene.getState().nodes[doorId]
|
||||
if (node?.type !== 'door' || node.openingKind === 'opening') return
|
||||
|
||||
if (progress < 1) {
|
||||
activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick))
|
||||
} else {
|
||||
activeDoorAnimations.delete(doorId)
|
||||
if (options?.persist ?? true) {
|
||||
updateDoorOpenState(doorId, { [field]: to })
|
||||
clearRuntimeDoorOpenState(doorId)
|
||||
} else {
|
||||
setRuntimeDoorOpenState(doorId, { [field]: to })
|
||||
}
|
||||
onComplete?.()
|
||||
}
|
||||
if (isOperationDoorType(node.doorType)) {
|
||||
const currentOpenAmount = getDisplayedDoorValue(doorId, 'operationState', node.operationState)
|
||||
startDoorOpenAnimation(doorId, 'operationState', currentOpenAmount, 0, options)
|
||||
return
|
||||
}
|
||||
|
||||
activeDoorAnimations.set(doorId, window.requestAnimationFrame(tick))
|
||||
const currentSwingAngle = getDisplayedDoorValue(doorId, 'swingAngle', node.swingAngle)
|
||||
startDoorOpenAnimation(doorId, 'swingAngle', currentSwingAngle, 0, options)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user