@call-me-sensei/toonlab 0.4.19 → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/NPM-LIBRARY.md +230 -0
  2. package/README.md +78 -36
  3. package/agents/PROMPTS.md +91 -0
  4. package/agents/README.md +9 -14
  5. package/agents/claude/CLAUDE.md +8 -0
  6. package/agents/codex/AGENTS.md +161 -0
  7. package/agents/cursor/toonlab.mdc +37 -0
  8. package/agents/references/mcp-asset-discovery.md +12 -9
  9. package/agents/references/runtime-entry-points.md +5 -4
  10. package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
  11. package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
  12. package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
  13. package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
  14. package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
  15. package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
  16. package/cli/toonlab.mjs +3 -2
  17. package/database/fal-patina.mjs +131 -0
  18. package/database/generation-service.mjs +108 -7
  19. package/database/migrations/0007_creation_revisions.sql +80 -0
  20. package/database/migrations/0008_creation_revision_hardening.sql +98 -0
  21. package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
  22. package/database/migrations/0010_external_catalog_assets.sql +28 -0
  23. package/database/providers.mjs +195 -16
  24. package/database/repository.mjs +518 -70
  25. package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
  26. package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
  27. package/mcp/public-catalog.mjs +4 -52
  28. package/mcp/server.mjs +175 -12
  29. package/mcp/vite-plugin.mjs +78 -2
  30. package/package.json +32 -12
  31. package/scripts/generate-catalog-seed.mjs +34 -7
  32. package/src/asset-policy/catalogLicenses.js +64 -6
  33. package/src/assetlib/loadImported.js +15 -1
  34. package/src/catalog/officialCatalogAssetRuntime.js +13 -2
  35. package/src/catalog/officialCatalogLod.js +34 -1
  36. package/src/catalog/officialCatalogPlacement.js +29 -14
  37. package/src/character/animationRetarget.js +2 -0
  38. package/src/character/characterRig.js +6 -1
  39. package/src/character/characterRuntime.js +175 -7
  40. package/src/environment/environmentMaterialAdapter.js +30 -0
  41. package/src/environment/environmentPresets.js +90 -0
  42. package/src/environment/environmentSunShadowPass.js +42 -0
  43. package/src/environment/toonLabSurfaceLighting.js +108 -10
  44. package/src/environment/urbanPropMaterial.js +104 -2
  45. package/src/lighting/lightingSystem.js +65 -10
  46. package/src/renderer/index.js +7 -0
  47. package/src/renderer/styleComparison.js +985 -0
  48. package/src/rock-shader/index.js +5 -0
  49. package/src/rock-shader/rockGeometryDetail.js +701 -0
  50. package/src/rock-shader/rockMaterial.js +782 -39
  51. package/src/rock-shader/rockRegionRuntime.js +185 -0
  52. package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
  53. package/src/rock-shader/rockShaderRuntime.js +354 -67
  54. package/src/rock-shader/rockShaderSettings.js +246 -16
  55. package/src/rock-shader/rockTangentIntegrity.js +136 -0
  56. package/src/rock-shader/rockTextureIntegrity.js +115 -0
  57. package/src/rockgen/index.js +1 -0
  58. package/src/rockgen/rockDocument.js +27 -3
  59. package/src/rockgen/surface/c7GeologySurface.js +372 -0
  60. package/src/shaders-tsl/water.js +8 -0
  61. package/src/sky/cloudShadow.js +25 -1
  62. package/src/sky/skySystem.js +9 -0
  63. package/src/styles/index.js +23 -0
  64. package/src/styles/neutralStylePresets.js +712 -0
  65. package/src/styles/sceneStyleRuntime.js +51 -4
  66. package/src/styles/styleAdapters.js +13 -2
  67. package/src/styles/styleBundle.js +1 -1
  68. package/src/texgen/evaluateTexture.js +30 -3
  69. package/src/texgen/textureGenerators.js +63 -31
  70. package/src/texgen/textureSettings.js +25 -8
  71. package/src/toon/toonSettings.js +96 -2
  72. package/src/vegetation/branchTree.js +327 -42
  73. package/src/vegetation/index.js +24 -2
  74. package/src/vegetation/scatter.js +372 -1
  75. package/src/vegetation/stylizedTree.js +312 -126
  76. package/src/vegetation/stylizedTreeFoliage.js +229 -7
  77. package/src/vegetation/treeSurfaceTextures.js +328 -0
  78. package/src/version.js +1 -1
  79. package/src/water/waterSettings.js +15 -7
  80. package/types/asset-policy/catalogLicenses.d.ts +2 -0
  81. package/types/asset-policy/index.d.ts +2 -0
  82. package/types/assetlib/loadImported.d.ts +3 -1
  83. package/types/catalog/officialCatalogLod.d.ts +63 -5
  84. package/types/character/characterRig.d.ts +59 -0
  85. package/types/character/characterRuntime.d.ts +1 -0
  86. package/types/environment/environmentMaterialAdapter.d.ts +3 -1
  87. package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
  88. package/types/index.d.ts +89 -7
  89. package/types/lighting/lightingSystem.d.ts +7 -0
  90. package/types/react/index.d.ts +20 -0
  91. package/types/renderer/index.d.ts +1 -0
  92. package/types/renderer/styleComparison.d.ts +409 -0
  93. package/types/rock-shader/index.d.ts +5 -0
  94. package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
  95. package/types/rock-shader/rockMaterial.d.ts +3 -0
  96. package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
  97. package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
  98. package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
  99. package/types/rock-shader/rockShaderSettings.d.ts +19 -5
  100. package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
  101. package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
  102. package/types/rockgen/index.d.ts +1 -0
  103. package/types/rockgen/rockDocument.d.ts +29 -1
  104. package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
  105. package/types/sky/cloudShadow.d.ts +1 -0
  106. package/types/styles/index.d.ts +1 -0
  107. package/types/styles/neutralStylePresets.d.ts +944 -0
  108. package/types/styles/sceneStyleRuntime.d.ts +20 -0
  109. package/types/vegetation/branchTree.d.ts +4 -0
  110. package/types/vegetation/index.d.ts +9 -7
  111. package/types/vegetation/scatter.d.ts +1 -0
  112. package/types/vegetation/stylizedTree.d.ts +1 -2
  113. package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
  114. package/types/version.d.ts +1 -1
  115. package/agents/references/geology-playbook.md +0 -118
  116. package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
  117. package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
  118. package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
  119. package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
  120. package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
  121. package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
  122. package/src/vegetation/legacyTreePresets.js +0 -120
  123. package/types/vegetation/legacyTreePresets.d.ts +0 -22
