Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,33 @@ instancing pay off; a unique inspector seed just renders as its own variant.
## Manifest

```ts
import { treesPlugin } from '@pascal-app/plugin-trees'
import {
treesCoreManifest,
treesEditorManifest,
treesViewerManifest,
} from '@pascal-app/plugin-trees'
import { loadCoreManifest } from '@pascal-app/core'
import { loadViewerManifest } from '@pascal-app/viewer'
import { loadEditorManifest } from '@pascal-app/editor'

// host:
setPluginDiscovery(async () => [treesPlugin])
loadCoreManifest(treesCoreManifest)
loadViewerManifest(treesViewerManifest)
loadEditorManifest(treesEditorManifest)
```

`treesPlugin` exports three node kinds (`trees:tree`, `trees:flower`,
`trees:grass`) for the core `loadPlugin` path. The editor app separately imports
`treesHostPanel` to describe and surface the Nature rail entry; panels are not
part of the v1 core plugin manifest. The panel declares `pluginId` and
Plugin API v2 ships three manifests instead of one `NodeDefinition` list. The
core manifest declares the three kinds (`trees:tree`, `trees:flower`,
`trees:grass`) — a kind exists because it is there. The viewer and editor
manifests attach geometry/renderers and parametrics/tools/plan symbols to those
kinds by `kind`, and a host rejects a contribution whose kind has no base
definition. Each manifest names only what its own package owns, which is what
lets `@pascal-app/core` stop publishing React and Three.js types to plugin
authors.

The editor app separately imports `treesHostPanel` to describe and surface the
Nature rail entry; panels are not part of the core plugin manifest. The panel
declares `pluginId` and
`defaultInstalled: true`, so Nature is listed in the Plugins sidebar and is
enabled for new and legacy scenes unless a project explicitly uninstalls it.

