@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
@@ -98,6 +98,187 @@ function seededRandom(seed) {
98
98
  };
99
99
  }
100
100
 
101
+ /**
102
+ * Canopy pruning — decide which branch tips carry foliage and which are bared.
103
+ *
104
+ * A recursive skeleton puts a tuft on every terminal twig, and terminal twigs
105
+ * fill the crown volume evenly. The result is always a dome, however the
106
+ * individual tufts are shaped: a hundred small masses packed shoulder to
107
+ * shoulder is a dome, and forty larger ones is a bigger dome. That is correct
108
+ * for a broadleaf and it is wrong for anything whose identity is DISCRETE
109
+ * foliage masses with sky and bare limb between them.
110
+ *
111
+ * Three independent policies, applied in this order. Each is off by default,
112
+ * so a caller that passes nothing — or a bare `padCount`, the original
113
+ * signature — gets exactly the geometry it always did.
114
+ *
115
+ * crownBase Bare every tip below this fraction of the crown's own
116
+ * vertical extent. A skeleton that branches low enough to read
117
+ * multi-stemmed also hangs foliage all the way down that low
118
+ * wood and buries the trunk it just built; on a garden tree the
119
+ * trunk is half the subject. This lifts the skirt WITHOUT
120
+ * raising `branches.start`, so the limb structure keeps its low
121
+ * fork and only the foliage moves.
122
+ *
123
+ * layers Horizontal PLATES: keep tips inside `count` bands across the
124
+ * crown height and bare the tips between them. Pad pruning
125
+ * (below) spaces masses isotropically, which is right for a
126
+ * cloud-pruned pine — a pine's pads are a 3D scatter. It is
127
+ * wrong for the layered broadleaves, whose signature is flat
128
+ * tiers with sky and limb showing BETWEEN them: an Acer
129
+ * palmatum, a dogwood, a Japanese hornbeam. Isotropic spacing
130
+ * can never produce that structure at any count, because the
131
+ * structure is anisotropic. Bands are placed edge to edge
132
+ * across the crown (the lowest sits at the crown base, the
133
+ * highest at the apex) so pruning never decapitates the tree.
134
+ *
135
+ * padCount Keep this many well-separated survivors by farthest-point
136
+ * sampling, seeded from the topmost tip, so kept pads spread
137
+ * over the whole crown instead of clumping. This is what makes
138
+ * a cloud-pruned pine possible at all.
139
+ *
140
+ * Every policy is a pure function of the skeleton: same tree, same pruning,
141
+ * every build. Bared attachments use the existing `densityScale: 0` override,
142
+ * which is already the documented way to strip a branch, so nothing new is
143
+ * needed downstream. `densityBoost` multiplies the survivors' card counts, so
144
+ * a crown can be restructured into plates at an unchanged card total instead
145
+ * of simply losing the foliage the pruning removed.
146
+ *
147
+ * A caller's own override for an attachment always wins: pruning is a layout
148
+ * policy, not an authority over explicit per-branch authoring.
149
+ */
150
+ export function resolvePadPruning(attachments, options = null, existingOverrides = null) {
151
+ const total = Array.isArray(attachments) ? attachments.length : 0;
152
+ if (!total) return existingOverrides;
153
+
154
+ // The original signature took the pad count positionally.
155
+ const config = (options && typeof options === 'object') ? options : { padCount: options };
156
+ const padCount = Number.isFinite(config.padCount) ? Math.round(config.padCount) : 0;
157
+ const crownBase = Number.isFinite(config.crownBase)
158
+ ? THREE.MathUtils.clamp(config.crownBase, 0, 0.95)
159
+ : 0;
160
+ const layers = config.layers && typeof config.layers === 'object' ? config.layers : null;
161
+ const layerCount = layers && Number.isFinite(layers.count) ? Math.round(layers.count) : 0;
162
+ const layerThickness = layers && Number.isFinite(layers.thickness)
163
+ ? THREE.MathUtils.clamp(layers.thickness, 0.02, 1)
164
+ : 0.5;
165
+ const layerJitter = layers && Number.isFinite(layers.jitter)
166
+ ? THREE.MathUtils.clamp(layers.jitter, 0, 1)
167
+ : 0;
168
+ const densityBoost = Number.isFinite(config.densityBoost)
169
+ ? THREE.MathUtils.clamp(config.densityBoost, 0.1, 12)
170
+ : 1;
171
+ const hasHeightPolicy = crownBase > 0 || layerCount >= 2;
172
+ const hasPadPolicy = padCount > 0 && padCount < total;
173
+ if (!hasHeightPolicy && !hasPadPolicy && densityBoost === 1) return existingOverrides;
174
+
175
+ let alive = new Array(total);
176
+ for (let i = 0; i < total; i += 1) alive[i] = true;
177
+
178
+ if (hasHeightPolicy) {
179
+ let minY = Infinity;
180
+ let maxY = -Infinity;
181
+ for (let i = 0; i < total; i += 1) {
182
+ const { y } = attachments[i].position;
183
+ if (y < minY) minY = y;
184
+ if (y > maxY) maxY = y;
185
+ }
186
+ const span = Math.max(maxY - minY, 1e-4);
187
+ // Crown height is measured on the attachments themselves, not on the
188
+ // trunk anchor: it is the foliage envelope that is being restructured,
189
+ // and a leaning or gnarled trunk makes anchor-relative height a
190
+ // different quantity per variant.
191
+ const heights = new Float64Array(total);
192
+ for (let i = 0; i < total; i += 1) heights[i] = (attachments[i].position.y - minY) / span;
193
+
194
+ if (crownBase > 0) {
195
+ for (let i = 0; i < total; i += 1) if (heights[i] < crownBase) alive[i] = false;
196
+ }
197
+
198
+ if (layerCount >= 2) {
199
+ // Re-normalize above the lifted skirt so the plates divide the foliage
200
+ // that actually survives rather than the empty band below it.
201
+ const base = crownBase > 0 ? crownBase : 0;
202
+ const usable = Math.max(1 - base, 1e-4);
203
+ const pitch = 1 / (layerCount - 1);
204
+ const halfWidth = (layerThickness * pitch) / 2;
205
+ const centres = new Float64Array(layerCount);
206
+ for (let k = 0; k < layerCount; k += 1) {
207
+ // Deterministic per-plate wobble: real tiers are not a comb.
208
+ const wobble = layerJitter
209
+ ? (Math.sin((k + 1) * 12.9898) * 43758.5453 % 1 + 1) % 1 - 0.5
210
+ : 0;
211
+ centres[k] = k * pitch + wobble * layerJitter * pitch;
212
+ }
213
+ for (let i = 0; i < total; i += 1) {
214
+ if (!alive[i]) continue;
215
+ const t = (heights[i] - base) / usable;
216
+ let inside = false;
217
+ for (let k = 0; k < layerCount; k += 1) {
218
+ if (Math.abs(t - centres[k]) <= halfWidth) { inside = true; break; }
219
+ }
220
+ if (!inside) alive[i] = false;
221
+ }
222
+ // A band-pass that keeps nothing is a bug, not a style. Fall back to
223
+ // the un-plated crown rather than shipping an invisible tree.
224
+ if (!alive.some(Boolean)) alive = new Array(total).fill(true);
225
+ }
226
+ }
227
+
228
+ if (hasPadPolicy) {
229
+ const candidates = [];
230
+ for (let i = 0; i < total; i += 1) if (alive[i]) candidates.push(i);
231
+ if (candidates.length > padCount) {
232
+ // Seed from the highest surviving tip: a garden tree's uppermost mass
233
+ // is the one a viewer registers first, so it is never the one pruned.
234
+ let firstSlot = 0;
235
+ for (let s = 1; s < candidates.length; s += 1) {
236
+ if (attachments[candidates[s]].position.y > attachments[candidates[firstSlot]].position.y) {
237
+ firstSlot = s;
238
+ }
239
+ }
240
+ const chosen = [firstSlot];
241
+ const nearest = new Float64Array(candidates.length);
242
+ for (let s = 0; s < candidates.length; s += 1) {
243
+ nearest[s] = attachments[candidates[s]].position
244
+ .distanceToSquared(attachments[candidates[firstSlot]].position);
245
+ }
246
+ nearest[firstSlot] = -1;
247
+ while (chosen.length < padCount) {
248
+ let best = -1;
249
+ let bestDistance = -1;
250
+ for (let s = 0; s < candidates.length; s += 1) {
251
+ if (nearest[s] > bestDistance) { bestDistance = nearest[s]; best = s; }
252
+ }
253
+ if (best < 0 || bestDistance <= 0) break;
254
+ chosen.push(best);
255
+ nearest[best] = -1;
256
+ for (let s = 0; s < candidates.length; s += 1) {
257
+ if (nearest[s] < 0) continue;
258
+ const distance = attachments[candidates[s]].position
259
+ .distanceToSquared(attachments[candidates[best]].position);
260
+ if (distance < nearest[s]) nearest[s] = distance;
261
+ }
262
+ }
263
+ const keptSlots = new Set(chosen);
264
+ for (let s = 0; s < candidates.length; s += 1) {
265
+ if (!keptSlots.has(s)) alive[candidates[s]] = false;
266
+ }
267
+ }
268
+ }
269
+
270
+ const overrides = { ...(existingOverrides ?? {}) };
271
+ for (let i = 0; i < total; i += 1) {
272
+ if (overrides[i]) continue;
273
+ if (!alive[i]) {
274
+ overrides[i] = { densityScale: 0, cardsPerCluster: 0 };
275
+ } else if (densityBoost !== 1) {
276
+ overrides[i] = { densityScale: densityBoost };
277
+ }
278
+ }
279
+ return overrides;
280
+ }
281
+
101
282
  // Curved trunk along a seeded 3D spine. Returns { geometry, canopyAnchor }:
