Skip to content

Wall lifecycle: rectangle walls, loop-cut split and merge, with 2D navigation and selection fixes - #902

Merged
wass08 merged 8 commits into
pascalorg:mainfrom
AxiomeCG:feat/walls
Sep 22, 2026
Merged

wass08 merged 8 commits into
pascalorg:mainfrom
AxiomeCG:feat/walls

Conversation

@AxiomeCG

@AxiomeCG AxiomeCG commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Completes the wall lifecycle — draw, split, merge — and fixes four main bugs found while testing it in the floor plan. Balconies and the plan workspace (calibrate / trace SVG plans) follow in separate PRs stacked on this one.

Walls

  • Rectangle walls: R toggles line / rectangle inside the wall tool (HUD "Shape" chip cycles the same store). In 2D the rectangle drafts through the plan's own cursor, snapping, mitered footprints and measurement plates; in 3D it keeps the wall cursor and the line draft's look.
  • Split as a loop cut, driven by the HUD: scrolling sets 1–32 cuts, one cut follows the pointer with the snapping modes (Shift cycles, Ctrl tap steps the grid, Alt frees), several divide the wall evenly, and a click commits them as one undo step (planWallDivisions). Openings re-host on their segment. The old floating panel is gone.
  • Merge adjoining walls: select walls that continue each other → "Merge" next to Split (same component in 2D and 3D). planWallMerge keeps the wall with the most attachments and never turns it around; an absorbed wall read backwards mirrors its children's local frame (side, depth, yaw — hinges and swing hang off the yaw and stay as stored); every child is re-hosted at its world position, stale child ids are dropped, rooms follow; it refuses T/cross joints, walls out of line, different thickness / visible height / finish, or curved walls — the reason shows on hover.
  • Rectangle sides reuse existing walls: a side drawn along an existing wall inserts only the uncovered remainder (uncoveredWallSegments); a side that T-joins or crosses a room wall keeps the room on the replacement walls.
  • Openings keep their metadata when moved; transient previews never host walls; windows place through grid events on hidden walls.

Behaviour changes on main that come with the shared planners (worth knowing when reviewing):

  • planWallInsertion — the line tool's planner too — now rewrites a room's boundaryWallIds when it splits one of that room's walls; before, the zone kept the removed wall's id.
  • The delete heal's joining rule and attachment re-hosting (resolveMergedWallEndpoints, buildMergedWallAttachmentUpdates) moved from node-actions.ts into systems/wall/wall-merge.ts; at a start-to-start joint the heal no longer reverses the kept wall (which flipped its frontSide / backSide and hosted faces).

Where it lives. Split and merge are wall code, so they live in packages/nodes/src/wall/ (split-session.ts, split-store.ts, split-preview.ts, split-pointer.ts, split-tool.tsx, split-floorplan-layer.tsx, actions.tsx). The editor only gains kind-agnostic seams, so the next kind-owned reshape needs no editor change:

  • ToolManager mounts def.affordanceTools[reshape] for any reshaping scope without a dedicated arm (the split runs as reshape: 'split', which also gives it the polygon snapping chip).
  • FloorplanRegisteredToolLayer mounts the floorplan extension's reshapeLayers[reshape] while that scope runs.
  • HelperManager renders def.affordanceHints[reshape] (the cut-count chip) like toolHints.
  • NodeActionMenu renders the floorplan extension's actionMenu.actions for the selected kinds (Split / Merge come from the wall).
  • Slab and ceiling declare selectionCounterparts on the same extension for the paired deselect below.

