@call-me-sensei/toonlab 0.1.1

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 (146) hide show
  1. package/ATTRIBUTION.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/package.json +97 -0
  5. package/src/character/characterRig.js +353 -0
  6. package/src/character/freestyleSwimClip.js +541 -0
  7. package/src/character/index.js +4 -0
  8. package/src/character/modelLoader.js +433 -0
  9. package/src/core/materialRoles.js +438 -0
  10. package/src/core/presetDocuments.js +110 -0
  11. package/src/core/shaderBackend.js +24 -0
  12. package/src/debrisgen/debrisFields.js +544 -0
  13. package/src/debrisgen/debrisGenerator.js +1929 -0
  14. package/src/debrisgen/debrisPalettes.js +71 -0
  15. package/src/debrisgen/debrisPhysics.js +198 -0
  16. package/src/debrisgen/debrisPresets.js +155 -0
  17. package/src/debrisgen/debrisSettings.js +333 -0
  18. package/src/debrisgen/debrisTextures.js +380 -0
  19. package/src/debrisgen/index.js +5 -0
  20. package/src/debug/fieldValues.js +95 -0
  21. package/src/debug/index.js +7 -0
  22. package/src/debug/settingsPanel.js +170 -0
  23. package/src/environment/environmentAmbientProbe.js +160 -0
  24. package/src/environment/environmentMaterialAdapter.js +336 -0
  25. package/src/environment/environmentMaterialClassifier.js +178 -0
  26. package/src/environment/environmentPlanarReflection.js +201 -0
  27. package/src/environment/environmentPresets.js +443 -0
  28. package/src/environment/environmentRigs.js +684 -0
  29. package/src/environment/environmentSettings.js +504 -0
  30. package/src/environment/environmentShaderMaterials.js +318 -0
  31. package/src/environment/environmentSunShadowPass.js +268 -0
  32. package/src/environment/environmentTextureResolver.js +161 -0
  33. package/src/environment/environmentTimeOfDay.js +167 -0
  34. package/src/environment/environmentVertexAo.js +188 -0
  35. package/src/environment/index.js +8 -0
  36. package/src/environment/scanAssetStylize.js +103 -0
  37. package/src/index.js +21 -0
  38. package/src/loaders/index.js +1 -0
  39. package/src/post/index.js +2 -0
  40. package/src/post/postProcessing.js +1087 -0
  41. package/src/rockgen/export/glbExport.js +177 -0
  42. package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
  43. package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
  44. package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
  45. package/src/rockgen/index.js +14 -0
  46. package/src/rockgen/mesh/meshAttributes.js +374 -0
  47. package/src/rockgen/mesh/meshDocument.js +158 -0
  48. package/src/rockgen/mesh/surfaceNets.js +329 -0
  49. package/src/rockgen/noise/cellularNoise3.js +92 -0
  50. package/src/rockgen/noise/prng.js +62 -0
  51. package/src/rockgen/noise/simplexNoise3.js +89 -0
  52. package/src/rockgen/noise/valueNoise3.js +87 -0
  53. package/src/rockgen/rockDocument.js +255 -0
  54. package/src/rockgen/rockHelpers.js +8 -0
  55. package/src/rockgen/rockgenPresets.js +559 -0
  56. package/src/rockgen/rockgenSettings.js +756 -0
  57. package/src/rockgen/sdf/fieldCompiler.js +544 -0
  58. package/src/rockgen/sdf/sculptEdits.js +66 -0
  59. package/src/rockgen/sdf/sdfModifiers.js +45 -0
  60. package/src/rockgen/sdf/sdfOps.js +37 -0
  61. package/src/rockgen/sdf/sdfPrimitives.js +88 -0
  62. package/src/shaders-tsl/anime.js +963 -0
  63. package/src/shaders-tsl/chunks/character-color.js +64 -0
  64. package/src/shaders-tsl/chunks/character-highlights.js +174 -0
  65. package/src/shaders-tsl/chunks/character-lighting.js +309 -0
  66. package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
  67. package/src/shaders-tsl/chunks/character-roles.js +52 -0
  68. package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
  69. package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
  70. package/src/shaders-tsl/chunks/character-skinning.js +140 -0
  71. package/src/shaders-tsl/chunks/environment-color.js +47 -0
  72. package/src/shaders-tsl/chunks/environment-debug.js +79 -0
  73. package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
  74. package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
  75. package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
  76. package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
  77. package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
  78. package/src/shaders-tsl/chunks/water-color.js +190 -0
  79. package/src/shaders-tsl/chunks/water-common.js +124 -0
  80. package/src/shaders-tsl/chunks/water-foam.js +93 -0
  81. package/src/shaders-tsl/chunks/water-lighting.js +116 -0
  82. package/src/shaders-tsl/chunks/water-ripple.js +64 -0
  83. package/src/shaders-tsl/chunks/water-waves.js +100 -0
  84. package/src/shaders-tsl/environment-ao-overlay.js +60 -0
  85. package/src/shaders-tsl/environment.js +680 -0
  86. package/src/shaders-tsl/flower.js +267 -0
  87. package/src/shaders-tsl/grass.js +193 -0
  88. package/src/shaders-tsl/post-composite.js +461 -0
  89. package/src/shaders-tsl/sky.js +175 -0
  90. package/src/shaders-tsl/tree-leaf.js +264 -0
  91. package/src/shaders-tsl/water-breaker.js +423 -0
  92. package/src/shaders-tsl/water-kelp.js +171 -0
  93. package/src/shaders-tsl/water-rain.js +107 -0
  94. package/src/shaders-tsl/water-simulation.js +140 -0
  95. package/src/shaders-tsl/water-splash.js +298 -0
  96. package/src/shaders-tsl/water.js +509 -0
  97. package/src/sky/index.js +2 -0
  98. package/src/sky/stylizedSky.js +341 -0
  99. package/src/toon/characterRenderPasses.js +646 -0
  100. package/src/toon/index.js +4 -0
  101. package/src/toon/settings/alphaSettings.js +170 -0
  102. package/src/toon/settings/averageShadowSettings.js +181 -0
  103. package/src/toon/settings/baseTextureSettings.js +133 -0
  104. package/src/toon/settings/celShadeSettings.js +96 -0
  105. package/src/toon/settings/contactShadowSettings.js +114 -0
  106. package/src/toon/settings/eyeHighlightSettings.js +187 -0
  107. package/src/toon/settings/faceLightingSettings.js +135 -0
  108. package/src/toon/settings/furSettings.js +112 -0
  109. package/src/toon/settings/glitterSettings.js +102 -0
  110. package/src/toon/settings/hairHighlightSettings.js +325 -0
  111. package/src/toon/settings/indirectLightSettings.js +244 -0
  112. package/src/toon/settings/localLightSettings.js +176 -0
  113. package/src/toon/settings/materialMapSettings.js +299 -0
  114. package/src/toon/settings/outlineSettings.js +263 -0
  115. package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
  116. package/src/toon/settings/rimLightSettings.js +268 -0
  117. package/src/toon/settings/sceneShadowSettings.js +147 -0
  118. package/src/toon/settings/selfShadowSettings.js +221 -0
  119. package/src/toon/settings/shadowColorSettings.js +238 -0
  120. package/src/toon/settings/skinToneSettings.js +150 -0
  121. package/src/toon/settings/specularSettings.js +314 -0
  122. package/src/toon/settings/stickerSettings.js +102 -0
  123. package/src/toon/toonMaterialAdapter.js +2235 -0
  124. package/src/toon/toonSettings.js +860 -0
  125. package/src/vegetation/flowerSpecies.js +260 -0
  126. package/src/vegetation/index.js +10 -0
  127. package/src/vegetation/stylizedBush.js +136 -0
  128. package/src/vegetation/stylizedFlower.js +213 -0
  129. package/src/vegetation/stylizedFlowers.js +297 -0
  130. package/src/vegetation/stylizedGrass.js +491 -0
  131. package/src/vegetation/stylizedTree.js +2449 -0
  132. package/src/vegetation/stylizedTreeFoliage.js +691 -0
  133. package/src/vegetation/treeExport.js +300 -0
  134. package/src/vegetation/treeRecipe.js +647 -0
  135. package/src/water/index.js +2 -0
  136. package/src/water/water.js +20 -0
  137. package/src/water/waterBreakerSystem.js +591 -0
  138. package/src/water/waterInteraction.js +193 -0
  139. package/src/water/waterMaterial.js +263 -0
  140. package/src/water/waterRain.js +136 -0
  141. package/src/water/waterRippleSimulation.js +227 -0
  142. package/src/water/waterScenePasses.js +415 -0
  143. package/src/water/waterSettings.js +1138 -0
  144. package/src/water/waterSplashSystem.js +251 -0
  145. package/src/water/waterSurface.js +475 -0
  146. package/src/water/waterVegetation.js +83 -0
