Skip to content

feat(units): apartments and hotel rooms as a zone-referencing overlay under building - #877

Merged
wass08 merged 7 commits into
mainfrom
feat/units
Sep 16, 2026
Merged

wass08 merged 7 commits into
mainfrom
feat/units

Conversation

@wass08

@wass08 wass08 commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Units — apartments, hotel rooms and commercial lots inside one building

Adds the unit node kind: a building child that groups manually drawn zones (possibly across levels, e.g. a duplex) with no geometry of its own. The physical hierarchy site → building → level and the vertical model are untouched. Mirrors IFC's IfcSpatialZone(OCCUPANCY).

Core

  • UnitNode schema (name, kind apartment | hotel-room | commercial | common, members: zoneId[], color); building.children admits units. No migration.
  • unit-containment.ts: derived member levels, contained nodes, boundary walls (including the walls of the detected room enclosing a member zone), supports, and the isolate list. unit-report.ts: member count, level span, gross area.
  • Zone deletes strip members in the same commit; clones and level duplication remap members.

Editor UX (agreed with Wassim after two hands-on passes)

  • Focus: clicking a unit switches to the Zones layer, jumps to its lowest level, highlights its zones in 3D and tints them in 2D, dims other zones, shows a ● Unit × chip near the level selector and a dot on spanned levels.
  • Paint: while focused, a plain click on a zone (3D or 2D) toggles membership with move semantics (one unit per zone in the editor); double-click selects the zone; zone rows in the Scene panel get a checkbox; newly drawn zones join in the same history step.
  • Unit inspector (name, kind, color, members, warnings, report), zone inspector Unit select as fallback, command palette entries.
  • Zone fill takes the unit color, the outline stays the zone's own color, the label carries a unit chip.
  • Shared read-only ViewerUnitsPanel under the level list in the editor Preview (also used by the community viewer).

MCP

  • create_unit, set_unit_members, list_units (+ annotation manifest rows).

Verification

  • core 1600 / viewer 342 / nodes 2552 / editor 992 / mcp 380 tests pass, biome clean.
  • Community e2e unit-focus.spec.ts (private repo) drives the whole flow on :3005 and is green.

Plan: private repo plans/editor-units.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MpE7N6c5YfaS9V3rfYLnKN


Note

Medium Risk
Touches scene graph deletion/cloning, selection routing, and floor-plan caching across editor and viewer; geometry is derived rather than stored, but membership and focus state must stay consistent with undo and multi-unit zone sharing.

Overview
Introduces UnitNode as a building-level overlay that groups zones (including multi-level duplexes) without changing the site → building → level hierarchy. Core adds deriveUnit, buildUnitReport, membership helpers, delete/clone remapping, and UnitEvent wiring; zones lose unit membership when deleted.

Editor workflow centers on unit focus: select a unit to enter the zones layer, tint/dim zones on the floor plan and in 3D, and paint membership via clicks (with double-click to select a zone). New zones can auto-join the focused unit in one undo step; the site tree, level selector chip, zone checkboxes, inspector panels, and command palette support create/focus/exit.

Presentation: zone floor plan fill uses the owning unit color; floorplanDependencies now receives the full scene so unit membership invalidates zone geometry. MCP exposes create_unit, set_unit_members, and list_units. Viewer gets focusedUnitId and a shared ViewerUnitsPanel for read-only browsing/focus in preview.

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

wass08 and others added 6 commits September 15, 2026 16:10
Adds the `unit` kind (apartment / hotel room / commercial / common): a
building child listing manually drawn zones, possibly across levels, with
no geometry of its own. Containment, boundary walls, supports and the
isolate list are derived (unit-containment.ts); reports sum member areas
(unit-report.ts). Zone deletes strip members in the same commit; clones
and level duplication remap members.

Editor: Units section in the site panel (new unit, member rows, common
group, warnings), active unit joins newly drawn zones in one history
step, isolate unit through the viewer isolate filter, unit chip beside
the level selector, command palette entries, unit inspector, zone
inspector unit picker, and member tint + unit chip in 3D and 2D.

