Skip to content

fix: make room walls and slabs join cleanly#497

Merged
wass08 merged 15 commits into
mainfrom
fix/room-walls-pass
Jul 15, 2026
Merged

fix: make room walls and slabs join cleanly#497
wass08 merged 15 commits into
mainfrom
fix/room-walls-pass

Conversation

@wass08

@wass08 wass08 commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • derive slab render boundaries per wall-backed span using the matched wall thickness, with centerline joins for equal rooms and higher-slab ownership for unequal elevations
  • compute side-aware, piecewise wall support profiles so stacked, adjacent, partial-span, curved, and holed slabs do not lower or overextend walls incorrectly
  • improve wall endpoint splitting, attachment migration, snapping, and automatic slab/ceiling reconciliation around T-junctions and room topology changes
  • keep slab boundary reshaping canonical while applying wall-face and sibling joins at render time
  • include the branch's KTX2 loader and tree wind transform-order fixes

Test plan

  • bun check-types — 10/10 tasks
  • bun run build — 7/7 tasks
  • focused slab/wall geometry suite — 55 passing tests
  • manual validation with stacked slabs, unequal adjacent rooms, partial shared spans, and offset-room wall transitions

Generated with Codex.


Note

High Risk
Changes core floor/wall elevation and footprint math used by rendering, placement, and wall extrusion, with broad interaction between slabs, holes, curved walls, and undo—mitigated by a large focused test suite but still easy to miss edge layouts.

Overview
Slab footprints are no longer a single uniform outset/inset. getRenderableSlabPolygon now takes level walls and sibling slabs, splits each stored edge into spans, and projects them as interior seams (equal height on centerline; unequal height gives the higher slab the full wall band), wall-backed outer faces, or free geometry. Stored polygons stay canonical; 3D/2D fills, spatial overlap, and dirty-marking all use the same rendered footprint (including slabPolygonContextFromGeometry and a new slab dependency system so meshes refresh when neighbours or walls change).

Wall base height moves from “max overlapping slab elevation + point samples in holes” to computeWallSlabSupport: clip centerline and face polylines against rendered slabs minus holes, pool elevations, require majority support before lifting the whole wall, and emit baseSegments / baseElevation for piecewise bottoms. SpatialGridManager.getSlabSupportForWall delegates here and caches level walls from the scene store.

Editor / nodes: slab edge drags snap the edge onto wall bands (resolveSlabEdgeBandSnap, snapSlabEdgeToWallBand); wall commits and endpoint moves use resolveEndpointWallSplit inside a single undo step; wall chains stop on T-junctions / room close (chainEndJoinsExistingWall); auto slab/ceiling sync demotes orphaned autos or deletes merge leftovers by coverage sampling.

Smaller UX fixes: floorplan Alt/Shift re-applies active drag sessions; polygon resize arrows ignore depth test; 2D wall chaining mirrors 3D auto-close rules.

Reviewed by Cursor Bugbot for commit a6c4de3. Bugbot is set up for automated code reviews on this repo. Configure here.

wass08 and others added 14 commits July 14, 2026 14:27
…o-move drop

- restore linked walls to original positions the moment alt-detach engages
- re-run the endpoint preview on alt keydown/keyup so re-attach doesn't wait for a mousemove; preview, HUD badge, and commit share one alt source
- second click at an unmoved position cleanly drops the endpoint (no history entry) instead of leaving the interaction stuck
- 2D floorplan parity: clear stale linked overrides on detach, re-apply drag sessions on modifier changes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eleting

Deleting a wall that encloses a room no longer destroys the room's
slab/ceiling (paint, holes, elevation). Unmatched auto surfaces are only
deleted when >=60% of their footprint is still covered by a detected room
(rooms-merged case); otherwise they are demoted to manual nodes.

- demoted slab polygons are baked (inset by SLAB_OUTSET + AUTO_SLAB_INSET)
  so the rendered footprint doesn't jump between the auto and manual paths
- auto-creation suppression now also matches manual nodes by mutual
  footprint coverage, so re-closing the room doesn't stack a duplicate
  auto surface on the demoted one

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…linked walls

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…existing walls

