Pascal güncellemesini al (beta.2 → beta.5, 56 commit) - #5
Open
ovurrsl wants to merge 66 commits into
Open
Conversation
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.
…pascalorg#578) `useAutoSave` refuses to persist a graph that drops from populated to a bare scaffold, on the assumption that it is an accidental full deletion. The baseline it compares against was seeded once when the hook mounted — which happens before the scene has loaded, so it sat at the scaffold count for the whole session and the guard could never fire. The one write it exists to stop is the one it let through: an autosave racing the initial load overwrites the stored scene with the scaffold. The loading branch of the store subscription already refreshed the snapshot, collections, materials and plugin refs; it just never refreshed the count. Rather than add a fourth assignment to a branch whose contract was implicit, the baseline now lives in `createStoredNodeCountTracker`, which distinguishes the two things that were being conflated: a graph read from storage becomes the new baseline, an edited graph does not. That also removes the duplicated guard between `executeSave` and `flushOnExit`, and makes the invariant testable without React — the same approach `floorplan-camera-sync.ts` takes for its closure state. Surfaced by @evolv3ai in pascalorg#551, which fixed the symptom with a `nodeCount === 0` check in the standalone app's save route. This fixes it in the shared hook instead, so the hosted editor and npm consumers are covered too, and a blocked write can't be laundered through the 409 conflict path that `scene-loader.tsx` treats as success. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Redirect `/editor/:id` → `/scene/:id` so MCP `editorUrl` links resolve in the standalone app, and add a reversible light-preview toggle that threads `disablePostFx` through Editor → Viewer (the URL flag alone is captured at module load, so it does not survive client navigation). Co-authored-by: ActArtech <ActArtech@users.noreply.github.com>
* editor: session multi-select groups (Ctrl+G) Add editor-only session selection groups so multi-select furniture/items can be regrouped with Ctrl/Cmd+G and reselected by plain click. Group/Ungroup icons sit on the multi-select floating pill and side panel. Not scene-graph; not saved with the project. * editor: fix session group Bugbot issues Clear session groups on scene load, prune memberships on cut, and pass a proper options object for floorplan Cmd/Ctrl+click toggle. * editor: clear session groups when entering version preview Preview only applied the preview graph; groups from the edit session could still expand selection. Clear groups on preview entry. * editor: keep session-group membership through delete/undo Deleting a group member committed the pruned membership back to the store, so Cmd+Z restored the node outside its group — and deleting two of three dropped the group past the two-member floor for good, unrecoverable since session groups are not in the undo history. Every read already filtered against the live scene, so keeping full membership and filtering only at read fixes undo without new state: the delete hooks in group-actions and the unused pruneSessionGroupsToScene export both go away, and the several other deleteNodes callers that never called the hook are now correct too. Renames pruneSessionGroups to liveSessionGroups to say what it is. Also: - add the missing `alt` to SelectionModifierKeys initializers, and track Alt in the 3D modifier ref so Alt+click actually reaches the 3D path - thread expand + Alt through resolveFloorplanBackgroundSelection, the 2D background hit-test path that bypassed group expand entirely - narrow the Ctrl/Cmd+G capture handler to e.code, and note why its stopPropagation is safe against the sibling capture listeners - document how session groups relate to the persisted collections concept 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>
…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>
`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.
Captures are re-renderable artifacts, not user originals, so the snapshot pipeline (and the thumbnail generator's non-WebGPU fallback) now encode webp at q0.9 instead of PNG — a 1920x1080 hero shot lands roughly an order of magnitude smaller on the wire and in storage. Alpha survives, so transparent item/preset captures are unaffected. SNAPSHOT_MIME/SNAPSHOT_QUALITY are exported so embedders keep their canvas captures and upload content types in sync. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * fix: reconcile room surfaces with wall topology * fix: index room topology reconciliation * fix: address topology reconciliation review * test: broaden topology equivalence coverage * test: cover customized surface split in editor * style: format wall topology regression test --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(editor): add Streetscape plugin to Pascal Register @pascal-app/plugin-streetscape (pinned by commit) alongside the trees and mint plugins: plugin discovery + host panel at bootstrap, Next transpilation, and Tailwind @source so its panel styles compile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): credit the Streetscape plugin's actual author Override creator/pluginUrl at registration — the upstream manifest points at a pascalorg repo that doesn't exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(editor): bump Streetscape pin to webp thumbnails Upstream converted the 37 PNG catalog thumbnails (~17 MB) to webp/svg (~630 KB), bringing the packed CLI back under its release budget, and fixed pluginUrl/repository metadata — keep only the creator-name override. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…calorg#630) * fix(editor): keep non-terrain walls plane-bound on creation The terrain drafting flow stamped the ghost's explicit height and a supportOffset onto every wall whose frozen construction plane sat above y=0 — i.e. any wall started on a slab or deck, not just ground-hosted terrain chains. Such walls stopped following the level height and no longer re-elected their base from slab support. Gate the stamping on a ground-drafted plane (the terrain exception), and re-resolve the aimed support surface per commit for non-ground chains so a later segment still elects the slab it visibly crosses instead of being capped at the first click's elevation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(nodes): rework wall panel around plane-bound top Top: "Follows level" (no explicit height, shows the resolved height) vs "Custom height" (seeded from the effective height so geometry doesn't jump on detach). Bottom: the terrain infill toggle no longer materializes an explicit height, so toggling it can't silently detach the wall top from the storey plane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): keep flat-ground wall drafts plane-bound Pointing at bare ground freezes a GROUND construction plane, and the commit path treated every ground-preferred draft as a terrain chain: stamped ghost height, persisted ground host, election capped at the draft plane. On flat ground (no sculpted terrain) all three are wrong — the wall showed "Custom height 2.50" instead of following the level, and a slab drawn later could never lift it, leaving the wall buried in the slab (z-fighting band at the base). Gate the terrain exception on terrainSupportLift(): ground-preferred drafts with no terrain support drop their draft options entirely and commit plane-bound. The terrain-freeze test now seeds a real terrain field; a new regression test pins the flat-ground shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(nodes): restore full plane-bound defaults from the follows-level toggle Switching Top back to "Follows level" only cleared the stored height, so regression-era walls (stamped ground host + draft offset) kept their base pinned at the level floor, still buried in any slab. The toggle now also drops the draft offset, and drops a ground host when no sculpted terrain supports it — giving existing broken walls a one-click repair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(nodes): carry the base repair through the bottom Auto toggle The follows-level toggle repairs regression-era walls, but walls whose Bottom already displays "Auto" while secretly ground-pinned had no clickable path that kept their custom height — the pin isn't reflected in the control, and the base stayed buried in the slab (z-fighting its side faces). "Auto" now performs the same re-election repair (drop the draft offset, drop a terrain-less ground host), and the segmented control fires on already-selected clicks, so clicking "Auto" itself heals the wall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(core): self-heal buried ground pins in the vertical canonicalization Regression-era walls carry supportSlabId 'ground', which short-circuits slab election: the base stays at the level floor, buried in the room slab and z-fighting its side faces, while the panel truthfully-but- uselessly displays the base as automatic. Users shouldn't have to click anything: the load-time canonicalization now strips the pin (and draft offset) when the un-pinned election would land the wall on a slab its base is currently embedded in. Deliberately narrow so the one legitimate flat-scene ground pin survives: a wall kept on the ground under a hovering deck is not buried (the deck's occupied interval sits above the base) and keeps its pin, and scenes with sculpted terrain are skipped wholesale — the pin is load-bearing there and the live terrain field isn't visible to this pure, authority-shared pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pascalorg#639) Viewport and area crop modes captured at the raw canvas backing-store resolution, so retina/5K displays produced multi-MB WebP snapshots that blow past upload transport limits. Clamp the output long edge to 2048 (matching the 1920-class standard presets) in the snapshot pipeline, the non-pipeline fallback, and the capture HUD resolution chip. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ries (pascalorg#641) A noPanel tab highlights in the rail and activates without opening the sidebar panel column, for host surfaces that swap the stage instead (e.g. the hosted item builder). Mobile layout skips noPanel tabs. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rg#642) Level height, wall custom height, ceiling custom height, and column height were all UI-capped at 6m, which rules out tall single-space builds (climbing gyms, warehouses, halls). 20m covers the tallest realistic single-storey uses — lead-climbing walls top out around 15-18m — while keeping the sliders usable in the common 2-4m band. The caps were never enforced in core schemas; only the controls change. Co-authored-by: Claude Fable 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.
mainmirrorspascalorg/editorand is 10 commit(s) ahead ofintegration.This carries those changes into the integration branch, where everything this fork adds lives. Conflicts are expected — the rule for each file that regularly conflicts is in
UPSTREAM.md, and theUpstream checkworkflow reports the list before you start.Generated by Claude Code