MCP: create_unit, set_unit_members, list_units.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MpE7N6c5YfaS9V3rfYLnKN
A zone drawn inside a room, short of the wall centerlines, still means that
room, so isolate must keep its walls. Spaces whose polygon encloses a member
centroid (or whose centroid the member encloses) contribute their wall ids.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MpE7N6c5YfaS9V3rfYLnKN
…et-active button

Row click already sets the active unit. Color dots are the identity of
units and zones, so the tree no longer desaturates them when unselected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MpE7N6c5YfaS9V3rfYLnKN
Clicking a unit focuses it: Zones layer on, jump to its lowest level, members
highlighted in 3D and tinted in 2D, other zones dimmed, chip near the level
selector, unit dot on spanned levels. While focused a plain click on a zone
toggles membership (move semantics, one unit per zone in the editor), a
double-click selects the zone and ends focus, panel zone rows get a checkbox,
and newly drawn zones join in the same history step. Focus ends on Escape,
non-zone selection, New unit, or a manual layer switch, restoring the layer
and keeping the level.

Removed: isolate eye and viewer isolate wiring, set-active button, unit
dropdown under the level selector, Common group, per-zone popovers,
shared-zone warning. Zone outlines keep the zone color; only the fill takes
the unit color.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MpE7N6c5YfaS9V3rfYLnKN
Selecting, renaming or recolouring a zone row while a unit was focused ended
focus and restored the Elements layer. Zones live on the layer focus uses, so
selecting one now keeps focus; only non-zone selection, Escape, New unit, or
a manual layer switch leaves. Double-click on a zone selects it without
leaving. The membership checkbox is now a styled primitive instead of a bare
input.

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

One ViewerUnitsPanel (kind, zone count, area; click focuses and reveals the
unit's level) mounted under the level list in the preview scene header and
exported for the community viewer page.

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

pascal Bot commented Sep 16, 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: 9f62ffe0-1a9b-479f-8ebc-5a757e99cd1c

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

if (focusedUnitId) {
const zone = zoneAtLevelPoint(event.localPosition[0], event.localPosition[2])
if (zone) paintZoneMembership(focusedUnitId, zone.id)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty ground click keeps unit focus

Medium Severity

A 3D ground click outside any zone is supposed to end unit focus and leave the unit unselected, but the focused-unit branch always returns after an optional paint. Clicks on empty floor never call leaveUnitFocus, so paint mode stays on until Escape or the chip.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9dd62da. Configure here.

paintZoneMembership(focusedUnitId, clickedNode.id)
swallowNextClick(200)
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2D double-click cancels zone paint

High Severity

While a unit is focused, a 2D click on a zone schedules paint on pointer-down and swallows the following click. The second pointer-down of a double-click hits paintZoneMembership again, which cancels that paint, and the registry path never selects the zone. The floorplan background handler that treats detail >= 2 as select never sees those clicks.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9dd62da. Configure here.

pendingPaints.delete(zoneId)
toggleZoneMembership(unitId, zoneId)
}, ZONE_PAINT_DELAY_MS),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Paint delay misses double-click window

Medium Severity

ZONE_PAINT_DELAY_MS is 250ms, shorter than the usual OS double-click interval. A normal 3D double-click whose second click lands after that delay already toggles membership, then the double-click handler selects the zone, so the unit gains or loses the zone as a side effect of selecting it.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9dd62da. Configure here.

…ventory (46 → 49)

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

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

There are 4 total unresolved issues (including 3 from previous reviews).

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

toggleZoneMembership(unitId, zoneId)
}, ZONE_PAINT_DELAY_MS),
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pending paints fire after focus ends

Medium Severity

paintZoneMembership keeps a 250ms timer that still calls toggleZoneMembership after focus ends or moves. leaveUnitFocus and unit switches do not clear pendingPaints; timers are only dropped when useUnitFocusRules unmounts.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e51a352. Configure here.

@wass08
wass08 merged commit 3318b4b into main Sep 16, 2026
4 checks passed
@wass08
wass08 deleted the feat/units branch September 16, 2026 18:12
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