Splitting a room with a wall now splits the walls it lands on and migrates
hosted doors/windows/items — matching what already happened to the slab and
ceiling. The split machinery existed but was gated on magnetic snapping
('lines' mode) while the wall tool defaults to 'grid'; split resolution now
always runs, with the join radius scoped to the active mode (0.35 magnetic,
0.05 connect snap otherwise), and the whole commit lands as one undo step.

The drawing chain now terminates when a committed segment ends on a wall
outside the current chain (T-junction), like the room auto-close — users
don't draw overlapping walls. Applied in 3D and 2D, and the 2D-only path
gains the previously missing wallClosesRoom parity check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…terlines

Replaces the stored-polygon render offsets (auto: centroid inset 0.02,
manual: flat outset 0.05) with one uniform per-edge rule computed at
geometry build time, for auto and manual slabs alike:

- edge shared with a sibling slab (collinear-overlap test, T-junction
  sub-segments included) → small relief inset; adjacent rooms tile
  exactly and can never overlap
- edge on a wall centerline with no slab neighbor → expand outward by
  that wall's thickness/2, flush with the facade
- free edge → rendered exactly as drawn

Slab demotion no longer bakes polygons (offsets never live in node
data); no stored-data migration. New slab system marks level slabs
dirty when wall geometry/thickness or sibling slab footprints change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Addons.js aggregate re-exports LottieLoader/TTFLoader whose CDN URL
imports (lottie-web, opentype.js) abort bun test in every package that
transitively imports the viewer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rior

Moving a wall endpoint onto another wall's mid-span now splits that wall
and migrates its doors/windows/items, matching the draw tool — previously
the room closed (detection planarizes internally) but the wall node stayed
whole. New resolveEndpointWallSplit reuses the draw path's split pipeline;
endpoint write + split commit as one undo step. Applied in 3D and 2D.
Corner drops still join without splitting; straddling openings skip the
split; alt-detached commits split the stationary former sibling correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing, rendered-footprint wall elevation

Slab edges near a wall now render projected onto the wall's exact
geometry instead of getting relative offsets, healing legacy scenes
(face-aligned edges, old baked outsets, hand-drag slop) with no data
migration:

- adoption band: an edge collinear with a wall centerline within
  thickness/2 + 6cm (both sides) is wall-backed; nearest centerline
  wins, a sibling slab in the same band forces the interior seam
- wall-backed exterior edges project to the outer face; interior edges
  to the centerline minus the relief gap; free edges render as drawn
- slab edge resize: wall snapping is now edge-based, not cursor-based —
  fixes the drop landing short by the grab offset (0.34m arrow gap in
  3D, hit-stroke slop in 2D); the snap translates the edge onto the
  wall centerline (canonical stored position), beacon/preview/commit
  agree, full band in lines mode, 5cm stick otherwise; 2D shows a
  dashed stored-boundary skeleton when it diverges from the fill
- wall elevation tests the slab's rendered footprint instead of the
  stored polygon with a 0.1mm epsilon, so walls sit on legacy slabs
  and re-elevate when slabs are reshaped

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A slab edge backed differently along its length (offset rooms sharing a
wall over part of the span, a wall shorter than the edge, collinear
walls of different thickness) is now subdivided at the backing-span
breakpoints; each sub-span classifies and projects independently, with
a perpendicular step connector at intra-edge transitions that lands
inside the crossing wall's footprint. Breakpoints closer than 5cm merge
so no sliver geometry reaches the ring, and same-target spans re-fuse
(curved-wall sampling doesn't balloon vertex counts; whole-edge cases
render bit-identically to before).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ghest graze

