feat(plugins): plugin contract + first-party Nature pack (trees/flowers/grass)#457
Conversation
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>
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>
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>
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>
… 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>
…nails + 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>
… 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>
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>
- 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>
…lowers/grass) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rail Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
…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>
…olor 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>
…ic 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>
…nd 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>
…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>
Reverts the Plugin.onSceneLoad hook + viewer application + modelScale compensation (commits 893ca83, 6c9ee00). 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>
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>
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>
…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>
…r 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>
…-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>
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>
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>
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>
…nails 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>
… 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>
- 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>
… 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>
- 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>
…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>
…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>
…le wiki path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b4e5268. Configure here.
| const world = lastWorld ?? event.position | ||
| const [lx, , lz] = toLevelLocal(activeLevelId, world) | ||
| const [sx, sz] = snapXZ(lx, lz) | ||
| commitRef.current([sx, 0, sz]) |
There was a problem hiding this comment.
Plant placement snap frame mismatch
Medium Severity
The placement ghost snaps using grid:move building-local XZ, but clicks commit after converting the world hit to level-local XZ and snapping there. Built-in shelf placement keeps one building-local cursor snapshot for both preview and commit, so rotated or offset buildings can show the ghost in one place and plant somewhere else.
Reviewed by Cursor Bugbot for commit b4e5268. Configure here.
| } else { | ||
| store.setGrassPreset((node as unknown as GrassNode).preset ?? 'meadow') | ||
| } | ||
| }) as never, |
There was a problem hiding this comment.
Find catalog never opens panel
Medium Severity
The new selection:find-node handler only updates the Nature store (mode/preset). It never opens the sidebar panel via panelRegistry.panelForKind, and nothing else in this PR wires that up, so “find in catalog” won’t reveal the Nature rail unless the user already opened it.
Reviewed by Cursor Bugbot for commit b4e5268. Configure here.
| return Object.values(scene).filter( | ||
| (n) => (n.type as string) === kind && !active.has(n.id as string), | ||
| ) as unknown as N[] | ||
| }, [scene, kind, activeKey]) |
There was a problem hiding this comment.
Instanced batch ignores visibility
Medium Severity
InstancedKindSystem includes every node of a kind except hover/selection exclusions, but never checks visible. KindProxy hides when visible is false, so toggling visibility can leave the collective instanced mesh drawing while the per-node proxy disappears.
Reviewed by Cursor Bugbot for commit b4e5268. Configure here.
| // loaded plugin re-renders them live from the scene graph — hide the frozen | ||
| // baked mesh so only the live one shows. Gated on the registry, so with no | ||
| // plugin loaded the policy is `'static'` and the baked mesh stays. | ||
| if (bakePolicyOf(extras.kind ?? '') === 'replace') object.visible = false |
There was a problem hiding this comment.
Replace bake hides without live layer
Medium Severity
GlbScene hides baked meshes when bakePolicyOf is replace, but live trees only appear if the host passes replaceNodes into GlbReplaceInstances. This PR exports buildGlbReplaceNodes yet no in-repo caller passes replaceNodes, so a GLB viewer that loads the trees plugin can show no vegetation at all.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b4e5268. Configure here.
| doneRef.current = true | ||
| const run = async () => { | ||
| try { | ||
| // Signal export so instanced kinds (trees/flowers/grass) swap their |
There was a problem hiding this comment.
Second bake export never runs
Medium Severity
BakeExporter sets doneRef to true on the first active export and never clears it when active goes false. A later bake in the same session stays blocked because the effect bails out on doneRef.current.
Reviewed by Cursor Bugbot for commit b4e5268. Configure here.


What does this PR do?
Lands the editor's plugin system end-to-end, proven by a first-party example plugin:
@pascal-app/core) —Pluginmanifest (id,apiVersion,nodes,panels),loadPluginwith apiVersion gate + duplicate-kind protection,setPluginDiscovery/discoverPluginsbootstrap hook, and an observablepanelRegistry(namespaced panel ids,panelForKindso "find in catalog" can open the panel that places a kind, per-panelworkspacesscoping — default edit-only).@pascal-app/plugin-trees— the worked example: three node kinds (trees:treevia ez-tree, proceduraltrees:flower+trees:grass) rendered as InstancedMeshes with true-silhouette hover/select, TSL vertex-bend wind (WebGPU), 2D plan symbols with trunk-sized footprints, placement/move/rotate parity with built-ins, and a Nature presets rail panel driving the native inspector parametrics. Peer-deps on@pascal-app/*— structurally identical to a third-party pack.static/strip/replace) — the GLB bake and the baked viewer consultdef.bakeinstead of hardcoding kinds;replacekinds bake as static geometry (plain glTF viewers still see them) while our viewer hides the baked meshes and re-renders them live per level (wind + instancing), plus a BVH over the baked scene for cheap hover/pick.document), so the package keeps it off every eagerly-imported path (variant-utils.tssplit); the Next.js app builds/prerenders cleanly.wiki/architecture/plugin-authoring.mdlinked from README + CONTRIBUTING; stale wiki path inregistry.tsfixed.How to test
bun install && bun dev, open http://localhost:3000.main.bun check-typesandbun run buildpass (the build previously crashed on SSR prerender if ez-tree leaked into the eager import graph).Screenshots / screen recording
Visual change — screen recording to be added before review.
Checklist
bun devbun checkto verify)mainbranchNote
Medium Risk
Touches core registry, GLB bake/export, and baked-viewer rendering/raycasting; regressions could affect built-in bake behavior or viewer performance, though changes are mostly additive behind the new plugin and bake-policy APIs.
Overview
Introduces a first-class plugin surface in
@pascal-app/core:Plugingains optionalpanels,loadPluginregisters namespaced rail panels and maps node kinds to panels for “find in catalog”, and an observablepanelRegistrylets the sidebar pick up async discovery. Node definitions addBakePolicy(static/strip/replace) plus optionalbakeReplaceRenderer; scan/guide built-ins move tobake: 'strip'and GLB export/viewer paths usebakePolicyOfinstead of hardcoded kinds.The editor merges plugin panels into v1/v2 sidebars (
usePluginPanels, workspace filtering), widensToolfor plugin tool ids, and togglesisExportingduring bake so instanced kinds emit real meshes for GLB capture.@pascal-app/plugin-treesis the reference pack: three instanced furnish kinds (ez-tree + procedural flowers/grass), Nature presets panel, placement tools, parametrics, 2D floorplan symbols, TSL wind, andbake: 'replace'live re-render in the baked viewer. The app registers it viasetPluginDiscoveryand transpiles the package for Next SSR (ez-tree kept off eager import paths).The baked GLB viewer re-adds
stripreference nodes via registry policy, hides and replacesreplacemeshes with collectiveGlbReplaceInstances, and adds BVH on baked meshes for hover/pick performance. Docs/README/CONTRIBUTING point atplugin-authoring.md.Reviewed by Cursor Bugbot for commit b4e5268. Bugbot is set up for automated code reviews on this repo. Configure here.