fix: respect scene depth for site and ceiling handles

This commit is contained in:
Aymeric Rabot
2026-06-09 13:17:04 -04:00
parent 555cf06c12
commit 8ce26154d9
3 changed files with 45 additions and 25 deletions
@@ -18,9 +18,9 @@ const BRACKET_THICKNESS = 0.04
const BRACKET_HEIGHT = 0.04 const BRACKET_HEIGHT = 0.04
const BRACKET_Y_OFFSET = 0.035 const BRACKET_Y_OFFSET = 0.035
const HIT_BOX_SIZE: [number, number, number] = [0.28, 0.08, 0.28] const HIT_BOX_SIZE: [number, number, number] = [0.28, 0.08, 0.28]
// Draw the corner handles after everything else and with depth testing // Draw the corner handles after the ceiling surface so they read cleanly
// off (see materials below) so they stay visible — and clickable — even // when unobstructed, while material depth testing still lets other scene
// when a wall, roof, or the ceiling itself would otherwise occlude them. // geometry hide them.
const CORNER_RENDER_ORDER = 1000 const CORNER_RENDER_ORDER = 1000
type CornerBracketData = { type CornerBracketData = {
@@ -189,7 +189,7 @@ const CornerBracket = ({
<boxGeometry args={HIT_BOX_SIZE} /> <boxGeometry args={HIT_BOX_SIZE} />
<meshBasicMaterial <meshBasicMaterial
color={cubeColor} color={cubeColor}
depthTest={false} depthTest
depthWrite={false} depthWrite={false}
opacity={cubeOpacity} opacity={cubeOpacity}
transparent transparent
@@ -227,13 +227,7 @@ const BracketLeg = ({
rotation={[0, angle, 0]} rotation={[0, angle, 0]}
> >
<boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} /> <boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
<meshBasicMaterial <meshBasicMaterial color={color} depthTest depthWrite={false} opacity={opacity} transparent />
color={color}
depthTest={false}
depthWrite={false}
opacity={opacity}
transparent
/>
</mesh> </mesh>
) )
} }
@@ -6,6 +6,7 @@ import {
BufferGeometry, BufferGeometry,
Color, Color,
CylinderGeometry, CylinderGeometry,
DoubleSide,
ExtrudeGeometry, ExtrudeGeometry,
Float32BufferAttribute, Float32BufferAttribute,
type Line, type Line,
@@ -20,7 +21,6 @@ import {
ARROW_COLOR as EDGE_ARROW_COLOR, ARROW_COLOR as EDGE_ARROW_COLOR,
ARROW_HOVER_COLOR as EDGE_ARROW_HOVER_COLOR, ARROW_HOVER_COLOR as EDGE_ARROW_HOVER_COLOR,
ARROW_SCALE as EDGE_ARROW_SCALE, ARROW_SCALE as EDGE_ARROW_SCALE,
useArrowMaterial,
useInvisibleHitAreaMaterial, useInvisibleHitAreaMaterial,
} from '../../editor/node-arrow-handles' } from '../../editor/node-arrow-handles'
import { snapToHalf } from '../item/placement-math' import { snapToHalf } from '../item/placement-math'
@@ -181,7 +181,7 @@ function usePolygonNodeMaterial(color: string, opacity = 1): MeshBasicNodeMateri
() => () =>
new MeshBasicNodeMaterial({ new MeshBasicNodeMaterial({
color: new Color('#ffffff'), color: new Color('#ffffff'),
depthTest: false, depthTest: true,
depthWrite: true, depthWrite: true,
opacity: 1, opacity: 1,
transparent: true, transparent: true,
@@ -198,11 +198,24 @@ function usePolygonNodeMaterial(color: string, opacity = 1): MeshBasicNodeMateri
return material return material
} }
function usePolygonArrowMaterial(): MeshBasicNodeMaterial {
return useMemo(
() =>
new MeshBasicNodeMaterial({
color: new Color(EDGE_ARROW_COLOR),
depthTest: true,
depthWrite: true,
opacity: 1,
side: DoubleSide,
transparent: true,
}),
[],
)
}
// One mesh per handle: lives on SCENE_LAYER with a node material so the // One mesh per handle: lives on SCENE_LAYER with a node material so the
// post-processing ink-edge pass outlines it, and carries the pointer handlers // post-processing ink-edge pass outlines it. The visual material still
// directly so it stays grabbable — matching the registry arrow gizmos in // depth-tests, so walls/items in front can occlude it.
// node-arrow-handles.tsx. No paired hit mesh is needed; the R3F event
// raycaster picks SCENE_LAYER meshes too.
function OutlinedCylinderHandle({ function OutlinedCylinderHandle({
radius, radius,
height, height,
@@ -290,7 +303,7 @@ function OutlinedEdgeArrowHandle({
rotationY: number rotationY: number
scale: number scale: number
} & PolygonHandleHandlers) { } & PolygonHandleHandlers) {
const material = useArrowMaterial() const material = usePolygonArrowMaterial()
useEffect(() => { useEffect(() => {
material.color.set(color) material.color.set(color)
}, [color, material]) }, [color, material])
@@ -741,9 +754,9 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
const handleHeight = Math.max(MIN_HANDLE_HEIGHT, surfaceHeight + 0.02) const handleHeight = Math.max(MIN_HANDLE_HEIGHT, surfaceHeight + 0.02)
const edgeHandleY = editY + handleHeight - EDGE_HANDLE_HEIGHT / 2 const edgeHandleY = editY + handleHeight - EDGE_HANDLE_HEIGHT / 2
// Interactive handles are single SCENE_LAYER node-material meshes (like the // Interactive handles are SCENE_LAYER node-material meshes so the ink-edge
// registry arrow gizmos) so the ink-edge pass outlines them while they stay // pass outlines them while normal scene depth can hide them. The edge BAR and
// grabbable. The edge BAR and border line stay on EDITOR_LAYER, visual-only // border line stay on EDITOR_LAYER, visual-only
// (raycast disabled) so they never steal clicks from the vertex/midpoint // (raycast disabled) so they never steal clicks from the vertex/midpoint
// handles overlapping them — edge dragging starts from the chevron arrow // handles overlapping them — edge dragging starts from the chevron arrow
// outside the polygon edge. // outside the polygon edge.
@@ -38,6 +38,7 @@ const SITE_FLAG_HALO_COLOR = '#6366f1'
type TintableMaterial = { type TintableMaterial = {
color?: Color color?: Color
depthTest: boolean
depthWrite: boolean depthWrite: boolean
opacity: number opacity: number
needsUpdate: boolean needsUpdate: boolean
@@ -69,10 +70,9 @@ function SiteFlagModel({
mesh.frustumCulled = false mesh.frustumCulled = false
mesh.raycast = NO_RAYCAST mesh.raycast = NO_RAYCAST
mesh.receiveShadow = false mesh.receiveShadow = false
mesh.renderOrder = 1010
mesh.material = new MeshBasicNodeMaterial({ mesh.material = new MeshBasicNodeMaterial({
color: new Color(ARROW_COLOR), color: new Color(ARROW_COLOR),
depthTest: false, depthTest: true,
depthWrite: opacity >= 0.999, depthWrite: opacity >= 0.999,
opacity, opacity,
transparent: opacity < 0.999, transparent: opacity < 0.999,
@@ -93,6 +93,7 @@ function SiteFlagModel({
for (const material of materials as Array<MeshBasicNodeMaterial & TintableMaterial>) { for (const material of materials as Array<MeshBasicNodeMaterial & TintableMaterial>) {
material.color?.copy(color) material.color?.copy(color)
material.depthTest = true
material.opacity = opacity material.opacity = opacity
material.transparent = opacity < 0.999 material.transparent = opacity < 0.999
material.depthWrite = opacity >= 0.999 material.depthWrite = opacity >= 0.999
@@ -217,11 +218,23 @@ function SiteFlagFallback({
<group position={[0, SITE_FLAG_BASE_Y + (active ? SITE_FLAG_ACTIVE_LIFT : 0), 0]}> <group position={[0, SITE_FLAG_BASE_Y + (active ? SITE_FLAG_ACTIVE_LIFT : 0), 0]}>
<mesh layers={SCENE_LAYER} position={[0, 0.16, 0]} raycast={NO_RAYCAST}> <mesh layers={SCENE_LAYER} position={[0, 0.16, 0]} raycast={NO_RAYCAST}>
<cylinderGeometry args={[0.11, 0.16, 0.32, 24]} /> <cylinderGeometry args={[0.11, 0.16, 0.32, 24]} />
<meshBasicMaterial color={color} depthTest={false} opacity={opacity} transparent /> <meshBasicMaterial
color={color}
depthTest
depthWrite={opacity >= 0.999}
opacity={opacity}
transparent={opacity < 0.999}
/>
</mesh> </mesh>
<mesh layers={SCENE_LAYER} position={[0, 0.34, 0]} raycast={NO_RAYCAST}> <mesh layers={SCENE_LAYER} position={[0, 0.34, 0]} raycast={NO_RAYCAST}>
<cylinderGeometry args={[0.04, 0.11, 0.14, 24]} /> <cylinderGeometry args={[0.04, 0.11, 0.14, 24]} />
<meshBasicMaterial color={color} depthTest={false} opacity={opacity} transparent /> <meshBasicMaterial
color={color}
depthTest
depthWrite={opacity >= 0.999}
opacity={opacity}
transparent={opacity < 0.999}
/>
</mesh> </mesh>
</group> </group>
) )