@@ -0,0 +1,647 @@
1
+ import {
2
+ DEFAULT_STYLIZED_TREE_SETTINGS,
3
+ STYLIZED_TREE_SETTING_FIELD_SCHEMA,
4
+ StylizedTree,
5
+ TREE_RECIPE_SCHEMA,
6
+ TREE_RECIPE_VERSION,
7
+ TREE_TRUNK_STYLES,
8
+ serializableTreeOptions,
9
+ } from './stylizedTree.js';
10
+ import { StylizedBush } from './stylizedBush.js';
11
+ import { StylizedFlower } from './stylizedFlower.js';
12
+ import { FLOWER_SPECIES } from './flowerSpecies.js';
13
+ import { resolveCanopyColor } from './stylizedTreeFoliage.js';
14
+
15
+ export { TREE_RECIPE_SCHEMA, TREE_RECIPE_VERSION };
16
+
17
+ // Tree/bush recipe layer: the single source of truth Tree Lab's UI,
18
+ // preset files, and runtime rebuilds all share.
19
+ //
20
+ // settings — flat { group: { key: value } } state edited by the panel
21
+ // (TREE_SETTING_GROUPS / TREE_SETTING_FIELD_SCHEMA drive the UI)
22
+ // recipe — { schema: 'treeRecipe', version, type: 'tree'|'bush'|'flower',
23
+ // options } where options feed the StylizedTree/StylizedBush/
24
+ // StylizedFlower constructor
25
+ //
26
+ // const recipe = recipeFromSettings(settings);
27
+ // const plant = createPlantFromRecipe(recipe); // deterministic
28
+ // const settings2 = settingsFromRecipe(recipe); // best-effort inverse
29
+ //
30
+ // Colors in settings are sRGB triplets [r, g, b] (0..1) — the same shape the
31
+ // debug panel's color controls round-trip — and resolveCanopyColor accepts
32
+ // them back verbatim.
33
+ //
34
+ // The canonical field metadata (ranges, labels, descriptions, defaults) lives
35
+ // in STYLIZED_TREE_SETTING_FIELD_SCHEMA / DEFAULT_STYLIZED_TREE_SETTINGS
36
+ // (stylizedTree.js) — the same source the generated settings reference uses.
37
+ // This module only re-shapes it for the editor (flat panel groups, trunk
38
+ // style presets, auto/pin toggles, wind heading) and hand-authors the few
39
+ // editor-only fields the constructor schema keeps opaque (plant type, blob
40
+ // layout numbers inside canopyLayout, palette pins).
41
+
42
+ const TRUNK_STYLE_KEYS = ['bend', 'lean', 'twist', 'gnarl', 'height', 'radiusBottom', 'leanOffset'];
43
+
44
+ const CANONICAL_DEFAULTS = DEFAULT_STYLIZED_TREE_SETTINGS;
45
+
46
+ export const TREE_SETTING_DEFAULTS = Object.freeze({
47
+ // Curated starter plant (a pleasant mid-size tree), not the constructor
48
+ // defaults: the editor should open on something worth looking at.
49
+ plant: Object.freeze({ type: 'tree', seed: 3, size: 1.7 }),
50
+ trunk: Object.freeze({
51
+ style: 'straight',
52
+ height: CANONICAL_DEFAULTS.trunk.height,
53
+ radiusBottom: CANONICAL_DEFAULTS.trunk.radiusBottom,
54
+ ...TREE_TRUNK_STYLES.straight,
55
+ bendDirectionAuto: true,
56
+ bendDirection: 0,
57
+ leanOffsetAuto: true,
58
+ leanOffset: Math.PI,
59
+ }),
60
+ skeleton: Object.freeze({
61
+ generator: CANONICAL_DEFAULTS.skeleton.generator,
62
+ levels: CANONICAL_DEFAULTS.skeleton.levels,
63
+ childrenCount: CANONICAL_DEFAULTS.skeleton.childrenCount,
64
+ branchAngle: CANONICAL_DEFAULTS.skeleton.branchAngle,
65
+ branchStart: CANONICAL_DEFAULTS.skeleton.branchStart,
66
+ lengthRatio: CANONICAL_DEFAULTS.skeleton.lengthRatio,
67
+ radiusRatio: CANONICAL_DEFAULTS.skeleton.radiusRatio,
68
+ gnarliness: CANONICAL_DEFAULTS.skeleton.gnarliness,
69
+ forceStrength: CANONICAL_DEFAULTS.skeleton.forceStrength,
70
+ conifer: CANONICAL_DEFAULTS.skeleton.conifer,
71
+ attractionCount: CANONICAL_DEFAULTS.skeleton.attractionCount,
72
+ segmentLength: CANONICAL_DEFAULTS.skeleton.segmentLength,
73
+ influenceRadius: CANONICAL_DEFAULTS.skeleton.influenceRadius,
74
+ killRadius: CANONICAL_DEFAULTS.skeleton.killRadius,
75
+ maxNodes: CANONICAL_DEFAULTS.skeleton.maxNodes,
76
+ radialSegments: CANONICAL_DEFAULTS.skeleton.radialSegments,
77
+ tipRadius: CANONICAL_DEFAULTS.skeleton.tipRadius,
78
+ minLimbRadius: CANONICAL_DEFAULTS.skeleton.minLimbRadius,
79
+ attractionReachAuto: true,
80
+ attractionReach: 0.65, // the canopy-mode auto value (canonical default is null)
81
+ }),
82
+ canopy: Object.freeze({
83
+ width: CANONICAL_DEFAULTS.tree.canopyWidth,
84
+ depth: CANONICAL_DEFAULTS.tree.canopyDepth,
85
+ canopyScale: CANONICAL_DEFAULTS.tree.canopyScale,
86
+ // createCanopyBlobs parameter defaults — opaque `canopyLayout` object in
87
+ // the constructor schema, exploded into sliders here.
88
+ flatten: 0.5,
89
+ lobeCount: 6,
90
+ spread: 1.25,
91
+ coreRadius: 0.9,
92
+ }),
93
+ leaves: Object.freeze({
94
+ placement: CANONICAL_DEFAULTS.tree.leafPlacement,
95
+ density: CANONICAL_DEFAULTS.tree.leafDensity,
96
+ cardCount: CANONICAL_DEFAULTS.canopy.cardCount,
97
+ cardSizeMin: CANONICAL_DEFAULTS.canopy.cardSizeRange[0],
98
+ cardSizeMax: CANONICAL_DEFAULTS.canopy.cardSizeRange[1],
99
+ cardsPerCluster: CANONICAL_DEFAULTS.canopy.cardsPerCluster,
100
+ clusterRadius: CANONICAL_DEFAULTS.canopy.clusterRadius,
101
+ shellFill: CANONICAL_DEFAULTS.canopy.shellFill,
102
+ }),
103
+ color: Object.freeze({
104
+ canopy: Object.freeze([...CANONICAL_DEFAULTS.tree.canopyColor]),
105
+ pinLit: false,
106
+ lit: Object.freeze([...CANONICAL_DEFAULTS.tree.canopyColor]),
107
+ pinShadow: false,
108
+ shadow: Object.freeze([0.18, 0.42, 0.28]),
109
+ pinCrown: false,
110
+ crown: Object.freeze([0.55, 0.78, 0.42]),
111
+ }),
112
+ flower: Object.freeze({
113
+ species: 'daisy',
114
+ pinHeadColor: false,
115
+ headColor: Object.freeze([1, 0.98, 0.92]),
116
+ headScale: 1,
117
+ }),
118
+ wind: Object.freeze({
119
+ speed: CANONICAL_DEFAULTS.foliage.windSpeed,
120
+ strength: CANONICAL_DEFAULTS.foliage.windStrength,
121
+ heading: Math.round(Math.atan2(
122
+ CANONICAL_DEFAULTS.foliage.windDirection[1],
123
+ CANONICAL_DEFAULTS.foliage.windDirection[0]) * 100) / 100,
124
+ }),
125
+ });
126
+
127
+ export const TREE_SETTING_GROUPS = Object.freeze([
128
+ Object.freeze({ id: 'plant', label: 'Plant', description: 'Plant type, seed, and overall size.' }),
129
+ Object.freeze({ id: 'trunk', label: 'Trunk', description: 'Trunk silhouette: bow, lean, twist, gnarl. Trees only.' }),
130
+ Object.freeze({ id: 'skeleton', label: 'Branches', description: 'Space-colonization limb growth. Trees only.' }),
131
+ Object.freeze({ id: 'canopy', label: 'Crown Shape', description: 'Blob layout that defines the crown/bush silhouette.' }),
132
+ Object.freeze({ id: 'leaves', label: 'Leaves', description: 'Leaf-card coverage, density, and tuft behavior.' }),
133
+ Object.freeze({ id: 'flower', label: 'Bloom', description: 'Flower-head species, color, and size. Flower plants only.' }),
134
+ Object.freeze({ id: 'color', label: 'Color', description: 'Canopy palette: base color plus pinnable lit/shadow/crown tones.' }),
135
+ Object.freeze({ id: 'wind', label: 'Wind', description: 'Leaf flutter animation (live; not part of exported GLBs).' }),
136
+ ]);
137
+
138
+ // Canonical constructor-schema metadata lookup; throws at module load if a
139
+ // mapped field is renamed upstream, so drift is caught immediately.
140
+ function canonicalField(ref) {
141
+ const [group, key] = ref.split('.');
142
+ const metadata = STYLIZED_TREE_SETTING_FIELD_SCHEMA[group]?.[key];
143
+ if (!metadata) throw new Error(`treeRecipe: unknown canonical field "${ref}"`);
144
+ return metadata;
145
+ }
146
+
147
+ // Editor field metadata. `from: 'group.key'` inherits label/description/type/
148
+ // range/options from STYLIZED_TREE_SETTING_FIELD_SCHEMA so the editor stays
149
+ // in lockstep with the constructor schema (and the generated settings
150
+ // reference); explicit values in `extra` win. Editor-only fields (type
151
+ // switch, style presets, auto/pin toggles, blob-layout sliders) author
152
+ // everything by hand.
153
+ // bake: 'rebuild' fields regenerate geometry; 'live' fields only write shader
154
+ // uniforms, so the designer applies them without a rebuild.
155
+ function field(group, key, extra = {}) {
156
+ const base = extra.from ? canonicalField(extra.from) : null;
157
+ const defaultValue = TREE_SETTING_DEFAULTS[group][key];
158
+ const type = extra.type ?? base?.type ??
159
+ (typeof defaultValue === 'boolean' ? 'boolean'
160
+ : typeof defaultValue === 'number' ? 'number' : 'text');
161
+ return Object.freeze({
162
+ id: `${group}.${key}`,
163
+ group,
164
+ key,
165
+ label: extra.label ?? base?.label ?? key,
166
+ description: extra.description ?? base?.description ?? '',
167
+ type,
168
+ defaultValue,
169
+ serializable: true,
170
+ bake: extra.bake ?? 'rebuild',
171
+ range: extra.range ?? base?.range ?? null,
172
+ options: extra.options ?? base?.options ?? null,
173
+ optionLabels: extra.optionLabels ?? base?.optionLabels ?? null,
174
+ });
175
+ }
176
+
177
+ const TRUNK_STYLE_OPTIONS = [...Object.keys(TREE_TRUNK_STYLES), 'custom'];
178
+
179
+ export const TREE_SETTING_FIELD_SCHEMA = Object.freeze({
180
+ plant: Object.freeze({
181
+ type: field('plant', 'type', {
182
+ label: 'Type',
183
+ description: 'Tree (trunk + crown), bush (foliage mass on the ground), or flower (stem + branches tipped with blooms).',
184
+ type: 'select',
185
+ options: ['tree', 'bush', 'flower'],
186
+ optionLabels: { tree: 'Tree', bush: 'Bush / Shrub', flower: 'Flower' },
187
+ }),
188
+ seed: field('plant', 'seed', { from: 'tree.seed' }),
189
+ size: field('plant', 'size', { from: 'tree.size' }),
190
+ }),
191
+ trunk: Object.freeze({
192
+ style: field('trunk', 'style', {
193
+ label: 'Style',
194
+ description: 'Ready-made trunk personality; editing any slider below switches to Custom.',
195
+ type: 'select',
196
+ options: TRUNK_STYLE_OPTIONS,
197
+ optionLabels: {
198
+ straight: 'Straight', leaning: 'Leaning', curved: 'Curved', gnarled: 'Gnarled',
199
+ bonsai: 'Bonsai', swooping: 'Swooping (Liyue)', custom: 'Custom',
200
+ },
201
+ }),
202
+ height: field('trunk', 'height', { from: 'trunk.height' }),
203
+ radiusBottom: field('trunk', 'radiusBottom', { from: 'trunk.radiusBottom' }),
204
+ bend: field('trunk', 'bend', { from: 'trunk.bend' }),
205
+ lean: field('trunk', 'lean', { from: 'trunk.lean' }),
206
+ twist: field('trunk', 'twist', { from: 'trunk.twist' }),
207
+ gnarl: field('trunk', 'gnarl', { from: 'trunk.gnarl' }),
208
+ bendDirectionAuto: field('trunk', 'bendDirectionAuto', {
209
+ label: 'Auto Bend Heading',
210
+ description: 'Pick the bow heading from the seed instead of the slider below.',
211
+ }),
212
+ bendDirection: field('trunk', 'bendDirection', { from: 'trunk.bendDirection' }),
213
+ leanOffsetAuto: field('trunk', 'leanOffsetAuto', {
214
+ label: 'Auto Lean Offset',
215
+ description: 'Pick the lean heading from the seed; off + \u03c0 gives the serpentine S-trunk.',
216
+ }),
217
+ leanOffset: field('trunk', 'leanOffset', { from: 'trunk.leanOffset' }),
218
+ }),
219
+ skeleton: Object.freeze({
220
+ generator: field('skeleton', 'generator', { from: 'skeleton.generator' }),
221
+ levels: field('skeleton', 'levels', { from: 'skeleton.levels' }),
222
+ childrenCount: field('skeleton', 'childrenCount', { from: 'skeleton.childrenCount' }),
223
+ branchAngle: field('skeleton', 'branchAngle', { from: 'skeleton.branchAngle' }),
224
+ branchStart: field('skeleton', 'branchStart', { from: 'skeleton.branchStart' }),
225
+ lengthRatio: field('skeleton', 'lengthRatio', { from: 'skeleton.lengthRatio' }),
226
+ radiusRatio: field('skeleton', 'radiusRatio', { from: 'skeleton.radiusRatio' }),
227
+ gnarliness: field('skeleton', 'gnarliness', { from: 'skeleton.gnarliness' }),
228
+ forceStrength: field('skeleton', 'forceStrength', { from: 'skeleton.forceStrength' }),
229
+ conifer: field('skeleton', 'conifer', { from: 'skeleton.conifer' }),
230
+ attractionCount: field('skeleton', 'attractionCount', { from: 'skeleton.attractionCount' }),
231
+ segmentLength: field('skeleton', 'segmentLength', { from: 'skeleton.segmentLength' }),
232
+ influenceRadius: field('skeleton', 'influenceRadius', { from: 'skeleton.influenceRadius' }),
233
+ killRadius: field('skeleton', 'killRadius', { from: 'skeleton.killRadius' }),
234
+ maxNodes: field('skeleton', 'maxNodes', { from: 'skeleton.maxNodes' }),
235
+ radialSegments: field('skeleton', 'radialSegments', { from: 'skeleton.radialSegments' }),
236
+ tipRadius: field('skeleton', 'tipRadius', { from: 'skeleton.tipRadius' }),
237
+ minLimbRadius: field('skeleton', 'minLimbRadius', { from: 'skeleton.minLimbRadius' }),
238
+ attractionReachAuto: field('skeleton', 'attractionReachAuto', {
239
+ label: 'Auto Reach',
240
+ description: 'Derive attraction depth from leaf placement (buried for canopy, near-shell for tips).',
241
+ }),
242
+ attractionReach: field('skeleton', 'attractionReach', { from: 'skeleton.attractionReach' }),
243
+ }),
244
+ canopy: Object.freeze({
245
+ width: field('canopy', 'width', { from: 'tree.canopyWidth' }),
246
+ depth: field('canopy', 'depth', { from: 'tree.canopyDepth' }),
247
+ flatten: field('canopy', 'flatten', {
248
+ label: 'Flatten',
249
+ description: 'Vertical squash of the lobes; low values give the wide layered look.',
250
+ range: { min: 0.1, max: 1, step: 0.05 },
251
+ }),
252
+ lobeCount: field('canopy', 'lobeCount', {
253
+ label: 'Lobes',
254
+ description: 'Satellite blobs ringing the crown core.',
255
+ range: { min: 2, max: 10, step: 1 },
256
+ }),
257
+ spread: field('canopy', 'spread', {
258
+ label: 'Spread',
259
+ description: 'Horizontal reach of the lobes (1 \u2248 core radius).',
260
+ range: { min: 0.5, max: 2, step: 0.05 },
261
+ }),
262
+ coreRadius: field('canopy', 'coreRadius', {
263
+ label: 'Core Radius',
264
+ description: 'Central blob radius.',
265
+ range: { min: 0.4, max: 1.2, step: 0.05 },
266
+ }),
267
+ canopyScale: field('canopy', 'canopyScale', { from: 'tree.canopyScale' }),
268
+ }),
269
+ leaves: Object.freeze({
270
+ placement: field('leaves', 'placement', { from: 'tree.leafPlacement' }),
271
+ density: field('leaves', 'density', { from: 'tree.leafDensity' }),
272
+ cardCount: field('leaves', 'cardCount', { from: 'canopy.cardCount' }),
273
+ cardSizeMin: field('leaves', 'cardSizeMin', {
274
+ label: 'Card Size Min',
275
+ description: canonicalField('canopy.cardSizeRange').description,
276
+ range: { min: 0.4, max: 2, step: 0.05 },
277
+ }),
278
+ cardSizeMax: field('leaves', 'cardSizeMax', {
279
+ label: 'Card Size Max',
280
+ description: canonicalField('canopy.cardSizeRange').description,
281
+ range: { min: 0.4, max: 2.4, step: 0.05 },
282
+ }),
283
+ cardsPerCluster: field('leaves', 'cardsPerCluster', { from: 'canopy.cardsPerCluster' }),
284
+ clusterRadius: field('leaves', 'clusterRadius', { from: 'canopy.clusterRadius' }),
285
+ shellFill: field('leaves', 'shellFill', { from: 'canopy.shellFill' }),
286
+ }),
287
+ flower: Object.freeze({
288
+ species: field('flower', 'species', {
289
+ label: 'Species',
290
+ description: 'Common-flower head at every branch tip (see FLOWER_SPECIES).',
291
+ type: 'select',
292
+ options: FLOWER_SPECIES.map((entry) => entry.id),
293
+ optionLabels: Object.fromEntries(FLOWER_SPECIES.map((entry) => [entry.id, entry.label])),
294
+ }),
295
+ pinHeadColor: field('flower', 'pinHeadColor', {
296
+ label: 'Pin Head Color',
297
+ description: 'Use the explicit petal color below instead of the species default.',
298
+ }),
299
+ headColor: field('flower', 'headColor', {
300
+ label: 'Head Color',
301
+ description: 'Bloom petal color (when pinned).',
302
+ type: 'color',
303
+ }),
304
+ headScale: field('flower', 'headScale', {
305
+ label: 'Head Size',
306
+ description: 'Bloom size relative to the leaf tufts.',
307
+ range: { min: 0.4, max: 2, step: 0.05 },
308
+ }),
309
+ }),
310
+ color: Object.freeze({
311
+ canopy: field('color', 'canopy', { from: 'tree.canopyColor', bake: 'live' }),
312
+ pinLit: field('color', 'pinLit', {
313
+ label: 'Pin Lit',
314
+ description: 'Use the explicit lit tone below instead of the base color.',
315
+ bake: 'live',
316
+ }),
317
+ lit: field('color', 'lit', {
318
+ label: 'Lit Tone', description: 'Sunlit leaf tone (when pinned).', type: 'color', bake: 'live',
319
+ }),
320
+ pinShadow: field('color', 'pinShadow', {
321
+ label: 'Pin Shadow',
322
+ description: 'Use the explicit shadow tone below instead of deriving it.',
323
+ bake: 'live',
324
+ }),
325
+ shadow: field('color', 'shadow', {
326
+ label: 'Shadow Tone', description: 'Shaded leaf tone (when pinned).', type: 'color', bake: 'live',
327
+ }),
328
+ pinCrown: field('color', 'pinCrown', {
329
+ label: 'Pin Crown',
330
+ description: 'Use the explicit crown-crest tone below instead of deriving it.',
331
+ bake: 'live',
332
+ }),
333
+ crown: field('color', 'crown', {
334
+ label: 'Crown Tone',
335
+ description: 'Sun-struck crest tone on the crown top (when pinned).',
336
+ type: 'color',
337
+ bake: 'live',
338
+ }),
339
+ }),
340
+ wind: Object.freeze({
341
+ speed: field('wind', 'speed', { from: 'foliage.windSpeed', bake: 'live' }),
342
+ strength: field('wind', 'strength', { from: 'foliage.windStrength', bake: 'live' }),
343
+ heading: field('wind', 'heading', {
344
+ label: 'Heading',
345
+ description: canonicalField('foliage.windDirection').description,
346
+ range: { min: 0, max: 6.29, step: 0.01 },
347
+ bake: 'live',
348
+ }),
349
+ }),
350
+ });
351
+
352
+ export function cloneTreeSettings(settings = TREE_SETTING_DEFAULTS) {
353
+ return JSON.parse(JSON.stringify(settings));
354
+ }
355
+
356
+ function paletteFromSettings(color) {
357
+ const palette = {};
358
+ if (color.pinLit) palette.lit = [...color.lit];
359
+ if (color.pinShadow) palette.shadow = [...color.shadow];
360
+ if (color.pinCrown) palette.crown = [...color.crown];
361
+ return palette;
362
+ }
363
+
364
+ export function windOptionsFromSettings(settings) {
365
+ const { speed, strength, heading } = settings.wind;
366
+ return {
367
+ windDirection: [Math.cos(heading), Math.sin(heading)],
368
+ windSpeed: speed,
369
+ windStrength: strength,
370
+ };
371
+ }
372
+
373
+ // Panel state → constructor options. Trunk style is UI sugar: the sliders
374
+ // always hold the concrete values, so options stay explicit and recipes never
375
+ // depend on preset tables.
376
+ export function treeOptionsFromSettings(settings) {
377
+ const { plant, trunk, skeleton, canopy, leaves, color } = settings;
378
+ const shared = {
379
+ size: plant.size,
380
+ seed: plant.seed,
381
+ canopyColor: [...color.canopy],
382
+ canopyPalette: paletteFromSettings(color),
383
+ leafDensity: leaves.density,
384
+ canopy: {
385
+ cardCount: leaves.cardCount,
386
+ cardSizeRange: [leaves.cardSizeMin, Math.max(leaves.cardSizeMax, leaves.cardSizeMin)],
387
+ cardsPerCluster: leaves.cardsPerCluster,
388
+ clusterRadius: leaves.clusterRadius,
389
+ shellFill: leaves.shellFill,
390
+ },
391
+ foliage: windOptionsFromSettings(settings),
392
+ };
393
+
394
+ if (plant.type === 'bush') {
395
+ return {
396
+ ...shared,
397
+ width: canopy.width,
398
+ depth: canopy.depth,
399
+ flatten: canopy.flatten,
400
+ canopyLayout: {
401
+ lobeCount: canopy.lobeCount,
402
+ spread: canopy.spread,
403
+ coreRadius: canopy.coreRadius,
404
+ },
405
+ };
406
+ }
407
+
408
+ const treeOptions = {
409
+ ...shared,
410
+ canopyWidth: canopy.width,
411
+ canopyDepth: canopy.depth,
412
+ canopyScale: canopy.canopyScale,
413
+ leafPlacement: leaves.placement,
414
+ canopyLayout: {
415
+ lobeCount: canopy.lobeCount,
416
+ spread: canopy.spread,
417
+ flatten: canopy.flatten,
418
+ coreRadius: canopy.coreRadius,
419
+ },
420
+ trunk: {
421
+ height: trunk.height,
422
+ radiusBottom: trunk.radiusBottom,
423
+ bend: trunk.bend,
424
+ lean: trunk.lean,
425
+ twist: trunk.twist,
426
+ gnarl: trunk.gnarl,
427
+ bendDirection: trunk.bendDirectionAuto ? null : trunk.bendDirection,
428
+ leanOffset: trunk.leanOffsetAuto ? null : trunk.leanOffset,
429
+ },
430
+ skeleton: {
431
+ generator: skeleton.generator,
432
+ levels: skeleton.levels,
433
+ childrenCount: skeleton.childrenCount,
434
+ branchAngle: skeleton.branchAngle,
435
+ branchStart: skeleton.branchStart,
436
+ lengthRatio: skeleton.lengthRatio,
437
+ radiusRatio: skeleton.radiusRatio,
438
+ gnarliness: skeleton.gnarliness,
439
+ forceStrength: skeleton.forceStrength,
440
+ conifer: skeleton.conifer,
441
+ attractionCount: skeleton.attractionCount,
442
+ segmentLength: skeleton.segmentLength,
443
+ influenceRadius: skeleton.influenceRadius,
444
+ killRadius: skeleton.killRadius,
445
+ maxNodes: skeleton.maxNodes,
446
+ radialSegments: skeleton.radialSegments,
447
+ tipRadius: skeleton.tipRadius,
448
+ minLimbRadius: skeleton.minLimbRadius,
449
+ attractionReach: skeleton.attractionReachAuto ? null : skeleton.attractionReach,
450
+ },
451
+ };
452
+
453
+ if (plant.type === 'flower') {
454
+ // A flower is a tree with blooms: same wood/leaf options plus the head.
455
+ const flower = settings.flower ?? TREE_SETTING_DEFAULTS.flower;
456
+ return {
457
+ ...treeOptions,
458
+ species: flower.species,
459
+ headColor: flower.pinHeadColor ? [...flower.headColor] : null,
460
+ headScale: flower.headScale,
461
+ };
462
+ }
463
+ return treeOptions;
464
+ }
465
+
466
+ const PLANT_TYPES = new Set(['tree', 'bush', 'flower']);
467
+
468
+ function normalizePlantType(type) {
469
+ return PLANT_TYPES.has(type) ? type : 'tree';
470
+ }
471
+
472
+ export function recipeFromSettings(settings) {
473
+ return {
474
+ schema: TREE_RECIPE_SCHEMA,
475
+ version: TREE_RECIPE_VERSION,
476
+ type: normalizePlantType(settings.plant.type),
477
+ options: serializableTreeOptions(treeOptionsFromSettings(settings)),
478
+ };
479
+ }
480
+
481
+ // Which trunk style matches these explicit values? Best-effort for the UI.
482
+ export function matchTrunkStyle(trunk) {
483
+ for (const [name, style] of Object.entries(TREE_TRUNK_STYLES)) {
484
+ const matches = TRUNK_STYLE_KEYS.every((key) => {
485
+ if (style[key] === undefined) return true;
486
+ return Math.abs((trunk[key] ?? Number.NaN) - style[key]) < 1e-6;
487
+ });
488
+ if (matches) return name;
489
+ }
490
+ return 'custom';
491
+ }
492
+
493
+ function srgbTriplet(spec, seed) {
494
+ const color = resolveCanopyColor(spec, seed).convertLinearToSRGB();
495
+ return [color.r, color.g, color.b];
496
+ }
497
+
498
+ // Recipe options → panel state, best-effort: advanced color specs collapse to
499
+ // their seed-resolved color, unknown option keys survive only in the recipe.
500
+ export function settingsFromRecipe(recipe) {
501
+ const settings = cloneTreeSettings();
502
+ const options = recipe?.options ?? {};
503
+ const type = normalizePlantType(recipe?.type);
504
+ const defaults = TREE_SETTING_DEFAULTS;
505
+
506
+ settings.plant.type = type;
507
+ settings.plant.seed = options.seed ?? defaults.plant.seed;
508
+ settings.plant.size = options.size ?? defaults.plant.size;
509
+
510
+ if (options.canopyColor !== undefined) {
511
+ settings.color.canopy = srgbTriplet(options.canopyColor, settings.plant.seed);
512
+ }
513
+ const palette = options.canopyPalette ?? {};
514
+ for (const tone of ['lit', 'shadow', 'crown']) {
515
+ if (palette[tone] !== undefined) {
516
+ const pin = `pin${tone[0].toUpperCase()}${tone.slice(1)}`;
517
+ settings.color[pin] = true;
518
+ settings.color[tone] = srgbTriplet(palette[tone], settings.plant.seed);
519
+ }
520
+ }
521
+
522
+ settings.leaves.density = options.leafDensity ?? defaults.leaves.density;
523
+ const canopyOptions = options.canopy ?? {};
524
+ if (canopyOptions.cardCount !== undefined) settings.leaves.cardCount = canopyOptions.cardCount;
525
+ if (canopyOptions.cardSizeRange) {
526
+ settings.leaves.cardSizeMin = canopyOptions.cardSizeRange[0];
527
+ settings.leaves.cardSizeMax = canopyOptions.cardSizeRange[1] ?? canopyOptions.cardSizeRange[0];
528
+ }
529
+ if (canopyOptions.cardsPerCluster !== undefined) settings.leaves.cardsPerCluster = canopyOptions.cardsPerCluster;
530
+ if (canopyOptions.clusterRadius !== undefined) settings.leaves.clusterRadius = canopyOptions.clusterRadius;
531
+ if (canopyOptions.shellFill !== undefined) settings.leaves.shellFill = canopyOptions.shellFill;
532
+
533
+ const layout = options.canopyLayout ?? {};
534
+ if (layout.lobeCount !== undefined) settings.canopy.lobeCount = layout.lobeCount;
535
+ if (layout.spread !== undefined) settings.canopy.spread = layout.spread;
536
+ if (layout.coreRadius !== undefined) settings.canopy.coreRadius = layout.coreRadius;
537
+ if (layout.flatten !== undefined) settings.canopy.flatten = layout.flatten;
538
+
539
+ const foliage = options.foliage ?? {};
540
+ if (foliage.windSpeed !== undefined) settings.wind.speed = foliage.windSpeed;
541
+ if (foliage.windStrength !== undefined) settings.wind.strength = foliage.windStrength;
542
+ if (foliage.windDirection) {
543
+ settings.wind.heading =
544
+ (Math.atan2(foliage.windDirection[1], foliage.windDirection[0]) + Math.PI * 2) % (Math.PI * 2);
545
+ }
546
+
547
+ if (type === 'bush') {
548
+ settings.canopy.width = options.width ?? defaults.canopy.width;
549
+ settings.canopy.depth = options.depth ?? defaults.canopy.depth;
550
+ settings.canopy.flatten = options.flatten ?? settings.canopy.flatten;
551
+ return settings;
552
+ }
553
+
554
+ settings.canopy.width = options.canopyWidth ?? defaults.canopy.width;
555
+ settings.canopy.depth = options.canopyDepth ?? defaults.canopy.depth;
556
+ settings.canopy.canopyScale = options.canopyScale ?? defaults.canopy.canopyScale;
557
+ settings.leaves.placement = options.leafPlacement ?? defaults.leaves.placement;
558
+
559
+ const trunk = options.trunk ?? {};
560
+ for (const key of ['height', 'radiusBottom', 'bend', 'lean', 'twist', 'gnarl']) {
561
+ if (trunk[key] !== undefined) settings.trunk[key] = trunk[key];
562
+ }
563
+ settings.trunk.bendDirectionAuto = trunk.bendDirection == null;
564
+ if (trunk.bendDirection != null) settings.trunk.bendDirection = trunk.bendDirection;
565
+ settings.trunk.leanOffsetAuto = trunk.leanOffset == null;
566
+ if (trunk.leanOffset != null) settings.trunk.leanOffset = trunk.leanOffset;
567
+ settings.trunk.style = matchTrunkStyle(settings.trunk);
568
+
569
+ const skeleton = options.skeleton ?? {};
570
+ for (const key of ['generator', 'levels', 'childrenCount', 'branchAngle', 'branchStart',
571
+ 'lengthRatio', 'radiusRatio', 'gnarliness', 'forceStrength', 'conifer',
572
+ 'attractionCount', 'segmentLength', 'influenceRadius', 'killRadius',
573
+ 'maxNodes', 'radialSegments', 'tipRadius', 'minLimbRadius']) {
574
+ if (skeleton[key] !== undefined) settings.skeleton[key] = skeleton[key];
575
+ }
576
+ settings.skeleton.attractionReachAuto = skeleton.attractionReach == null;
577
+ if (skeleton.attractionReach != null) settings.skeleton.attractionReach = skeleton.attractionReach;
578
+
579
+ if (type === 'flower') {
580
+ settings.flower.species = options.species ?? defaults.flower.species;
581
+ settings.flower.pinHeadColor = options.headColor != null;
582
+ if (options.headColor != null) settings.flower.headColor = [...options.headColor];
583
+ settings.flower.headScale = options.headScale ?? defaults.flower.headScale;
584
+ }
585
+
586
+ return settings;
587
+ }
588
+
589
+ // Same contract as validateToonPresetDocument: { ok, value?, errors? }.
590
+ // Structural checks only — options intentionally may exceed the panel's
591
+ // slider ranges (recipes are a superset of what the UI edits).
592
+ export function validateTreeRecipeDocument(input, { requireIdentity = false } = {}) {
593
+ const errors = [];
594
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
595
+ return { ok: false, errors: ['Recipe document must be an object.'] };
596
+ }
597
+ if (input.schema !== TREE_RECIPE_SCHEMA) {
598
+ errors.push(`schema must be "${TREE_RECIPE_SCHEMA}".`);
599
+ }
600
+ if (!Number.isInteger(input.version) || input.version < 1 || input.version > TREE_RECIPE_VERSION) {
601
+ errors.push(`version must be an integer between 1 and ${TREE_RECIPE_VERSION}.`);
602
+ }
603
+ if (!PLANT_TYPES.has(input.type)) {
604
+ errors.push('type must be "tree", "bush", or "flower".');
605
+ }
606
+ if (!input.options || typeof input.options !== 'object' || Array.isArray(input.options)) {
607
+ errors.push('options must be an object.');
608
+ }
609
+ if (requireIdentity) {
610
+ if (typeof input.id !== 'string' || !input.id.trim()) errors.push('id must be a non-empty string.');
611
+ if (typeof input.label !== 'string' || !input.label.trim()) errors.push('label must be a non-empty string.');
612
+ }
613
+ if (input.description !== undefined && typeof input.description !== 'string') {
614
+ errors.push('description must be a string when present.');
615
+ }
616
+ if (errors.length) return { ok: false, errors };
617
+
618
+ const value = {
619
+ schema: TREE_RECIPE_SCHEMA,
620
+ version: input.version,
621
+ type: input.type,
622
+ options: input.options,
623
+ };
624
+ if (typeof input.id === 'string' && input.id.trim()) value.id = input.id.trim();
625
+ if (typeof input.label === 'string' && input.label.trim()) value.label = input.label.trim();
626
+ if (input.description) value.description = input.description;
627
+ return { ok: true, value };
628
+ }
629
+
630
+ // Rebuild a plant from a recipe (or bare constructor options). Deterministic:
631
+ // the same recipe always grows the identical plant.
632
+ // import { createPlantFromRecipe } from '@call-me-sensei/toonlab/vegetation';
633
+ export function createPlantFromRecipe(recipe, { trunkMaterial = null } = {}) {
634
+ const document = recipe?.options
635
+ ? recipe
636
+ : { schema: TREE_RECIPE_SCHEMA, version: TREE_RECIPE_VERSION, type: 'tree', options: recipe ?? {} };
637
+ if (document.type === 'bush') {
638
+ return new StylizedBush(document.options);
639
+ }
640
+ if (document.type === 'flower') {
641
+ // No bark material: the flower grows its own toon stem material.
642
+ return new StylizedFlower(document.options);
643
+ }
644
+ return new StylizedTree(
645
+ trunkMaterial ? { ...document.options, trunkMaterial } : document.options,
646
+ );
647
+ }
@@ -0,0 +1,2 @@
1
+ // Water system barrel. Import from '@call-me-sensei/toonlab/water'.
2
+ export * from './water.js';
@@ -0,0 +1,20 @@
1
+ // Stylized water module barrel. Import from '@call-me-sensei/toonlab/water'.
2
+ //
3
+ // Quick start:
4
+ // import { WaterSurface } from '@call-me-sensei/toonlab/water';
5
+ // const water = new WaterSurface({ width: 20, depth: 20, preset: 'lake' });
6
+ // water.position.y = 0.4;
7
+ // scene.add(water);
8
+ // // each frame, before renderer.render(scene, camera):
9
+ // water.update(renderer, scene, camera, delta);
10
+
11
+ export * from './waterSettings.js';
12
+ export * from './waterMaterial.js';
13
+ export * from './waterRippleSimulation.js';
14
+ export * from './waterSplashSystem.js';
15
+ export * from './waterBreakerSystem.js';
16
+ export * from './waterScenePasses.js';
17
+ export * from './waterInteraction.js';
18
+ export * from './waterSurface.js';
19
+ export * from './waterRain.js';
20
+ export * from './waterVegetation.js';