diff --git a/packages/nodes/src/wall/wall-batch-system.test.ts b/packages/nodes/src/wall/wall-batch-system.test.ts index bc6ce39aa..286bcb266 100644 --- a/packages/nodes/src/wall/wall-batch-system.test.ts +++ b/packages/nodes/src/wall/wall-batch-system.test.ts @@ -1,7 +1,8 @@ import { afterEach, describe, expect, test } from 'bun:test' import { sceneRegistry, useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' import { BufferGeometry, Float32BufferAttribute, Mesh, MeshBasicMaterial } from 'three' -import { collectWallBatchCandidates } from './wall-batch-system' +import { collectTintedWalls, collectWallBatchCandidates } from './wall-batch-system' const registeredIds: string[] = [] @@ -14,6 +15,7 @@ afterEach(() => { sceneRegistry.nodes.delete(id) } useScene.setState({ nodes: {}, rootNodeIds: [] } as never) + useViewer.setState({ hoverHighlightMode: 'default', hoveredId: null } as never) }) function registerWall(id: string) { @@ -45,3 +47,28 @@ describe('collectWallBatchCandidates', () => { expect(candidates.map((candidate) => candidate.nodeId)).toEqual(wallIds.slice(8)) }) }) + +describe('collectTintedWalls', () => { + // Regression: hover feedback for a wall is an outline, and the outline node + // renders through the main camera — which never sees a sewn wall. A hovered + // wall has to leave its batch or hovering it lights up nothing, in select + // mode and in paint mode alike. + const wallIds = new Set(['wall_a', 'wall_b']) + + test.each([ + 'default', + 'paint-ready', + 'paint-disabled', + 'delete', + ])('a %s hover takes the wall out of its batch', (hoverHighlightMode) => { + useViewer.setState({ hoverHighlightMode, hoveredId: 'wall_a' } as never) + + expect([...collectTintedWalls(wallIds)]).toEqual(['wall_a']) + }) + + test('a hover over a non-wall node tints nothing', () => { + useViewer.setState({ hoverHighlightMode: 'default', hoveredId: 'slab_a' } as never) + + expect([...collectTintedWalls(wallIds)]).toEqual([]) + }) +}) diff --git a/packages/nodes/src/wall/wall-batch-system.tsx b/packages/nodes/src/wall/wall-batch-system.tsx index bb1694651..e4dc99f80 100644 --- a/packages/nodes/src/wall/wall-batch-system.tsx +++ b/packages/nodes/src/wall/wall-batch-system.tsx @@ -182,21 +182,29 @@ export function canBatchWalls(wallMode: WallMode, isolationActive: boolean): boo } /** - * Walls the cutaway pass is currently tinting — a selection or a delete hover. + * Walls the viewer is currently lighting up — a selection, or any hover. * - * It paints them by swapping the materials on the wall's own mesh, which the - * merged mesh does not follow, so a lit wall goes back to drawing itself. There - * are only ever a handful, and a handful of extra draw calls is what the tint - * costs. + * A selection or delete hover paints the wall by swapping the materials on its + * own mesh, which the merged mesh does not follow. Every other hover draws an + * outline instead, and that needs the same thing for a different reason: the + * outline node renders `outliner.hoveredObjects` through the main camera, which + * enables no batched layer, so a sewn wall reaches neither mask pass and + * hovering it lights up nothing at all — in select mode, and in paint mode + * where the outline is the only signal for which surface the next click lands + * on. + * + * Both wants are the same one: a lit wall goes back to drawing its own + * geometry. Only ever a handful are lit at once, and a handful of extra draw + * calls is what lighting them costs. */ -function collectTintedWalls(wallIds: ReadonlySet): Set { +export function collectTintedWalls(wallIds: ReadonlySet): Set { const viewer = useViewer.getState() const tinted = new Set() for (const id of viewer.selection.selectedIds) if (wallIds.has(id)) tinted.add(id) for (const id of viewer.previewSelectedIds) if (wallIds.has(id)) tinted.add(id) - const hovered = viewer.hoverHighlightMode === 'delete' ? viewer.hoveredId : null + const hovered = viewer.hoveredId if (hovered && wallIds.has(hovered)) tinted.add(hovered) return tinted