Known gap, left for a follow-up: the 3D rectangle draft has no alignment guides yet (the 2D one shares the line draft's).

Fixes on main (each its own commit)

  • WASD and orbit did nothing in 2D-only view (the 3D canvas is paused there since perf: tighten viewer runtime ownership #675, and a session opened straight into 2D never mounted the camera). The plan owns them in 2D-only view with the camera's key state, guards and speed (lib/keyboard-pan.ts) and publishes the pose to 3D when the move ends.
  • Drawing onto an existing wall in 2D selected it instead of placing the point: registry entries now yield to the active tool in build mode, as 3D does; T-junctions draw in 2D.
  • Group selection box: it measured world-space meshes (wrong under a rotated building, unbuilt in 2D-only view) and grew the selection to every connected wall. Group transforms now act on the selection (connected walls stretch at their shared ends), and the box is measured from plan data (groupPlanBounds); the 2D box, 3D rotate gizmo and keyboard R/T share its centre. This is a behaviour change: two walls of a room no longer move the whole room.
  • A room's slab and ceiling deselect together in the plan (the ceiling is drawn as an unfilled outline a click can't reach; a marquee still picks it).

How to test

  1. bun dev, open a scene with a few rooms, switch to the 2D view.
  2. Wall tool: press R — the HUD shows Shape: Rectangle; click two opposite corners → four joined walls, mitered, with plates during the draft. Press R again → line mode; end a wall on the side of an existing wall → a T-junction, not a selection.
  3. Select a wall → scissors (Split): scroll to change the cut count, Shift to cycle snapping, click to commit; undo once removes all cuts. Select the two halves → Merge (two arrows) → one wall again; hover a greyed Merge to read why.
    • Draw two walls toward each other (start-to-start), put a door on one, merge: the kept wall keeps its facing and the door keeps its hinge in the world. Draw a rectangle whose side runs along an existing wall and past its end: the overhang is added, not a doubled wall. Draw a rectangle whose side T-joins a room's wall: the room stays one zone.
  4. In 2D-only view, hold WASD and use the orbit buttons: the plan moves / turns; switch to 3D — the camera is where the plan left it.
  5. Marquee a room (walls + slab + ceiling): the dashed box hugs the room; Shift-click the slab → the ceiling leaves with it and the box hugs the walls. Rotate the building first to check the box stays on the walls.
  6. bun test in packages/{core,editor,nodes} (all green), bun check.

Screenshots / screen recording

Screen recording to be added by the author (2D rectangle draft, loop-cut split, merge).

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

🤖 Generated with Claude Code

https://claude.ai/code/session_017sG15rKXusC8rbBg6gjSRm


Note

Medium Risk
Changes wall topology, opening re-hosting, and zone boundary references; group-move behavior no longer drags entire connected wall components, which can surprise users but is localized to editor geometry.

Overview
Adds core wall planners for rectangle drawing (planWallRectangle + uncoveredWallSegments to reuse collinear walls), explicit split/division (planWallDivision / planWallDivisions), and merge (planWallMerge in wall-merge.ts, shared with delete-heal). Merge/split paths re-host openings (including frame mirroring when a wall is read backwards), update zone boundaryWallIds, and validate collinearity, joints, and style.

Editor integration: registry seams for kind-owned reshapes (reshapeLayers, affordanceHints, actionMenu.actions), floorplan rectangle wall draft preview, and build-mode clicks that yield to tools instead of selecting walls underneath. Group transforms no longer auto-expand to every connected wall; the dashed box / gizmo pivot uses level-frame plan bounds (groupPlanBounds / computeGroupPlanBox). 2D-only view drives WASD/orbit via shared keyboard-pan while the 3D camera is paused; slab/ceiling deselect together in the plan via selectionCounterparts.

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

@pascal

pascal Bot commented Sep 21, 2026

Copy link
Copy Markdown

I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…).

Please try again, rephrase, or reach out if it keeps failing.

