docs(mcp): document plan↔world coordinate convention (#356)

Adds a Coordinate conventions section to packages/mcp/README.md and a 71-node MCP demo scene that exercises every claim it makes. Closes #337.

Covers: right-handed scene with X/Z ground plane and Y up; metres for lengths and radians for rotations as Euler [x, y, z]; the [x, z] → (x, y, z) plan-to-world mapping with no sign flip in the stored contract; level/building-local framing (world only under identity transform); rotation-not-reflection caveat for the 2-D plan panel and the iso-default top-down azimuth offset; a worked 30° rotated slab example; and the wall-local-metres trap for door/window/place_item coordinates.

Companion: examples/coordinate-conventions-demo.{json,md} with a reference compass at the origin and Demos A/B/C/D illustrating axis-aligned baseline, the rotated example, and the page-intent vs world-result L pair.

Co-authored-by: Marcel Gruber <marcel@grubertech.com>
This commit is contained in:
Marcel Gruber
2026-06-03 13:41:52 -04:00
committed by GitHub
co-authored by Marcel Gruber
parent 2694bcf555
commit c986ced14d
4 changed files with 2263 additions and 0 deletions
+70
View File
@@ -227,6 +227,76 @@ console.log(scene)
See [`examples/embed-in-agent.ts`](./examples/embed-in-agent.ts) for a See [`examples/embed-in-agent.ts`](./examples/embed-in-agent.ts) for a
compilable version. compilable version.
## Coordinate conventions
Pascal is a **right-handed** scene where **X and Z form the ground plane and Y
is up**. Lengths are in **metres**; rotations are **radians**, stored as Euler
`[x, y, z]` tuples.
**Plan → world.** Every 2-D point you pass is a level/building-local
ground-plane coordinate
`[x, z]` — this includes `wall.start` / `wall.end` and the `polygon` / `holes`
arrays of `slab`, `zone`, and `ceiling`. With the default identity building
transform, it appears in world space as:
```
[x, z] → (x, y, z) // the 2nd component is world Z (depth), not "up"
```
There is no sign flip in the stored convention: tooling consumes the second
component as world Z directly. The vertical `y` starts from the owning level's
stacked height as computed by the level system from accumulated level heights,
plus the element's own height; slabs additionally carry an absolute
`elevation`.
**Heads-up when you compute coordinates outside the editor.** Pascal's
viewports apply their own rotations on top of the world axes: the 2-D plan
panel wraps its content in a 90° rotation (`FLOORPLAN_VIEW_ROTATION_DEG`), and
the 3-D "top-down" snap preserves the camera's current azimuth, so when invoked
from the iso default position, world and screen axes are offset by ~45° until
you orbit to an axis-aligned view. So a layout authored as if
*"Y = north, viewed top-down"* — common in land surveys, north-up site plans,
and 2-D plotting libraries — will arrive **rotated** relative to its source
when viewed in Pascal (and possibly further reflected, depending on which
viewport and camera state you're in). The editor's own 2-D and 3-D tools are
internally consistent with their stored coordinates, so this only affects
geometry authored programmatically. To verify orientation before trusting
externally-computed coordinates, place a scaled guide image at known anchor
points and check alignment; apply whatever rotation (or reflection) your
authoring side needs to match.
A worked demonstration of all of this — axis-aligned baseline, the rotated
30° example below, and a paired "page-intent vs world-result" L for the
external-coordinate gotcha — lives in
[`examples/coordinate-conventions-demo.md`](./examples/coordinate-conventions-demo.md)
and [`examples/coordinate-conventions-demo.json`](./examples/coordinate-conventions-demo.json).
Load the JSON with
`pascal-mcp --stdio --scene examples/coordinate-conventions-demo.json`.
**Example — a 6 × 4 m slab rotated 30° about its first corner** (coordinates
rounded to 3 dp; sides ≈ 6 m / 4 m; not axis-aligned, so the mapping is
actually exercised):
```json
{
"op": "create",
"parentId": "<levelId>",
"node": {
"type": "slab",
"elevation": 0.0,
"polygon": [[0, 0], [5.196, 3.0], [3.196, 6.464], [-2.0, 3.464]]
}
}
```
This lands flat on the ground (Y = 0), about 6 m along a heading 30° off the +X
axis and 4 m along its perpendicular — i.e. occupying world (x, z) directly.
One separate gotcha: wall-attached coordinates are wall-local, not plan
coordinates. Stored door/window `position[0]`, and `place_item` `position[0]`
when the target is a wall, are metres along the wall; wall-attached rotations
are wall-local too.
## Tools ## Tools
All tools validate their inputs and outputs with Zod. Mutation tools are All tools validate their inputs and outputs with Zod. Mutation tools are
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,68 @@
# Coordinate-conventions demo
Companion scene for the **Coordinate conventions** section of
[`../README.md`](../README.md). Stress-tests every claim that section
makes against real Pascal-generated geometry, built entirely through the
MCP API.
## Load it
```bash
pascal-mcp --stdio --scene examples/coordinate-conventions-demo.json
```
The scene fits in a 50 m × 50 m ground-plane footprint at `level: 0`.
## What's in the scene
A flat ground level with four demos, plus a labelled compass at the
origin so the world axes are unambiguous regardless of which viewport
you're in.
| Section | What | Why |
|---|---|---|
| Reference compass at the origin | 12 m `+X` and `+Z` axis bars with arrowhead tips, short `-X` / `-Z` stubs, an origin marker, plus zone labels naming each component of `[x, z]`. | Establishes which way `+X` and `+Z` actually point in world space, independent of viewport rotation. |
| **Demo A** at world `(18, 0)` | Axis-aligned 6 m × 4 m rectangle. | Baseline that matches the only example currently in `examples/generate-apartment.md` style (`[0,0] → [10,0]`). |
| **Demo B** at world `(18, 10)` | The proposed README example, **verbatim**: `polygon: [[0,0],[5.196,3.0],[3.196,6.464],[-2.0,3.464]]` at this offset. | Programmatically verified to be a 6 m × 4 m rectangle whose first edge is heading 30° CCW from `+X` (side lengths 6/4/6/4 m to 4 dp, `AB · AD = 0`, heading = 30.0007°). Confirms the README's worked example produces the geometry it claims. |
| **Demo C** at world `(0, 22)` and `(10, 22)` | An **L** authored on a north-up page (page-+x right, page-+y up) drawn at half scale and faded; alongside it, the same L pasted into `[x, z]` **uncorrected**, drawn at full scale and vivid. | Makes the external-coordinate gotcha visual: the author's page-+y direction lands on world +Z, which does not correspond to "screen-up" in any of Pascal's viewports. |
| **Demo D** at world `(22, 22)` and `(32, 22)` | Same pairing for the L with its second coordinate reflected (`z → 5 z`) before paste. | Makes plain that z-reflection only "corrects" a true mirror; Pascal's viewports apply a *rotation*, so the reflection produces a mirror-image of Demo C rather than a page-correct L. |
| Takeaway band on the south edge | Short labels summarising the convention. | Self-documenting; readable in any viewport without an external README. |
## What it confirms
Open the scene in Pascal (or render it from the JSON) and you can read
each claim directly off the geometry:
1. **`[x, z] → world (x, 0, z)` is exact, no sign flip.** Demo A and
Demo B both sit flat on the floor at Y = 0; their polygon vertices
round-trip through `save_scene` / `get_scene` byte-identical to what
was authored (graph hash matches, see `validate_scene` output).
2. **Demo B is the rectangle the README says it is.** A simple analytic
check on its 4 vertices yields side lengths 6, 4, 6, 4 m (to 4 dp)
with the first edge at 30.0007° from +X and perpendicular adjacent
edges. No rendering required.
3. **External page coordinates *rotate* (not mirror) when pasted as
`[x, z]` and viewed in Pascal.** Inspecting Demo C in the 2-D plan
panel: the page-up L lands rotated 90° clockwise, so the author's
"stem-up, foot-bottom-right" reads as "stem-on-right, foot-along-top"
on screen. Inspecting Demo D right next to it: the z-reflected
variant lands as the *mirror* of Demo C — neither matches a
page-correct L. That demonstrates why "reflect across the axis" is
the wrong corrective for an issue that is, in this viewport, a
rotation.
4. **The 3-D "top-down" snap is offset 45° from world axes by default.**
Viewing the scene in 3-D and snapping to top-down from the default
iso camera shows the rectangular site polygon as a *diamond* (its
long edges are diagonals on screen, not axis-aligned). That's the
camera's "up" vector being inherited from the iso start; once you
orbit to a true axis-aligned top-down it goes away. The label
inviting reviewers to "verify against a guide image" exists for this
reason.
## Reproducibility
The canonical demo is the JSON file in this directory; load that file to compare
the geometry against the notes above.
+1
View File
@@ -42,6 +42,7 @@
}, },
"files": [ "files": [
"dist", "dist",
"examples",
"README.md", "README.md",
"CHANGELOG.md" "CHANGELOG.md"
], ],