The first-party example plugin for the Pascal editor. It contributes a procedural trees node and a left-rail presets panel, and exists to prove — and document — the minimal host surface every future plugin reuses.
It is structurally identical to a third-party plugin: it peer-depends on
@pascal-app/{core,viewer,editor} (plus react/three/@react-three/fiber/
zustand) and bundles @dgreenheck/ez-tree for the geometry. It imports
nothing private. Copy this folder as the starting point for a new plugin.
The contribution paths a plugin has:
- Left-rail panel —
panelsin the manifest.presets-panel.tsxis a plain React component the host mounts behind an error boundary. - Right inspector for free —
def.parametrics(parametrics.ts). The host renders the preset/height/seed controls + the Randomize action with zero tree-specific code. - Placement —
def.tool/def.preview(tool.tsx,preview.tsx). The tool respects the active snapping mode (isGridSnapActive()+gridSnapStep) exactly like the built-in item/shelf tools. - Instanced rendering (the generic core in
instanced.tsx, shared by both kinds) — instead of the per-nodedef.geometrypath, plants render via two pieces:def.system— a collective renderer mounted once that groups every node of the kind by its geometry variant and draws each variant as oneInstancedMeshper sub-mesh. A forest of N is a handful of draw calls. Variant geometry is generated once and cached.def.renderer— a featherweight per-node proxy: a stable invisible box collider (the raycast target) in an outer group, plus the real geometry (invisible, mounted only while hovered/selected) in an inner registered group. So the host's outline pass traces the true silhouette, picking stays on the box, and selection / outline / zone machinery works unchanged with no instanceId bookkeeping.
trees:tree— ez-tree geometry (geometry.ts); species presets Oak / Pine / Aspen / Ash / Bush / Trellis × a Small/Medium/Large size (all of ez-tree's built-in presets), a Deciduous/Evergreen type, curated params (foliage density, trunk thickness, leafless), and leaf/branch colour tints — all folded into the variant key. Colours are edit-only (inspector), not on the placement brush.trees:flower— simple procedural geometry (flower-geometry.ts, merged per material); presets daisy / tulip / lavender, with a per-flower petal colour.trees:grass— procedural blade tufts (grass-geometry.ts); presets meadow / fescue / reed, with a per-tuft blade colour.
Flowers and grass are sibling kinds that reuse the exact same instanced core +
placement helper (instanced.tsx / placement.tsx) and the shared procedural
RNG (mulberry32 in geometry.ts) — the template for adding more plant kinds.
It also shows the communication triangle: presets-panel → plugin store
(store.ts) → def.tool → SceneApi → scene → reactive useScene read-back
(the "N planted" counter in the panel).
@dgreenheck/ez-tree ships its bark/leaf textures inlined as base64, so there
are no assets to host. Placement seeds are drawn from a small bounded pool
(TREE_SEED_POOL) so trees share variants — that sharing is what makes the
instancing pay off; a unique inspector seed just renders as its own variant.
import { treesPlugin } from '@pascal-app/plugin-trees'
// host:
setPluginDiscovery(async () => [treesPlugin])treesPlugin exports three node kinds (trees:tree, trees:flower,
trees:grass) and one panel (Trees), loaded through the same loadPlugin
path as the built-ins.
createNodeand thefloorPlaced.footprintcallback are typed against the host's hand-maintainedAnyNodeunion, so the node is cast (as AnyNode/as TreeNode). The registry derivesAnyNodepost-migration.- The placement tool re-derives level-local conversion from the public
sceneRegistrybecause the built-infloor-placementhelpers aren't part of the public@pascal-app/*surface yet — a candidate for a future@pascal-app/plugin-apire-export package. - The instance matrices fold in the parent level's world transform; a building move while plants are static won't refresh until a node of that kind next changes.
- Heavy per-node tweaking of geometry params (or unique seeds) erodes instancing batching — but it degrades gracefully: such a node just becomes its own single-instance variant, never worse than the non-instanced path.
See wiki/architecture/plugin-authoring.md for the full contract.