@@ -1,16 +1,53 @@
1
1
  import * as THREE from 'three';
2
2
 
3
3
  import { StylizedTree } from './stylizedTree.js';
4
+ import { resolveTreeSurfaceProfileId } from './treeSurfaceTextures.js';
4
5
 
5
6
  export const BRANCH_TREE_DOCUMENT_TYPE = 'toonlab/branch-tree';
6
7
  export const BRANCH_TREE_DOCUMENT_VERSION = 1;
7
8
 
9
+ /**
10
+ * BranchTree's architecture identity, used by the LOD compiler to pick its
11
+ * triangle envelope. BranchTree is not a legacy aggregate-card tree: it hard-
12
+ * sets the recursive `branching` generator and carries a four-level scaffold,
13
+ * so it budgets like the other recursive woody engines rather than like the
14
+ * legacy default.
15
+ */
16
+ export const BRANCH_TREE_ARCHITECTURE = Object.freeze({
17
+ id: 'branch-tree',
18
+ engine: 'branch-tree',
19
+ version: 1,
20
+ });
21
+
8
22
  export const BRANCH_TREE_LEAF_SHAPES = Object.freeze([
9
23
  'teardrop',
10
24
  'round',
11
25
  'oak',
12
26
  'maple',
27
+ // Acer palmatum's deeply dissected seven-lobe leaf. 'maple' above is the
28
+ // blunt five-point sugar-maple star; the two are different species reads
29
+ // and a garden's hero maple needs the dissected one.
30
+ 'palmate',
13
31
  'gingko',
32
+ // Conifer organs. A needle-bearing BranchTree is not a contradiction: the
33
+ // architecture is a recursive woody skeleton with foliage at the tips,
34
+ // which is exactly how a cloud-pruned pine is built. What it must NOT do is
35
+ // set `skeleton.conifer`, which shortens children toward the leader and
36
+ // produces the excurrent cone a pruned garden pine is defined against.
37
+ 'needle',
38
+ 'needle-fascicle',
39
+ ]);
40
+
41
+ /**
42
+ * Canopy tuft architectures a BranchTree may select.
43
+ *
44
+ * `null` keeps StylizedTree's own choice, which is what every document
45
+ * authored before this field existed relies on.
46
+ */
47
+ export const BRANCH_TREE_CANOPY_ARCHITECTURES = Object.freeze([
48
+ 'cloud-cards',
49
+ 'layered-sprays',
50
+ 'needle-whorls',
14
51
  ]);
