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:
co-authored by
Marcel Gruber
parent
2694bcf555
commit
c986ced14d
@@ -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.
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
"examples",
|
||||||
"README.md",
|
"README.md",
|
||||||
"CHANGELOG.md"
|
"CHANGELOG.md"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user