fix(editor): improve guided manipulation and snap affordances
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
type AnyNodeId,
|
||||
emitter,
|
||||
type GridEvent,
|
||||
type GutterEvent,
|
||||
type RoofEvent,
|
||||
sceneRegistry,
|
||||
} from '@pascal-app/core'
|
||||
import { DragBoundingBox } from '@pascal-app/editor'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Vector3 } from 'three'
|
||||
|
||||
const INVALID_PREVIEW_COLOR = 0xef_44_44
|
||||
type ValidTarget = 'roof' | 'gutter'
|
||||
|
||||
export function RoofAttachmentFallbackPreview({
|
||||
activeBuildingId,
|
||||
isValidRoofTarget,
|
||||
lift = 0,
|
||||
onInvalidTarget,
|
||||
size,
|
||||
validTarget = 'roof',
|
||||
}: {
|
||||
activeBuildingId: string | null | undefined
|
||||
isValidRoofTarget?: (event: RoofEvent) => boolean
|
||||
lift?: number
|
||||
onInvalidTarget?: () => void
|
||||
size: [number, number, number]
|
||||
validTarget?: ValidTarget
|
||||
}) {
|
||||
const [position, setPosition] = useState<[number, number, number] | null>(null)
|
||||
const lastValidTargetEventRef = useRef<unknown>(null)
|
||||
const localPointRef = useRef(new Vector3())
|
||||
const isValidRoofTargetRef = useRef(isValidRoofTarget)
|
||||
const onInvalidTargetRef = useRef(onInvalidTarget)
|
||||
isValidRoofTargetRef.current = isValidRoofTarget
|
||||
onInvalidTargetRef.current = onInvalidTarget
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeBuildingId) {
|
||||
setPosition(null)
|
||||
lastValidTargetEventRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
const trackValidHit = (nativeEvent: unknown) => {
|
||||
lastValidTargetEventRef.current = nativeEvent
|
||||
setPosition(null)
|
||||
}
|
||||
const showInvalidAt = (x: number, y: number, z: number) => {
|
||||
setPosition([x, y + lift, z])
|
||||
onInvalidTargetRef.current?.()
|
||||
}
|
||||
const showInvalidAtWorld = (event: RoofEvent) => {
|
||||
const point = localPointRef.current.set(...event.position)
|
||||
const building = sceneRegistry.nodes.get(activeBuildingId as AnyNodeId)
|
||||
if (building) {
|
||||
building.updateWorldMatrix(true, false)
|
||||
building.worldToLocal(point)
|
||||
}
|
||||
showInvalidAt(point.x, 0, point.z)
|
||||
}
|
||||
const onRoofHit = (event: RoofEvent) => {
|
||||
if (isValidRoofTargetRef.current?.(event) === false) {
|
||||
showInvalidAtWorld(event)
|
||||
return
|
||||
}
|
||||
trackValidHit(event.nativeEvent)
|
||||
}
|
||||
const onGutterHit = (event: GutterEvent) => trackValidHit(event.nativeEvent)
|
||||
|
||||
const onGridMove = (event: GridEvent) => {
|
||||
if (event.nativeEvent === lastValidTargetEventRef.current) return
|
||||
const [x, y, z] = event.localPosition
|
||||
showInvalidAt(x, y, z)
|
||||
}
|
||||
|
||||
if (validTarget === 'roof') {
|
||||
emitter.on('roof:enter', onRoofHit)
|
||||
emitter.on('roof:move', onRoofHit)
|
||||
} else {
|
||||
emitter.on('gutter:enter', onGutterHit)
|
||||
emitter.on('gutter:move', onGutterHit)
|
||||
}
|
||||
emitter.on('grid:move', onGridMove)
|
||||
|
||||
return () => {
|
||||
if (validTarget === 'roof') {
|
||||
emitter.off('roof:enter', onRoofHit)
|
||||
emitter.off('roof:move', onRoofHit)
|
||||
} else {
|
||||
emitter.off('gutter:enter', onGutterHit)
|
||||
emitter.off('gutter:move', onGutterHit)
|
||||
}
|
||||
emitter.off('grid:move', onGridMove)
|
||||
}
|
||||
}, [activeBuildingId, lift, validTarget])
|
||||
|
||||
if (!(activeBuildingId && position)) return null
|
||||
|
||||
return (
|
||||
<DragBoundingBox
|
||||
color={INVALID_PREVIEW_COLOR}
|
||||
nodeId="roof-attachment-fallback"
|
||||
position={position}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user