15
52
 
16
53
  export const DEFAULT_BRANCH_TREE_SETTINGS = Object.freeze({
@@ -23,25 +60,110 @@ export const DEFAULT_BRANCH_TREE_SETTINGS = Object.freeze({
23
60
  gnarliness: 0.16,
24
61
  lengthRatio: 0.48,
25
62
  levels: 3,
63
+ // Total branches the breadth-first skeleton may grow, and the ceiling on
64
+ // foliage attachments. null = the generator's historical 420 / 380.
65
+ //
66
+ // These are authorable because the fixed budget is a silent quality trap:
67
+ // the skeleton grows breadth-first, so at high `children` levels 1 and 2
68
+ // consume the whole allowance before the leaf-bearing level-3 twigs are
69
+ // ever queued and the tree ships nearly bare (D19-028 — measured 12
70
+ // children → 19 leaf cards). Raising the budget is also the only way to
71
+ // build a many-tipped structure such as a cloud-pruned pine, whose whole
72
+ // silhouette is the count and separation of its foliage pads.
73
+ maxAttachments: null,
74
+ maxBranches: null,
26
75
  radialSegments: 8,
27
76
  radiusRatio: 0.68,
28
77
  start: 0.34,
29
78
  }),
30
79
  trunk: Object.freeze({
31
80
  bend: 0.18,
81
+ // World heading of the trunk's bow, in radians. null = derived from the
82
+ // seed. Authoring it is what makes a coherent windswept stand possible:
83
+ // without it every seed leans a different way and a ridge of trees reads
84
+ // as damage rather than as wind.
85
+ bendDirection: null,
32
86
  color: Object.freeze([0.48, 0.29, 0.16]),
33
87
  gnarl: 0.18,
34
88
  height: 2.1,
35
89
  lean: 0.12,
90
+ // Lean heading relative to the bow, in radians. null = derived from the
91
+ // seed; 0 pins the lean along the bow, Math.PI pulls the top back against
92
+ // it for an S-curve.
93
+ leanOffset: null,
36
94
  radialSegments: 10,
37
95
  radiusBottom: 0.22,
38
96
  radiusTop: 0.075,
97
+ // Whether the crown's own shadow map falls on the tree's wood. false is
98
+ // the anime read StylizedTree already documents for pale-limbed trees,
99
+ // and it matters most on a tips-placement tree, where bare limb crosses
100
+ // the canopy in full view: with it on, those limbs render as near-black
101
+ // scribbles inside the crown at close camera.
102
+ receiveShadow: true,
39
103
  textureRef: null,
40
104
  twist: 0.08,
41
105
  }),
42
106
  leaves: Object.freeze({
43
107
  color: Object.freeze([0.19, 0.48, 0.22]),
44
108
  coverageScale: 1,
109
+ // How one tuft of foliage is built at a branch tip. Every field is
110
+ // nullable and every null forwards nothing, so a document authored before
111
+ // this block existed resolves to exactly the geometry it always did.
112
+ //
113
+ // This is the lever the canopy actually needs. `coverageScale` saturates
114
+ // at `clamp(size × coverageScale, 0.4)²` capped at 9, so on any tree
115
+ // larger than size 3 it is inert; `density` only scales an already-fixed
116
+ // per-tuft count; and `branches.children` buys cards by adding wood.
117
+ // The tuft itself was the one thing an author could not reach.
118
+ cluster: Object.freeze({
119
+ // 'cloud-cards' (default): cards fill a small sphere at the tip.
120
+ // 'layered-sprays': cards fill a flattened disc across the tip, which
121
+ // is how a cloud-pruned pad or a horizontally layered maple tier
122
+ // reads. 'needle-whorls': cards align their long axis to the twig,
123
+ // for needle-bearing branchlets.
124
+ architecture: null,
125
+ cards: null, // cards per tuft
126
+ // Bare every tuft below this fraction of the crown's own height. A
127
+ // skeleton that forks low enough to read multi-stemmed also hangs
128
+ // foliage down that low wood and buries the trunk it just built; in a
129
+ // garden the bare trunk is as much of the subject as the canopy. This
130
+ // lifts the foliage skirt without touching `branches.start`, so the
131
+ // limb structure keeps its low fork and only the leaves move.
132
+ crownBase: null,
133
+ // Multiplies the card count of every tuft that SURVIVES pruning, so a
134
+ // crown can be restructured at an unchanged card total rather than
135
+ // simply losing the foliage that pruning removed.
136
+ densityBoost: null,
137
+ // Keep only this many well-separated tufts and bare every other branch
138
+ // tip. This is what makes a cloud-pruned pine possible at all: a
139
+ // recursive skeleton hangs foliage on every terminal twig, and terminal
140
+ // twigs fill the crown evenly, so without pruning the crown is a dome
141
+ // no matter how each individual tuft is shaped.
142
+ pads: null,
143
+ // Horizontal plates: { count, thickness, jitter }. `pads` spaces
144
+ // masses isotropically, which is a pine's 3D scatter and is not a
145
+ // layered broadleaf: an Acer palmatum's signature is flat tiers with
146
+ // sky and limb showing BETWEEN them, and no isotropic count produces an
147
+ // anisotropic structure. This keeps tufts inside `count` height bands
148
+ // and bares the tufts in the gaps.
149
+ plates: null,
150
+ // 0..1, `layered-sprays` only. 0 builds each spray disc across the
151
+ // twig, so a twig rising at 50 deg carries a disc tilted at 50 deg. 1
152
+ // builds it flat with the GROUND, which is what makes a tier read as a
153
+ // tier rather than as a thicket of tilted saucers.
154
+ plateAlignment: null,
155
+ // Drops each plate's outer rim as a fraction of its own radius. A
156
+ // perfectly flat tier is a machined shelf — the Cornus controversa /
157
+ // acacia read — and this is what makes it hang instead.
158
+ plateDroop: null,
159
+ radius: null, // tuft radius, tree-local units
160
+ sizeRange: null, // [min, max] card size multiplier
161
+ sprayLayers: null, // layered-sprays: tiers stacked along the twig
162
+ spraySpread: null, // layered-sprays: pad radius across the twig
163
+ sprayThickness: null, // layered-sprays: pad depth along the twig
164
+ whorlArms: null, // needle-whorls: branchlets per whorl
165
+ whorlRadius: null, // needle-whorls: whorl radius
166
+ }),
45
167
  density: 1,
46
168
  palette: Object.freeze({}),
47
169
  shape: 'round',
@@ -87,6 +209,73 @@ function reference(value) {
87
209
  return typeof value === 'string' && value.trim() ? value.trim() : null;
88
210
  }
89
211
 
212
+ function nullableAngle(value, fallback) {
213
+ if (value === null) return null;
214
+ if (value === undefined) return fallback;
215
+ const number = Number(value);
216
+ return Number.isFinite(number)
217
+ ? THREE.MathUtils.euclideanModulo(number, Math.PI * 2)
218
+ : fallback;
219
+ }
220
+
221
+ function nullableFinite(value, minimum, maximum) {
222
+ if (value === null || value === undefined) return null;
223
+ const number = Number(value);
224
+ return Number.isFinite(number)
225
+ ? THREE.MathUtils.clamp(number, minimum, maximum)
226
+ : null;
227
+ }
228
+
229
+ function nullableInteger(value, minimum, maximum) {
230
+ const resolved = nullableFinite(value, minimum, maximum);
231
+ return resolved === null ? null : Math.round(resolved);
232
+ }
233
+
234
+ function nullableRange(value, minimum, maximum) {
235
+ if (!Array.isArray(value) || value.length < 2) return null;
236
+ const low = nullableFinite(value[0], minimum, maximum);
237
+ const high = nullableFinite(value[1], minimum, maximum);
238
+ if (low === null || high === null) return null;
239
+ return [Math.min(low, high), Math.max(low, high)];
240
+ }
241
+
242
+ // `leaves.cluster.plates`. A count below 2 is not a plate structure — one
243
+ // band is the whole crown — so it resolves to null rather than silently
244
+ // pruning the tree down to a single slab of foliage.
245
+ function leafPlates(value) {
246
+ if (value === null || value === undefined || typeof value !== 'object') return null;
247
+ const count = nullableInteger(value.count, 2, 24);
248
+ if (count === null) return null;
249
+ return {
250
+ count,
251
+ jitter: nullableFinite(value.jitter, 0, 1) ?? 0,
252
+ thickness: nullableFinite(value.thickness, 0.02, 1) ?? 0.5,
253
+ };
254
+ }
255
+
256
+ function leafCluster(value) {
257
+ const source = plainObject(value);
258
+ return {
259
+ architecture: BRANCH_TREE_CANOPY_ARCHITECTURES.includes(source.architecture)
260
+ ? source.architecture
261
+ : null,
262
+ cards: nullableInteger(source.cards, 1, 64),
263
+ crownBase: nullableFinite(source.crownBase, 0, 0.95),
264
+ densityBoost: nullableFinite(source.densityBoost, 0.1, 12),
265
+ pads: nullableInteger(source.pads, 1, 2000),
266
+ plates: leafPlates(source.plates),
267
+ plateAlignment: nullableFinite(source.plateAlignment, 0, 1),
268
+ plateDroop: nullableFinite(source.plateDroop, 0, 2),
269
+ radius: nullableFinite(source.radius, 0.01, 4),
270
+ sizeRange: nullableRange(source.sizeRange, 0.02, 6),
271
+ sprayLayers: nullableInteger(source.sprayLayers, 1, 12),
272
+ spraySpread: nullableFinite(source.spraySpread, 0.05, 4),
273
+ sprayThickness: nullableFinite(source.sprayThickness, 0, 2),
274
+ whorlArms: nullableInteger(source.whorlArms, 3, 24),
275
+ whorlRadius: nullableFinite(source.whorlRadius, 0.05, 3),
276
+ };
277
+ }
278
+
90
279
  function canopyPalette(value) {
91
280
  const source = plainObject(value);
92
281
  const result = {};
@@ -126,6 +315,8 @@ export function createBranchTreeSettings(options = {}) {
126
315
  gnarliness: finite(branches.gnarliness, defaults.branches.gnarliness, 0, 0.6),
127
316
  lengthRatio: finite(branches.lengthRatio, defaults.branches.lengthRatio, 0.15, 0.9),
128
317
  levels: integer(branches.levels, defaults.branches.levels, 1, 4),
318
+ maxAttachments: nullableInteger(branches.maxAttachments, 8, 4000),
319
+ maxBranches: nullableInteger(branches.maxBranches, 8, 6000),
129
320
  radialSegments: integer(
130
321
  branches.radialSegments,
131
322
  defaults.branches.radialSegments,
@@ -137,10 +328,12 @@ export function createBranchTreeSettings(options = {}) {
137
328
  },
138
329
  trunk: {
139
330
  bend: finite(trunk.bend, defaults.trunk.bend, -1.2, 1.2),
331
+ bendDirection: nullableAngle(trunk.bendDirection, defaults.trunk.bendDirection),
140
332
  color: color(trunk.color, defaults.trunk.color),
141
333
  gnarl: finite(trunk.gnarl, defaults.trunk.gnarl, 0, 1),
142
334
  height: finite(trunk.height, defaults.trunk.height, 0.2, 30),
143
335
  lean: finite(trunk.lean, defaults.trunk.lean, -1.2, 1.2),
336
+ leanOffset: nullableAngle(trunk.leanOffset, defaults.trunk.leanOffset),
144
337
  map: trunk.map?.isTexture ? trunk.map : null,
145
338
  radialSegments: integer(
146
339
  trunk.radialSegments,
@@ -150,11 +343,15 @@ export function createBranchTreeSettings(options = {}) {
150
343
  ),
151
344
  radiusBottom: finite(trunk.radiusBottom, defaults.trunk.radiusBottom, 0.01, 5),
152
345
  radiusTop: finite(trunk.radiusTop, defaults.trunk.radiusTop, 0.002, 3),
346
+ receiveShadow: trunk.receiveShadow === undefined
347
+ ? defaults.trunk.receiveShadow
348
+ : Boolean(trunk.receiveShadow),
153
349
  textureRef: reference(trunk.textureRef),
154
350
  twist: finite(trunk.twist, defaults.trunk.twist, -Math.PI * 4, Math.PI * 4),
155
351
  },
156
352
  leaves: {
157
353
  color: color(leaves.color, defaults.leaves.color),
354
+ cluster: leafCluster(leaves.cluster),
158
355
  coverageScale: finite(
159
356
  leaves.coverageScale,
160
357
  defaults.leaves.coverageScale,
@@ -226,6 +423,113 @@ export function parseBranchTreeDocument(input) {
226
423
  };
227
424
  }
228
425
 
426
+ /**
427
+ * The StylizedTree option object a BranchTree builds from its settings.
428
+ *
429
+ * Shared by the constructor and by createBranchTreeRecipe so the LOD compiler
430
+ * meshes exactly the tree the runtime renders. Runtime-only inputs (live
431
+ * textures, foliage/wind state, shader profile) are layered on by the caller.
432
+ */
433
+ export function branchTreeStylizedOptions(settingsInput = {}) {
434
+ const settings = settingsInput?.branches && settingsInput?.leaves && settingsInput?.trunk
435
+ ? settingsInput
436
+ : createBranchTreeSettings(settingsInput);
437
+ // Only forward what was actually authored. StylizedTree keeps its
438
+ // tips-placement and branching-generator canopy presets unless a caller
439
+ // hands it a value that differs from the module default, so forwarding a
440
+ // resolved default here would silently overwrite those presets and move
441
+ // every existing document's geometry.
442
+ const cluster = settings.leaves.cluster ?? {};
443
+ const canopyCluster = {};
444
+ const forward = (key, value) => {
445
+ if (value !== null && value !== undefined) canopyCluster[key] = value;
446
+ };
447
+ forward('architecture', cluster.architecture);
448
+ forward('cardsPerCluster', cluster.cards);
449
+ forward('padCount', cluster.pads);
450
+ forward('crownBase', cluster.crownBase);
451
+ forward('padDensityBoost', cluster.densityBoost);
452
+ forward('padLayers', cluster.plates);
453
+ forward('plateAlignment', cluster.plateAlignment);
454
+ forward('plateDroop', cluster.plateDroop);
455
+ forward('clusterRadius', cluster.radius);
456
+ forward('cardSizeRange', cluster.sizeRange);
457
+ forward('sprayLayers', cluster.sprayLayers);
458
+ forward('spraySpread', cluster.spraySpread);
459
+ forward('sprayThickness', cluster.sprayThickness);
460
+ forward('whorlArms', cluster.whorlArms);
461
+ forward('whorlRadius', cluster.whorlRadius);
462
+
463
+ const skeletonBudget = {};
464
+ if (settings.branches.maxBranches !== null) {
465
+ skeletonBudget.maxBranches = settings.branches.maxBranches;
466
+ }
467
+ if (settings.branches.maxAttachments !== null) {
468
+ skeletonBudget.maxAttachments = settings.branches.maxAttachments;
469
+ }
470
+ return {
471
+ tree: {
472
+ canopyColor: settings.leaves.color,
473
+ canopyPalette: settings.leaves.palette,
474
+ canopyDepth: 0.92,
475
+ canopyScale: 1,
476
+ canopyWidth: 1.18,
477
+ leafDensity: settings.leaves.density,
478
+ leafPlacement: 'tips',
479
+ seed: settings.seed,
480
+ size: settings.size,
481
+ trunkColor: settings.trunk.color,
482
+ trunkReceiveShadow: settings.trunk.receiveShadow,
483
+ },
484
+ trunk: {
485
+ bend: settings.trunk.bend,
486
+ bendDirection: settings.trunk.bendDirection,
487
+ gnarl: settings.trunk.gnarl,
488
+ height: settings.trunk.height,
489
+ lean: settings.trunk.lean,
490
+ leanOffset: settings.trunk.leanOffset,
491
+ radialSegments: settings.trunk.radialSegments,
492
+ radiusBottom: settings.trunk.radiusBottom,
493
+ radiusTop: settings.trunk.radiusTop,
494
+ twist: settings.trunk.twist,
495
+ },
496
+ skeleton: {
497
+ branchAngle: settings.branches.angle,
498
+ branchStart: settings.branches.start,
499
+ childrenCount: settings.branches.children,
500
+ forceStrength: settings.branches.forceStrength,
501
+ generator: 'branching',
502
+ gnarliness: settings.branches.gnarliness,
503
+ lengthRatio: settings.branches.lengthRatio,
504
+ levels: settings.branches.levels,
505
+ radialSegments: settings.branches.radialSegments,
506
+ radiusRatio: settings.branches.radiusRatio,
507
+ ...skeletonBudget,
508
+ },
509
+ canopy: {
510
+ // Physical tree size still participates so leaf cards retain a useful
511
+ // world-space density. The explicit factor lets an author tune that
512
+ // coverage independently instead of enlarging the whole tree.
513
+ coverageScale: settings.size * settings.leaves.coverageScale,
514
+ ...canopyCluster,
515
+ // BranchTree knows exactly which tuft fields the author supplied,
516
+ // because every unauthored one is null. Declaring them keeps a value
517
+ // that happens to equal a module default from being mistaken for
518
+ // "unset" and quietly replaced by the branching-generator preset.
519
+ explicit: Object.keys(canopyCluster),
520
+ },
521
+ leafShape: settings.leaves.map ? null : { preset: settings.leaves.shape },
522
+ roots: { preset: settings.roots },
523
+ // A trunk textureRef that names a registered bark profile (or one of its
524
+ // short aliases, e.g. 'beech') selects that profile. An unrecognized ref
525
+ // stays caller-owned provenance and falls through to the style default,
526
+ // so existing documents keep working unchanged.
527
+ trunkSurfaceProfile: settings.trunk.map
528
+ ? null
529
+ : resolveTreeSurfaceProfileId(settings.trunk.textureRef),
530
+ };
531
+ }
532
+
229
533
  /**
230
534
  * Stable first-party procedural broadleaf tree.
231
535
  *
@@ -238,52 +542,11 @@ export class BranchTree extends StylizedTree {
238
542
  const settings = createBranchTreeSettings(options);
239
543
  const source = plainObject(options);
240
544
  super({
241
- tree: {
242
- canopyColor: settings.leaves.color,
243
- canopyPalette: settings.leaves.palette,
244
- canopyDepth: 0.92,
245
- canopyScale: 1,
246
- canopyWidth: 1.18,
247
- leafDensity: settings.leaves.density,
248
- leafPlacement: 'tips',
249
- seed: settings.seed,
250
- size: settings.size,
251
- trunkColor: settings.trunk.color,
252
- },
253
- trunk: {
254
- bend: settings.trunk.bend,
255
- gnarl: settings.trunk.gnarl,
256
- height: settings.trunk.height,
257
- lean: settings.trunk.lean,
258
- radialSegments: settings.trunk.radialSegments,
259
- radiusBottom: settings.trunk.radiusBottom,
260
- radiusTop: settings.trunk.radiusTop,
261
- twist: settings.trunk.twist,
262
- },
263
- skeleton: {
264
- branchAngle: settings.branches.angle,
265
- branchStart: settings.branches.start,
266
- childrenCount: settings.branches.children,
267
- forceStrength: settings.branches.forceStrength,
268
- generator: 'branching',
269
- gnarliness: settings.branches.gnarliness,
270
- lengthRatio: settings.branches.lengthRatio,
271
- levels: settings.branches.levels,
272
- radialSegments: settings.branches.radialSegments,
273
- radiusRatio: settings.branches.radiusRatio,
274
- },
545
+ ...branchTreeStylizedOptions(settings),
275
546
  foliage: {
276
547
  ...plainObject(source.foliage),
277
548
  ...(settings.leaves.map ? { leafMap: settings.leaves.map } : {}),
278
549
  },
279
- canopy: {
280
- // Physical tree size still participates so leaf cards retain a useful
281
- // world-space density. The explicit factor lets an author tune that
282
- // coverage independently instead of enlarging the whole tree.
283
- coverageScale: settings.size * settings.leaves.coverageScale,
284
- },
285
- leafShape: settings.leaves.map ? null : { preset: settings.leaves.shape },
286
- roots: { preset: settings.roots },
287
550
  styleTarget: source.styleTarget ?? {},
288
551
  trunkMap: settings.trunk.map,
289
552
  vegetationShader: source.vegetationShader ?? null,
@@ -297,6 +560,28 @@ export class BranchTree extends StylizedTree {
297
560
  }
298
561
  }
299
562
 
563
+ /**
564
+ * Wrap BranchTree settings in the tree-recipe envelope the LOD compiler
565
+ * consumes, tagged with BranchTree's own architecture so it is budgeted
566
+ * against the BranchTree triangle envelope rather than the legacy default.
567
+ *
568
+ * The portable BranchTree document stays the asset's identity; this is the
569
+ * adapter that lets `compileTreeLodLevels` build its three LODs from it.
570
+ */
571
+ export function createBranchTreeRecipe(options = {}, { id = null, label = null } = {}) {
572
+ const settings = createBranchTreeSettings(options);
573
+ const recipe = {
574
+ schema: 'treeRecipe',
575
+ version: 3,
576
+ type: 'tree',
577
+ architecture: { ...BRANCH_TREE_ARCHITECTURE },
578
+ options: branchTreeStylizedOptions(settings),
579
+ };
580
+ if (id) recipe.id = id;
581
+ if (label) recipe.label = label;
582
+ return recipe;
583
+ }
584
+
300
585
  export function createBranchTree(options = {}) {
301
586
  return new BranchTree(options);
302
587
  }
@@ -4,9 +4,29 @@ export * from './grassClump.js';
4
4
  export * from './callMeSenseiGrass.js';
5
5
  export * from './stylizedFlowers.js';
6
6
  export * from './flowerSpecies.js';
7
- export * from './stylizedTree.js';
7
+ // Public tree construction/runtime surface. Authored tree recipes are assets
8
+ // and are discovered through ToonLab MCP; showcase/example recipe arrays are
9
+ // deliberately not re-exported from the npm package.
10
+ export {
11
+ DEFAULT_STYLIZED_TREE_SETTINGS,
12
+ STYLIZED_TREE_SETTING_FIELD_SCHEMA,
13
+ STYLIZED_TREE_SETTING_GROUPS,
14
+ TREE_RECIPE_SCHEMA,
15
+ TREE_RECIPE_VERSION,
16
+ TREE_TRUNK_STYLES,
17
+ StylizedTree,
18
+ createBranchTubeGeometry,
19
+ createBranchingTreeSkeleton,
20
+ createStylizedTreeSettings,
21
+ createTreeSkeleton,
22
+ createTreeTrunkGeometry,
23
+ getStylizedTreePresetOptions,
24
+ polarProfileFromOutline,
25
+ registerStylizedTreePreset,
26
+ resolvePadPruning,
27
+ serializableTreeOptions,
28
+ } from './stylizedTree.js';
8
29
  export * from './branchTree.js';
9
- export * from './legacyTreePresets.js';
10
30
  export * from './stylizedTreeFoliage.js';
11
31
  export * from './stylizedBush.js';
12
32
  export * from './stylizedFlower.js';
@@ -19,10 +39,12 @@ export * from './contactShadowField.js';
19
39
  export * from './vegetationShaders.js';
20
40
  export {
21
41
  TREE_SURFACE_PROFILES,
42
+ TREE_SURFACE_PROFILE_ALIASES,
22
43
  TREE_SURFACE_PROFILE_DEFAULTS,
23
44
  createTreeSurfaceTexture,
24
45
  createTreeSurfaceTextureData,
25
46
  getTreeSurfaceProfileOptions,
47
+ resolveTreeSurfaceProfileId,
26
48
  } from './treeSurfaceTextures.js';
27
49
  export * from './importedVegetationMaterial.js';
28
50
  export * from './grassPalettes.js';