A wall touching a raised slab only at an endpoint no longer lifts
entirely to that slab. Elevation selection is now coverage-based:
per-slab support is the wall's centerline+face length covered by the
slab's rendered footprint minus holes; slabs within 0.1mm of elevation
pool their support (party walls spanning two rooms still lift); the
wall sits on the highest elevation covering >=50% of its length, else
the best-covered group (ties prefer higher), else 0. Sub-5cm grazes
are ignored outright.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the 2cm interior relief inset: both rooms' seam edges project
onto the same centerline (or the symmetric sibling midline when no wall
backs the seam), so adjacent slabs tile with a shared edge — the 4cm
slit under shared walls, visible at its open end on the facade, is
gone. Safe against z-fighting because slab side quads are single-sided
(FrontSide is enforced repo-wide for the MRT scene pass) and the
coincident seam faces have opposite normals; junction step pockets grow
to 5cm but remain strictly inside the intersecting wall footprints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and slit

With unequal room elevations, both slabs stopping at the shared wall's
centerline left the lower room's half of the wall band open between the
lower slab top and the wall base (the wall seats on the higher slab).
Interior seams across a wall are now elevation-aware: equal elevations
keep the exact centerline seam; unequal elevations project BOTH slabs
to the wall face on the lower side — the higher slab runs through the
band under the wall, the lower butts the same plane. Wall-less unequal
seams keep the sibling midline (a visible step face is correct there).
Slab elevation joins the level dependency signature so height edits
rebuild sibling slabs live.

Verified against real local scenes: all wall bands solid where the
seam rule applies (remaining pockets are the known flat-wall-base
limitation, tracked separately).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The r184->r185 upgrade broke the plant wind: r185 fixed TSL's statement
emission order so a material's positionNode now runs *after* the
instancing transform (r184 emitted it before — verified in the compiled
WGSL of both versions). The wind nodes were tuned against the r184
order, reading positionLocal as geometry-local coordinates, so on r185
the displacement moved into level space: sway no longer scaled with the
per-instance scale (scaled-down trees thrashed like a storm, leaf cards
visibly detaching from branches), leaf phase followed world placement,
and STEM_BEND's height term read the floor elevation, so grass/flowers
on upper levels slid around rigidly.

Restore the r184 semantics explicitly: WindNodeMaterial assigns the wind
node to positionLocal inside setupPosition() before super applies the
instance transform, instead of using positionNode. The emitted WGSL is
statement-for-statement identical to r184's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d71cb94. Configure here.

const createdWall = viewIs2DOnly ? createWallOnCurrentLevel(draftStart, point) : null
if (createdWall) {
wallChainWallIdsRef.current.push(createdWall.id)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate walls on 2D clicks

High Severity

In 2D-only wall placement, each segment commit still emits grid:click to the 3D wall tool before the panel calls createWallOnCurrentLevel. If that tool handler is still subscribed (same as split view), one click can create two identical walls.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d71cb94. Configure here.

if (node.type === 'wall') walls.push(node as WallNode)
else if (node.type === 'slab') siblingSlabs.push(node as SlabNode)
}
return { walls, siblingSlabs }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Level wall lists disagree

Medium Severity

3D slab meshes build render polygons from walls whose parentId equals the level, while wall slab support walks the parent chain to the level. Walls not direct level children are omitted from slab seams but still affect wall elevation support.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d71cb94. Configure here.

thickness: wall.thickness,
},
slab.polygon,
renderedPolygon,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Floor items ignore rendered slabs

Medium Severity

markNodesOverlappingSlab now tests walls against the slab’s rendered footprint, but floor-placed items still use the stored slab.polygon for overlap and dirty marking. When support geometry only grows via wall-band adoption, items in that band may never re-elevate after slab or wall context changes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d71cb94. Configure here.

const rightElevation = polylines.length >= 3 ? highestAt(2, midpoint) : Number.NEGATIVE_INFINITY
const faceElevations = [leftElevation, rightElevation].filter(Number.isFinite)
const segmentElevation =
faceElevations.length > 0 ? Math.min(...faceElevations) : Math.max(highestAt(0, midpoint), 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curved wall support param mismatch

Medium Severity

computeWallSlabSupport merges normalized support intervals from the centerline and offset face polylines into one [0,1] breakpoint axis, then samples highestAt on every polyline at the same t. On curved walls those polylines have different arc lengths, so a shared t is not the same physical station and piecewise baseSegments can mis-profile the wall base.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d71cb94. Configure here.

@wass08 wass08 merged commit 4fca38a into main Jul 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant