fix: make room walls and slabs join cleanly#497
Conversation
…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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ 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) | ||
| } |
There was a problem hiding this comment.
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.
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 } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit d71cb94. Configure here.
| thickness: wall.thickness, | ||
| }, | ||
| slab.polygon, | ||
| renderedPolygon, |
There was a problem hiding this comment.
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)
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) |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit d71cb94. Configure here.


Summary
Test plan
bun check-types— 10/10 tasksbun run build— 7/7 tasksGenerated 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.
getRenderableSlabPolygonnow 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 (includingslabPolygonContextFromGeometryand 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 emitbaseSegments/baseElevationfor piecewise bottoms.SpatialGridManager.getSlabSupportForWalldelegates 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 useresolveEndpointWallSplitinside 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.