Expand Down
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pascal-app/plugin-trees",
"version": "0.1.0",
"version": "0.2.0",
"description": "First-party example Pascal editor plugin — a procedural trees node with a presets rail panel. Structurally identical to a third-party plugin (peer-deps on @pascal-app/*).",
"repository": {
"type": "git",
Expand Down Expand Up @@ -31,9 +31,9 @@
"@dgreenheck/ez-tree": "^1.1.0"
},
"peerDependencies": {
"@pascal-app/core": ">=0.9.1 <1",
"@pascal-app/editor": ">=0.9.1 <1",
"@pascal-app/viewer": ">=0.9.1 <1",
"@pascal-app/core": ">=1.0.0-beta.4",
"@pascal-app/editor": ">=1.0.0-beta.4",
"@pascal-app/viewer": ">=1.0.0-beta.4",
"@react-three/fiber": "^9",
"react": "^18 || ^19",
"three": "^0.185",
Expand Down
41 changes: 24 additions & 17 deletions src/definition.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { HandleDescriptor, NodeDefinition } from '@pascal-app/core'
import type { CoreNodeDefinition } from '@pascal-app/core'
import type { EditorNodeContribution, HandleDescriptor } from '@pascal-app/editor'
import type { ViewerNodeContribution } from '@pascal-app/viewer'
import { buildTreeFloorplan, treeTrunkRadius } from './floorplan'
import { treeParametrics } from './parametrics'
import { TreeNode } from './schema'

type TreeDefinition = NodeDefinition<typeof TreeNode> & Record<string, unknown>

const ROTATE_RING_OFFSET = 0.35
/** Ring hugs the ground like the item gizmo — high enough to clear the grass,
* low enough to read as a floor affordance. */
Expand Down Expand Up @@ -60,15 +60,12 @@ const treeFloorPlacement = {
* outline / zone machinery works unchanged. `parametrics` gives the inspector
* for free; `tool`/`preview` drive placement. No host dispatch code per kind.
*/
export const treeDefinition: TreeDefinition = {

export const treeCoreDefinition: CoreNodeDefinition<typeof TreeNode> = {
kind: 'trees:tree',
// Static in the bake for portability; our viewer removes the baked meshes and
// re-renders live (wind, LODs) via this def's own path. See plans → Part D.
bake: 'replace',
schemaVersion: 1,
schema: TreeNode,
category: 'furnish',
snapProfile: 'item',

defaults: () => ({
object: 'node',
Expand Down Expand Up @@ -112,17 +109,32 @@ export const treeDefinition: TreeDefinition = {
floorPlaced: treeFloorPlacement,
},

parametrics: treeParametrics,
// 2D plan symbol: dashed canopy ring + trunk dot (see floorplan.ts).
floorplan: buildTreeFloorplan,
handles: [treeRotateHandle()],
mcp: {
description:
'A procedural ez-tree (example plugin node). Species presets (oak/pine/aspen/ash/bush/trellis) × size, deciduous/evergreen type, adjustable height, foliage/trunk, leaf & branch tint, and a seed for variation.',
},
}

export const treeViewerContribution: ViewerNodeContribution<typeof TreeNode> = {
// Static in the bake for portability; our viewer removes the baked meshes and
// re-renders live (wind, LODs) via this def's own path. See plans → Part D.
bake: 'replace',

// Instanced rendering: an invisible per-node proxy for selection/outline...
renderer: { kind: 'parametric', module: () => import('./proxy-renderer') },
// ...and a collective system that batches every tree into InstancedMeshes.
system: { module: () => import('./system'), priority: 3 },
// Baked `/viewer` re-render for `bake: 'replace'` — collective, instanced.
bakeReplaceRenderer: { module: () => import('./static-renderer') },
}

export const treeEditorContribution: EditorNodeContribution<typeof TreeNode> = {
snapProfile: 'item',

parametrics: treeParametrics,
// 2D plan symbol: dashed canopy ring + trunk dot (see floorplan.ts).
floorplan: buildTreeFloorplan,
handles: [treeRotateHandle()],

preview: () => import('./preview'),
tool: () => import('./tool'),
Expand All @@ -138,9 +150,4 @@ export const treeDefinition: TreeDefinition = {
paletteSection: 'furnish',
hidden: true,
},

mcp: {
description:
'A procedural ez-tree (example plugin node). Species presets (oak/pine/aspen/ash/bush/trellis) × size, deciduous/evergreen type, adjustable height, foliage/trunk, leaf & branch tint, and a seed for variation.',
},
}
6 changes: 3 additions & 3 deletions src/elevation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
BuildingNode,
initSpatialGridSync,
LevelNode,
loadPlugin,
loadCoreManifest,
SlabNode,
useScene,
} from '@pascal-app/core'
import { draftElevation, plantElevation } from './elevation'
import { treesPlugin } from './index'
import { treesCoreManifest } from './index'

/**
* The elevation contract an instanced kind has to satisfy by hand.
Expand Down Expand Up @@ -90,7 +90,7 @@ function plant(kind: string, x: number, z: number) {

beforeAll(async () => {
// The resolver reads each kind's `floorPlaced` footprint off the registry.
await loadPlugin(treesPlugin as never)
loadCoreManifest(treesCoreManifest)
initSpatialGridSync()
})

Expand Down
33 changes: 20 additions & 13 deletions src/flower-definition.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { NodeDefinition } from '@pascal-app/core'
import type { CoreNodeDefinition } from '@pascal-app/core'
import type { EditorNodeContribution } from '@pascal-app/editor'
import type { ViewerNodeContribution } from '@pascal-app/viewer'
import { buildFlowerFloorplan } from './floorplan'
import { flowerParametrics } from './flower-parametrics'
import { FlowerNode } from './flower-schema'

type FlowerDefinition = NodeDefinition<typeof FlowerNode> & Record<string, unknown>

const flowerFloorPlacement = {
footprint: (node: unknown) => {
const flower = node as FlowerNode
Expand All @@ -23,13 +23,12 @@ const flowerFloorPlacement = {
* featherweight `def.renderer` proxy keeps selection working, `parametrics`
* gives the inspector, `tool`/`preview` drive placement.
*/
export const flowerDefinition: FlowerDefinition = {

export const flowerCoreDefinition: CoreNodeDefinition<typeof FlowerNode> = {
kind: 'trees:flower',
bake: 'replace', // static in bake, live-rebuilt in our viewer — see plans → Part D
schemaVersion: 1,
schema: FlowerNode,
category: 'furnish',
snapProfile: 'item',

defaults: () => ({
object: 'node',
Expand Down Expand Up @@ -58,12 +57,25 @@ export const flowerDefinition: FlowerDefinition = {
floorPlaced: flowerFloorPlacement,
},

parametrics: flowerParametrics,
floorplan: buildFlowerFloorplan,
mcp: {
description:
'A procedural flower (example plugin node) — daisy, tulip, or lavender, instanced like the trees.',
},
}

export const flowerViewerContribution: ViewerNodeContribution<typeof FlowerNode> = {
bake: 'replace', // static in bake, live-rebuilt in our viewer — see plans → Part D

renderer: { kind: 'parametric', module: () => import('./flower-proxy-renderer') },
system: { module: () => import('./flower-system'), priority: 3 },
bakeReplaceRenderer: { module: () => import('./flower-static-renderer') },
}

export const flowerEditorContribution: EditorNodeContribution<typeof FlowerNode> = {
snapProfile: 'item',

parametrics: flowerParametrics,
floorplan: buildFlowerFloorplan,

preview: () => import('./flower-preview'),
tool: () => import('./flower-tool'),
Expand All @@ -79,9 +91,4 @@ export const flowerDefinition: FlowerDefinition = {
paletteSection: 'furnish',
hidden: true,
},

mcp: {
description:
'A procedural flower (example plugin node) — daisy, tulip, or lavender, instanced like the trees.',
},
}
2 changes: 1 addition & 1 deletion src/flower-parametrics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ParametricDescriptor } from '@pascal-app/core'
import type { ParametricDescriptor } from '@pascal-app/editor'
import type { FlowerNode } from './flower-schema'

/** Inspector for a placed flower — rendered for free by the host's
Expand Down
33 changes: 20 additions & 13 deletions src/grass-definition.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { NodeDefinition } from '@pascal-app/core'
import type { CoreNodeDefinition } from '@pascal-app/core'
import type { EditorNodeContribution } from '@pascal-app/editor'
import type { ViewerNodeContribution } from '@pascal-app/viewer'
import { buildGrassFloorplan } from './floorplan'
import { grassParametrics } from './grass-parametrics'
import { GrassNode } from './grass-schema'

type GrassDefinition = NodeDefinition<typeof GrassNode> & Record<string, unknown>

const grassFloorPlacement = {
footprint: (node: unknown) => {
const grass = node as GrassNode
Expand All @@ -23,13 +23,12 @@ const grassFloorPlacement = {
* featherweight `def.renderer` proxy keeps selection working, `parametrics`
* gives the inspector, `tool`/`preview` drive placement.
*/
export const grassDefinition: GrassDefinition = {

export const grassCoreDefinition: CoreNodeDefinition<typeof GrassNode> = {
kind: 'trees:grass',
bake: 'replace', // static in bake, live-rebuilt in our viewer — see plans → Part D
schemaVersion: 1,
schema: GrassNode,
category: 'furnish',
snapProfile: 'item',

defaults: () => ({
object: 'node',
Expand Down Expand Up @@ -58,12 +57,25 @@ export const grassDefinition: GrassDefinition = {
floorPlaced: grassFloorPlacement,
},

parametrics: grassParametrics,
floorplan: buildGrassFloorplan,
mcp: {
description:
'A procedural grass tuft (example plugin node) — meadow, fescue, or reed, instanced like the trees.',
},
}

export const grassViewerContribution: ViewerNodeContribution<typeof GrassNode> = {
bake: 'replace', // static in bake, live-rebuilt in our viewer — see plans → Part D

renderer: { kind: 'parametric', module: () => import('./grass-proxy-renderer') },
system: { module: () => import('./grass-system'), priority: 3 },
bakeReplaceRenderer: { module: () => import('./grass-static-renderer') },
}

export const grassEditorContribution: EditorNodeContribution<typeof GrassNode> = {
snapProfile: 'item',

parametrics: grassParametrics,
floorplan: buildGrassFloorplan,

preview: () => import('./grass-preview'),
tool: () => import('./grass-tool'),
Expand All @@ -79,9 +91,4 @@ export const grassDefinition: GrassDefinition = {
paletteSection: 'furnish',
hidden: true,
},

mcp: {
description:
'A procedural grass tuft (example plugin node) — meadow, fescue, or reed, instanced like the trees.',
},
}
2 changes: 1 addition & 1 deletion src/grass-parametrics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ParametricDescriptor } from '@pascal-app/core'
import type { ParametricDescriptor } from '@pascal-app/editor'
import type { GrassNode } from './grass-schema'

/** Inspector for a placed grass tuft — rendered for free by the host's
Expand Down
Loading