Files
editor/packages/nodes
f6d28f6794 Fix pass: walkthrough, deck-coupled wall items, live-preview elevation, room snap modes, import validation (#537)
* fix(core): elect wall slab support from the carrying profile, not face coverage

An elevated deck drawn against a house wall covers the wall's outer face
line end-to-end (boundary contact counts), so the max-across-polylines
election handed the wall origin to the deck: every wall-hosted window/door
rode along whenever the deck height changed, and placement local-Y was
clamped above the deck top. Elect from the carrying profile instead (per
arc segment: highest support per face, min across supported faces), with
the pointer cap applied inside the profile so a capped-away deck still
falls back to the floor that carries the wall. Also pass curveOffset/
thickness/supportSlabId at the window/door tool query sites so their
cursor agrees with the rendered wall frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): honor live node overrides in spatial-grid support queries

Group drags publish translated slab polygons and wall endpoints to
useLiveNodeOverrides only; the committed spatial index made floor items
and walls re-elect support against the pre-drag slab footprint, so
multi-selection moves and room-preset placement jumped vertically until
the validating click committed the batch. Support queries now read
live-effective slab/wall records and bypass the rendered-polygon cache
while a slab or wall on the level has an override; the committed cached
path stays the fast path otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(editor): give the room-preset stamp tool a polygon snap context

The host app's room stamp drives placement with tool='room', which has no
registry entry, so snapContextOf resolved null: Shift never cycled the
snapping mode and the HUD chip stayed hidden during room preset placement.
A tool-level context map hands the stamp the no-angle polygon set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): only synthesize walkthrough fallback floors for ground levels

Every slab-less visible level got a >=30x30m opening-free fallback floor
box at its elevation, so a walkthrough spawned on an upper level (the
no-spawn-node fallback ray picks the highest surface) stood on a phantom
plane it could never descend from. Match the baked-GLB viewer policy:
only the lowest level of each building (derived baseY === 0) gets the
fallback; upper levels rely on their real slabs, whose stair openings
are cut into the geometry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): fold useLiveTransforms slab deltas into support queries

The slab move tool and the room-preset stamp publish a translation DELTA
to useLiveTransforms (no polygon override), so the spatial index still
elected support against the slab's committed footprint: furniture riding
a room-preset preview (or sitting on a dragged deck) dropped to ground
under the visually-moved deck until the validating click. Effective slab
records now apply the live delta to polygon/holes/elevation, mapped
exactly once at each query's loop entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(viewer): reapply floor lift every frame for nodes with live previews

FloorElevationSystem only wrote mesh Y for dirty nodes, but the React
commit that rebinds a dragged node's base-Y group position can land
between frames, after priority-2 systems consumed the dirty mark — the
lift then vanished until the next pointer tick re-dirtied the node,
blinking the Y of items dragged over elevated slabs. Nodes holding a
live override or transform now get the lift reapplied every frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): stop the entry camera swap from insta-cancelling walkthrough

Entering walkthrough with a persisted orthographic camera swaps it to
perspective, which recreates the interaction callbacks and re-ran the
pointer-lock effect: its cleanup called exitPointerLock, and the unlock
handler read that as "user left walkthrough" — instantly cancelling the
fresh entry and arming the browser's ~1.25s re-lock cooldown (hence
needing multiple button presses). The effect is now mount-stable (the
changing callback rides a ref, deps down to [gl]), and the entry lock
request swallows async cooldown rejections like the P-resume path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): don't block build-JSON import over plugin-node children

Exports from projects with plugins carry nodes like trees:tree whose ids
sit in level.children; the import validator parsed parents against the
static children id union, so one tree id hard-failed the level schema and
blocked the whole import — while the same data loads fine from the DB
(setScene never runs this gate). Parents are now validated against a copy
with non-static-schema child ids filtered out; those nodes keep surfacing
through the unknown-types warning and the imported payload is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(core): treat registered plugin kinds as first-class in import validation

Nodes of runtime-registered plugin kinds (trees:tree, trees:grass) were
lumped into the unknown-types warning even when the plugin is loaded.
The validator now consults the node registry: registered kinds validate
against their own registered schema (corrupt plugin nodes still block),
count under stats.pluginTypes, and raise no warning — only genuinely
unregistered types do, which stays correct for hosts without the plugin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: appease biome (format + forEach block body)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:31:09 -04:00
..

@pascal-app/nodes

Built-in node definitions for the Pascal viewer and editor.

Installation

npm install @pascal-app/core @pascal-app/viewer @pascal-app/editor @pascal-app/nodes

The package declares the remaining React, Next.js, Three.js, and UI libraries it needs as peer dependencies. Install any peers reported by your package manager.

Usage

Load builtinPlugin once before mounting a Pascal viewer or editor:

import { loadPlugin } from '@pascal-app/core'
import { builtinPlugin } from '@pascal-app/nodes'

await loadPlugin(builtinPlugin)

The plugin registers the built-in schemas, renderers, geometry builders, tools, and systems. Hosts can load additional plugins through the same loadPlugin API.

See the @pascal-app/viewer quick start for bootstrap ordering in a React application.

License

MIT