Error id: 7c700410-3ae4-4d68-b42a-b59f7c189810

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/systems/wall/wall-operations.ts Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/systems/wall/wall-operations.ts

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/systems/wall/wall-merge.ts
@AxiomeCG
AxiomeCG force-pushed the feat/walls branch 2 times, most recently from ee60958 to 2cd04ba Compare September 21, 2026 22:47

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/editor/src/components/editor/group-rotate-handle.tsx
Comment thread packages/editor/src/components/editor/floorplan-panel.tsx
AxiomeCG and others added 4 commits September 22, 2026 00:57
The 2D view had no navigation keys of its own: WASD and the orbit buttons
drove the 3D camera, which the plan followed. Since the 3D canvas pauses
while hidden (pascalorg#675), both did nothing in 2D-only view, and a session opened
straight into 2D never mounted the camera at all.

The plan now owns them in 2D-only view with the camera's own key state,
guards and speed (lib/keyboard-pan), pans through its existing viewport
pipeline and publishes the pose to 3D when the move ends. The camera stands
down there, so each view mode has one owner. Split view is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sG15rKXusC8rbBg6gjSRm
…tead of selecting

A registry entry selected itself on pointer-down and stopped the click, so
with the wall tool armed, clicking an existing wall to start or end a T
junction selected that wall and the tool never saw the click. Only door and
window placement passed through.

Entries now yield to the active tool in build mode, matching 3D where the
selection manager only runs in select mode; select and delete keep
selecting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sG15rKXusC8rbBg6gjSRm
…en moved

The window tool only heard wall raycasts, so with walls hidden (cutaway,
2D-driven placement) it could not place: grid events now carry the surface
hit under the pointer and the tool resolves the wall from it
(wallEventFromGrid). Moving a door or window rewrote its metadata to {},
dropping persistent ownership (arrays, assets); commitOpeningMove strips
only the draft flags. A transient preview child never hosts a wall
attachment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sG15rKXusC8rbBg6gjSRm
…ping

panel-wrapper clamps floating panels to the closest [data-viewer-bounds];
the mobile layout never set it, so panels could slide under its chrome.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sG15rKXusC8rbBg6gjSRm

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/systems/wall/wall-merge.ts
@AxiomeCG
AxiomeCG force-pushed the feat/walls branch 2 times, most recently from 9356c8b to 94cff0e Compare September 21, 2026 23:12

@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.

Stale Bugbot comment from a previous run.

@AxiomeCG
AxiomeCG force-pushed the feat/walls branch 3 times, most recently from c11834d to aa71a1b Compare September 21, 2026 23:34

@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 1 potential issue.

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 aa71a1b. Configure here.

Comment thread packages/editor/src/hooks/use-keyboard.ts Outdated
AxiomeCG and others added 4 commits September 22, 2026 01:51
- Rectangle walls: R toggles line/rectangle inside the wall tool, shown as a
  Shape chip in the HUD; in 2D the rectangle drafts through the panel's own
  cursor, snapping, mitered footprints and measurement plates, and in 3D it
  keeps the wall icon and the line draft's look. A side drawn along an
  existing wall adds only the uncovered remainder (uncoveredWallSegments).
- Split: a HUD-driven loop cut. Scrolling sets 1-32 cuts, one cut follows
  the pointer with the snapping modes (Shift/Ctrl/Alt as elsewhere), several
  divide the wall evenly, and a click commits them as one undo step
  (core planWallDivision/planWallDivisions).
- Merge: selecting walls that continue each other shows Merge next to Split.
  core planWallMerge keeps the wall with the most attachments and never turns
  it around; an absorbed wall read backwards mirrors its children's local
  frame (side, depth, yaw); every child is re-hosted and rooms follow.
- Openings keep their metadata when moved, transient previews never host
  walls, and windows place through grid events on hidden walls.

Behaviour changes on main that come with the shared planners:
- planWallInsertion (the line tool too) rewrites a room's boundaryWallIds
  when it splits one of its walls; the room used to keep the removed id.
- The delete heal's joining rule and attachment re-hosting moved into
  systems/wall/wall-merge.ts; at a start-to-start joint the heal no longer
  reverses the kept wall.

Split and merge live in packages/nodes/src/wall/ (session, store, preview,
pointer, 3D tool, plan layer, actions). The editor only gains kind-agnostic
seams: a reshape name without a dedicated ToolManager arm mounts
def.affordanceTools[reshape]; the plan mounts the floorplan extension's
reshapeLayers[reshape] during that scope; HelperManager renders
def.affordanceHints[reshape]; NodeActionMenu renders the floorplan
extension's actionMenu.actions for the selected kinds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sG15rKXusC8rbBg6gjSRm
The multi-selection box measured the meshes in world space and mapped two
corners of that box into the level frame; under a rotated building those
corners land beside the meshes, so the 2D dashed box sat away from the
walls and group move/rotate pivoted on the wrong point. In 2D-only view it
was worse still: the 3D scene is paused there, so meshes are unbuilt
placeholders or carry stale cached bounds, and a selection holding a room's
floor and ceiling boxed 13 m of nothing. It also grew the selection to
every wall connected through junctions, so two walls of a room boxed (and
moved) the whole connected structure.

Group transforms now act on the selection: connected walls outside it
stretch at their shared ends to stay joined (the existing neighbour links).
groupPlanBounds measures from node data wherever the plan draws from data —
walls by their mitered outline, slabs, ceilings and zones by their polygon,
fences by their run — and only placed objects by their meshes, with fresh
bounds, placeholders skipped and the anchor as fallback. The 2D box, group
move and duplicate bounds, keyboard R/T and the 3D rotate gizmo share its
centre as the pivot, and the 3D dashed box turns with the building like the
2D one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sG15rKXusC8rbBg6gjSRm
A room's slab and ceiling overlap in the plan and only the slab takes a
click, so after a marquee picked both, Shift-clicking the slab off left a
ceiling nobody could remove from the plan, and the group box kept covering
the room. Removing either surface now removes its counterpart (same level,
same outline); adding stays single. 3D keeps single toggles, where each
surface is clickable on its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sG15rKXusC8rbBg6gjSRm
@wass08
wass08 merged commit 7b98596 into pascalorg:main Sep 22, 2026
4 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.

2 participants