From 3ec838824a041ee2c517b6c958557a4d2693feaa Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Fri, 21 Aug 2026 17:30:45 -0400 Subject: [PATCH] fix(core): a slab-sourced wall draft is never coerced to a ground base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #699. In a fresh scene the newly drawn slab is the only registered top surface, so the pointer resolver hands the wall tool a node-top sourceNodeId; the tool then treated ANY source as flatConstructionBase and resolveWallConstruction force-elected 'ground', stamping the level height and the slab elevation as supportOffset — walls on slabs in new projects persisted terrain-hosted. Scenes with pre-existing geometry route the ray differently, which is why seeded fixtures never showed it. The fix is deliberately narrow: a construction source that IS a slab suppresses the flat-base ground coercion, nothing more — normal capped support election picks the underlying slab, caller-supplied preferences stay authoritative, and genuine terrain drafts keep their by-design explicit height/offset stamping. (A first attempt pinned the source slab as preferredSlabId; review caught that it would break cross-slab walls, now covered by tests: majority-coverage slab wins, a grazing source slab is not pinned, and the fresh-scene shape stays plane-bound with live election resolving the slab.) Acceptance: private-editor's e2e wall-plane-bound spec (fresh org project, currently test.fixme'd on this bug) passes twice with the fix; wall-hover and undo-redo e2e unaffected. Co-Authored-By: Claude Fable 5 --- .../hooks/spatial-grid/support-host-patch.ts | 21 +-- .../tools/wall/wall-drafting.test.ts | 130 ++++++++++++++++++ 2 files changed, 142 insertions(+), 9 deletions(-) diff --git a/packages/core/src/hooks/spatial-grid/support-host-patch.ts b/packages/core/src/hooks/spatial-grid/support-host-patch.ts index f2e4556a8..3d5deb3ca 100644 --- a/packages/core/src/hooks/spatial-grid/support-host-patch.ts +++ b/packages/core/src/hooks/spatial-grid/support-host-patch.ts @@ -443,6 +443,9 @@ export function resolveWallConstruction( let resolvedNodes = nodes let sourceSupportUpdate: WallConstructionResolution['sourceSupportUpdate'] = null const constructionSourceNodeId = options?.constructionSourceNodeId + const constructionSourceIsSlab = constructionSourceNodeId + ? nodes[constructionSourceNodeId]?.type === 'slab' + : false if (constructionSourceNodeId) { const sourceNode = nodes[constructionSourceNodeId] const currentSupport = @@ -475,13 +478,14 @@ export function resolveWallConstruction( !options.flatConstructionBase ? undefined : options - const preferredSupportSlabId = - wallOptions?.flatConstructionBase === true - ? GROUND_SUPPORT_ID - : (wallOptions?.preferredSupportSlabId ?? - (wallOptions?.constructionElevation != null && terrainBase != null - ? GROUND_SUPPORT_ID - : null)) + const flatConstructionBase = + wallOptions?.flatConstructionBase === true && !constructionSourceIsSlab + const preferredSupportSlabId = flatConstructionBase + ? GROUND_SUPPORT_ID + : (wallOptions?.preferredSupportSlabId ?? + (wallOptions?.constructionElevation != null && terrainBase != null + ? GROUND_SUPPORT_ID + : null)) const supportPatch = resolveWallSupportSlabPatch(wallWithParent, resolvedNodes, { maxElevation: wallOptions?.supportCap ?? null, preferredSlabId: preferredSupportSlabId, @@ -496,8 +500,7 @@ export function resolveWallConstruction( wallOptions?.supportCap ?? null, ) const groundDraft = - preferredSupportSlabId === GROUND_SUPPORT_ID && - (terrainBase != null || wallOptions?.flatConstructionBase === true) + preferredSupportSlabId === GROUND_SUPPORT_ID && (terrainBase != null || flatConstructionBase) const supportOffset = groundDraft && wallOptions?.constructionElevation != null ? wallOptions.constructionElevation - sourceSupport.elevation diff --git a/packages/editor/src/components/tools/wall/wall-drafting.test.ts b/packages/editor/src/components/tools/wall/wall-drafting.test.ts index eaf7b662d..d2befe0b5 100644 --- a/packages/editor/src/components/tools/wall/wall-drafting.test.ts +++ b/packages/editor/src/components/tools/wall/wall-drafting.test.ts @@ -423,6 +423,136 @@ describe('createWallOnCurrentLevel', () => { expect(created?.supportSlabId).not.toBe(GROUND_SUPPORT_ID) }) + test('a fresh-scene wall started on a slab node-top elects that slab plane-bound', () => { + const slab = SlabNode.parse({ + id: 'slab_fresh_floor', + parentId: LEVEL_ID, + polygon: [ + [-1, -1], + [5, -1], + [5, 5], + [-1, 5], + ], + elevation: 0.05, + thickness: 0.05, + }) + seedLevel([], [slab]) + spatialGridManager.clear() + spatialGridManager.handleNodeCreated(slab as AnyNode, LEVEL_ID) + + const created = createWallOnCurrentLevel([2, 2], [3, 2], { + supportCap: 0.05, + preferredSupportSlabId: null, + constructionElevation: 0.05, + constructionHeight: 2.5, + constructionSourceNodeId: slab.id, + flatConstructionBase: true, + }) + + expect(created).not.toBeNull() + expect(created?.supportSlabId).toBeUndefined() + expect(created?.height).toBeUndefined() + expect(created?.supportOffset).toBeUndefined() + const support = spatialGridManager.getSlabSupportForWall( + LEVEL_ID, + created!.start, + created!.end, + created!.curveOffset, + created!.thickness, + created!.supportSlabId, + ) + expect(support.electedSlabId).toBe(slab.id) + expect(support.elevation).toBeCloseTo(0.05) + }) + + test('a direct slab source does not pin a cross-slab wall away from the higher majority support', () => { + const sourceSlab = SlabNode.parse({ + id: 'slab_source_low', + parentId: LEVEL_ID, + polygon: [ + [-0.1, -1], + [0.1, -1], + [0.1, 1], + [-0.1, 1], + ], + elevation: 0.1, + thickness: 0.05, + }) + const majoritySlab = SlabNode.parse({ + id: 'slab_majority_high', + parentId: LEVEL_ID, + polygon: [ + [0.1, -1], + [4.1, -1], + [4.1, 1], + [0.1, 1], + ], + elevation: 0.6, + thickness: 0.1, + }) + seedLevel([], [sourceSlab, majoritySlab]) + spatialGridManager.clear() + spatialGridManager.handleNodeCreated(sourceSlab as AnyNode, LEVEL_ID) + spatialGridManager.handleNodeCreated(majoritySlab as AnyNode, LEVEL_ID) + + const created = createWallOnCurrentLevel([0, 0], [4, 0], { + supportCap: 0.6, + preferredSupportSlabId: null, + constructionElevation: 0.1, + constructionHeight: 2.5, + constructionSourceNodeId: sourceSlab.id, + flatConstructionBase: true, + }) + + expect(created?.supportSlabId).toBe(majoritySlab.id) + expect(created?.height).toBeUndefined() + expect(created?.supportOffset).toBeUndefined() + }) + + test('a grazing direct slab source does not override the commit elevation cap', () => { + const sourceSlab = SlabNode.parse({ + id: 'slab_source_high', + parentId: LEVEL_ID, + polygon: [ + [-0.1, -1], + [0.1, -1], + [0.1, 1], + [-0.1, 1], + ], + elevation: 0.6, + thickness: 0.1, + }) + const cappedSlab = SlabNode.parse({ + id: 'slab_capped_low', + parentId: LEVEL_ID, + polygon: [ + [-0.1, -1], + [4.1, -1], + [4.1, 1], + [-0.1, 1], + ], + elevation: 0.1, + thickness: 0.05, + }) + seedLevel([], [sourceSlab, cappedSlab]) + spatialGridManager.clear() + spatialGridManager.handleNodeCreated(sourceSlab as AnyNode, LEVEL_ID) + spatialGridManager.handleNodeCreated(cappedSlab as AnyNode, LEVEL_ID) + + const created = createWallOnCurrentLevel([0, 0], [4, 0], { + supportCap: 0.1, + preferredSupportSlabId: null, + constructionElevation: 0.6, + constructionHeight: 2.5, + constructionSourceNodeId: sourceSlab.id, + flatConstructionBase: true, + }) + + expect(created?.supportSlabId).toBe(cappedSlab.id) + expect(created?.height).toBeUndefined() + expect(created?.supportOffset).toBeUndefined() + }) + test('a non-ground draft never freezes the ghost height, even at a raised plane', () => { const created = createWallOnCurrentLevel([2, 2], [3, 2], { supportCap: 1.2,