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