102
283
  // the anchor is the spine's top, so the crown follows the trunk's lean.
103
284
  //
@@ -758,6 +939,12 @@ export function createBranchingTreeSkeleton({
758
939
  twist = 0,
759
940
  gnarl = 0,
760
941
  lean = 0,
942
+ // World heading of the bow, and the lean heading relative to it — the same
943
+ // meaning they carry on the space-colonization generator. Without them a
944
+ // stand of wind-shaped trees leans a different way per seed, which reads
945
+ // as damage rather than as wind. null keeps the historical seeded pick.
946
+ bendDirection = null,
947
+ leanOffset = null,
761
948
  } = trunk;
762
949
  const rand = seededRandom(seed * 4.87 + 2.3);
763
950
  let randKey = 0;
@@ -807,11 +994,19 @@ export function createBranchingTreeSkeleton({
807
994
  const workVector = new THREE.Vector3();
808
995
  const workAxis = new THREE.Vector3();
809
996
  const workQuaternion = new THREE.Quaternion();
810
- const trunkBendHeading = seededRandom(seed * 9.17 + 4.3)(17) * Math.PI * 2;
997
+ // The bow's world heading. Tilting about a horizontal axis displaces the
998
+ // trunk 90° away from that axis, so an authored heading is stored rotated
999
+ // back by a quarter turn — that way `bendDirection` means "the direction the
1000
+ // trunk bows toward", identical to the space-colonization generator.
1001
+ const seededBendHeading = seededRandom(seed * 9.17 + 4.3)(17) * Math.PI * 2;
1002
+ const trunkBowHeading = bendDirection == null
1003
+ ? seededBendHeading + Math.PI / 2
1004
+ : bendDirection;
1005
+ const trunkBendAxisHeading = trunkBowHeading - Math.PI / 2;
811
1006
  const trunkBendAxis = new THREE.Vector3(
812
- Math.cos(trunkBendHeading),
1007
+ Math.cos(trunkBendAxisHeading),
813
1008
  0,
814
- Math.sin(trunkBendHeading),
1009
+ Math.sin(trunkBendAxisHeading),
815
1010
  );
816
1011
 
817
1012
  // { origin, quaternion, length, radius, level, segments } — BFS like EZ.
@@ -1069,9 +1264,18 @@ export function createBranchingTreeSkeleton({
1069
1264
  // Trunk: optional initial lean carries the classic trunk styles over.
1070
1265
  const rootQuaternion = new THREE.Quaternion();
1071
1266
  if (!spinePoints && lean) {
1072
- const heading = next() * Math.PI * 2;
1267
+ // The seeded draw is consumed either way, so pinning the lean direction
1268
+ // never shifts the rest of this tree's random stream and every existing
1269
+ // seed keeps its exact geometry.
1270
+ const seededLeanHeading = next() * Math.PI * 2;
1271
+ const leanHeading = leanOffset == null
1272
+ ? seededLeanHeading + Math.PI / 2
1273
+ : trunkBowHeading + leanOffset;
1274
+ const leanAxisHeading = leanHeading - Math.PI / 2;
1073
1275
  rootQuaternion.setFromAxisAngle(
1074
- new THREE.Vector3(Math.cos(heading), 0, Math.sin(heading)), lean * 0.45);
1276
+ new THREE.Vector3(Math.cos(leanAxisHeading), 0, Math.sin(leanAxisHeading)),
1277
+ lean * 0.45,
1278
+ );
1075
1279
  }
1076
1280
  queue.push({
1077
1281
  origin: new THREE.Vector3(0, 0, 0),
@@ -1130,88 +1334,6 @@ export const TREE_TRUNK_STYLES = Object.freeze({
1130
1334
  height: 1.8, radiusBottom: 0.24 },
1131
1335
  });
1132
1336
 
1133
- // Ready-made example recipes, ordered least → most complex configuration.
1134
- // Used by Tree Lab and the playground scene's showcase row; each is a
1135
- // complete options object for `new StylizedTree(...)`.
1136
- export const STYLIZED_TREE_EXAMPLES = Object.freeze([
1137
- // 1. Baseline: straight trunk, default crown, one flat color.
1138
- { seed: 3, size: 1.7, canopyColor: 0x4da258, leafDensity: 1,
1139
- trunk: TREE_TRUNK_STYLES.straight },
1140
- // 2. Leaning trunk, same simple crown.
1141
- { seed: 8, size: 1.8, canopyColor: 0x54a85e, leafDensity: 1,
1142
- trunk: TREE_TRUNK_STYLES.leaning },
1143
- // 3. Slight see-through: gap pockets open, branches peek through.
1144
- { seed: 5, size: 1.9, canopyColor: 0x5eb063, leafDensity: 0.85,
1145
- trunk: TREE_TRUNK_STYLES.leaning },
1146
- // 4. Curved trunk + its own irregular crown layout.
1147
- { seed: 11, size: 2.0, canopyColor: 0x58ab5c, leafDensity: 0.95,
1148
- trunk: TREE_TRUNK_STYLES.curved },
1149
- // 5. Color picked from a list, per-seed (forest variation from one spec).
1150
- { seed: 17, size: 2.0, canopyColor: [0x4da258, 0x7fb84e, 0x9cbf46], leafDensity: 0.95,
1151
- trunk: TREE_TRUNK_STYLES.curved },
1152
- // 6. Wide-and-shallow crown (X reach 1.6, Z reach 0.7).
1153
- { seed: 9, size: 2.0, canopyColor: 0x6db54f, leafDensity: 0.95,
1154
- canopyWidth: 1.6, canopyDepth: 0.7, trunk: TREE_TRUNK_STYLES.leaning },
1155
- // 7. Autumn blend: seeded mix between two colors.
1156
- { seed: 21, size: 2.1, canopyColor: { from: 0xe8a33c, to: 0xd96f29 }, leafDensity: 0.9,
1157
- trunk: TREE_TRUNK_STYLES.curved },
1158
- // 8. Gnarled old tree: knotted growth, sparser crown shows the wood.
1159
- { seed: 14, size: 2.0, canopyColor: 0x8f9e44, leafDensity: 0.72,
1160
- trunk: TREE_TRUNK_STYLES.gnarled },
1161
- // 9. Bonsai: corkscrew twist, flat wide pads, HSL-range blossom color.
1162
- { seed: 26, size: 1.7, leafDensity: 0.8, canopyWidth: 1.4, canopyDepth: 1.2,
1163
- canopyColor: { hue: [0.9, 1.0], saturation: [0.45, 0.6], lightness: [0.62, 0.72] },
1164
- trunk: TREE_TRUNK_STYLES.bonsai },
1165
- // 10. The Liyue golden gingko: fat-based serpentine trunk (bow right,
1166
- // top swept hard left, S locked by leanOffset), extra-wide crown,
1167
- // pinned pale-gold highlight tone. bendDirection 0 keeps the S in the
1168
- // X-Y plane so the silhouette reads head-on in the showcase row.
1169
- { seed: 12, size: 2.4, canopyColor: 0xf5c531, canopyPalette: { crown: 0xffe98a },
1170
- leafDensity: 0.95, canopyWidth: 1.5,
1171
- skeleton: { radialSegments: 10 },
1172
- trunk: { ...TREE_TRUNK_STYLES.swooping, bend: 0.5, lean: 0.95,
1173
- bendDirection: 0, height: 2.0, radiusBottom: 0.28 } },
1174
- // 11. Sumeru-style: long bare pale limbs reaching out of the crown with
1175
- // violet leaf bushes only at the branch ends (leafPlacement 'tips').
1176
- { seed: 31, size: 2.3, pale: true, canopyColor: 0x8578e6,
1177
- canopyPalette: { crown: 0xbdb2ff },
1178
- leafDensity: 0.9, canopyWidth: 1.45, leafPlacement: 'tips',
1179
- trunkReceiveShadow: false,
1180
- skeleton: { attractionCount: 70, influenceRadius: 1.35 },
1181
- trunk: { ...TREE_TRUNK_STYLES.curved, bend: 0.3, lean: 0.35, gnarl: 0.45,
1182
- height: 1.9, radiusBottom: 0.26 } },
1183
- // 12. MASSIVE climbable Sumeru tree: thick bare limbs long and low enough
1184
- // to stand or sit on (scenes read `climbable: true` and collide the
1185
- // wood as a trimesh instead of a trunk capsule), sparse skeleton so
1186
- // the pale limbs stay on show, foliage clouds only at the limb ends.
1187
- { seed: 46, size: 4.0, pale: true, climbable: true,
1188
- canopyColor: 0x8578e6, canopyPalette: { crown: 0xbdb2ff },
1189
- leafDensity: 0.92, canopyWidth: 1.75, canopyDepth: 1.2,
1190
- leafPlacement: 'tips', trunkReceiveShadow: false,
1191
- skeleton: { attractionCount: 55, influenceRadius: 1.7, killRadius: 0.55,
1192
- segmentLength: 0.36, attractionReach: 0.95, radialSegments: 14,
1193
- tipRadius: 0.05, minLimbRadius: 0.04, maxNodes: 130 },
1194
- canopy: { cardsPerCluster: 12, clusterRadius: 0.62 },
1195
- trunk: { ...TREE_TRUNK_STYLES.leaning, bend: 0.24, lean: 0.42,
1196
- height: 1.4, radiusBottom: 0.48 } },
1197
- ]);
1198
-
1199
- // Centered X offsets for a showcase row: cumulative spacing from each tree's
1200
- // approximate crown footprint, so a massive example doesn't swallow its
1201
- // neighbors the way fixed spacing would.
1202
- export function layoutTreeRow(configs, { margin = 1.6 } = {}) {
1203
- const footprints = configs.map((config) =>
1204
- (config.size ?? 1) * (config.canopyWidth ?? 1) * 2.3 + 1.4);
1205
- const offsets = [];
1206
- let cursor = 0;
1207
- footprints.forEach((footprint, index) => {
1208
- if (index > 0) cursor += (footprints[index - 1] + footprint) / 2 + margin;
1209
- offsets.push(cursor);
1210
- });
1211
- const center = cursor / 2;
1212
- return offsets.map((offset) => offset - center);
1213
- }
1214
-
1215
1337
  // Recipe documents: a plant serialized as { schema, version, type, options }.
1216
1338
  // The options are exactly what the constructor takes, so a recipe rebuilds
1217
1339
  // the identical plant (generation is deterministic per seed). Defined here —
@@ -1289,6 +1411,24 @@ function booleanOption(value, fallback) {
1289
1411
  return value === undefined ? fallback : Boolean(value);
1290
1412
  }
1291
1413
 
1414
+ // `canopy.padLayers` — the horizontal-plate band-pass. A count below 2 is not
1415
+ // a plate structure (one band is the whole crown), so it resolves to null
1416
+ // rather than silently pruning to a single slab.
1417
+ function padLayerOption(value, fallback) {
1418
+ if (value === undefined) return fallback;
1419
+ if (value === null || typeof value !== 'object') return null;
1420
+ const count = integerNumber(value.count, 0, { min: 0, max: 24 });
1421
+ if (count < 2) return null;
1422
+ return Object.freeze({
1423
+ count,
1424
+ // Fraction of each band's pitch that carries foliage; the rest is the
1425
+ // gap a viewer sees sky and limb through.
1426
+ thickness: finiteNumber(value.thickness, 0.5, { min: 0.02, max: 1 }),
1427
+ // Deterministic per-band height wobble, as a fraction of the pitch.
1428
+ jitter: finiteNumber(value.jitter, 0, { min: 0, max: 1 }),
1429
+ });
1430
+ }
1431
+
1292
1432
  function colorArray(value, fallback) {
1293
1433
  if (value?.isColor) return value.clone().convertLinearToSRGB().toArray();
1294
1434
  if (Array.isArray(value) && value.length >= 3) {
@@ -1398,6 +1538,29 @@ export const DEFAULT_STYLIZED_TREE_SETTINGS = Object.freeze({
1398
1538
  clusterRadius: 0.48,
1399
1539
  frondCount: 7,
1400
1540
  frondLength: 1.25,
1541
+ // Bare every branch tip below this fraction of the crown's own vertical
1542
+ // extent, lifting the foliage skirt so the trunk and the lower limb
1543
+ // structure read as form. 0 = no lift, which is every historical tree.
1544
+ crownBase: 0,
1545
+ // Keep only this many well-separated foliage attachments and bare the
1546
+ // rest, so the crown reads as discrete pads with visible limb between
1547
+ // them. null = no pruning, which is every historical tree.
1548
+ padCount: null,
1549
+ // Multiplies the card count of every tip that SURVIVES pruning, so a
1550
+ // crown can be restructured into plates or pads at an unchanged card
1551
+ // total instead of simply losing the foliage the pruning removed.
1552
+ padDensityBoost: 1,
1553
+ // Horizontal plates: { count, thickness, jitter }. Keeps tips inside
1554
+ // `count` bands across the crown height and bares the tips between them,
1555
+ // which is the structure of a layered broadleaf and the one thing
1556
+ // isotropic pad pruning cannot produce. null = no plating.
1557
+ padLayers: null,
1558
+ // 0..1, layered-sprays only. Rotates each tuft's spray disc from
1559
+ // across-the-twig (0) to world-horizontal (1). See stylizedTreeFoliage.
1560
+ plateAlignment: 0,
1561
+ // Drops the outer edge of each spray disc as a fraction of its radius, so
1562
+ // a plate sags rather than reading as a machined shelf. 0 = flat.
1563
+ plateDroop: 0,
1401
1564
  shellFill: true,
1402
1565
  sprayLayers: 3,
1403
1566
  spraySpread: 0.8,
@@ -1422,37 +1585,15 @@ export const DEFAULT_STYLIZED_TREE_SETTINGS = Object.freeze({
1422
1585
  }),
1423
1586
  });
1424
1587
 
1425
- // Named tree presets (grouped partial settings): 'default' is the baseline;
1426
- // 'call_me_sensei' is the studio-managed signature look, curated and updated
1427
- // over releases. Community presets register alongside them via
1428
- // registerStylizedTreePreset().
1588
+ // Named runtime settings: npm ships only the neutral baseline. Authored tree
1589
+ // recipes live in the public Gallery and may be registered explicitly after
1590
+ // MCP discovery when an application wants name-based selection.
1429
1591
  const stylizedTreePresetRegistry = new Map([
1430
1592
  ['default', Object.freeze({
1431
1593
  description: 'Baseline stylized tree.',
1432
1594
  label: 'Default',
1433
1595
  settings: Object.freeze({}),
1434
1596
  })],
1435
- ['call_me_sensei', Object.freeze({
1436
- description: 'Studio-managed signature tree, curated by Call Me Sensei and updated over releases. Open broadleaf with visible branching: leaf clusters sit at the branch tips instead of filling a solid shell, so the limb structure reads through the crown.',
1437
- label: 'Call Me Sensei',
1438
- settings: Object.freeze({
1439
- skeleton: Object.freeze({ attractionCount: 55, influenceRadius: 1.35 }),
1440
- tree: Object.freeze({
1441
- canopyWidth: 1.35,
1442
- leafDensity: 1.15,
1443
- leafPlacement: 'tips',
1444
- trunkColor: Object.freeze([0.58, 0.36, 0.2]),
1445
- }),
1446
- trunk: Object.freeze({
1447
- ...TREE_TRUNK_STYLES.curved,
1448
- bend: 0.24,
1449
- gnarl: 0.22,
1450
- height: 1.9,
1451
- lean: 0.24,
1452
- radiusBottom: 0.2,
1453
- }),
1454
- }),
1455
- })],
1456
1597
  ]);
1457
1598
 
1458
1599
  /**
@@ -1592,6 +1733,12 @@ export function createStylizedTreeSettings(options = {}) {
1592
1733
  clusterRadius: finiteNumber(canopySource.clusterRadius, base.canopy.clusterRadius, { min: 0.01 }),
1593
1734
  frondCount: integerNumber(canopySource.frondCount, base.canopy.frondCount, { min: 3, max: 24 }),
1594
1735
  frondLength: finiteNumber(canopySource.frondLength, base.canopy.frondLength, { min: 0.1, max: 4 }),
1736
+ crownBase: finiteNumber(canopySource.crownBase, base.canopy.crownBase, { min: 0, max: 0.95 }),
1737
+ padCount: nullableNumber(canopySource.padCount, base.canopy.padCount, { min: 1, max: 2000 }),
1738
+ padDensityBoost: finiteNumber(canopySource.padDensityBoost, base.canopy.padDensityBoost, { min: 0.1, max: 12 }),
1739
+ padLayers: padLayerOption(canopySource.padLayers, base.canopy.padLayers),
1740
+ plateAlignment: finiteNumber(canopySource.plateAlignment, base.canopy.plateAlignment, { min: 0, max: 1 }),
1741
+ plateDroop: finiteNumber(canopySource.plateDroop, base.canopy.plateDroop, { min: 0, max: 2 }),
1595
1742
  shellFill: booleanOption(canopySource.shellFill, base.canopy.shellFill),
1596
1743
  sprayLayers: integerNumber(canopySource.sprayLayers, base.canopy.sprayLayers, { min: 1, max: 12 }),
1597
1744
  spraySpread: finiteNumber(canopySource.spraySpread, base.canopy.spraySpread, { min: 0.05, max: 4 }),
@@ -2469,16 +2616,25 @@ export class StylizedTree extends THREE.Group {
2469
2616
  baseZ + Math.sin(bend) * (baseRadius * 0.2 + length * t),
2470
2617
  ];
2471
2618
  };
2619
+ // The tip has to end UNDER the surface. The historical profile dived
2620
+ // to a fixed -0.18 however far the root ran, so every preset beyond a
2621
+ // stub laid a long tapered spike flat on the ground in full view —
2622
+ // this is why `medium` and `large` read as black spider legs around
2623
+ // the collar in any frame low enough to see the base, and it lands
2624
+ // squarely on the Gate 1 ground-contact shot. Diving in proportion to
2625
+ // the root's own reach keeps the collar flare on show and puts the
2626
+ // rest of the root where a root belongs.
2627
+ const dive = Math.max(0.18, length * 0.44);
2472
2628
  const tube = createBranchTubeGeometry({
2473
2629
  flareBase: false,
2474
2630
  irregularity: 0.2,
2475
2631
  points: [
2476
2632
  pointAt(0, collarY),
2477
2633
  pointAt(0.2, collarY * 0.45),
2478
- pointAt(0.4, 0.0),
2479
- pointAt(0.6, -0.03),
2480
- pointAt(0.8, -0.07),
2481
- pointAt(1, -0.18),
2634
+ pointAt(0.4, -dive * 0.1),
2635
+ pointAt(0.6, -dive * 0.3),
2636
+ pointAt(0.8, -dive * 0.6),
2637
+ pointAt(1, -dive),
2482
2638
  ],
2483
2639
  radialSegments: 7,
2484
2640
  radiusEnd: baseRadius * rootSpec.radius * 0.4,
@@ -2550,9 +2706,22 @@ export class StylizedTree extends THREE.Group {
2550
2706
  // Only forward canopy values that differ from the defaults, so the
2551
2707
  // tips-placement geometry presets below keep winning unless the caller
2552
2708
  // explicitly overrides them (exactly the legacy sparse-options behavior).
2709
+ //
2710
+ // That difference test cannot distinguish "not supplied" from "supplied,
2711
+ // and equal to the module default", so a caller that authors the
2712
+ // documented default gets the preset instead — cardsPerCluster: 5 lands
2713
+ // as the branching preset's 2, silently halving the canopy. Callers that
2714
+ // do know which fields they were handed may declare them in
2715
+ // `canopy.explicit`, and those are forwarded whatever their value.
2716
+ // Nothing that omits `explicit` changes behaviour.
2717
+ const explicitCanopyKeys = new Set(
2718
+ Array.isArray(settings.canopy.explicit) ? settings.canopy.explicit : [],
2719
+ );
2553
2720
  const canopyOverrides = {};
2554
2721
  for (const [key, value] of Object.entries(settings.canopy)) {
2555
- if (!(key in DEFAULT_STYLIZED_TREE_SETTINGS.canopy) ||
2722
+ if (key === 'explicit') continue;
2723
+ if (explicitCanopyKeys.has(key) ||
2724
+ !(key in DEFAULT_STYLIZED_TREE_SETTINGS.canopy) ||
2556
2725
  !sameSettingValue(value, DEFAULT_STYLIZED_TREE_SETTINGS.canopy[key])) {
2557
2726
  canopyOverrides[key] = value;
2558
2727
  }
@@ -2589,7 +2758,16 @@ export class StylizedTree extends THREE.Group {
2589
2758
  ? { shellFill: true, shellBudget: extraBlobs.length * 8 }
2590
2759
  : { shellFill: false, ...(attachments.length ? {} : { cardCount: 0 }) })
2591
2760
  : {}),
2592
- attachmentOverrides: branchOverrides,
2761
+ attachmentOverrides: resolvePadPruning(
2762
+ attachments,
2763
+ {
2764
+ crownBase: settings.canopy.crownBase,
2765
+ densityBoost: settings.canopy.padDensityBoost,
2766
+ layers: settings.canopy.padLayers,
2767
+ padCount: settings.canopy.padCount,
2768
+ },
2769
+ branchOverrides,
2770
+ ),
2593
2771
  blobs: (generator === 'drawn' || generator === 'branching') && extraBlobs.length
2594
2772
  ? extraBlobs : blobs,
2595
2773
  });
@@ -2604,6 +2782,19 @@ export class StylizedTree extends THREE.Group {
2604
2782
  vegetationShader,
2605
2783
  });
2606
2784
 
2785
+ // A caller can select the style either as `preset` or by handing over the
2786
+ // vegetation shader profile itself (a string, or { style } / { preset } —
2787
+ // the same shape setVegetationShader accepts). Testing only `options.preset`
2788
+ // meant every generator that forwards `vegetationShader` but not `preset` —
2789
+ // BranchTree among them — silently built a bare, untextured trunk.
2790
+ const requestedStyleId = String(
2791
+ options.preset
2792
+ ?? (typeof vegetationShader === 'string'
2793
+ ? vegetationShader
2794
+ : vegetationShader?.style ?? vegetationShader?.preset ?? ''),
2795
+ ).trim();
2796
+ const styleDefaultSurfaceProfile = TREE_SURFACE_PROFILE_DEFAULTS[requestedStyleId] ?? null;
2797
+
2607
2798
  this.trunkMesh = new THREE.Mesh(
2608
2799
  trunkGeometry,
2609
2800
  trunkMaterial ?? createWoodySurfaceNodeMaterial({
@@ -2612,11 +2803,8 @@ export class StylizedTree extends THREE.Group {
2612
2803
  map: trunkMap ?? (
2613
2804
  trunkSurfaceProfile && trunkSurfaceProfile !== 'none'
2614
2805
  ? createTreeSurfaceTexture({ profileId: trunkSurfaceProfile, seed })
2615
- : options.preset === 'call_me_sensei'
2616
- ? createTreeSurfaceTexture({
2617
- profileId: TREE_SURFACE_PROFILE_DEFAULTS.call_me_sensei,
2618
- seed,
2619
- })
2806
+ : styleDefaultSurfaceProfile
2807
+ ? createTreeSurfaceTexture({ profileId: styleDefaultSurfaceProfile, seed })
2620
2808
  : null
2621
2809
  ),
2622
2810
  sceneShadowStrength: trunkReceiveShadow ? 1 : 0,
@@ -2630,9 +2818,7 @@ export class StylizedTree extends THREE.Group {
2630
2818
  ? null
2631
2819
  : trunkSurfaceProfile && trunkSurfaceProfile !== 'none'
2632
2820
  ? trunkSurfaceProfile
2633
- : options.preset === 'call_me_sensei'
2634
- ? TREE_SURFACE_PROFILE_DEFAULTS.call_me_sensei
2635
- : null;
2821
+ : styleDefaultSurfaceProfile;
2636
2822
  this.trunkMesh.material.userData.toonlabBarkSurface = {
2637
2823
  profileId: selectedTrunkSurfaceProfile,
2638
2824
  source: authoredTrunkMap