fix: improve ceiling handle feedback and WebGPU placeholders

This commit is contained in:
Aymeric Rabot
2026-06-09 14:49:42 -04:00
parent 8ce26154d9
commit 265acdb2be
11 changed files with 661 additions and 55 deletions
+44 -2
View File
@@ -1,9 +1,9 @@
'use client'
import { type CeilingNode, resolveLevelId, useLiveNodeOverrides, useScene } from '@pascal-app/core'
import { PolygonEditor } from '@pascal-app/editor'
import { PolygonEditor, triggerSFX } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useCallback, useEffect } from 'react'
import { useCallback, useEffect, useRef } from 'react'
/**
* Phase 5 Stage D — ceiling boundary editor (registry-driven).
@@ -23,6 +23,8 @@ export const CeilingBoundaryEditor: React.FC<{ ceilingId: CeilingNode['id'] }> =
const updateNode = useScene((s) => s.updateNode)
const markDirty = useScene((s) => s.markDirty)
const setSelection = useViewer((s) => s.setSelection)
const setHoveredId = useViewer((s) => s.setHoveredId)
const ownsCeilingHoverRef = useRef(false)
const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null
@@ -48,10 +50,43 @@ export const CeilingBoundaryEditor: React.FC<{ ceilingId: CeilingNode['id'] }> =
[ceilingId, markDirty],
)
const setCeilingHandleHover = useCallback(
(active: boolean) => {
if (active) {
ownsCeilingHoverRef.current = true
setHoveredId(ceilingId)
return
}
if (ownsCeilingHoverRef.current && useViewer.getState().hoveredId === ceilingId) {
setHoveredId(null)
}
ownsCeilingHoverRef.current = false
},
[ceilingId, setHoveredId],
)
const handleHandleHoverChange = useCallback(
(index: number | null) => {
setCeilingHandleHover(index !== null)
},
[setCeilingHandleHover],
)
const handleDragStateChange = useCallback(
(isDragging: boolean) => {
setCeilingHandleHover(isDragging)
},
[setCeilingHandleHover],
)
useEffect(() => {
return () => {
useLiveNodeOverrides.getState().clear(ceilingId)
useScene.getState().markDirty(ceilingId)
if (ownsCeilingHoverRef.current && useViewer.getState().hoveredId === ceilingId) {
useViewer.getState().setHoveredId(null)
}
ownsCeilingHoverRef.current = false
}
}, [ceilingId])
@@ -61,10 +96,17 @@ export const CeilingBoundaryEditor: React.FC<{ ceilingId: CeilingNode['id'] }> =
<PolygonEditor
allowEdgeMove
color="#d4d4d4"
highlightConnectedHandles
levelId={resolveLevelId(ceiling, useScene.getState().nodes)}
minVertices={3}
onDragStateChange={handleDragStateChange}
onDragCommit={() => triggerSFX('sfx:item-place')}
onDragStart={() => triggerSFX('sfx:item-pick')}
onEdgeHoverChange={handleHandleHoverChange}
onMidpointHoverChange={handleHandleHoverChange}
onPolygonChange={handlePolygonChange}
onPolygonPreview={handlePolygonPreview}
onVertexHoverChange={handleHandleHoverChange}
polygon={ceiling.polygon}
surfaceHeight={ceiling.height ?? 2.5}
/>
@@ -13,13 +13,17 @@ import { BufferGeometry, Float32BufferAttribute } from 'three'
* (count 0) makes three.js create no GPU buffer for it, so vertex buffer slot 0
* is never bound and WebGPU rejects the draw with "Vertex buffer slot 0 … was
* not set", which poisons the whole command encoder (cascading into "Invalid
* CommandBuffer" on every queue submit). Three real vertices give it a bound
* buffer; the `groupCount` count-0 groups keep nothing drawn while matching the
* mesh's material-array length so raycasts / BVH never index past the materials.
* CommandBuffer" on every queue submit). The zero normals and UVs keep lit
* node-material pipelines from compiling additional required-but-unbound
* vertex buffers. Three real vertices give it bound buffers; the `groupCount`
* count-0 groups keep nothing drawn while matching the mesh's material-array
* length so raycasts / BVH never index past the materials.
*/
export function createPlaceholderGeometry(groupCount = 0): BufferGeometry {
const geometry = new BufferGeometry()
geometry.setAttribute('position', new Float32BufferAttribute(new Float32Array(9), 3))
geometry.setAttribute('normal', new Float32BufferAttribute(new Float32Array(9), 3))
geometry.setAttribute('uv', new Float32BufferAttribute(new Float32Array(6), 2))
for (let group = 0; group < groupCount; group++) {
geometry.addGroup(0, 0, group)
}
+19 -6
View File
@@ -15,8 +15,15 @@ import {
useNodeEvents,
useViewer,
} from '@pascal-app/viewer'
import { useMemo, useRef } from 'react'
import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three'
import { useEffect, useMemo, useRef } from 'react'
import {
BufferGeometry,
Float32BufferAttribute,
type Group,
Path,
Shape,
ShapeGeometry,
} from 'three'
import { MeshLambertNodeMaterial } from 'three/webgpu'
const Y_OFFSET = 0.01
@@ -134,6 +141,13 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
if (!polygonPoints || polygonPoints.length < 2) return null
return createBoundaryLineGeometry(polygonPoints)
}, [polygonPoints])
useEffect(() => () => lineGeometry?.dispose(), [lineGeometry])
const groundGeometry = useMemo(() => {
if (!groundShape) return null
return new ShapeGeometry(groundShape)
}, [groundShape])
useEffect(() => () => groundGeometry?.dispose(), [groundGeometry])
const handlers = useNodeEvents(node, 'site')
@@ -149,15 +163,14 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
))}
{/* Ground fill: site polygon with slab holes, occludes below-grade geometry */}
{groundShape && (
{groundGeometry && (
<mesh
geometry={groundGeometry}
material={groundMaterial}
position={[0, -0.05, 0]}
receiveShadow
rotation={[-Math.PI / 2, 0, 0]}
>
<shapeGeometry args={[groundShape]} />
</mesh>
/>
)}
{/* Simple boundary line */}