Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/nodes/src/wall/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,31 @@ const WallRenderer = ({ node }: { node: WallNode }) => {
}
}, [collisionPlaceholderGeometry, placeholderGeometry])

const handlers = useNodeEvents(node, 'wall')
const rawHandlers = useNodeEvents(node, 'wall')
// Hidden walls are pointer-TRANSPARENT: when the wall-mode pass hides this
// wall (`WallCutout` stamps `userData.wallHidden` — X-ray 'down' mode,
// cutaway-hidden faces, auto-mode interior partitions), its invisible
// full-height collision mesh must not swallow pointer events aimed at
// visible objects behind it (wall-mounted plugin nodes, items). Returning
// early without stopPropagation lets R3F continue to the next intersection.
// Delete mode keeps the events so hidden walls stay hover-targetable for
// deletion (the deleteInvisible highlight flow).
const handlers = useMemo(() => {
const gated = {} as typeof rawHandlers
for (const key of Object.keys(rawHandlers) as (keyof typeof rawHandlers)[]) {
const fn = rawHandlers[key] as (e: unknown) => void
;(gated as Record<string, (e: unknown) => void>)[key] = (e: unknown) => {
if (
ref.current?.userData?.wallHidden === true &&
useViewer.getState().hoverHighlightMode !== 'delete'
) {
return
}
fn(e)
}
}
return gated
}, [rawHandlers])
const shading = useViewer((s) => s.shading)
const textures = useViewer((s) => s.textures)
const colorPreset = useViewer((s) => s.colorPreset)
Expand Down
13 changes: 13 additions & 0 deletions packages/viewer/src/systems/wall/wall-cutout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,19 @@ export const WallCutout = () => {
if (wallNode?.type !== 'wall') return

const hideWall = getWallHideState(wallNode, wallMesh as Mesh, wallMode, u)
// Pointer transparency for hidden walls: the wall's full-height
// collision mesh keeps raycasting even when the wall draws with the
// invisible material ('down' mode, cutaway-hidden faces, auto-mode
// interior partitions), so it silently swallows clicks aimed at
// VISIBLE objects standing behind it — e.g. a plugin's wall-mounted
// device/service boxes in X-ray mode (night-5 D4: the arm click on a
// south-wall receptacle selected an invisible wall two meters in
// front of it instead, and the follow-up click committed a WALL
// move). The wall renderer's pointer handlers read this stamp and
// pass hidden walls through (delete mode excepted — hidden walls
// must stay hover-targetable for deletion). Translucent walls are
// visible, so they keep their events.
;(wallMesh as Mesh).userData.wallHidden = wallMode !== 'translucent' && hideWall
const isDeleteHighlighted = deleteHoveredWallId === wallId
const isSelectionHighlighted = !isDeleteHighlighted && highlightedWallIds.has(wallId)
const levelId = resolveLevelId(wallNode, sceneState.nodes)
Expand Down
Loading