Commit Graph
91 Commits
Author SHA1 Message Date
Wassim SAMADandGitHub 9ca3eaa7fe feat: add project plugin management (#501) 2026-07-16 10:26:59 -04:00
a590747748 Adjustments pass: three 0.185, undo/cancel semantics, guide & panel fixes (#496)
* fix(editor): only start handle drags on primary button

Right-click over a rotation/move/resize handle started the gesture and
stopPropagation()'d, fighting the camera orbit. Guard every gesture
starter (shared useHandleDrag, group rotate gizmo, wall endpoint/height/
move, fence move, roof trim) with event.button !== 0 before it swallows
the event.

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

* feat(viewer): default units from timezone/locale until user picks

Derive the metric/imperial default from the IANA timezone (US, Liberia,
Myanmar zones -> imperial; anything else -> metric), falling back to an
explicit locale region subtag only when no timezone resolves. Timezone
tracks actual location, unlike navigator.language where en-US is a common
default far outside the US. The unit is only persisted once the user
explicitly sets it, so an untouched preference keeps tracking location;
existing persisted values are treated as explicit and left alone.

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

* fix(viewer): toggle shadows via renderer.shadowMap.enabled, not castShadow

Flipping a light's castShadow at runtime crashes three r184's WebGPU
renderer: toggling off disposes the shadow map's GPU texture, but the
node builder cache evicts with the post-toggle key, so the shadows-on
entry survives still referencing the destroyed texture. Re-enabling
reuses that stale state and every frame submit fails with
GPUValidationError ("Invalid CommandBuffer from CommandEncoder").

Keep castShadow static and drive the user-facing toggle through the
Canvas shadows prop (renderer.shadowMap.enabled), which rebuilds
materials without disposing shadow resources.

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

* fix(core,editor): floor undo history at scene load

Undo could step back past the scene load into the pre-load (empty)
state, wiping the whole project — which autosave would then persist.
Two defects: clearSceneHistory() had zero call sites, so every load left
the empty pre-load state in zundo's pastStates; and setScene wrote the
store twice, recording a half-normalized intermediate as a second undo
target.

- applySceneGraphToEditor, JSON import, and reset-to-default now clear
  history so the loaded scene is the undo floor
- setScene collapses to a single tracked write (final state identical)
- clearSceneHistory also resumes tracking so a load landing inside a
  pause window can't strand undo recording off

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

* build(three): upgrade runtime to 0.185.1, pin @types/three at 0.184

r185 renames directionToColor/colorToDirection to packNormalToRGB/
unpackRGBToNormal and splits SSGI's packed rgba output into separate AO
(getAONode, single channel) and GI (getGINode) textures; wind-node's
positionLocal reads become positionGeometry.

@types/three stays at 0.184.1: the 0.185 typings send tsgo's inference
into unbounded allocation (microsoft/typescript-go#2125 class — it ate
~70GB/90s and OOM-killed the machine). viewer/lib/tsl-compat.ts bridges
the two renamed TSL exports with 0.184-typed signatures; drop it and the
pin together once tsgo copes.

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

* fix(viewer): gate .ktx2 preset-texture loads on detectSupport

KTX2Loader.load throws before detectSupport has run, and materials
created while a standalone capture canvas's renderer was still
initializing cached themselves permanently texture-less — fabric slots
rendered white in item thumbnails. .ktx2 loads now await whenKtx2Ready()
(resolved by the first successful ensureKtx2Support), which is exported
so hosts with standalone canvases can arm it.

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

* feat(viewer): accept host-supplied country for the unit default

applyCountryUnitDefault lets the host app feed an authoritative
IP-derived country (e.g. Vercel's x-vercel-ip-country) into the unit
default. Stronger signal than the timezone heuristic applied at store
creation, still never overrides an explicit user choice.

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

* fix(editor): box select starts over locked guide images

A locked guide's hit-rect swallowed pointer-down via stopPropagation, so
marquee selection couldn't start on top of it. Locked guides now let the
event bubble to the svg root; click-to-select and the unlock affordance
still work because a non-drag release fires onClick as before.

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

* feat(editor): live rotation readout while rotating a guide image

Rotating a guide with the 2D handles gave no angle feedback. Reuse the
registry layer's RotationAngleOverlay (wedge + degree chip) for guide
rotate drags: sweeps from the grabbed corner's bearing at grab to its
current snapped bearing, suppressed under ~0.5deg so a fresh grab doesn't
flash a sliver.

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

* feat(editor): lingo free-text input in the guide set-scale dialog

The real-length field accepts natural measurement text via
@pascal-app/lingo — 5'11", 180cm, 1m80, 12ft — parsed in the dropdown's
unit (a bare number still means that unit, a typed unit wins). A faint
'= 1.80 m' hint previews non-trivial input, unparseable text gets a
clear error, and the odd onBlur force-reset to 0.0001 is gone.

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

* feat(editor): cmd+z mid-interaction cancels the gesture instead of undoing

Undo pressed while the mouse is mid-action (moving, drawing, dragging a
handle) used to history-jump under the live pointer — stale carry, half
gestures committing against a rewound scene. Now it reads as 'abort this
action', exactly like Escape:

- the global undo/redo arms first route through the tool:cancel path
  (covers build drafts, placement ghosts, move tools) and skip the
  history jump when anything was in flight (consumed, scope-active, or
  inputDragging);
- pointer drags that only knew pointercancel (generic handle drags,
  group rotate, wall side/height handles, roof trim) gain the same
  capture-phase Escape/cmd+z keydown the group-move drags already had —
  fixing Escape for them too;
- the existing capture-phase handlers (3D/2D group move, 2D registry
  move overlay) additionally accept cmd+z as cancel.

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

* fix(editor): box select arms over any guide image that won't drag

Follow-up to cd830279, which only let LOCKED guides bubble pointer-down.
An unlocked, unselected guide also swallowed the event for nothing (no
translate drag starts), so marquee selection could never start on top of
it. Now only the one case that uses the event consumes it — selected +
unlocked → translate drag — and everything else bubbles to the svg root.
Click-to-select still works: a non-drag release never crosses the
box-select threshold, so the trailing click fires the guide's onClick.

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

* fix(editor): cmd+z during draft placement cancels instead of undoing

The preset/item placement flow (useDraftNode + placement coordinator)
registers no interaction scope and holds no pointer, so the cmd+z cancel
guard from c699d74e saw it as idle and history-jumped mid-placement.
Paused scene history is the universal tell — the draft cycle (and every
adopted-move session) keeps temporal paused for the whole gesture, and
an undo against a paused store lands on a stale baseline anyway. Treat
!isTracking as in-flight.

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

* fix(editor): cmd+z mid-placement completes the cancel, not just skips undo

4ffda723 stopped the history jump during preset/item placement but left
the draft alive: the item tool passes no coordinator onCancel — it is
Escape's fall-through (switch to select, unmount the tool) that actually
destroys the draft. Extract that fall-through and run it from the cmd+z
path too whenever a gesture is live and nothing consumed tool:cancel,
so cmd+z now behaves exactly like Escape end to end.

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

* fix(editor): node selection clears a lingering reference selection

Selecting a guide clears the node selection (handleGuideSelect), but the
reverse was never wired: clicking a wall with a floorplan reference
selected left selectedReferenceId set, and the panel manager's
reference-first priority kept showing the floorplan panel until it was
closed by hand. PanelManager now drops the stale reference the moment a
scene selection (nodes or zone) appears.

Also: the inspector's expanded state is shared across panel swaps by
design, but it survived close/reopen too — deselecting everything now
resets it, so a fresh selection opens the panel collapsed again.

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

* fix(plugin-trees): wind displacement reads positionLocal, not positionGeometry

The three-0.185 migration renamed positionLocal to positionGeometry in
the wind nodes, but positionLocal was never removed in r185 — and the
two are not interchangeable here. NodeMaterial.setupPosition applies the
instance transform by mutating positionLocal, then overwrites it with
positionNode's output; reading raw positionGeometry therefore discarded
every instance matrix — leaf cards rendered unscaled at tree-local
coordinates (a giant canopy filling the sky) and grass/flower instances
collapsed invisibly. Reading positionLocal (instance transform included)
restores r184 behavior exactly.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:26:35 -04:00
Aymeric RabotandClaude Opus 4.8 8476a991f4 chore(deps): bump @pascal-app/lingo to 0.2.0
0.2.0 is API-compatible with our usage (parseQuantity, quantity, Kind);
it adds i18n/locale entrypoints and does not change the symbols we import.
Verified: @pascal-app/editor and @pascal-app/mcp typecheck, and all 31
measurement tests pass, including the AMBIGUOUS_NUMBER escalate guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 22:34:31 +02:00
Aymeric RabotandClaude Opus 4.8 af1fe6a34c chore(deps): add @pascal-app/lingo to the lockfile; note duplicated helper
Records @pascal-app/lingo@^0.1.0 in bun.lock (added to @pascal-app/editor and
@pascal-app/mcp), so `bun install --frozen-lockfile` in CI resolves it. Also
documents that measurement.ts is intentionally duplicated with the AI-chat copy
across the submodule boundary and must be kept in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:32:36 +02:00
5c071ecad5 feat(plugins): plugin contract + first-party Nature pack (trees/flowers/grass) (#457)
* feat(plugins): trees tracer-bullet plugin + minimal host panel surface

Ship one real first-party plugin and only the host surface it forces
into existence, proving the plugin contribution paths every future
plugin (mint.gg generator, Home Assistant, environments, room volume)
will reuse.

Host surface (core + editor):
- core: Plugin.panels + observable panelRegistry; loadPlugin routes
  panels (namespaced by plugin id, dup-throws like nodes).
- editor: AppSidebar merges panelRegistry into the icon rail via
  useSyncExternalStore; each plugin panel lazy-loaded behind an error
  boundary; host extraPanels keep precedence.
- editor: widen Tool to `KnownTool | (string & {})` so plugin tool ids
  (e.g. 'trees:tree') typecheck; dispatch already registry-first.

Trees plugin (packages/plugin-trees), structurally a third-party pack
(peer-deps on @pascal-app/*):
- trees:tree node — procedural low-poly geometry (oak/pine/birch/palm),
  free parametric inspector (preset/height/seed + Randomize), placement
  tool + ghost preview built from public primitives only.
- presets rail panel — panel -> plugin store -> def.tool -> SceneApi ->
  scene -> reactive read-back ("N planted").

Loading + docs:
- apps/editor: setPluginDiscovery([treesPlugin]) in bootstrap;
  transpilePackages + dep.
- wiki/architecture/plugin-authoring.md: panels field, error-boundary
  contract, styling, tool-from-panel note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(plugins): merge plugin panels into the v2 sidebar layout too

The community shell renders <Editor layoutVersion="v2" sidebarTabs={...}>,
a separate rail from AppSidebar. Merge registry panels into the v2
tabMap + tab bar (and thus mobile) so plugin panels show up there as
well, not only in the v1 AppSidebar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(plugin-trees): ez-tree geometry + instanced rendering + snapping

Replace the placeholder low-poly geometry with dgreenheck/ez-tree
(self-contained — bark/leaf textures inlined as base64, no assets to
host) and render the forest with true GPU instancing.

Rendering:
- def.system (system.tsx): one collective renderer that groups every
  trees:tree node by (preset, seed) variant and draws each variant as
  one InstancedMesh per ez-tree sub-mesh, composing the parent level's
  world matrix into per-instance matrices. ~1 draw call per variant.
- def.renderer (proxy-renderer.tsx): an invisible, raycastable per-node
  proxy so the host's existing selection / outline / zone machinery
  keeps working — no instanceId bookkeeping. (Outline highlights the
  proxy bbox; documented.)
- geometry per variant generated once by ez-tree and cached; seeds drawn
  from a bounded pool so trees actually share variants.

Presets remapped to ez-tree built-ins (oak/pine/aspen/ash/bush).

Placement now respects the active snap mode — isGridSnapActive() +
gridSnapStep + snapPointToGrid, like the built-in item/shelf tools.

transpilePackages += @dgreenheck/ez-tree in the editor app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(plugin-trees): true-silhouette hover/select + richer presets panel

Selection/hover now outlines the real tree shape, not the bbox. The
proxy splits into an outer group (stable invisible box collider +
pointer handlers, the raycast target) and an inner registered group that
mounts the real ez-tree geometry (invisible, non-raycasting) only while
the node is hovered or selected. The host outline pass reads the
registered inner group, so it traces the true silhouette; picking stays
on the steady box. Geometry for the highlight reuses the cached variant.

Panel: redesigned cards with gradient swatches + selected state, a
"planted" count chip, and a height slider that seeds the next tree's
height (a per-instance scale — never touches placed trees or instancing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(plugin-trees): curated tree params + procedural flowers (sibling kind)

Tree params: expose foliageDensity, trunkThickness, and a leafless toggle
in the inspector and the panel brush. Each is folded into the instancing
variant key and mapped onto ez-tree options (radius scale, leaf count),
so editing one tree only re-buckets that tree — instancing degrades
gracefully, never worse than per-node.

Flowers: add a `trees:flower` sibling kind — simple procedural geometry
(stem + petals + center, merged per material), presets daisy / tulip /
lavender, instanced + selectable exactly like trees.

Refactor: extract the instanced renderer + selection proxy into a generic
`instanced.tsx` (InstancedKindSystem + KindProxy) and the snap/level/grid
placement wiring into `placement.tsx` (usePlacement). Trees and flowers
are now thin bindings — the template for future plant kinds.

Panel: a Trees / Flowers toggle, gradient preset cards, per-kind planted
count, and brush sliders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(plugin-trees): native inspector controls in panel + preset thumbnails + ez-tree credit

- swap the panel's custom slider/checkbox/toggle for the host's exported
  SliderControl/ToggleControl/SegmentedControl so the brush matches the
  right-hand inspector pixel-for-pixel
- replaceable preset thumbnails (inline SVG data URIs, no asset hosting) render
  as <img> cards instead of gradient swatches
- credit footer linking @dgreenheck/ez-tree (Daniel Greenheck, MIT)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugin-trees): all ez-tree presets + type + leaf/branch colours, flower petal colour, grass kind

- tree presets now cover all of ez-tree's built-ins via species × size
  (Small/Medium/Large + Bush 1/2/3 + Trellis), plus a Deciduous/Evergreen type
- edit-only leaf & branch colour tints (leaves.tint / bark.tint), all folded
  into the instancing variant key
- flowers gain a per-flower petal colour (baked from preset at placement)
- new trees:grass instanced kind — procedural blade tufts (meadow/fescue/reed)
  with a per-tuft blade colour, reusing the generic instanced + placement core
- panel: Trees/Flowers/Grass segmented switch, size + type controls, native
  host controls throughout; credit links Daniel Greenheck's X

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(plugin-trees): tolerate nodes persisted before new fields existed

treeSpecOf/flowerPetalColor now default every geometry field, and hexToInt
guards undefined — trees/flowers placed before size/type/colour fields were
added load with neutral defaults instead of crashing the instanced renderer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugin-trees): shared always-on wind + pin credits to panel bottom

- wind.ts injects a sway into each material's begin_vertex driven by one shared
  uTime uniform (advanced per frame in the instanced system); USE_INSTANCING
  guards keep the same material valid for the non-instanced placement ghost.
  Applied to tree, flower, and grass materials — a whole scene sways like ez-tree.
- credits footer is now sticky to the bottom of the panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugin-trees): rename rail panel Trees -> Nature (covers trees/flowers/grass)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugin-trees): rename panel header Plant -> Nature to match the rail

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: bake-exportable instanced plants + renderer-agnostic wind

Bake (GLB export): add a transient useViewer.isExporting flag. BakeExporter
flips it, waits for the commit, then exports. KindProxy watches it and, during
export, emits each plant's REAL visible geometry under scene-renderer (which the
exporter clones) instead of the invisible collider — so instanced kinds
(def.system) that live outside that subtree are captured. The collider box is
dropped during export so it doesn't bake as a phantom solid.

Wind: the previous onBeforeCompile wind was a no-op under the editor's WebGPU
renderer (WebGL-only hook). Replace it with a renderer-agnostic per-instance
base-pivot tilt animated in useFrame (which FrameLimiter drives continuously).
Reads as wind under any renderer; removes the dead wind.ts + applyWind calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugin-trees): TSL vertex-bend wind (WebGPU), replacing CPU tilt

wind-node.ts adds a TSL positionNode that bends each plant proportional to
height above its base, phased per-instance (instanceIndex) and per-vertex, so
tips sway and roots stay planted — animated on the GPU via the renderer's time
node. ez-tree is untouched: its materials are copied into MeshStandardNodeMaterial
(toWindMaterial); flowers/grass build node materials directly (windStandardMaterial).
Instance matrices go back to static.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(plugin-trees): tree materials render black under wind (Phong not Standard)

ez-tree's bark/leaves are MeshPhongMaterial; copying them into a
MeshStandardNodeMaterial swapped the shading model (specular/shininess ->
roughness/metalness) and rendered black. Convert into the matching node
material type (Phong->Phong, Lambert->Lambert, else Standard) so map/color/
alphaTest/side are preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(plugin-trees): trees still black — build node material from map/color explicitly

Material.copy() doesn't transfer a classic material's map/color onto a node
material, so textured trees rendered black (flowers/grass were fine — they carry
no map). Re-create the ez-tree materials as MeshStandardNodeMaterial with map,
alphaMap, color, side, alphaTest passed explicitly in the constructor — the same
proven path the flower/grass materials use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(plugin-trees): tree wind — per-leaf flutter (ez-tree style), static trunk

The whole-tree height-based bend was a rigid rotation about the base ('rotating
in place'). Replicate ez-tree's actual approach: scale sway by the leaf card's
uv.y and apply it ONLY to the 'leaves' material (multi-frequency wave, phased per
instance + per leaf) so leaves flutter from their attachment while bark/branches
stay static. Flowers/grass keep the gentle whole-plant stem bend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugins): Plugin.onSceneLoad hook + viewer application; trees wind via it

Adds a broad, plugin-declared post-load scene hook so effects survive GLB export:
- core: Plugin.onSceneLoad (lazy, three-free) + observable sceneHookRegistry + loadPlugin routing.
- viewer: applyPluginSceneHooks util; PluginSceneHooksSystem re-runs it on the live
  scene as nodes change; GlbScene runs it on each loaded baked GLB.
- plugin-trees: declare onSceneLoad; wind is no longer welded into the materials —
  the hook re-attaches LEAF_FLUTTER/STEM_BEND by material name (leaves / flower-* /
  grass-*), so it now animates in the baked /viewer too, not just live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(plugin-trees): scale-invariant leaf wind (÷ modelScale) so baked GLB matches editor

The bake's quantiser renormalises each plant mesh to [-1,1] and moves real size
into a large node scale (~3 on trees). LEAF_FLUTTER added a constant local offset,
so in the baked GLB it was multiplied by that scale → ~3x over-swing. Divide the
sway by modelScale.x to keep a constant world-space amplitude; editor (scale 1)
is unchanged, baked now matches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert: onSceneLoad wind approach → back to wind-in-materials

Reverts the Plugin.onSceneLoad hook + viewer application + modelScale compensation
(commits 893ca839, 6c9ee003). Superseded by the bake-policy 'replace' model
(plans/editor-plugin-trees-example.md Part D): trees render live in the viewer via
their own path, so wind is a plain positionNode on the material again — no hook,
no baked-geometry decoration, no coordinate-space fights.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(bake): per-kind bake policy (static/strip/replace)

Adds NodeDefinition.bake ('static' default | 'strip' | 'replace') + registry
helpers bakePolicyOf/kindsWithBakePolicy in core. Generalises the previously
hardcoded scan/guide handling:
- glb-export strips kinds with bake==='strip' from the artifact (was a
  scan/guide type check); 'replace' kinds stay baked (portable static snapshot).
- glb-reference-nodes selects rebuild candidates by policy instead of type.
- scan/guide declare bake:'strip'; trees/flower/grass declare bake:'replace'.

Viewer-side 'replace' swap (strip baked meshes + live-rebuild) is the next slice.
Part of plans/editor-plugin-trees-example.md Part D.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(bake): viewer 'replace' swap — hide baked meshes, re-render live

Adds NodeDefinition.bakeReplaceRenderer (falls back to renderer). The baked
/viewer now, for kinds with bake:'replace' and a loaded plugin:
- hides the static baked meshes (bakePolicyOf(kind)==='replace') in GlbScene's
  identity pass — capability-gated, so with no plugin the meshes stay;
- re-renders each node live via buildGlbReplaceNodes + the shared portal-into-
  baked-level path (GlbReferenceNode now prefers bakeReplaceRenderer), so trees
  ride level stacking automatically and wind runs in real coordinate space.

plugin-trees ships a hookless KindStatic (real geometry + wind materials, no
collider/selection) and per-kind static-renderer binds; tree/flower/grass point
bakeReplaceRenderer at them. Completes plans/editor-plugin-trees-example.md Part D.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(viewer): memoize GlbReferenceNode(s) — skip forest re-render on camera move

GlbScene re-renders per frame during camera movement (hover raycast / walkthrough
HUD). The rebuilt nodes reconciled every frame — fine for 1-2 scans/guides, but a
bake:'replace' forest puts dozens of nodes here (profiler: KindStatic x56 6ms,
GlbReferenceNode x57 4ms per frame). memo at the node boundary skips the whole
subtree (incl. the plugin renderer) when (node, anchor) are unchanged; both props
are stable refs, so it short-circuits cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(plugin-trees): NO_RAYCAST on KindStatic meshes — cheap hover over a forest

The baked scene's <primitive> has pointer handlers, so R3F recursively raycasts
every descendant on each pointer move (incl. orbit drags). The live replace trees
had fully-raycastable dense ez-tree geometry, so hover cost ~400ms/frame over a
forest (profiler: 'JavaScript' 413ms during camera move). They carry no pascalId
(a hit resolves to the level, not the tree), so they're scenery, not pick targets
— mirror KindProxy, which already NO_RAYCASTs its real geometry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(bake): viewer 'replace' renders instanced per level (was per-node)

Replaces the per-node KindStatic viewer path with a collective instanced one,
mirroring the editor's system instead of diverging from it:

- extract InstancedNodes from InstancedKindSystem (shared by both) with a
  localSpace flag: editor folds parent world matrix (root instances); viewer uses
  level-local matrices + NO_RAYCAST, portaled into the baked level.
- bakeReplaceRenderer is now a collective renderer (BakeReplaceRenderer<N>,
  receives {nodes}); GlbReplaceInstances groups replace nodes by (level, kind) and
  portals each kind's instanced renderer into that baked level. GlbReferenceNode
  reverts to per-node renderer (strip kinds only).

Fixes: forest hover cost (100+ tree meshes → a few instanced draws, R3F walks far
fewer objects) and per-tree wind phase (instanceIndex varies again; per-node
KindStatic gave every tree phase 0 → unison sway). Trees still ride level stacking
(portaled) and stay static in a plain glTF viewer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(viewer): BVH the baked scene — accelerate hover/pick raycasts

The baked /viewer runs with useBvh={false}, so GlbScene's hover/pick raycasts
against the baked building were brute-force triangle intersection (profiler:
_computeIntersections + intersectTriangle ~30%). The parametric viewer wraps its
scene in <SceneBvh>; the baked one never did. SceneBvh's effect is one-shot on
mount and can't catch the async-loaded GLB, so compute a per-mesh BVH inside
GlbScene keyed on gltf.scene instead. 'replace' instances are NO_RAYCAST, so the
raycast===Mesh.prototype.raycast guard skips them (mirrors SceneBvh).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(plugin-trees): inherit ez-tree preset by default (seed/type/tints)

Our node defaults (seed:1, treeType:deciduous, colors:#ffffff) were applied as
*overrides* on top of loadPreset, discarding each preset's tuned seed, growth
model, and tints — so our trees looked nothing like eztree.dev (pine grew
deciduous, foliage washed white, every silhouette off).

Mirror the flower petalColor pattern: seed/treeType/leafColor/branchColor are now
optional; generateTree only overrides an ez-tree option when the node set it,
otherwise loadPreset's value stands. Placement stores none of them (fresh tree =
pure preset; all same-preset trees share one instancing variant); the deciduous/
evergreen brush toggle is gone (growth model comes from the preset). The inspector
keeps them as per-tree overrides, and Randomize still varies the seed.

Verified against ez-tree: fresh Oak Medium → seed 35729 + preset tints; Pine
Medium → evergreen + seed 13977; overrides apply when set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugin-trees): bundle real preset artwork (webp, in-package)

Replaces the placeholder SVG thumbnails + lucide panel icon with the real nature
art. 13 webp (~126KB total, 256px cards / 128px icon) live in src/assets and are
imported via art.ts — both consumers are Next, so transpilePackages runs them
through the image pipeline (hashed, cached /_next/static/media URLs). No CDN, no
per-app public/ mirroring; the assets travel with the package.

- art.ts: central webp imports → TREE_ART / FLOWER_ART / GRASS_ART / NATURE_ICON
- presets/flower-presets/grass-presets: thumbnail ← bundled art (was *Thumbnail())
- index.ts: Nature panel icon ← NATURE_ICON (was lucide:leaf)
- assets.d.ts: ambient *.webp decl (no next type dep)
- remove thumbnails.ts (placeholder generators, now dead)

Verified on the running dev server: webp emitted + served (HTTP 200, image/webp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugin-trees): square preset cards on light-gray flattened thumbnails

Regenerate the 12 thumbnails with the transparency flattened onto gray-100
(#f3f4f6) and render the card square (aspect-square, was h-16 crop). The
nature-icon keeps its transparency for the icon rail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(plugin-trees): correct move-drag + outline behavior for instanced plants

Two selection defects, one root cause each:

1. Outline moved mirror-wise during a move drag. The host move tool drives the
   registered Object3D imperatively with absolute level-local positions (and
   mirrors them via useLiveTransforms) — the contract ParametricNodeRenderer
   satisfies by putting position + registration on the same group. KindProxy
   registered a child nested inside the transform-carrying group, so drag
   positions landed in the node's rotated frame (placement gives every plant a
   random Y rotation → deltas rotated up to 180°). Restructure: the registered
   group now carries position/rotation (live-transform aware); the box collider
   is a positioned sibling, staying out of the outline mask.

2. Outline froze while the mesh swayed. The outline mask pass renders with a
   shared override material, so it can never follow the material positionNode
   wind. Instead, while hovered/selected the collective system skips the node
   and the proxy mounts the real geometry with static twins of the wind
   materials (toStaticMaterial — explicit property transfer; node-material
   clone() drops map/color). Outlined mesh == visible mesh, both still.

Bonus from the same restructure: a move drag now animates the actual plant in
realtime (the proxy is what the tool drives), not just the drag box.

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

* feat(plugin-trees): 2D plan symbols + trunk-sized footprint

- def.floorplan for the three kinds — the registry floor-plan layer renders any
  kind that provides one, so this is all plugin nodes need to appear in 2D.
  Tree: dashed canopy ring (dashed = overhead element) in the preset swatch +
  solid trunk dot; ring is pointer-events:stroke so the large disc doesn't
  steal clicks from what's under the canopy. Flower/grass: small colour dots.
  Selected → palette stroke + move-handle; hovered → hover stroke.
- Tree floorPlaced.footprint is trunk-sized (treeTrunkRadius) instead of
  canopy-sized, so the move/placement drag box hugs where the tree actually
  plants instead of spanning the crown. The invisible hover collider keeps its
  larger radius — only the displayed box shrinks.

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

* fix(plugin-trees): trunk-sized drag box, 45°-aligned rotation, rotate gizmo

- The move drag box ignored floorPlaced.footprint: with collides:false the tool
  auto-measures the rendered mesh — which, since the selection swap-out, is the
  whole canopy. Declare capabilities.dragBounds (trunk-sized, node-local) so the
  box hugs the trunk and rotates with the node instead of wrapping the crown as
  a world AABB.
- Placement wrote a fully random Y rotation, so the box/gizmo never sat on a 45°
  step. Snap the random rotation to 45° increments (variety preserved, alignment
  restored) and widen rotatable.snapAngles to 8×45° on all three kinds.
- Add the standard rotate gizmo (def.handles, shelf-style arc handle): ring
  around the trunk near the ground — not the canopy, which would put the handle
  meters out on a large oak.

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

* fix(plugin-trees): live rotate preview + ground-level rotate ring

- KindProxy now folds useLiveNodeOverrides into its transform (mirrors
  ParametricNodeRenderer) — the rotate/resize gizmos publish mid-drag patches
  there, so the plant turns in realtime instead of snapping on commit. The
  snap-on-commit (with the arc delta wrapping to ±180°) is also what made long
  drags land 'the wrong way'; with live feedback the direction reads correctly
  and matches the item gizmo pipeline exactly.
- Rotate ring drops from mid-trunk to 0.25m — a floor affordance like the item
  gizmo, not a waist-height hoop.

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

* feat(registry): panelForKind — 'find in catalog' opens a plugin kind's panel

loadPlugin now associates every node kind with its plugin's first (namespaced)
panel id; panelRegistry.panelForKind(kind) exposes it. A host's find-node
handler can open the right panel for any plugin kind with zero per-plugin
knowledge.

plugin-trees uses it end-to-end: a module-level selection:find-node listener
(find-sync.ts, imported by the manifest so it's live from plugin load) points
the panel store at the found node's section + preset — panel section state
moved from panel-local useState into the store to make it addressable.

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

* feat(registry): plugin panels declare their workspaces (default edit-only)

Plugin panels rode into the studio rail: the host swaps its own sidebar tabs by
workspaceMode, but usePluginPanels appended registry panels unconditionally.

PluginPanel gains workspaces?: readonly ('edit'|'studio')[] — manifest metadata,
default ['edit'] (an authoring panel has no business in the clean render
workspace; a plugin shipping studio tooling opts in explicitly). usePluginPanels
filters by the current workspaceMode, covering both the v1 AppSidebar and v2 tab
bar paths. Nature declares nothing and disappears from studio via the default;
the v2 layout's existing active-tab fallback handles a panel vanishing mid-use.

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

* docs: link plugin-authoring guide from README + CONTRIBUTING, fix stale wiki path

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

* fix(plugin-trees): keep ez-tree off the eager SSR path

ez-tree loads its inlined textures at module scope (needs document), so any
eager import chain reaching geometry.ts crashed Next prerender
(ReferenceError: document is not defined on /_not-found). Move the pure
helpers (mulberry32, naturalHeight) to variant-utils.ts so the
flower/grass builders and floorplan no longer pull ez-tree, and drop the
generateTree re-export from the package barrel (no external consumers).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 13:45:20 -04:00
Wassim SAMADandClaude Opus 4.8 b87b1f999d feat(editor): floorplan export (multi-page PDF, per level)
Add a Floorplan group to the settings Export panel alongside the 3D
model exports, with "Full floorplan" and "Structure only" buttons.

Export re-runs the live registry-driven floorplan pipeline
(def.floorplan -> FloorplanGeometryRenderer) headlessly with a neutral
viewState, fits each level to its own page, and titles each page with
the level label. Every level of the active building becomes a page in
one landscape A4 PDF. "Structure only" keeps category === 'structure'
nodes; "Full" keeps every visible node with a floorplan builder.

- new lib/floorplan/floorplan-export.tsx; jsPDF + svg2pdf dynamically
  imported so they only load on export
- export five pure helpers from floorplan-registry-layer for reuse
  (buildContext, getFloorplanLevelData, floorplanLayerRank,
  splitFloorplanOverlay, isFloorplanNodeVisible) — no behaviour change
- bake vector-effect:non-scaling-stroke widths into real units before
  svg2pdf (which ignores the hint and would otherwise draw door/window/
  stair linework as metre-wide strokes)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 08:49:53 -04:00
Aymeric RabotandClaude Opus 4.8 1b4c656901 chore(ts7): adopt the TypeScript 7 native compiler (tsgo)
Make the editor packages compatible with, and benefit from, the native Go
compiler (TypeScript 7 / tsgo) while keeping the npm publish path on stable tsc.

- Add @typescript/native-preview (pinned 7.0.0-dev.20260624.1). tsgo coexists
  with typescript@6, which Next typegen and the IDE plugin still need until the
  TS 7.1 programmatic API ships.
- Fix two react-three-fiber JSX augmentations the native checker rejects but
  tsc tolerated:
  - viewer: map only LineBasicNodeMaterial (the one webgpu node material used as
    a JSX tag) instead of the whole three/webgpu namespace (TS2320/TS2590).
  - plane-box-select-tool: drop the redundant `IntrinsicElements extends
    ThreeElements` block — it duplicated R3F's global augmentation and, by
    referencing @react-three/fiber's ThreeElements directly, hit a bun peer-dep
    variant-directory duplicate under tsgo (TS2320). r3f.d.ts already covers
    those JSX intrinsics. Types-only — no runtime change.
- check-types -> tsgo --noEmit (editor, @repo/ui, editor app, ifc-converter app)
- emit-package dev watch -> tsgo --build --watch
- build/publish stay tsc --build (prepublishOnly + release.yml unchanged;
  emitted .d.ts is byte-identical to tsc output)
- bump next 16.2.6 -> 16.2.9

tsc remains the source of truth and fallback. Published artifacts unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:37:58 -04:00
cf24b62c44 fix(viewer): UI flicker on camera move from interactive overlays + dirty-mark leaks (#401)
* chore: sync bun.lock with 0.9.1 workspace versions

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

* fix(viewer): stop interactive overlays from starving frames and flickering the UI

Every interactive item mounted a drei <Html occlude> overlay
unconditionally — invisible (opacity 0) when no zone was selected, but
still alive. With `occlude` as a bare boolean, drei raycasts the entire
scene per overlay on every camera-move frame, and rewrites each
element's z-index while toggling display when the occlusion flips. On
scenes with hundreds of interactive items (recessed lights, ceiling
fans) this starved the frame budget and made the whole DOM UI blink
during camera moves while the WebGPU canvas stayed healthy.

Overlays now mount only while a zone is selected and the item sits
inside its polygon, fade in/out over 300ms (the child components stay
rendered so the exit transition can play before the <Html> unmounts),
and drop `occlude` entirely. eps=-1 works around a drei mount bug: its
mount path writes the element transform without the distanceFactor
scale, and with a static camera the eps guard never re-applies it, so
freshly mounted overlays stayed mis-scaled until the camera moved.

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

* feat(viewer): break down dirty nodes by kind in the perf overlay

DIRTY now reads e.g. "29 (12 wall, 9 ceiling, 8 item)" — sorted by
count, only non-zero kinds, with a "missing" bucket for dirty ids whose
node no longer exists. Makes dirty-mark leaks attributable at a glance.

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

* perf(core): skip dirty marks for kinds with no dirty consumer

dirtyNodes is consumed by GeometrySystem (def.geometry),
FloorElevationSystem (capabilities.floorPlaced), and the legacy
per-kind viewer systems. Site, building, level, zone, and guide match
none of those, so their marks were never cleared: they accumulated for
the whole session (every child create/delete dirties its parent),
permanently defeated every consumer's empty-set early exit each frame,
and polluted the perf overlay's DIRTY readout.

NodeDefinition gains an explicit dirtyTracking?: boolean opt-out
(default tracked — no derivable predicate exists since wall's dirty
consumption lives in the viewer while zone/guide/level declare
def.system for unrelated per-frame work). markDirty consults the
registry; the five structural kinds opt out.

Also fixes a second leak: deleteNodesAction never removed deleted ids
from the dirty set, and every consumer skips missing nodes without
clearing them, so marks on deleted nodes lived forever.

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

* style: replace concise-arrow forEach with for...of in deleteNodesAction

biome's useIterableCallbackReturn rejects forEach callbacks that
implicitly return a value (Set.add / clearDirty).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:05:06 -04:00
Aymeric RabotandClaude Fable 5 9bfd75c2c7 chore(release): align core/viewer/editor to 0.9.0 ahead of v0.9.1
The v0.9.0 GitHub release declared the trio at 0.9.0 but the npm publish
never ran (npm still serves 0.8.0). Aligning manifests so the release
workflow's bump=patch lands everything at the announced-next 0.9.1
(mcp 0.3.0 -> 0.3.1, ifc-converter 0.1.0 -> 0.1.1 keep their own lines,
matching the per-package version table style of previous releases).
Inter-package peer/dev refs move to ^0.9.0 so the workspace stays
self-linked during the release run; the workflow re-syncs them to the
final versions after bumping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:54:42 -04:00
Aymeric RabotandClaude Fable 5 c57901e7d9 chore(deps): review-pass fixes — pin next 16.2.6, drop dead dep, dedupe three
- next 16.2.7 -> 16.2.6 in both apps. 16.2.7 regresses Turbopack dev route
  matching for API routes nested below a dynamic segment (probed in the
  private repo: /api/items/[id]/fork, /api/scenes/[id]/events 404 with the
  router's HTML not-found; production builds unaffected; bisected
  16.2.6 good / 16.2.7 bad). 16.2.6 carries all the 16.2.5/16.2.6
  security fixes — only the bugfix backports are forgone.
- Remove @react-three/uikit-lucide from @pascal-app/editor dependencies:
  zero imports in this repo or private-editor; dead weight in every npm
  consumer's install.
- Pin @visual-json/react "latest" -> "^0.4.0" (current resolution;
  "latest" in a published package's deps is unreproducible and was
  inconsistent with private-editor's ^0.4.0).
- Add root override three: 0.184.0, mirroring private-editor's dedupe —
  drops the nested stats-gl three@0.170.0 so the lockfile resolves a
  single three (the stated single-instance invariant now holds here too).

Verified: build, check-types, biome check, 931 package tests green;
lockfile resolves one next (16.2.6) and one three (0.184.0);
oxide/lightningcss optionalDependency pins still match resolutions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 16:28:02 -04:00
Aymeric RabotandClaude Fable 5 9d24e260c3 chore(deps): June 2026 dependency refresh
Range edits:
- next 16.2.1 -> 16.2.7 in apps/editor + apps/ifc-converter. 16.2.5/16.2.6
  fixed six high-severity advisories (middleware bypass, DoS, SSRF, XSS,
  RSC cache poisoning); 16.2.7 is bugfix backports. No API/config changes.
- typescript 6.0.2 -> 6.0.3 everywhere, including aligning the 5.9.3
  stragglers (@pascal-app/mcp, @repo/ui, @repo/eslint-config) that date
  from the original scaffold and were never intentionally held back.
- @types/node in @pascal-app/mcp: ^25.5.0 -> ^22.19.20 — mcp was the only
  workspace typed against non-LTS node 25; 22 matches the release
  workflow's node and every other workspace.
- @number-flow/react ^0.5.14 -> ^0.6.0 (packages/editor, apps/editor);
  the only breaking change (removed --number-flow-char-height CSS var)
  is unused here.
- agentation ^2.3.2 -> ^3.0.2 (apps/editor devtool; v3 is a drop-in for
  the props-less <Agentation /> usage, additions are opt-in).
- Root overrides: @types/react 19.2.14 -> 19.2.17,
  @types/three 0.184.0 -> 0.184.1 (types-only fixes).

Lockfile refresh within existing ranges picks up react 19.2.7 (pairs with
next 16.2.7 — 19.2.6 had a server-action FormData regression), motion
12.40.0, three-mesh-bvh 0.9.10, @react-three/uikit-lucide 1.0.73, the
June radix wave, lucide-react 1.17.0, zustand 5.0.14, tailwind-merge
3.6.0, geist 1.7.2 (fixes Geist Mono ligature regression), react-grab
0.1.44, biome 2.4.16 + ultracite 7.8.2, turbo 2.9.17.

Held: three stays 0.184.0 (npm latest; single-instance constraint),
tailwindcss 4.3.0 + lightningcss 1.32.0 already match the pinned
optionalDependencies native binaries, eslint 10 major not taken
(@repo/ui is unconsumed legacy scaffold).

Verified: turbo build, check-types, biome check, and 931 package tests
green; bun.lock stays lockfileVersion 1 (CI bun 1.3.0 compatible);
single next/three resolution confirmed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 14:48:40 -04:00
a144502c04 feat(editor): hierarchical function-tree Items browse (#345)
* feat(editor): hierarchical function-tree Items browse

Add an optional DB-driven taxonomy browse to the Items panel. When the
embedder supplies a `functionTree`, the panel renders roots as category
tabs and child tags as a secondary chip row, filtering items by the
selected node and any descendant slug; otherwise it falls back to the
legacy hardcoded category path untouched.

Adds `AssetInput.functionTags` so items can carry their function-axis
tag slugs, and exports `FunctionTreeNode` for embedders to type the tree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(editor): sync bun.lock (@pascal-app/mcp 0.3.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:47:40 -04:00
Wassim SAMADandClaude Opus 4.7 0df51219d0 feat: IFC → Pascal converter (package + app)
Brings the IFC-to-Pascal converter into the monorepo as a pure-logic
package plus a Next.js app, replacing the standalone repo that consumed
published @pascal-app/* packages (and drifted from their schemas).

packages/ifc-converter — pure conversion. Parses IFC via web-ifc and
maps elements onto @pascal-app/core node schemas (workspace-linked, so
no more version drift). Builds doors/windows, walls, slabs, columns,
roofs, stairs, sites/buildings/levels. Validates each node via the real
Zod schemas at build time (tryParse) and strips undefined metadata.

apps/ifc-converter — the UI: drop zone, example picker, element search,
JSON download, and a 3D preview rendered through the real
@pascal-app/viewer (registry bootstrap + read-only scene) with a custom
toolbar (camera/level/wall/grid/theme), level selector with
camera-focus, auto-fit, and selection bridged to an inspector.

Conversion specifics worth noting:
- Door/window vertical centering (height/2; windows + sill).
- Nearest-wall hosting fallback for files lacking IFCRELFILLSELEMENT,
  preferring walls long enough to contain the opening and clamping the
  along-wall position so cutouts can't overflow and break wall CSG.
- Plain IFCWALL (Brep/mapped geometry) falls back to default
  height/thickness instead of collapsing to zero-height slivers.
- Columns convert as plain structural shafts (no decorative
  base/capital) sized from the IFC profile.
- Beams + items are skipped for now (no Pascal beam type; items need a
  catalog asset) — counted in the conversion summary.

Large example IFCs are fetched from a public bucket at runtime; the four
small ones are committed. web-ifc.wasm is copied into public/ on
install/dev/build. README flags early-alpha + invites contributions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:45:12 -04:00
Wassim SAMADandClaude Opus 4.7 c4001a656f apps/editor: mount node-registry bootstrap at the root layout
The standalone editor app loaded `lib/bootstrap.ts` as a side-effect
import only from `components/scene-loader.tsx`. That worked for the
`/edit/[sceneId]` route but left every other page (homepage, settings,
viewer-only routes) hitting `<Viewer>` with an empty client-side
registry — node materials resolved to `null` and React surfaced a
`<html>`-level hydration mismatch on first paint.

Fix mirrors the community-app side that landed in pascalorg/private-
editor#27:

 - New `app/client-bootstrap.tsx` — thin client wrapper that imports
   `../lib/bootstrap` and renders children.
 - `app/layout.tsx` mounts `<ClientBootstrap>` around `{children}` so
   every page in the standalone editor gets the registry populated
   before its first `<Viewer>` / `<Editor>` mounts.
 - `lib/bootstrap.ts` switched to **synchronous** built-in registration
   via `registerNode(def)` per kind instead of `await loadPlugin(...)`.
   The previous async kick-off only resolved in a microtask, letting
   the first SSR / hydration pass see an empty registry. External
   plugin discovery (`discoverPlugins()`) stays async and runs via its
   own `loadExternalPlugins()` path, gated by `externalsKickedOff` so
   HMR doesn't re-fetch.
 - `components/scene-loader.tsx` drops the per-page side-effect import
   — the root provider handles it now.

`bun.lock` syncs `@pascal-app/editor` into `@pascal-app/nodes`'s
peerDependencies + devDependencies (already declared in
`packages/nodes/package.json`; only the lockfile lagged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:17:30 -04:00
Wassim SAMADandClaude Opus 4.7 b6d77206b4 Phase 2 spike: spawn migration (flagged) + new shelf node
The first time registry-driven nodes actually run in the editor.

Spawn migration (under NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag):
- New packages/nodes/src/spawn/ folder with renderer, tool, schema
  (re-exported from core), parametrics, definition, index.
- Spawn definition appended to builtinPlugin.nodes only when the flag
  is set. With the flag off, the Phase 0 shims fall through and the
  legacy SpawnRenderer / SpawnTool keep ownership.
- New no-props SpawnTool reads activeLevelId from useViewer directly,
  matches legacy placement behavior (half-meter snap, singleton-per-
  level, replace-on-reclick).
- Structural parity test (9 cases) validates definition shape +
  schema identity. Pixel-diff defers to Phase 4 alongside more nodes.

New shelf node (no legacy — registered unconditionally):
- ShelfNode schema in core/schema/nodes/shelf.ts (hand-maintained
  AnyNode union for now; Phase 6 derives the union from the registry
  and moves the schema fully into nodes/shelf/).
- packages/nodes/src/shelf/ folder: pure geometry builder
  (buildShelfGeometry returns a Three.js Group of top board +
  brackets), R3F renderer that mounts the built group, no-props
  placement tool, parametrics descriptor (width/depth/thickness/
  height/bracketStyle/color), definition with surfaces.top stackable
  surface for future stacking, and presentation metadata for the
  palette.
- 13 unit tests across schema bounds and geometry behavior.
- Palette wiring: 'shelf' added to StructureTool union + an entry in
  the structure-tools array (placeholder icon, replaced in Phase 4
  when palette is registry-driven).

Framework changes:
- @pascal-app/viewer now exports useNodeEvents from its public barrel
  so node bundles in @pascal-app/nodes can subscribe to node-specific
  pointer events. (Used by spawn renderer; shelf renderer skips it
  for now since useNodeEvents has a hardcoded kind list — Phase 4
  generalizes it via the registry.)
- @pascal-app/nodes gains @pascal-app/viewer as a peer + dev dep so
  node bundles can import from it.

630 tests pass across 76 files (22 new this phase). Editor app
continues to ship green with both legacy spawn and the new shelf
node co-existing through the Phase 0 dispatch shims.

To validate end-to-end in dev:
- bun dev:community → open editor → click 'Shelf' in structure
  toolbar → click to place. Confirms full registry path
  (NodeRenderer dispatch + ToolManager dispatch + sceneRegistry
  byType Proxy).
- Set NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN=1, restart dev, place spawn
  → visually identical to legacy. Confirms parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:07:38 -04:00
Wassim SAMADandClaude Opus 4.7 4d5226ce10 Wire loadPlugin(builtinPlugin) at editor app bootstrap
Adds apps/editor/lib/bootstrap.ts that calls loadPlugin(builtinPlugin)
as a module-side-effect on first import. Imported from scene-loader.tsx
so it runs on the client side where the editor mounts.

Idempotency guard handles HMR re-execution (would otherwise throw on
duplicate registerNode). For the empty plugin this commit, the entire
call is a no-op — included now so future commits that add real node
kinds only need to push them onto builtinPlugin.nodes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:28:01 -04:00
Wassim SAMADandClaude Opus 4.7 a41321e193 Lock bun.lock for @pascal-app/nodes workspace + sort registry barrel
Companion changes for the new nodes package: bun.lock entry from
`bun install`, and a Biome-auto-sort of the registry barrel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:25:27 -04:00
sudhir 0dcc72a57a Merge remote-tracking branch 'origin/main' into fix/fri-8-may 2026-05-12 09:52:32 +05:30
sudhir 2fc754108d Use community viewer toolbars in editor app 2026-05-12 09:49:10 +05:30
sudhir 564bfa229e Merge remote-tracking branch 'origin/main' into fix/fri-8-may
# Conflicts:
#	packages/editor/src/components/tools/fence/move-fence-endpoint-tool.tsx
#	packages/editor/src/components/tools/select/box-select-tool.tsx
#	packages/editor/src/components/tools/wall/move-wall-tool.tsx
#	packages/editor/src/components/ui/panels/column-panel.tsx
#	packages/editor/src/components/ui/panels/door-panel.tsx
#	packages/editor/src/components/ui/panels/window-panel.tsx
#	packages/viewer/src/components/renderers/column/column-renderer.tsx
#	packages/viewer/src/components/viewer/index.tsx
#	packages/viewer/src/systems/slab/slab-system.tsx
2026-05-12 00:27:06 +05:30
Wawa 6488efe296 bun lock 2026-05-10 09:13:03 -04:00
sudhir 4956de4910 Refine column support selection and polygon handling 2026-05-10 12:51:57 +05:30
Wawa 7f74e18456 Update bun.lock from macOS install
Lockfile dedup pass after running bun install on macOS — picks up the
darwin-arm64 native binaries hoisted to top-level node_modules and removes
a redundant cmdk@radix nested resolution edge that wasn't needed.
2026-05-10 00:33:36 +00:00
Wawa b27e53415f Hoist platform-specific native packages via root optionalDependencies
Real root cause:
Tailwind v4's lightningcss is invoked from a Turbopack-bundled postcss.js at
runtime. Turbopack resolves require() against the workspace's TOP-LEVEL
node_modules, not against bun's nested .bun/ store. So even when
lightningcss-darwin-arm64 is correctly installed inside .bun/ with proper
symlinks, Turbopack can't see it — and lightningcss falls back to a sibling
'.node' file which doesn't exist, hence the runtime error.

Private-editor accidentally works because it has TWO lightningcss versions
(1.27.0 from Expo + 1.32.0 from Tailwind), which forces bun's hoisting
algorithm to put one copy at the top level. Editor-v2 has only one version,
so bun keeps it nested in .bun/ and nothing is at top-level.

Fix: declare the platform binary packages as optionalDependencies at the
WORKSPACE ROOT (not apps/editor). Root deps are always hoisted to top-level
node_modules, so Turbopack's resolver finds them. Bun still applies the
os/cpu filter at install time, so each developer only ends up with their
host's binaries on disk.

Verified on Linux: node_modules/lightningcss-linux-arm64-gnu is a top-level
symlink after install. On macOS, the darwin variants will be hoisted instead.
2026-05-10 00:14:28 +00:00
Wawa e618175bba Reset core/viewer/editor/mcp packages and apps/editor from private-editor
Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.

Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
  the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
  the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
  references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
  valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
  @types/react, @types/react-dom, @types/three to prevent JSX namespace
  fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
  to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
  match the new versions on every bump (so viewer/editor/mcp tarballs always
  reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts

Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
  what private-editor currently reports

Open PRs against editor-v2 will need rebasing/conflict resolution.
2026-05-09 20:52:26 +00:00
Pascal 723ae14e9b release: v0.6.1 2026-05-09 02:25:15 +00:00
sudhir 23eec44714 Move render systems out of core 2026-05-01 15:07:47 +05:30
sudhir eee7af9e25 Add draggable level reordering to floating selector 2026-05-01 14:55:24 +05:30
sudhir 15daeaace6 Standardize stair controls on slider inputs 2026-04-28 14:50:51 +05:30
Aymeric Rabot 0711cad660 Merge remote-tracking branch 'origin/main' into feat/mcp-server 2026-04-27 17:22:14 -04:00
Aymeric Rabot b3d1f663f6 fix(mcp): use local sqlite scene storage 2026-04-24 13:32:04 -07:00
Pascal 4b5c3beadc release: v0.6.0
Bump @pascal-app/core, @pascal-app/viewer, and @pascal-app/editor to 0.6.0.
Update cross-package peer/dev dependency versions.
2026-04-21 20:52:02 +00:00
Adrian PerezandClaude Opus 4.7 e8d0b13ff5 feat(mcp,editor): Option A+B storage + 10 agent deliverables (Phase 7)
Ships the combined filesystem/Supabase storage adapter + MCP scene
lifecycle tools + Next.js API routes + editor /scene/[id] route, so
an MCP save is directly openable at /scene/<id> without any
injection hack. End-to-end verified: 10/10 e2e steps pass.

Storage (A1/A2/A3):
- SceneStore interface + error classes + slug helpers
- FilesystemSceneStore at $PASCAL_DATA_DIR (defaults XDG/~/.pascal)
  with atomic writes, .index sidecar, optimistic locking
- SupabaseSceneStore with scenes + scene_revisions tables, RLS
  migration SQL, mock-backed unit tests
- createSceneStore(env) auto-selects based on SUPABASE_URL +
  SUPABASE_SERVICE_ROLE_KEY

MCP tools (A4, A8, A9, A10):
- save_scene / load_scene / list_scenes / delete_scene / rename_scene
- list_templates / create_from_template (3 seed templates:
  empty-studio, two-bedroom, garden-house)
- generate_variants (7 mutation kinds, seeded RNG, save=true|false)
- photo_to_scene (vision sampling → scene graph → save)

Editor (A5, A6):
- /api/scenes + /api/scenes/[id] with RFC 7232 If-Match locking
- /scene/[id] and /scenes route pages with save button, SceneLoader
- Removed the window.__pascalScene dev injection hack

Security + UX edges (A7, A8):
- AssetUrl Zod validator: asset:// blob: data:image/ /path https:
  (http://localhost for dev) + PASCAL_ALLOWED_ASSET_ORIGINS env
  allowlist. Hardens scan.url, guide.url, item.asset.src,
  material.texture.url, MaterialMaps.*Map
- Auto-frame camera on empty→non-empty scene transition
  (camera-controls:fit-scene emitter event)

Shared utilities:
- rehydrateSiteChildren() extracted to packages/mcp/src/lib/ and
  used by both create-from-template and generate-variants to work
  around the SiteNode.children-as-objects vs. ids inconsistency
  (CROSS_CUTTING §2)
- Storage + MCP subpath exports added to packages/mcp/package.json
  (CROSS_CUTTING §4)

Tests: 293 pass / 0 fail across 40 files (was 142 pre-Phase-7).
Biome: clean.

Phase-7 e2e script at packages/mcp/test-reports/phase7-e2e.ts:
MCP HTTP + editor Next.js both point at $PASCAL_DATA_DIR =
/tmp/pascal-e2e, save_scene from MCP, GET /api/scenes/<id> from
editor server, /scenes list page renders all saved scenes, scene
page renders SceneLoader, delete_scene works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:29:28 +02:00
Adrian PerezandClaude Opus 4.7 2ae932f384 feat(mcp): scaffold package and confirm headless bridge viability
- Add packages/mcp/ with package.json, tsconfig, PLAN.md, CROSS_CUTTING.md
- Extend @pascal-app/core exports map with subpaths (./schema, ./store,
  ./material-library, ./spatial-grid, ./wall) so Node consumers can skip
  the graphics-bound systems re-export chain.
- Add scripts/spike.ts that proves useScene + temporal work in Node with
  a requestAnimationFrame polyfill. Spike passes end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:41:09 +02:00
PascalandGitHub 5c16aa4225 feat: improve thumbnails, placement, and editor responsiveness (#258)
* wip: backport monorepo PRs 278 280 281 283 284

* refactor(editor): align floorplan panel with monorepo split version
2026-04-17 15:14:40 -04:00
Pascal f25806ee8f release: v0.5.1 2026-04-14 05:01:30 +00:00
a205e4f778 Feat/stairs fence update (#226)
* feat: railing on the straight stairs and new fence

* feat: added spiral and curved stairs with bug fix for fence

* feat:fence are linked to each other ... so moving one move the other sharing the same coordinate

* fix: update stair railing logic to include front-side attachments for terminal landings

* Integrate fence rendering into the fence system

* fix: pass nodeId instead of undefined node to WallTreeNode and FenceTreeNode

TreeNode was passing `node` (undefined variable) instead of `nodeId` to
WallTreeNode and FenceTreeNode, causing a runtime ReferenceError.
Updated FenceTreeNode to accept nodeId and look up the node from the
scene store internally, consistent with all other tree node components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: update fence icon with new isometric design

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Aymeric Rabot <aymeric@pascal.app>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:31:57 -04:00
Pascal fab9077f65 fix: update lockfile for v0.5.0 release 2026-04-09 19:07:05 +00:00
PascalandGitHub 50eca057ca feat: editor layout redesign v2 + 3D box select (#207)
Major layout redesign introducing v2 editor layout:

**New Features**
- V2 two-column layout (resizable sidebar + viewer panel with toolbar slots)
- 3D box select tool with marquee selection (561 lines pure Three.js geometry)
- View mode system (3D/2D/Split) replacing old floorplan toggle
- Floating level selector on viewer panel
- Viewer toolbar: level mode, wall mode, units, theme, camera, walkthrough, preview
- Horizontal tab bar for sidebar panels

**Layout Changes**
- Fixed 5-button control bar: Select, Box Select, Site Edit, Build, Delete
- Simplified view toggles (scans/guides only)
- Always-mounted viewers (display:none) to preserve WebGL context
- First-person walkthrough forces 3D view, restores on exit
- Site edit as permanent control (phase='site' is sole signal)

**Technical**
- layoutVersion prop: 'v1' (default) or 'v2'
- New exports: FloatingLevelSelector, SettingsPanel, SidebarTab, ViewMode, ViewerToolbarLeft/Right
- isCollapsed/setIsCollapsed added to sidebar store
- isFirstPersonMode restored for walkthrough support

19 files changed, +3230/-1898
Built and verified: bun run build passes (all tasks, 0 errors)
2026-04-02 11:24:19 -04:00
Developer 12b6292623 feat: add material system for all node types
- Add MaterialSchema with 10 presets (white, brick, concrete, wood, glass, metal, plaster, tile, marble, custom)
- Add material field to Wall, Slab, Door, Window, Ceiling, Roof, RoofSegment nodes
- Create MaterialPicker UI component with preset selection and custom properties
- Update all renderers to support material rendering with caching
- Add Material section to all node panels (WallPanel, SlabPanel, DoorPanel, WindowPanel, CeilingPanel, RoofPanel, RoofSegmentPanel)
- Update AGENTS.md with Material System documentation
2026-03-30 12:15:16 +08:00
PascalandGitHub 49c045e861 chore: add community templates, simplify setup, fix lockfile (#177) 2026-03-25 21:20:25 -07:00
Aymeric RabotandClaude Opus 4.6 e2cde339ae fix: add @types/node to viewer devDependencies for process.env typing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:29:12 -07:00
Aymeric RabotandClaude Opus 4.6 6bbb1db334 chore: update lockfile
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:14:26 -07:00
wass08 f2501be2bf remove community 2026-03-13 09:07:46 +01:00
wass08 3eeba322a8 rename before moving to monorepo 2026-03-11 15:12:41 +01:00
wass08 7359c1fcbf splitting editor and community 2026-03-11 12:16:26 +01:00
Aymeric Rabot bc59752edb Bump devDependencies and update lockfile
Bump devDependencies in package.json: @biomejs/biome ^2.4.4→^2.4.6, turbo ^2.8.12→^2.8.15, ultracite ^7.2.4→^7.2.5. Regenerate bun.lock to reflect these updates and related transitive changes (e.g. @clack, glob, path-scurry/minipass, minimatch).
2026-03-10 15:02:55 -04:00
wass08 d0f2a1a06f actions command K 2026-03-10 13:35:05 +01:00
Aymeric RabotandClaude Opus 4.6 de6bfd8c00 Add react-scan and react-grab dev tools (development only)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 18:52:01 -05:00
Wassim SAMADandGitHub e0d97df8c8 Revert "Restore inline zone label editing on canvas (#134)"
This reverts commit 036344d705.
2026-03-06 13:04:52 +01:00