chore: sync with upstream pascalorg/editor 1.0.0-beta (64 commits) - #7
Open
ss251 wants to merge 65 commits into
Open
chore: sync with upstream pascalorg/editor 1.0.0-beta (64 commits)#7ss251 wants to merge 65 commits into
ss251 wants to merge 65 commits into
Conversation
…ascalorg#558) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Terrain sculpting and terrain-aware vertical editing
…1 @pascal-app/editor@1.0.0-beta.1 @pascal-app/nodes@1.0.0-beta.1 @pascal-app/mcp@1.0.0-beta.1 @pascal-app/ifc-converter@1.0.0-beta.1
Fix release package tag push
Auto-generated room floors and ceilings, and any node whose geometry
builder baked its own vertical origin, assumed the plane `y = 0`. On
sculpted ground that left room slabs and ceilings floating above or
buried under the walls that generated them, and plugin-shipped kinds
(the Nature plugin's trees, flowers, grass) planted at the storey plane
instead of on the hillside.
The root cause was that terrain was opt-in per kind: the only way to ask
for the ground was `terrainSupportLift(...) ?? 0`, so every new consumer
had to remember to ask, and the ones that forgot silently got a flat
world.
Three seams close that:
- `levelBaseElevationAt(nodes, levelId, x, z)` — a total function for
"how high is the floor of the world here", alongside the existing
nullable `terrainSupportLift` ("is there terrain here"). Consumers
resolve a base through it instead of hardcoding a zero.
- `ctx.levelBaseAt(x, z)` on `GeometryContext` — how a pure builder,
which must not import the scene store, inherits terrain. Calling it
also enrolls the kind in terrain invalidation, so asking for the
ground is the registration: a plugin follows sculpted terrain with no
flag, capability, or core change.
- Rooms derive their surfaces from the walls that enclose them. Auto
floors take the highest wall base and auto ceilings the lowest wall
top — the only pair that cannot open a hole, since a floor at the
lowest base leaves daylight under the higher walls and a ceiling at
the highest top pokes through the shortest one. Both stay flat (a slab
is one scalar elevation by schema), so a room on a slope is a level
room cut into the hillside, with the low-side walls filling down to
meet it. This replaces `consensusElevation`, which abstained whenever
the walls disagreed and so never placed a surface on a slope at all.
The wall geometry signature now folds in a terrain sample taken at the
same point the placement samples, so a stroke that moves ground under a
room re-derives its surfaces — sculpting touches only `site.terrain`, so
without that term every signature stayed byte-identical and the sync
early-exited. Sampling (not hashing the field, not resolving the full
slab election) keeps it per-stroke and avoids folding slab polygons into
the signature.
Also memoizes `siteOf` on the `nodes` identity — it is called per wall
per frame and per wall per store update, where an O(N) scan made those
callers O(N²).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…levation fix: inherit terrain elevation in rooms, geometry builders, and plugins
…2 @pascal-app/editor@1.0.0-beta.2 @pascal-app/nodes@1.0.0-beta.2 @pascal-app/mcp@1.0.0-beta.2 @pascal-app/ifc-converter@1.0.0-beta.2
…ation-resilience Fix realtime terrain and scene collaboration primitives
…3 @pascal-app/editor@1.0.0-beta.3 @pascal-app/nodes@1.0.0-beta.3 @pascal-app/mcp@1.0.0-beta.3 @pascal-app/ifc-converter@1.0.0-beta.3
…alidation Stored scenes could link site → building → level through children arrays while building/level carried parentId null: loadScene never wrote parent ids and the legacy embedded-site-child flatten kept the already-flat node's null parentId. The editor traverses children and renders these scenes, but the hosted scene authority validates parent/child symmetry and rejected every snapshot into a fatal read-only session. healSceneNodes now repairs a null parent link when exactly one parent claims the node (embedded legacy site children claim by id), loadScene writes explicit parent links, and both migrations are exported server-safe through @pascal-app/core/scene-migrations so the hosted authority can apply the same normalization before validating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-repair core: repair null parent links so stored scenes pass authority validation
…4 @pascal-app/editor@1.0.0-beta.4 @pascal-app/nodes@1.0.0-beta.4 @pascal-app/mcp@1.0.0-beta.4 @pascal-app/ifc-converter@1.0.0-beta.4
…y policy and code of conduct (pascalorg#575) * fix(mcp): declare every field the tools actually return `get_level_summary` returned `floorIndex` and `create_from_template` returned `saveSkipped`, neither of which was declared in the tool's `outputSchema`. The MCP SDK generates the JSON Schema with `additionalProperties: false`, so once a client has cached the schema from `tools/list` it rejects the response: MCP error -32602: Structured content does not match the tool's output schema: data must NOT have additional properties Every real host — Claude Desktop, Claude Code, Codex, Cursor — calls `tools/list` before `tools/call`, so both tools failed in production on an empty default scene while passing the whole test suite. The suite missed it because the SDK client only validates `structuredContent` once it has cached the schema, and no test called `listTools()` first. `output-schema-contract.test.ts` connects a client that lists before calling and exercises the read-only tools, so a payload field added without a matching schema entry now fails CI. Reported in pascalorg#566, which found `get_level_summary`; `create_from_template` turned up in the same sweep once the harness could see it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: add a security policy and code of conduct, fix the Discord invite The README's Discord badge pointed at an expired invite (pascalorg#552); this uses the same non-expiring invite the hosted app already advertises. The repo had no SECURITY.md, so there was no stated way to report a vulnerability privately — the only visible channels were public issues and discussions. It now points at GitHub private reporting and security@pascal.app, and says what is in scope. Adds Contributor Covenant 2.1 as the code of conduct, and a Contributing section in the README linking both files plus the plugin guidance that CONTRIBUTING.md already gives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: stop tracking apps/ifc-converter/next-env.d.ts Next rewrites the route-types import in `next-env.d.ts` depending on whether it was last touched by `next dev` (`.next/dev/types`) or `next typegen` / `next build` (`.next/types`), so a tracked copy shows up as a spurious diff in unrelated PRs. `apps/editor` already ignores it; move the rule to the root ignore so it covers every Next app instead of being repeated per app. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ayers (pascalorg#576) Hosts that render overlays inside `<Viewer>`'s children seam had no way to match wall-cutout visibility: they either re-derived the camera facing dot-product (which drifts from the source of truth) or inferred state from the assigned material variant's `transparent` flag. The predicate is already pure, so export it as-is and give `wallMode` a named `WallMode` type instead of a bare `string`. Closes pascalorg#572
pascalorg#530) Roof-segments migrated on scene load are cast rather than zod-parsed, so a segment saved before `trim` existed reaches the renderer with the field absent and the geometry key crashed reading `trim.left` off undefined. Route the key through core's `normalizeRoofSegmentTrim`, matching what `use-segment-trim-clip` and `ridge-vent/geometry` already do. Besides handling the absent field, this makes the cache key agree with the trim the geometry is actually built from — an out-of-range trim normalizes to a clamped value, so keying on the raw field could otherwise vary while the built mesh did not. Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Every `grid:move` built a fresh `BoxGeometry`, disposed the previous one, and reassigned it — allocation and GPU buffer churn on the editor's hottest interaction path. Mount one unit `boxGeometry` and scale the mesh instead. The preview is purely visual (nothing raycasts against it), so scaling is equivalent to rebuilding at the target dimensions. Extends the original wall-only change to the fence tool, which carried a byte-identical copy of the same update function. Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`measure` computed gross shoelace area for slabs and ceilings, ignoring `holes` — so a floor with a stairwell opening reported the area of the uncut polygon. Every other net-area path in the repo already subtracts them (`validate-build-json`, `polygonSurfaceArea` behind the quick-measure HUD), so MCP disagreed with what the editor showed for the same surface. Zones have no `holes` field, so their behavior is unchanged. The regression test for an absent `holes` field goes through `loadJSON`, which casts its nodes into the store without a schema parse — that is the real path to a document where the field never existed, rather than mutating the live store dict `getNodes()` returns. Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…g#490) Placing a plugin node (`trees:tree` from the first-party Nature pack) in a saved scene made every later autosave fail with 400: `apiGraphSchema` validated every node against the static `AnyNode` union, which cannot enumerate kinds a plugin registers at runtime. A node whose `type` is outside `AnyNode` is now validated the way `validate-build-json` already treats a kind it cannot resolve — as a foreign node, held to the `BaseNode` envelope plus core's `AssetUrl` allowlist applied to every URL-shaped string it carries. Membership is decided by "not in `AnyNode`", not by a namespace pattern: `plugin-authoring.md` requires plugin *ids* to look like `vendor:pack`, never kinds, and its worked example registers `kind: 'couch'`. Reusing `AssetUrl` keeps the Phase 3 posture intact on a branch that has to accept unknown fields. A scheme denylist would have to enumerate every hostile scheme; `AssetUrl` already enumerates the safe ones, so `//evil.example`, `ws:`, `gopher:`, `about:blank`, the instance-metadata endpoint, and control-character-obfuscated `java\tscript:` are all rejected, and `PASCAL_ALLOWED_ASSET_ORIGINS` keeps narrowing https origins on this path too. Only URL-shaped values are checked, so a plugin can still store prose in `name` / `metadata` exactly as builtin nodes do. The URL scan is depth- and visit-bounded. An unbounded walk over a deeply nested body throws `RangeError` past `safeParse`, which the route answers as a 500 where the contract is a 400 with issues. Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com> Co-authored-by: Kone Venkatesh <konevenkatesh@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* respect millimetre notation in 3d labels * fix(editor): thread millimeter notation through remaining 3d labels The original change covered the measurement pill and a handful of panels but left every other live readout formatting in meters, so switching to millimeters silently applied to only part of the UI. The wall length label — the most visible measurement in the editor — was among the surfaces still hardcoded to the meter format. Thread `metricNotation` from the viewer store into the remaining label surfaces: wall and fence draft tools, wall measurement annotations, site edge labels, elevation guides, the terrain brush cursor, placement boxes (items and cabinets), zone quantities, and the ceiling panel. The draft tool effects gained `metricNotation` in their dependency lists so the live pointer handlers re-register when the preference changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The repo has 313 test files (~60k lines) but nothing ran them on a pull request: `ci.yml` only did lint + type check, and `mcp-ci.yml` covered only packages/mcp plus two apps/editor files. So roughly 2,200 of the 2,491 tests never executed as a gate. - add a `test` task to turbo.json (`dependsOn: ["^build"]`) and a root `test` script, then a `Test` step to ci.yml - add the missing `test` scripts to packages/viewer, packages/editor, packages/ifc-converter and apps/editor, which all contained tests but had no way to run them - add `@pascal-app/core` to packages/viewer devDependencies. It was declared only as a peerDependency, which Turbo does not traverse, so `^build` resolved to nothing and viewer's tests could not resolve core's `dist/`. editor/nodes/mcp already declare it in both places. - scope each test glob to `src`/`tests`/`lib` so compiled tests under `dist/` are not collected a second time - document `bun test` in SETUP.md and CONTRIBUTING.md Verified from a clean tree (no dist, no turbo cache): 12/12 tasks, 2,491 tests, 0 failures; warm re-run is fully cached. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…EPO-EDITOR-59) (pascalorg#455) * fix(viewer): centralize renderer capability fallback * fix(viewer): recover from renderer initialization failures * fix(viewer): harden GPU renderer fallback * fix(viewer): keep the discrete-GPU hint and release abandoned WebGPU devices Supplying `device` to WebGPURenderer makes three skip its own `requestAdapter` (three.webgpu.js:83922 `if (parameters.device === undefined)`), so R3F's `powerPreference: 'high-performance'` default was silently dropped and dual-GPU users could land on the integrated GPU. Thread it from the props R3F hands the gl factory into the adapter request instead of hardcoding it. Owning the device also means owning its lifetime: three deliberately declines to destroy a caller-supplied device, and `Renderer.dispose()` early-returns when `init()` never completed, so the WebGPU->WebGL fallback path leaked the device. Destroy it explicitly there, and reclaim a device that resolves after the request has already timed out. Also document why the gl factory returns a never-settling promise — that is the part that actually silences MONOREPO-EDITOR-59, since the old code rethrew into R3F's uncaught `await glConfig(...)`. --------- Co-authored-by: Anton Pascal <anton-pascal@users.noreply.github.com> Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
…ed (pascalorg#577) `@pascal-app/lingo` builds its unit registry at module-eval time and `registerKind` deep-copies each kind definition with `structuredClone`, so a browser without that global (Chromium <98, reported from Honor Browser 9.8 as `ReferenceError: structuredClone is not defined`) fails while the module graph is still evaluating. Sentry MONOREPO-EDITOR-FB. The shim belongs next to the import that makes lingo load-reachable, not in an app entry: `packages/editor` is what the OSS app, the hosted app and every npm consumer all load. An app-level polyfill covers only the app that declares it. Scoped narrowly and documented as such — lingo's kind table is plain JSON, so a JSON round-trip suffices and needs no new dependency. It is not spec-compliant and must not be relied on for real structured-clone semantics.
…ascalorg#584) `two-bedroom.ts:13-14` documents that positive z points south; `garden-house.ts` relied on the same convention without saying so, which read as a contradiction (pascalorg#362 was filed partly on that basis). The geometry already agrees — `wall_n` sits at `-HOUSE_D`, `wall_s` at `+HOUSE_D`, and `zone_garden` extends further into -z — so this records the assumption rather than changing it. The `GARDEN_DEPTH` comment said "along +z direction" while the garden is built at `-HOUSE_D - GARDEN_DEPTH`; corrected to -z. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`ci.yml` used `oven-sh/setup-bun@v2` with no version, so the main gate ran against whatever bun was latest at the time. A bun release could then break CI with no change in the repo, and CI could disagree with what contributors run locally — the worst kind of red build, because bisecting the repo finds nothing. `mcp-ci.yml` already pinned, but to 1.3.0, so the two workflows tested different runtimes. `packageManager` also said 1.3.0 while the committed lockfile is produced by 1.3.14. All three now name one version. Also adds `permissions: contents: read` to the quality job, matching mcp-ci. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…rg#556) * perf(core,viewer): stop rebuilding wall geometry every frame Three separate hot paths were doing full-scene work per frame on a 1081-wall floor, adding up to 5.6 s of main-thread stalls per six scroll ticks: - findJunctions was O(J x N) over every wall end; a spatial-grid prefilter narrows it to real neighbours (level assembly 59.3 s -> 0.01 s, calculateLevelMiters 436 ms -> 10.8 ms, identical output) - miter data is now cached per level and keyed on the exact wall inputs, so draining the dirty queue no longer recomputes it - getLevelElevations was called inside the per-wall loop; memoised - slab support and the wall appearance key were both unstable, which retriggered opening cutouts on every frame for no reason Measured on scene e5f5822f8837, six wheel ticks in split view: long tasks 5652 ms -> 4322 ms -> 0 ms. Co-Authored-By: Claude <noreply@anthropic.com> * fix(viewer): clear the level miter cache when the wall system unmounts The cache lives at module scope, so it outlived the mount it was created for: every level ever visited kept its wall array reachable, and a remount or a second project in the same tab simply added more. Editor teardown already resets the other shared singletons; the cache now does the same from the system's unmount effect. Also records the rule in the systems wiki, since the same trap is waiting for the next module-level memo. Co-Authored-By: Claude <noreply@anthropic.com> * fix(core,viewer): keep junction order stable and cover the miter cache The grid prefilter visits the per-cell bucket before the oversized-wall fallback, so a wall spanning more than JUNCTION_GRID_MAX_CELLS_PER_WALL cells was appended after shorter walls it precedes in the input. Collinear walls overlapping a junction tie on angle, and the sort in calculateJunctionIntersections is stable, so that reordering picked the other wall's thickness for the miter: a 20 m facade with a collinear infill of a different thickness moved the spur's boundary by 0.32 m. Restore the input order before appending. Extract the level miter cache so it is reachable from a test — replacing sameMiterInputs with `return true` previously left the whole suite green, which meant the cache the PR is built around had no coverage at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(core): key the level-elevation memo weakly The single-slot memo held a strong reference to the whole node record, so closing a project left its entire graph reachable until the next call. The adjacent wrapper in terrain-support.ts already uses a WeakMap for the same value; match it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
* mcp: layout clearance for doors and item overlaps
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.
* mcp: fix layout clearance review findings and add error log
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.
* mcp: tighten planned keep-out coverage and skip reasons
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.
* mcp: give living tv-stand door-wall nudge axes
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.
* mcp: fix the quality gate on layout clearance
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>
* mcp: keep check_collisions reporting real overlap, not proximity
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>
---------
Co-authored-by: wolf10drc <alaa@golead.io>
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(viewer): per-level base elevation parameter baseElevation is an offset: it shifts the level and every level above it within the same building (cumulative). Integrated into the level stacking computation so stacked, exploded, solo, floorplan, and snap-to-true-positions all respect it. Includes focused tests. Closes pascalorg#209 * fix: thread baseElevation through slab clamps, elevator stacking and migration Three Bugbot findings, all the same shape: baseElevation was applied in one path and ignored in another. - Covering-slab math assumed the floor above sat exactly one stored storey height away, so wall and ceiling clamps ignored the offset. A positive offset over-shortened walls under thick slabs and a negative one let them penetrate the slab above. Floor-to-floor distance now comes from the stacked elevations (above.baseY - current.baseY) via one helper, so the clamp math and getLevelElevations cannot drift apart. - Stair rise used the stored storey height for the same reason; it now uses the same helper. - Elevator level tables and the first-person elevator colliders built cumulative Y from storey heights, so cab stops desynced from the visible floors. Both now read baseY from getLevelElevations. first-person-controls had its own near-copy of that logic, which is deleted in favour of the shared resolveElevatorLevels. - Levels loaded from older project JSON could omit baseElevation, which made the editor control render NaN. Migration now normalizes it to 0 alongside level and children, with a defensive fallback at the control. * viewer: stop the level-system test mocking core, and fix the type gate The quality gate was red for two reasons. 1. `frameCallback?.({}, delta)` typed `never`. The useFrame mock assigns that binding while LevelSystem() runs, which TypeScript cannot see, so after the `frameCallback = null` reset it narrowed the binding to `null`. Reading it through a function keeps the declared type. 2. `mock.module('@pascal-app/core', ...)` replaced core's entire export surface. mock.module is process-wide and Bun does not restore it, so every viewer suite that ran after this file got the fake core: running the systems directory went from 47 pass / 0 fail to 29 / 2, and a whole-repo run lost 32 tests, including wall-support-extension's covering-slab case. The failure looked like a baseElevation regression and was not one. Core does not need mocking here — `sceneRegistry` is a real in-memory store with a `clear()`, and `useScene` is a zustand store with `setState`. The test now drives both directly and only mocks `@react-three/fiber` and `use-viewer`, the two modules that genuinely need a renderer or a React context. Dropping the `lerp` mock too: the real one is already pure. Also adds the now-required `baseElevation` to the level fixture in wall-drafting.test.ts — the schema default makes it required on LevelNode's output type, so the existing `as AnyNode` cast no longer held. Gates on the merge ref (main merged in): check clean, check-types 9/9, test 12/12 tasks with 0 fail, build 7/7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(core): build a fresh nodes record when re-asserting the lowered base elevation pascalorg#556 landed an identity-keyed WeakMap memo on getLevelElevations after this branch last ran green. The new base-elevation case mutated `stackedNodes` in place, so the second resolveStairTotalRise call handed the memo the same object and got the cached 2.9 back instead of 2.1. The memo's contract holds in production — every store write publishes a new record (updateNodesAction spreads into `nextNodes`) — so the fix belongs in the test. The sibling storey-height case in this same file already builds a fresh record; match it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(core): take the highest ceiling in the stack as the elevator shaft top The editor-side helper this PR consolidates into resolveElevatorLevels guarded the stack top with Math.max over every level; the core version reads only the topmost level. With the new baseElevation that is no longer the same thing — a negative offset can sink the top level's ceiling below the level beneath it, and the shaft then tops out under a served level and clips the cab (3 m -> 2.5 m for a -2.5 m offset on a 2 m top storey). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ride (pascalorg#587) Two package scripts use POSIX shell syntax that Bun's own shell does not implement, and Bun uses that shell for `bun run` on Windows: - `apps/editor`'s dev script passes `--port ${PORT:-3002}`, which arrives at Next verbatim: `option '-p, --port <port>' argument '${PORT:-3002}' is invalid`. Setting PORT does not help — the literal is never expanded. - the root dev script starts `set -a && . ./.env`, and `set` is not a Bun shell builtin, so it prints `bun: command not found: set` and silently skips loading `.env` entirely. Reproduced both on macOS with `bun run --shell=bun`, which selects the same shell Windows gets: $ echo port=${PORT:-3002} port=${PORT:-3002} # even with PORT=9999 in the environment $ set -a && echo set-worked bun: command not found: set Hardcoding the port would fix Windows but drop the PORT override that SETUP.md and .env.example both document. Instead, load a committed `.env.defaults` last and let `next dev` read PORT from the environment (the CLI already declares `.env('PORT')` on `-p, --port`). Nothing is shell-expanded, so it behaves the same on every platform, and the precedence stays shell PORT > .env.local > .env.defaults — verified at 3002 by default and 4321 with an override, under both shells. `.env.defaults` is needed because `.env` and `.env.local` are gitignored, so a checked-in default has nowhere else to live. Also corrects `.env.example`, which advertised a 3000 default the repo has not used since the port moved to 3002. Reported by @evolv3ai in pascalorg#551, including the Windows console output and the `set` finding. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#589) Documented in SETUP.md and .env.example, referenced by zero lines of apps/ or packages/ — there is no geocoder in this app, so the address search it promises does not exist. Noticed while reviewing pascalorg#268. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat: add docker support * fix(docker): persist scenes, drop root, and build with a real node Three problems kept the image from being usable: - Saved scenes went to $HOME/.pascal/data inside the container layer, so `docker compose down` silently discarded every project. Set PASCAL_DATA_DIR and back it with a named volume. - The container ran as root. The base image already ships a `bun` user. - `next build` runs under `node`, and oven/bun's `node` is a shim that re-execs bun. Next 16's build crashes it on both arm64 and amd64 — segfault on bun 1.3.14, a turbopack CommonJS error on 1.3.0. Install real nodejs. CI never hit this because runners have their own node. Also pins the base image to the bun version in `packageManager` and drops the `touch apps/editor/.env.local` workaround, which no longer matches the path the build script reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Aymeric Rabot <aymeric@pascal.app> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Docker support landed in pascalorg#268 with no mention in SETUP.md, so the only way to find it was to notice the compose file. Records the port constraint too, since remapping it 500s the /scenes page for a non-obvious reason. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…lorg#592) Space polygons are per-level footprints and stacked storeys share a footprint, so the upper slab's centroid sits inside the ground floor's space polygon. Room-scope paint on a ground-floor slab therefore also repainted the slab directly above it. Every other branch in resolvePaintScopeTargets already gates on space.levelId (resolveWallPaintSpace, connectedExteriorBoundaries, wallTargetsForBoundaries); the slab branch was the one that did not. Gate both the space lookup and the fan-out, matching how space-detection itself defines level membership (node.parentId === levelId). The existing slab fixture omitted parentId, which is why no test caught this — real slabs always have one, from both the slab tool and reconciliation. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ys reachable (pascalorg#593) The root-category grid is `grid shrink-0` with `aspect-square` tiles over `functionTree`, which is embedder-supplied and unbounded. Row count grows with the taxonomy while the item list below is the only scroller, so past about 20 categories at 5 columns the list is pushed out of the panel and nothing can scroll it back. Same shape as the hosted "By room" panel bug in pascalorg#562. `furnishTools` (the other header in this panel family) is a static 6-item array, so it cannot reach this state; this one can, because the array crosses the embedding boundary. Cap the grid at 40% of panel height and let it scroll independently. No visual change at today's category counts. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Integrate Mint asset plugin - register the Mint panel and plugin with Pascal Editor - add the same-origin OAuth and API proxy route - include Mint plugin styles and pin the public v0.1.3 package * Harden Mint plugin host integration * Support self-hosted Mint origins
`from_brief` told the model to "call `create_project` if the user asks for a new project" and nothing else about scene binding, so a brief against an existing project produced mutations with no bound scene. Those apply in memory only — nothing persists and nothing appears in the browser, and the prompt never said so. Name all three ways to bind (`create_project`, `list_scenes` + `load_scene`, `create_house_from_brief`), state the consequence of skipping it, and split the overloaded save/verify sentence out of the binding instruction. The Task section becomes bind / build / finish so the ordering is legible rather than one run-on line. The registered tool description still claimed the prompt "produces a plan of apply_patch calls"; it has produced semantic tool calls for some time. Prompt text only — no runtime behavior changes. Co-authored-by: Srujan Reddy <srujanreddygangireddy@gmail.com>
…alorg#597) A custom scene material survived nothing. `materials` was absent from the `SceneGraph` type, so every layer that rebuilds a graph field-by-field silently omitted it: `cloneSceneGraph`/`forkSceneGraph`, the MCP bridge's `exportJSON`/`loadJSON`, `exportSceneGraph` (which `save_scene`, `publishLiveSceneSnapshot` and variant generation all persist through), the SQLite read schema, and the editor's API graph schema. Reopen a scene and it came back with default surfaces. Nodes point at materials through `slots` values shaped `scene:mat_…`. Those are opaque strings to the clone remapping, so material ids are carried over unchanged — minting new ones would orphan every reference. Two things fell out of fixing the round trip: `loadJSON` dropped collections too, for a different reason: it applied plugin state in a second call after `setScene`, and `setScene` resets `collections` and `materials` to `{}` whenever they aren't in its `extra` bag. Everything now goes in one call, which also fixes dirty-tracking for plugin-owned nodes — `setScene` marks nodes dirty at the end, and `markDirty` skips nodes whose plugin isn't installed yet. The editor's echo-suppression signature omitted `materials`, so a local edit that touched only the palette signed identically to the last remote payload and the save was skipped — the edit was lost. The signature now defaults the fields `setScene` always writes, so a payload that omits them (MCP live sync sends exactly that) still matches the store that defaulted them. Materials are validated where they enter from the network, not where they are read back from disk. The API schema holds them to `SceneMaterial` in `superRefine` — they carry texture URLs, and that schema is where the `AssetUrl` allowlist is enforced — but keeps the parsed value untransformed, since the routes persist this schema's output and `SceneMaterial` injects defaults and strips unknown keys. The SQLite read path stays permissive: nothing validates on write and `parseGraph` throws, so a strict read shape would turn one odd stored value into a permanently unloadable scene. Co-authored-by: ShiroKSH <kushidashiro@gmail.com>
…fixes (pascalorg#598) Both pascalorg#596 and pascalorg#597 landed without a CHANGELOG line. Match the 0.6.0 style and name the contributors, since the release notes are how credit for an outside fix actually surfaces.
Adds the versioned standalone editor runtime, lifecycle and update tooling, release automation, tests, and documentation for @pascal-app/cli 0.1.0.
Syncs 64 upstream commits, taking upstream's core geometry and persistence semantics while keeping the magic-cabinet extensions on the refactored surfaces. Upstream wins (core semantics): - loadScene now writes explicit parentId links (Site -> Building -> Level); the scene authority rejects the parent/child asymmetry our version left. - setScene's `extra` bag (pascalorg#597) carries collections/materials/installedPlugins in one tracked write; the follow-up setInstalledPlugins call is gone, so materials survive every persistence boundary. - sqlite scene rows persist `materials`. - graph-schema validates foreign (plugin) nodes through the BaseNode envelope plus the AssetUrl allowlist walk, which drops our @magic-cabinet/pascal-plugin/schemas import out of a server route. Ours preserved, ported onto the new surfaces: - SceneBridge keeps its injected per-session store; SetSceneExtra is now derived from UseSceneStore instead of the useScene singleton. - HTTP transport keeps creation-identity sessions (resolveIdentity, localWorkspace, identity-mismatch 403) and gains upstream's GET /health. - pascal-mcp keeps the CreationContext server factory and gains the PASCAL_INSTANCE_ID health envelope. - Scene URLs take upstream's PASCAL_EDITOR_ORIGIN support but keep the /scene/:id route, the only one this app serves. Also: - @magic-cabinet/pascal-plugin pinned @pascal-app/* at ^0.9.2, so bun installed a stale published core beside the workspace one and the two type identities collided. Pinned to "*" like the rest of the workspace. - The Mint plugin upstream added is not wired into this fork: its dependency, API route, transpile entry and Tailwind source are omitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the fork up to date with
pascalorg/editor— 64 upstream commits,1.0.0-betaacross every package, Next.js 16.3. 18 files conflicted; all resolved.Why now
Several upstream commits bear directly on the architectural-drawing ingest work:
163ef0bestabilize collinear wall junction ordering (fix(core): stabilize collinear wall junction ordering pascalorg/editor#596)cdf00f6csubtract surface holes from measured area (fix(mcp): subtract surface holes from measured area pascalorg/editor#563)eecd799bper-level base elevation parameter (feat(viewer): per-level base elevation parameter pascalorg/editor#312)6d425507layout clearance for doors and item overlaps (mcp: layout clearance for doors and item overlaps pascalorg/editor#569)2e526e91preserve scene materials across every persistence boundary (fix: preserve scene materials across every persistence boundary pascalorg/editor#597)befbecb8start managed MCP with local editor (Start managed MCP with the local Pascal editor pascalorg/editor#612)75db47b8Next.js 16.3 (Upgrade Next.js to 16.3 pascalorg/editor#579),78b11a41docker support (feat: add docker support pascalorg/editor#268)Resolution principle
Upstream wins on core engine, geometry and persistence semantics. Magic Cabinet extensions were ported onto the new surfaces rather than reverting upstream refactors.
Upstream taken (core semantics)
packages/core/src/store/use-scene.tsloadScenehierarchy — writes explicitparentIdfor Site → Building → Level. The scene authority rejects the parent/child asymmetry our version left behind. Kept inside ourcreateSceneStore()factory.setInstalledPluginswas independently identical on both sides.packages/mcp/src/bridge/scene-bridge.tssetSceneextrabag (pascalorg#597): collections + materials + plugin state land in one tracked write, so materials survive persistence. Replaces our follow-upsetInstalledPluginscall, whichsetScenewas silently clobbering.packages/mcp/src/storage/sqlite-scene-store.tsmaterials; adoptPASCAL_EDITOR_ORIGIN.apps/editor/lib/graph-schema.ts+.test.tsBaseNodeenvelope plus theAssetUrlallowlist walk. Supersedes our bundled-plugin-schema lookup and removes a@magic-cabinet/pascal-plugin/schemasimport from a server route — a layer-boundary win perCLAUDE.md. Upstream's 15 tests replace our 3.bun.lockbun install.package.jsonpackageManager→bun@1.3.14.Ours kept, ported forward
packages/mcp/src/transports/http.tsresolveIdentity,localWorkspace, identity-mismatch 403, per-sessionMcpServer) — strictly richer than upstream's transport map — and grafted on upstream'sGET /health. Complementary, not competing.packages/mcp/src/bin/pascal-mcp.tsCreationContextserver factory and hoisted initial-scene read; added upstream'sPASCAL_INSTANCE_IDhealth envelope.packages/mcp/src/transports/http.test.ts,operations/scene-operations.test.tsapps/editor/lib/bootstrap.tsapps/editor/components/scene-loader.tsxDockerfilenodejsis installed because bun's node shim segfaults undernext build. Base image →oven/bun:1.3.14..dockerignore!packages/magic-cabinet-engine/distnegation..env.example,README.mdTwo changes worth a second look
1.
@magic-cabinet/pascal-pluginwas pinning@pascal-app/*at^0.9.2. Upstream's bump to1.0.0-betameant bun stopped linking the workspace packages and installed a stale published copy of@pascal-app/coreintopackages/magic-cabinet-plugin/node_modules. Two copies of core, two incompatible type identities, and the build failed on a phantom missingbaseElevation/CameraPose. Pinned to"*"to match the rest of the workspace.2. The Mint plugin upstream added is deliberately not wired in.
@mint/pascal-pluginis an externalgithub:dependency behind Mint OAuth againsteditor.pascal.app. Omitted: the dependency,app/api/plugins/mint/[...path]/route.ts, thenext.config.tstranspile entry, theglobals.css@source, and the.env.examplenote.packages/cliis untouched. This is a product call, not a merge call — say the word and it is one commit to add back.MCP tool-surface delta
No tools added, removed, or renamed. 54 registered before, 54 after, names identical. Verified live against the built server.
Behaviour did change inside three existing tools:
measure— polygon area is now net of holes (shoelaceArea(polygon) - holes, floored at 0); description now says "net area". Zone nodes are unaffected. Directly relevant to drawing-ingest area accuracy.verify_scene— now also reports door keep-out violations and rotation-aware item-item footprint overlaps; issues gained afloorIndexfield.check_collisions,create_room/add_door/add_window/furnish_room— reworked upstream alongside the newdoor-clearanceandlayout-clearancemodules.New internal modules:
door-clearance.ts,layout-clearance.ts,output-schema-contract.test.ts.Gates
All green.
Live MCP on port 8099 (
bun packages/mcp/dist/bin/pascal-mcp.js --http --port 8099):initialize→serverInfo: pascal-mcp-server 1.0.0-beta.5tools/list→ 54 tools, including upstream'screate_wall,create_level,cut_opening,place_item,validate_scene,measurecreate_cabinet_run,start_magic_kitchen_session,update_magic_kitchen,set_kitchen_palette,set_kitchen_handles,set_kitchen_camera,get_kitchen_bom,validate_magic_kitchen_sessionGET /health→ 200 with the instance envelope (upstream's new endpoint, through our path guard)Not verified
loadScenehierarchy change adds an explicitparentIdwhere our version relied onchildrenalone — type-checked and unit-tested, but not eyeballed in the 3D editor.nodejsfix is ported from upstream's reasoning, not independently reproduced.@pascal-app/cliis new from upstream and was not exercised.🤖 Generated with Claude Code