mcp: layout clearance for doors and item overlaps - #569
Conversation
Prevent furnish_room from placing furniture in door keep-outs or on other items. Add rotation-aware footprints, smart lateral/inset re-place, and report remaining issues from verify_scene and check_collisions.
Scope doors/items by level, keep planned room entrances when other doors exist, treat gap as minimum free space, honor item scale, and document pitfalls in docs/layout-clearance-error-log.md.
|
Addressed Bugbot findings L1-L4 in the follow-up commit:
Regression coverage is in the door/layout unit tests. |
Require planned center + 50% area overlap before treating a door keep-out as covering a room entrance. Report primary pose reject reasons so furnish skips cite door/overlap instead of last-nudge outside_bounds.
|
Addressed the latest Bugbot findings (N1 / N2, logged as L6 / L7):
Regression tests: door-clearance L6 + layout-clearance L7. |
TV sits on the door wall and hits keep-outs; pass along/inward so findValidPlacement can inset into the room instead of world-axis nudges.
|
Addressed the latest Bugbot finding (TV nudge axes):
|
Aymericr
left a comment
There was a problem hiding this comment.
Thanks for this — door clear zones and item footprints are a real gap in the MCP layout tools, and several parts of this are done well: the level-scoped comparisons via resolveNodeLevelId (door-clearance.ts:212-221), the expand-then-intersect gap semantics that fix the inverted sign (door-clearance.ts:41-49), and getScaledDimensions in itemNodePlanAabb (door-clearance.ts:98) are all correct and each has a real regression test. Also confirmed: bun run test in packages/mcp is 321/0, the branch merges onto current main with no conflicts, layoutIssuesFromScene costs only ~31ms on a synthetic 4.8k-node scene, and verify_scene stays silent on all three built-in templates.
Two blockers before this can land, plus a design question.
1. CI is red on this PR's own code. bun run --cwd packages/mcp build fails with three type errors — door-clearance.ts:109 (AnyNode has no position/width key to Pick), door-clearance.ts:261 (the synthetic edge-${i} id isn't wall_${string}), layout-clearance.ts:79 (branded-id Map). I checked that main alone is tsc- and biome-clean for this package and that merging main in doesn't change the errors, so this isn't a stale base. bun test is Bun's builtin transpiler and never typechecks, which is why your local run was green — please run bun run --cwd packages/mcp build and bun run check too. Fix that worked for me: swap the two Pick<> params for local structural types ({ id: string; start: [number, number]; end: [number, number] } and { id: string; position: number[]; width?: number }) and widen the map to new Map<string, AnyNode>(...). biome check --write clears the 10 lint/format errors; itemPlanAabb at layout-clearance.ts:14 is genuinely unused (the re-export block at :22-30 already covers it).
2. Wall- and ceiling-hosted items store host-local coordinates, not world plan XZ. place-item.ts:84-86 stores [localX, y, 0] for wall targets (same convention as DoorNode, documented at core/src/schema/nodes/door.ts:155), but itemNodePlanAabb reads position[0]/position[2] as world. Concretely: room [[0,0],[6,0],[6,5],[0,5]], add_door on wallIds[0] at t=0.5, then place_item a shelf on wallIds[2] (north wall, 5 m away) — verify_scene now reports "Door … is blocked by item Shelf". main reports zero issues for that scene. Two shelves at the same wall-local X on opposite walls likewise produce a phantom check_collisions hit. The fix is small and the predicate already exists in your own code: pass floorOnly: true at layout-clearance.ts:100, and add if (node.asset?.attachTo) continue / if (node.parentId && byId.get(node.parentId)?.type === 'wall') continue before door-clearance.ts:215. I verified that clears the false positives while real floor overlaps still report. Heads-up: it correctly breaks the pre-existing check-collisions.test.ts:54, which encodes this bug with two wall-parented items — that test should move to level-parented items.
3. The phantom keep-out fires even when the real door is known. room-tools.ts:585-600 always synthesizes a 0.9 m keep-out at t=0.5 of doorWallIndex ?? 0. With a real door at t=0.2 on that edge, the real keep-out spans x 0.10–1.10 and the planned one 1.00–2.00, so keepoutCoversPlanned's centre-inside test doesn't suppress it and furnish_room protects an opening that doesn't exist — in a 3×3 bathroom that skips the bathtub as blocks door clearance where main placed it. It also fires when doorWallIndex is omitted and the scene has no doors at all. Please match the entrance edge to a real door (compare wall start/end to the polygon edge, or thread edgeIndex from create_room metadata) and only synthesize when that edge has none — and only when doorWallIndex is explicitly supplied. Net placement counts vs main are currently mixed rather than uniformly better (bedroom 5×5 5→4, bathroom 3×3 2→1, kitchen 4×4 1→2, living 6×5 3→4), which I think is this mechanism.
Design question on verify_scene: it appends layout issues uncapped, while the schema-error path right above deliberately caps at 5 (scene-query.ts:826-832) — 40 near-coincident items produced 345 issue strings. And DEFAULT_ITEM_GAP = 0.08 is enforced as an error on read, so deliberately flush design is flagged: a run of four 0.6 m kitchen counters yields 3 "Items overlap", a bedside table against a bed yields 1. I'd suggest calling findItemItemCollisions with gap: 0 from verify_scene/check_collisions (true interpenetration only) and keeping 0.08 as a furnish_room packing preference, plus a cap + "+N more" line.
Smaller items: please drop packages/mcp/docs/layout-clearance-error-log.md:42 (L5), :57 (L8) and :73 — those are #570's apps/editor light-preview bugs and contradict this PR's "out of scope: editor UI". This would also be the repo's first packages/*/docs/ directory; I'd rather see the durable rules as header comments in the two new modules than a per-PR error log with review-round codes. And there's some dead code to remove: aabbFromPlan (door-clearance.ts:305, an identity function with no callers), nodeItemAabb (layout-clearance.ts:52, a same-file alias for the re-exported itemNodePlanAabb), and pointInBoundsWithPadding (geometry.ts:85), whose last caller this PR removed.
One thing worth your thoughts before the next round: packages/core already has rotation-aware plan-footprint geometry in itemOverlapsPolygon (core/src/hooks/spatial-grid/spatial-grid-manager.ts:263, exported via the @pascal-app/core/spatial-grid subpath that scene-query.ts:11 already imports). If the footprint math lived there instead, the editor UI and npm consumers would get the same clearance rules for free rather than this being a third copy of it inside packages/mcp.
Two separate failures, the second hidden behind the first:
- Biome import ordering across 7 files (`bun run check:fix`), plus the
`useOptionalChain` warning in `collectDoorKeepouts`.
- Three type errors in the clearance helpers, which CI never reported
because the lint step failed first and short-circuited the job.
The type errors were both real signature problems, not noise:
- `Pick<AnyNode, 'id' | 'position' | 'width'>` cannot work — `position`
and `width` exist on only some members of the `AnyNode` union, so
`Pick` rejects the keys outright. Replaced with an explicit
`DoorOpeningLike`.
- `Pick<WallNode, 'id' | 'start' | 'end'>` brands the id as
`wall_${string}`, but `keepoutForPolygonEdge` intentionally passes a
synthetic `edge-N` segment for room edges that have no wall node yet.
Replaced with `WallSegmentLike`, which is the shape these helpers
actually accept.
- `new Map(list.map((n) => [n.id, n] as const))` infers the branded
`AnyNodeId` key type, so `resolveNodeLevelId(node.id, byId)` failed on
a plain `string`. Annotated as `Map<string, AnyNode>`.
Gates: `bun run check` clean, `check-types` 9/9, core 917 / mcp 321 /
nodes 939 tests pass, 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consolidating check_collisions onto findItemItemCollisions also handed it that helper's DEFAULT_ITEM_GAP of 8cm, which is not the same question. The gap exists for furnish_room: when *placing* a new item you want breathing room around it, so "is this spot free" means "free plus 8cm." check_collisions answers a different question about an existing scene — "do these footprints actually intersect" — and an 8cm gap makes it report furniture merely standing next to other furniture as a collision. Two 1m items 7cm apart came back as overlapping. Passes gap: 0 explicitly and adds the tight regression test the suite was missing; the existing "do not overlap" case placed its items 20m apart, so nothing caught the change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d87f1fd. Configure here.
| } | ||
| for (const c of findItemItemCollisions({ nodes: list, levelId })) { | ||
| issues.push(c.message) | ||
| } |
There was a problem hiding this comment.
Verify reports near-miss overlaps
Medium Severity
layoutIssuesFromScene calls findItemItemCollisions without gap: 0, so it inherits DEFAULT_ITEM_GAP (8cm). verify_scene then flags items that are only close as overlapping, while check_collisions intentionally uses gap: 0 for actual footprint overlap. Agents treating hasIssues as hard failures can get false layout failures.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d87f1fd. Configure here.
|
Self-review before merging, since this is my own PR — treating it adversarially rather than waving it through. Gate fixes (
A behavioural bug I found reviewing my own consolidation (
With the gap applied, two 1m items standing 7cm apart came back as colliding. I verified it rather than assuming: Now passes That's the finding I'd most want a reviewer to catch, and it's the kind of thing that only shows up when you ask what a shared default means at each call site rather than whether the code compiles. On the change as a whole. 645 lines of implementation against 644 of tests, and it removes 89 lines from
Gates: Merging. |
|
Thank you for reviewing and merging. The feedback was clear and helpful. I enjoy working in this codebase and plan more small PRs as I go. Appreciate the time and the polish you added. |


What does this PR do?
Makes MCP layout tools respect door clear zones and item footprints, so agents stop producing valid-but-blocked rooms.
furnish_roomskips or nudges poses that block door keep-outs or overlap other floor items (rotation-aware AABBs, small lateral/inset search)verify_scenereports remaining door blocks and item–item overlapscheck_collisionsuses the same rotation-aware footprintsOut of scope: wall-facing / corner placement modes, massing/space-program, editor UI.
How to test
packages/mcp:bun test src/tools/door-clearance.test.ts src/tools/layout-clearance.test.ts src/tools/room-tools.test.ts src/tools/scene-query.test.ts src/tools/check-collisions.test.tsfurnish_roombathroom with the door on the furniture wall — fixtures should skip or move rather than sit in the opening;verify_sceneshould not list door blocks for items just placed.Screenshots / screen recording
N/A — non-visual change (MCP headless tools + tests).
Checklist
bun teston the packages abovemainbranchNote
Medium Risk
Changes core MCP placement and validation behavior agents rely on; risk is mitigated by broad tests and preserved check_collisions contract (gap 0), but regressions could still affect automated room furnishing quality.
Overview
Adds shared door keep-out and layout clearance logic so MCP scene tools stop placing or approving furniture that blocks doors or stacks footprints.
furnish_roomnow uses level-scoped door keep-outs (real doors plus a planned zone ondoorWallIndexwhen not already covered), rotation-aware footprints with scaled dimensions for scene items, an 8 cm default spacing gap between floor items, and lateral/inset nudge search viafindValidPlacement. Failed placements return clearer skip reasons (door clearance preferred over out-of-bounds).verify_sceneappends layout issues from the same helpers: blocked doors and item–item overlaps, evaluated per level so stacked floors do not false-positive.check_collisionsreusesfindItemItemCollisionswithgap: 0so it still reports only true overlap, not the furnish spacing rule.Also updates the agent guide, adds
layout-clearance-error-log.mdas a regression checklist, and expands unit/integration tests for gap semantics, multi-level isolation, planned keep-outs, and furnish behavior.Reviewed by Cursor Bugbot for commit d87f1fd. Bugbot is set up for automated code reviews on this repo. Configure here.