Skip to content

core: the site comes first — get_site, place_building, one setback envelope - #1044

Merged
Aymericr merged 5 commits into
mainfrom
feedback/site-first
Oct 13, 2026
Merged

Aymericr merged 5 commits into
mainfrom
feedback/site-first

Conversation

@Aymericr

@Aymericr Aymericr commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR places a house on its lot by moving the building node, never its walls. The model's coordinates stay as written. Every path places the house the same way: a layout program, a hand trace, a DXF import, the lot drop-in.

It is rebased on editor main (cc978fe8: Bundle 2's #1024, then #1039, #1040, #1041, #1043).

  • One envelope, in core.
    • The edge roles, setbackEnvelope (with Site: lot as a block in mapped surroundings, straight front setbacks #1017's straight-front rule) and the oriented yards move from editor/.../site-plan/geometry.ts to core/src/lib/setback-envelope.ts.
    • The building footprint (levelFootprintLoops, footprintOutline, siteFrame, levelFrame) moves to core/src/lib/building-footprint.ts.
    • The site plan, the lot code and the 3D site import them from core. The 3D copy in nodes/src/site/setbacks.ts is deleted, so the 2D and 3D envelopes can't drift apart. Its ribbon tests are now line-ribbon.test.ts.
    • siteFrontEdge(site) and siteSetbackEnvelope(site) are the one recipe the site plan, the 3D site and the agents use. The front edge is the recorded front edge, else the first street edge, else the north-facing guess.
  • core/src/lib/site-placement.ts (also exported as @pascal-app/core/site-placement):
    • siteBrief reads the site in the building's level frame:
      • the lot, and the buildable area with its state (lot / setbacks / none / advisory) and centre. Setbacks of zero exclude nothing.
      • the street side and the squareRotationDeg that squares the house to it
      • north, the address, APN and zoning, and the new-project placeholder flag
      • the footprint's fit, yards and overhang. Fit is judged by area; the overhang counts a piece's middle, so a house across a notch reports how far it reaches.
    • placeBuildingPose centres a footprint in the buildable area. The footprint is an outline about to be drawn, or the walls already standing. It tries the centroid first, then the most interior point.
      • It can place by the yards given (one of front/rear, one of left/right). These are solved together at the rotation asked, so a lot askew to the house still meets both, and a pose that misses them is refused.
      • A house with no walls yet is turned square to the street. A standing house keeps its rotation unless asked.
      • It never resizes the lot.
      • Its inputs don't depend on the current pose, so a repeated call changes nothing.
    • describeSiteBrief writes the same facts as a few lines. The hosted chat prints them at the top of its scene snapshot. The street line names a rotation only for a standing house that is askew.
  • Two shared tools, on both surfaces:
    • get_site (read).
    • place_building (writes only the building node). Refusals: needs_footprint, other_building_on_site, no_street, yards_conflict, yards_infeasible.
    • Both get annotations, justifications, the README table, and an MCP agent-guide line: "the site comes first, before the first wall".
  • Reported where the agent looks:
    • verify_scene reports footprint_outside_site, footprint_outside_setbacks and buildable_area_none. Off-centre is never an issue.
    • create_room and add_wall return site: { fit: 'outside', overhangM, sides } when the walls they raise, mitred and curved, cross the lot line. Only the lot is read to do it.
  • Lot drop-in: a house the new lot leaves outside is re-centred through placeBuildingPose, keeping its facing. buildingRecentreOffset is gone.
  • The site's facts move apart from the node schema: SiteAddress, SiteParcel, SiteSetbacks and migrateSiteMetadata now live in schema/nodes/site-facts.ts. This lets the site brief run inside a workflow, whose bundle refuses anything that reaches nanoid.

Reconciled with Bundle 2

Bundle 2 (#1024) and the editor PRs that landed after it touched the same registration and test files. Nothing in this PR was already there: Bundle 2 has no site placement.

How to test

  1. bun test packages/core/src/agent-operations. The tables in __fixtures__/site-cases.ts also run on the MCP (packages/mcp/src/tools/shared-tools.test.ts) and in the hosted chat. They cover get_site, place_building, the lot-line flags and the verify_scene issues:
    • a corner-traced house that lands centred
    • an outline on an empty building, and on a turned parcel, where it stands square to the street inside the setbacks
    • a replay that moves nothing
    • a parcel turned 20° off the origin; a yaw of 0.7 rad kept
    • yards, conflicts and infeasible yards, yards with a new rotation and its replay, and front and side yards on a lot askew to the house
    • two buildings; a fenced house; an oversize house centred without the lot being resized
    • the pole winning on a lot whose centroid is too near its edge
    • a U lot whose notch a house bridges with every corner on the lot, and how far it reaches past it
    • zero setbacks; a street edge with no front edge
    • a room drawn to the lot line, and a curved wall whose chord stays on the lot
  2. Draw a lot with setbacks in the site plan. The 2D and 3D dashed envelopes match.
  3. Drop in a lot by address on a project whose house falls outside the new lot. The house is re-centred and still faces the same way.

Screenshots / screen recording

Verified end to end through the hosted app in pascalorg/private-editor#910, which has the screenshots. Those runs used this code before the rebase; the hosted PR now pins this branch's head:

  • An Architect Low turn on a turned parcel stood the house square to the street, centred 0.00 m off the buildable centre, inside its setbacks, in one program.
  • An Architect Pro turn on a blueprint PDF read get_site and called place_building with the outline before its first wall. The house was centred on the lot when its walls first went up.

Review

The pascalorg/private-editor#910 reviews covered this code. On the editor side:

Fixed

  • Squaring. A house about to rise on a turned lot now stands square to the street.
  • Yards. Yards given with a new rotation are read at that rotation, and front and side yards on a lot askew to the house are solved together.
  • Front edge. The setbacks and the street use one front edge, and the 3D renderer now goes through siteSetbackEnvelope like the site plan.
  • Zero setbacks no longer read as "nothing buildable".
  • Notch. The overhang of a house across a notch counts the piece's middle.
  • create_room / add_wall measure their walls' mitred, curved bands, reading only the lot.

Rejected

  • which_building for the tools' ambiguous case. The tools resolve the building through targetBuilding, the helper every building-targeted tool shares, which refuses building_required. Renaming it would give one case two names.

Checklist

  • I've tested this locally with bun dev (through the hosted app's dev server, before the Bundle 2 rebase)
  • 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

Checks run on this branch (rebased on editor main cc978fe8), from the hosted worktree:

🤖 Generated with Claude Code


Note

Medium Risk
New shared placement/setback geometry affects editor, 3D site, and agent tools; incorrect math would misplace buildings or miss lot violations, though coverage is heavy on fixtures.

Overview
Adds site-first agent workflows: new read tool get_site and write tool place_building (building pose only; walls keep level coordinates), backed by shared core libraries site-placement, building-footprint, and a unified setback envelope in setback-envelope.

Consolidates lot geometry that was duplicated across editor site-plan, 3D site renderer, and nodes—editor/nodes now import siteFrontEdge, siteSetbackEnvelope, footprint loops, etc. from core; deletes nodes/src/site/setbacks.ts and buildingRecentreOffset. Site address/parcel/setback schemas move to site-facts.ts.

Surfaces lot compliance in verify_scene (footprint_outside_site, footprint_outside_setbacks, buildable_area_none) and in create_room / add_wall via lotOverhang on mitred wall bands. Lot drop-in re-centres with placeBuildingPose when the footprint is outside the new lot.

Large site-cases fixture suite exercises placement, yards, rotated parcels, and refusals; MCP README and agent guide updated.

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

@Aymericr
Aymericr force-pushed the feedback/site-first branch from f7b3a45 to c87f17c Compare October 13, 2026 02:35
Aymericr added a commit that referenced this pull request Oct 13, 2026
…d its walls are measured as built

From the review of the site placement (#1044):

- A house about to rise (no walls yet) is turned square to the street by place_building when no
  rotationDeg is given; a standing house still keeps its facing. On a turned lot a plan that fits
  the setbacks only squared now lands in them, and the brief names the rotation as the fix for a
  standing house only: named for one yet to rise, a Low run read it as the rotation to draw at.
- Yards are met together: on a lot askew to the house a front and a side yard move each other, so
  they are solved as one system, and a pose that misses them is refused. The street's side is read
  at the rotation the house will have, so yards given with a new rotation land on the right faces
  and a replay moves nothing.
- One front edge for the setbacks, the street and the yards: the recorded front edge, else the
  first street edge, else the north-facing guess. The 3D site and the site plan read it through
  siteFrontEdge and siteSetbackEnvelope.
- Setbacks of zero exclude nothing: the whole lot is buildable, not "none".
- A house bridging a notch reports how far its middle reaches past the lot line, not 0.
- create_room and add_wall measure the walls they raise, mitred and curved, against the lot line,
  and read only the lot to do it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aymericr added a commit that referenced this pull request Oct 13, 2026
…d its walls are measured as built

From the review of the site placement (#1044):

- A house about to rise (no walls yet) is turned square to the street by place_building when no
  rotationDeg is given; a standing house still keeps its facing. On a turned lot a plan that fits
  the setbacks only squared now lands in them, and the brief names the rotation as the fix for a
  standing house only: named for one yet to rise, a Low run read it as the rotation to draw at.
- Yards are met together: on a lot askew to the house a front and a side yard move each other, so
  they are solved as one system, and a pose that misses them is refused. The street's side is read
  at the rotation the house will have, so yards given with a new rotation land on the right faces
  and a replay moves nothing.
- One front edge for the setbacks, the street and the yards: the recorded front edge, else the
  first street edge, else the north-facing guess. The 3D site and the site plan read it through
  siteFrontEdge and siteSetbackEnvelope.
- Setbacks of zero exclude nothing: the whole lot is buildable, not "none".
- A house bridging a notch reports how far its middle reaches past the lot line, not 0.
- create_room and add_wall measure the walls they raise, mitred and curved, against the lot line,
  and read only the lot to do it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aymericr and others added 5 commits October 13, 2026 14:52
…ast its lot line reported

A house is placed on its lot by its building node's pose, never by moving its walls, so the
model's coordinates survive and every path (a layout program, a hand trace, a DXF import, the
lot drop-in) places it the same way.

- Core owns the setback envelope (the edge roles, setbackEnvelope with #1017's straight-front
  rule, the oriented yards) and the building footprint (levelFootprintLoops, footprintOutline,
  siteFrame, levelFrame). The editor's site plan, its lot code and the 3D site import them; the
  3D copy in nodes/src/site/setbacks.ts is deleted, so the 2D and 3D envelopes no longer diverge.
- lib/site-placement.ts: siteBrief reads the site in the building's level frame (the lot, the
  buildable area and its state: lot / setbacks / none / advisory, its centre, the street side
  and the yaw that squares the house to it, north, address, parcel, zoning, the new-project
  placeholder, and the footprint's fit, yards and overhang, judged by area so an edge across a
  notch counts). placeBuildingPose centres a footprint (an outline about to be drawn, or the
  walls) in the buildable area, the pole when the centroid will not hold it, or at the yards
  given; the yaw is kept unless asked, the lot never resized, and a replay changes nothing.
- Two shared tools, both surfaces: get_site (read) and place_building (the building node only),
  with annotations, justifications, the README table and the MCP agent guide.
- verify_scene reports footprint_outside_site, footprint_outside_setbacks and
  buildable_area_none; create_room and add_wall say when what they drew crosses the lot line.
- The lot drop-in re-centres a house the new lot leaves outside through placeBuildingPose,
  keeping its facing (buildingRecentreOffset is gone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e schema

The chat's durable lane serializes the scene snapshot inside a workflow, whose bundle refuses
any module that reaches nanoid; the site brief did, through migrateSiteMetadata in the site
node's schema. The address, parcel and setbacks schemas and their read-side lift now live in
schema/nodes/site-facts.ts (zod only), which site.ts and the brief import, and the brief is a
subpath of its own, @pascal-app/core/site-placement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Low run read "rotationDeg -20 squares the house" as layout()'s rotationDeg, which turns the
rooms inside their level, and broke its first plan; the rotation that squares the house is the
building's, which place_building sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d its walls are measured as built

From the review of the site placement (#1044):

- A house about to rise (no walls yet) is turned square to the street by place_building when no
  rotationDeg is given; a standing house still keeps its facing. On a turned lot a plan that fits
  the setbacks only squared now lands in them, and the brief names the rotation as the fix for a
  standing house only: named for one yet to rise, a Low run read it as the rotation to draw at.
- Yards are met together: on a lot askew to the house a front and a side yard move each other, so
  they are solved as one system, and a pose that misses them is refused. The street's side is read
  at the rotation the house will have, so yards given with a new rotation land on the right faces
  and a replay moves nothing.
- One front edge for the setbacks, the street and the yards: the recorded front edge, else the
  first street edge, else the north-facing guess. The 3D site and the site plan read it through
  siteFrontEdge and siteSetbackEnvelope.
- Setbacks of zero exclude nothing: the whole lot is buildable, not "none".
- A house bridging a notch reports how far its middle reaches past the lot line, not 0.
- create_room and add_wall measure the walls they raise, mitred and curved, against the lot line,
  and read only the lot to do it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e placeholder's

A Pro run read "… on its centre The lot is the new-project placeholder …" as one sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Aymericr
Aymericr force-pushed the feedback/site-first branch from c87f17c to 77931d5 Compare October 13, 2026 19:08

@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 77931d5. Configure here.

const drawing = buildSitePlanDrawing({
collections: after.collections,
installedPlugins: after.installedPlugins,
materials: after.materials,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lot drop-in skips multi-building recentre

Medium Severity

Lot drop-in recentres an outside house by calling placeBuildingPose without yards. That helper refuses with other_building_on_site whenever another walled building shares the site, so the update is skipped and recentred stays false. A house-plus-garage project whose house falls outside a newly imported lot is left sitting past the property line, with no error.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 77931d5. Configure here.

@Aymericr
Aymericr merged commit 6330239 into main Oct 13, 2026
5 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