@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,860 @@
1
+ import { createAlphaSettings, DEFAULT_ALPHA_SETTINGS } from './settings/alphaSettings.js';
2
+ import { createAverageShadowSettings, DEFAULT_AVERAGE_SHADOW_SETTINGS } from './settings/averageShadowSettings.js';
3
+ import {
4
+ BASE_TEXTURE_MATERIAL_COLOR_MODES,
5
+ BASE_TEXTURE_SATURATION_MODES,
6
+ createBaseTextureSettings,
7
+ DEFAULT_BASE_TEXTURE_SETTINGS,
8
+ } from './settings/baseTextureSettings.js';
9
+ import { createCelShadeSettings, DEFAULT_CEL_SHADE_SETTINGS } from './settings/celShadeSettings.js';
10
+ import { createContactShadowSettings, DEFAULT_CONTACT_SHADOW_SETTINGS } from './settings/contactShadowSettings.js';
11
+ import { createFurSettings, DEFAULT_FUR_SETTINGS } from './settings/furSettings.js';
12
+ import { createGlitterSettings, DEFAULT_GLITTER_SETTINGS } from './settings/glitterSettings.js';
13
+ import {
14
+ createPerspectiveRemovalSettings,
15
+ DEFAULT_PERSPECTIVE_REMOVAL_SETTINGS,
16
+ } from './settings/perspectiveRemovalSettings.js';
17
+ import {
18
+ createStickerSettings,
19
+ DEFAULT_STICKER_SETTINGS,
20
+ STICKER_BLEND_MODES,
21
+ } from './settings/stickerSettings.js';
22
+ import {
23
+ createEyeHighlightSettings,
24
+ DEFAULT_EYE_HIGHLIGHT_SETTINGS,
25
+ EYE_HIGHLIGHT_SOURCE_MASK_MODES,
26
+ } from './settings/eyeHighlightSettings.js';
27
+ import {
28
+ createFaceLightingSettings,
29
+ DEFAULT_FACE_LIGHTING_SETTINGS,
30
+ FACE_HEAD_SPACE_MODES,
31
+ } from './settings/faceLightingSettings.js';
32
+ import {
33
+ createHairHighlightSettings,
34
+ DEFAULT_HAIR_HIGHLIGHT_SETTINGS,
35
+ HAIR_HIGHLIGHT_MODES,
36
+ HAIR_HIGHLIGHT_SOURCE_MASK_MODES,
37
+ } from './settings/hairHighlightSettings.js';
38
+ import { createIndirectLightSettings, DEFAULT_INDIRECT_LIGHT_SETTINGS } from './settings/indirectLightSettings.js';
39
+ import { createLocalLightSettings, DEFAULT_LOCAL_LIGHT_SETTINGS } from './settings/localLightSettings.js';
40
+ import {
41
+ createMaterialMapSettings,
42
+ DEFAULT_MATERIAL_MAP_SETTINGS,
43
+ MATERIAL_MAP_SOURCE_MODES,
44
+ } from './settings/materialMapSettings.js';
45
+ import { createOutlineSettings, DEFAULT_OUTLINE_SETTINGS } from './settings/outlineSettings.js';
46
+ import { createRimLightSettings, DEFAULT_RIM_LIGHT_SETTINGS, RIM_LIGHT_MODES } from './settings/rimLightSettings.js';
47
+ import { createSceneShadowSettings, DEFAULT_SCENE_SHADOW_SETTINGS } from './settings/sceneShadowSettings.js';
48
+ import {
49
+ createSelfShadowSettings,
50
+ DEFAULT_SELF_SHADOW_SETTINGS,
51
+ SELF_SHADOW_SOURCE_MODES,
52
+ } from './settings/selfShadowSettings.js';
53
+ import { createShadowColorSettings, DEFAULT_SHADOW_COLOR_SETTINGS } from './settings/shadowColorSettings.js';
54
+ import { createSkinToneSettings, DEFAULT_SKIN_TONE_SETTINGS } from './settings/skinToneSettings.js';
55
+ import {
56
+ createSpecularSettings,
57
+ DEFAULT_SPECULAR_SETTINGS,
58
+ SPECULAR_DIRECTION_MODES,
59
+ SPECULAR_SOURCE_MASK_MODES,
60
+ } from './settings/specularSettings.js';
61
+ import {
62
+ createSettingsPresetDocument,
63
+ parsePresetDocument,
64
+ serializePresetDocument,
65
+ validateSettingsPresetDocument,
66
+ } from '../core/presetDocuments.js';
67
+
68
+ export const TOON_PRESET_DOCUMENT_TYPE = 'toonlab/toon-preset';
69
+ export const TOON_PRESET_SCHEMA_VERSION = 1;
70
+
71
+ export const TOON_PRESET_IDS = Object.freeze({
72
+ callMeSensei: 'call_me_sensei',
73
+ default: 'default',
74
+ showcase: 'showcase',
75
+ });
76
+
77
+ const DEFAULT_TOON_PRESET_SETTINGS = Object.freeze({});
78
+
79
+ export const TOON_SETTING_DEFAULTS = Object.freeze({
80
+ alpha: DEFAULT_ALPHA_SETTINGS,
81
+ averageShadow: DEFAULT_AVERAGE_SHADOW_SETTINGS,
82
+ baseTexture: DEFAULT_BASE_TEXTURE_SETTINGS,
83
+ celShade: DEFAULT_CEL_SHADE_SETTINGS,
84
+ contactShadow: DEFAULT_CONTACT_SHADOW_SETTINGS,
85
+ eyeHighlight: DEFAULT_EYE_HIGHLIGHT_SETTINGS,
86
+ faceLighting: DEFAULT_FACE_LIGHTING_SETTINGS,
87
+ fur: DEFAULT_FUR_SETTINGS,
88
+ glitter: DEFAULT_GLITTER_SETTINGS,
89
+ hairHighlight: DEFAULT_HAIR_HIGHLIGHT_SETTINGS,
90
+ perspectiveRemoval: DEFAULT_PERSPECTIVE_REMOVAL_SETTINGS,
91
+ sticker: DEFAULT_STICKER_SETTINGS,
92
+ indirectLight: DEFAULT_INDIRECT_LIGHT_SETTINGS,
93
+ localLights: DEFAULT_LOCAL_LIGHT_SETTINGS,
94
+ materialMaps: DEFAULT_MATERIAL_MAP_SETTINGS,
95
+ outline: DEFAULT_OUTLINE_SETTINGS,
96
+ rimLight: DEFAULT_RIM_LIGHT_SETTINGS,
97
+ sceneShadow: DEFAULT_SCENE_SHADOW_SETTINGS,
98
+ selfShadow: DEFAULT_SELF_SHADOW_SETTINGS,
99
+ shadowColor: DEFAULT_SHADOW_COLOR_SETTINGS,
100
+ skinTone: DEFAULT_SKIN_TONE_SETTINGS,
101
+ specular: DEFAULT_SPECULAR_SETTINGS,
102
+ });
103
+
104
+ export const TOON_SETTING_GROUPS = Object.freeze([
105
+ Object.freeze({
106
+ description: 'Preserves source texture, source material color, and saturation policy before toon lighting.',
107
+ id: 'baseTexture',
108
+ label: 'Base Texture',
109
+ }),
110
+ Object.freeze({
111
+ description: 'Classifies materials as skin, face, hair, eyes, costume, metal, transparent overlays, and outline.',
112
+ id: 'materialRoles',
113
+ label: 'Material Roles',
114
+ }),
115
+ Object.freeze({
116
+ description: 'Controls cutout, blend, opacity, eye overlay sorting, and transparent decoration behavior.',
117
+ id: 'alpha',
118
+ label: 'Alpha',
119
+ }),
120
+ Object.freeze({
121
+ description: 'Keeps skin and face shadows warm, readable, and separate from costume/hair shadows.',
122
+ id: 'skinTone',
123
+ label: 'Skin Tone',
124
+ }),
125
+ Object.freeze({
126
+ description: 'Overrides face-area cel response so noses, cheeks, and eyes do not receive harsh body shadows.',
127
+ id: 'faceLighting',
128
+ label: 'Face Lighting',
129
+ }),
130
+ Object.freeze({
131
+ description: 'Sets the primary directional cel band threshold, softness, and light-ignore amount.',
132
+ id: 'celShade',
133
+ label: 'Cel Shade',
134
+ }),
135
+ Object.freeze({
136
+ description: 'Tints and reshapes lit-to-shadow transitions and fully shadowed regions.',
137
+ id: 'shadowColor',
138
+ label: 'Shadow Color',
139
+ }),
140
+ Object.freeze({
141
+ description: 'Controls how renderer shadow maps darken character materials.',
142
+ id: 'sceneShadow',
143
+ label: 'Scene Shadows',
144
+ }),
145
+ Object.freeze({
146
+ description: 'Controls character-local self-shadow proxy contribution until a dedicated self-shadow pass exists.',
147
+ id: 'selfShadow',
148
+ label: 'Self Shadow',
149
+ }),
150
+ Object.freeze({
151
+ description: 'Adds averaged shadow visibility used for softer role-specific shadow damping.',
152
+ id: 'averageShadow',
153
+ label: 'Average Shadow',
154
+ }),
155
+ Object.freeze({
156
+ description: 'Mixes ambient, hemisphere, and environment light into toon shading.',
157
+ id: 'indirectLight',
158
+ label: 'Indirect Light',
159
+ }),
160
+ Object.freeze({
161
+ description: 'Controls point and spot light response for characters without overpowering cel bands.',
162
+ id: 'localLights',
163
+ label: 'Local Lights',
164
+ }),
165
+ Object.freeze({
166
+ description: 'Adds view-dependent edge light that can be blocked or softened by shadow.',
167
+ id: 'rimLight',
168
+ label: 'Rim Light',
169
+ }),
170
+ Object.freeze({
171
+ description: 'Adds thin screen-space contact shadows (hair-on-face, arm-on-torso) from the depth prepass.',
172
+ id: 'contactShadow',
173
+ label: 'Contact Shadow',
174
+ }),
175
+ Object.freeze({
176
+ description: 'Adds role-aware stylized highlights and optional source specular masks.',
177
+ id: 'specular',
178
+ label: 'Specular',
179
+ }),
180
+ Object.freeze({
181
+ description: 'Adds hair-specific highlight bands, optional anisotropic strand response, and source masks.',
182
+ id: 'hairHighlight',
183
+ label: 'Hair Highlight',
184
+ }),
185
+ Object.freeze({
186
+ description: 'Adds role-aware eye/catchlight boosts and optional source masks.',
187
+ id: 'eyeHighlight',
188
+ label: 'Eye Highlight',
189
+ }),
190
+ Object.freeze({
191
+ description: 'Routes source normal, AO, emissive, MatCap, ramp, detail, roughness, metalness, and specular maps.',
192
+ id: 'materialMaps',
193
+ label: 'Material Maps',
194
+ }),
195
+ Object.freeze({
196
+ description: 'Controls the inverted-hull outline pass, including role-specific widths and colors.',
197
+ id: 'outline',
198
+ label: 'Outlines',
199
+ }),
200
+ Object.freeze({
201
+ description: 'Adds procedural view-dependent sparkles for sparkly costumes and accessories. Off by default.',
202
+ id: 'glitter',
203
+ label: 'Glitter',
204
+ }),
205
+ Object.freeze({
206
+ description: 'Blends a decal/overlay texture into the albedo before lighting (ice, tattoos, damage). Off by default.',
207
+ id: 'sticker',
208
+ label: 'Sticker',
209
+ }),
210
+ Object.freeze({
211
+ description: 'Flattens perspective around the tracked head for anime-portrait closeups. Off by default.',
212
+ id: 'perspectiveRemoval',
213
+ label: 'Perspective Removal',
214
+ }),
215
+ Object.freeze({
216
+ description: 'Opt-in shell fur for matched materials (collars, trims, animal parts). Off by default.',
217
+ id: 'fur',
218
+ label: 'Fur',
219
+ }),
220
+ ]);
221
+
222
+ export const TOON_SETTING_GROUP_METADATA = Object.freeze(
223
+ Object.fromEntries(TOON_SETTING_GROUPS.map((group) => [group.id, group])),
224
+ );
225
+
226
+ const SELECT_FIELD_OPTIONS = Object.freeze({
227
+ 'baseTexture.materialColorMode': Object.values(BASE_TEXTURE_MATERIAL_COLOR_MODES),
228
+ 'baseTexture.saturationMode': Object.values(BASE_TEXTURE_SATURATION_MODES),
229
+ 'eyeHighlight.maskChannel': [0, 1, 2, 3],
230
+ 'eyeHighlight.sourceMaskMode': Object.values(EYE_HIGHLIGHT_SOURCE_MASK_MODES),
231
+ 'faceLighting.headSpaceMode': Object.values(FACE_HEAD_SPACE_MODES),
232
+ 'hairHighlight.maskChannel': [0, 1, 2, 3],
233
+ 'hairHighlight.mode': [HAIR_HIGHLIGHT_MODES.legacy, HAIR_HIGHLIGHT_MODES.anisotropic],
234
+ 'hairHighlight.sourceMaskMode': Object.values(HAIR_HIGHLIGHT_SOURCE_MASK_MODES),
235
+ 'hairHighlight.uvBandAxis': [0, 1],
236
+ 'hairHighlight.uvPreset': ['center', 'full', 'left', 'right', 'vertical', 'wide'],
237
+ 'materialMaps.sourceMode': Object.values(MATERIAL_MAP_SOURCE_MODES),
238
+ 'rimLight.mode': Object.values(RIM_LIGHT_MODES),
239
+ 'selfShadow.sourceMode': Object.values(SELF_SHADOW_SOURCE_MODES),
240
+ 'sticker.blendMode': Object.values(STICKER_BLEND_MODES),
241
+ 'sticker.uvChannel': [0, 1],
242
+ 'glitter.uvChannel': [0, 1],
243
+ 'specular.directionMode': Object.values(SPECULAR_DIRECTION_MODES),
244
+ 'specular.maskChannel': [0, 1, 2, 3],
245
+ 'specular.sourceMaskMode': Object.values(SPECULAR_SOURCE_MASK_MODES),
246
+ });
247
+
248
+ const SELECT_FIELD_OPTION_LABELS = Object.freeze({
249
+ 'baseTexture.materialColorMode': Object.freeze({
250
+ legacy: 'Compatibility',
251
+ source: 'Source Material Color',
252
+ texture: 'Texture Only',
253
+ white: 'White',
254
+ }),
255
+ 'baseTexture.saturationMode': Object.freeze({
256
+ custom: 'Custom',
257
+ legacy: 'Compatibility',
258
+ source: 'Source Saturation',
259
+ }),
260
+ 'eyeHighlight.maskChannel': Object.freeze({
261
+ 0: 'Red',
262
+ 1: 'Green',
263
+ 2: 'Blue',
264
+ 3: 'Alpha',
265
+ }),
266
+ 'eyeHighlight.sourceMaskMode': Object.freeze({
267
+ off: 'Off',
268
+ source: 'Source Material',
269
+ }),
270
+ 'hairHighlight.maskChannel': Object.freeze({
271
+ 0: 'Red',
272
+ 1: 'Green',
273
+ 2: 'Blue',
274
+ 3: 'Alpha',
275
+ }),
276
+ 'hairHighlight.mode': Object.freeze({
277
+ anisotropic: 'Strand Highlight',
278
+ legacy: 'Soft Highlight',
279
+ }),
280
+ 'hairHighlight.sourceMaskMode': Object.freeze({
281
+ off: 'Off',
282
+ source: 'Source Material',
283
+ }),
284
+ 'hairHighlight.uvBandAxis': Object.freeze({
285
+ 0: 'U / Horizontal',
286
+ 1: 'V / Vertical',
287
+ }),
288
+ 'materialMaps.sourceMode': Object.freeze({
289
+ off: 'Off',
290
+ source: 'Source Maps',
291
+ }),
292
+ 'faceLighting.headSpaceMode': Object.freeze({
293
+ headBone: 'Head Bone (Tracked)',
294
+ static: 'Static Proxy Normal',
295
+ }),
296
+ 'rimLight.mode': Object.freeze({
297
+ depthTexture: 'Depth Texture (Screen Space)',
298
+ fresnel: 'Fresnel (Classic)',
299
+ }),
300
+ 'selfShadow.sourceMode': Object.freeze({
301
+ 0: 'Off',
302
+ 1: 'Scene Shadow Proxy',
303
+ 2: 'Character Shadow Pass',
304
+ }),
305
+ 'specular.directionMode': Object.freeze({
306
+ light: 'Light Direction',
307
+ view: 'View Direction (Stable)',
308
+ }),
309
+ 'sticker.blendMode': Object.freeze({
310
+ add: 'Additive',
311
+ multiply: 'Multiply',
312
+ normal: 'Alpha Blend',
313
+ }),
314
+ 'sticker.uvChannel': Object.freeze({
315
+ 0: 'UV',
316
+ 1: 'UV2',
317
+ }),
318
+ 'glitter.uvChannel': Object.freeze({
319
+ 0: 'UV',
320
+ 1: 'UV2',
321
+ }),
322
+ 'specular.maskChannel': Object.freeze({
323
+ 0: 'Red',
324
+ 1: 'Green',
325
+ 2: 'Blue',
326
+ 3: 'Alpha',
327
+ }),
328
+ 'specular.sourceMaskMode': Object.freeze({
329
+ off: 'Off',
330
+ source: 'Source Material',
331
+ }),
332
+ });
333
+
334
+ const FIELD_TYPE_OVERRIDES = Object.freeze({
335
+ 'faceLighting.faceProxyNormal': 'vector3',
336
+ 'hairHighlight.direction': 'vector3',
337
+ 'materialMaps.detailRepeat': 'vector2',
338
+ 'materialMaps.normalScale': 'vector2',
339
+ 'shadowColor.lowSaturationFallbackColor': 'vector4',
340
+ });
341
+
342
+ const NON_SERIALIZABLE_FIELDS = Object.freeze(new Set([
343
+ 'eyeHighlight.maskMap',
344
+ 'hairHighlight.maskMap',
345
+ 'specular.maskMap',
346
+ ]));
347
+
348
+ const FIELD_LABEL_OVERRIDES = Object.freeze({
349
+ aoStrength: 'AO Strength',
350
+ uvBandAxis: 'UV Band Axis',
351
+ uvBandCenter: 'UV Band Center',
352
+ uvBandHalfWidth: 'UV Band Width',
353
+ uvPreset: 'UV Preset',
354
+ });
355
+
356
+ function labelFromFieldName(key) {
357
+ if (FIELD_LABEL_OVERRIDES[key]) return FIELD_LABEL_OVERRIDES[key];
358
+ return key
359
+ .replace(/([A-Z])/g, ' $1')
360
+ .replace(/^./, (char) => char.toUpperCase());
361
+ }
362
+
363
+ function descriptionForField(group, key) {
364
+ const label = labelFromFieldName(key).toLowerCase();
365
+ if (key === 'enabled') return `Turns ${group.label.toLowerCase()} on or off.`;
366
+ if (key.includes('Intensity')) return `Controls how strongly ${label} contributes.`;
367
+ if (key.includes('Strength')) return `Controls the blend strength for ${label}.`;
368
+ if (key.includes('MinLight') || key.includes('MinimumIndirectLight')) return `Sets the minimum light floor for ${label}.`;
369
+ if (key.includes('Softness') || key.includes('Range')) return `Controls transition softness for ${label}.`;
370
+ if (key.includes('MidPoint')) return `Moves the center point for ${label}.`;
371
+ if (key.includes('Tint') || key.includes('Color')) return `Sets the color used by ${label}.`;
372
+ if (key.includes('Power')) return `Controls the sharpness of ${label}.`;
373
+ if (key.includes('Mode') || key.endsWith('Preset')) return `Selects the policy used by ${label}.`;
374
+ if (key.includes('Width')) return `Controls the width used by ${label}.`;
375
+ if (key.includes('Order')) return `Controls transparent draw order for ${label}.`;
376
+ return `Configures ${label} for ${group.label.toLowerCase()}.`;
377
+ }
378
+
379
+ // Explicit ranges for fields whose names don't fit the inference patterns in
380
+ // rangeForNumberField. Keyed by field id ('group.key'). Add an entry here
381
+ // whenever a new numeric setting would otherwise get a nonsense slider —
382
+ // name-pattern inference is a fallback, not a contract.
383
+ const FIELD_RANGE_OVERRIDES = Object.freeze({
384
+ 'outline.referenceDistance': { max: 20, min: 0.5, step: 0.1 },
385
+ 'outline.referenceFov': { max: 120, min: 10, step: 1 },
386
+ 'outline.widthFadeDistance': { max: 100, min: 1, step: 0.5 },
387
+ 'rimLight.depthFadeStartDistance': { max: 100, min: 1, step: 0.5 },
388
+ });
389
+
390
+ function rangeForNumberField(key, value, id = '') {
391
+ if (FIELD_RANGE_OVERRIDES[id]) return FIELD_RANGE_OVERRIDES[id];
392
+ if (key.includes('Order')) return { max: 30, min: -30, step: 1 };
393
+ if (key.includes('Power')) return { max: 128, min: 1, step: 1 };
394
+ if (key.includes('Offset') && !key.includes('Depth')) return { max: 1, min: -1, step: 0.005 };
395
+ if (key.includes('HueOffset')) return { max: 0.25, min: -0.25, step: 0.001 };
396
+ if (key.includes('MaxDirectLight')) return { max: 8, min: 0, step: 0.05 };
397
+ if (key.includes('Width') || key.includes('Thickness')) return { max: 0.08, min: 0, step: 0.0005 };
398
+ if (key.includes('Brightness') || key.includes('ValueMul')) return { max: 2, min: 0, step: 0.01 };
399
+ if (key.includes('Saturation')) return { max: 2, min: 0, step: 0.01 };
400
+ if (key.includes('Intensity') || key.includes('Strength')) return { max: 8, min: 0, step: 0.01 };
401
+ if (key.includes('Softness') || key.includes('Range')) return { max: 1, min: 0, step: 0.001 };
402
+ if (key.includes('MidPoint')) return { max: 1, min: -1, step: 0.005 };
403
+ if (key.includes('MinLight') || key.includes('MinimumIndirectLight') || key.includes('Floor')) return { max: 1, min: 0, step: 0.01 };
404
+ if (key.includes('Cutoff') || key.includes('Threshold') || key.includes('Opacity')) return { max: 1, min: 0, step: 0.01 };
405
+ if (key.includes('Scale') || key.includes('Repeat')) return { max: 4, min: 0, step: 0.01 };
406
+ if (value >= 0 && value <= 1) return { max: 1, min: 0, step: 0.01 };
407
+ return { max: Math.max(2, value * 2), min: Math.min(0, value * 0.5), step: 0.01 };
408
+ }
409
+
410
+ function isColorField(key, value) {
411
+ if (!Array.isArray(value) || value.length !== 3) return false;
412
+ return /(color|tint|ambient|emissive)/i.test(key);
413
+ }
414
+
415
+ function fieldTypeFor(groupId, key, value) {
416
+ const fieldId = `${groupId}.${key}`;
417
+ if (FIELD_TYPE_OVERRIDES[fieldId]) return FIELD_TYPE_OVERRIDES[fieldId];
418
+ if (SELECT_FIELD_OPTIONS[fieldId]) return 'select';
419
+ if (typeof value === 'boolean') return 'boolean';
420
+ if (typeof value === 'number') return 'number';
421
+ if (isColorField(key, value)) return 'color';
422
+ if (Array.isArray(value) && value.length === 2) return 'vector2';
423
+ if (Array.isArray(value) && value.length === 3) return 'vector3';
424
+ if (Array.isArray(value) && value.length === 4) return 'vector4';
425
+ if (typeof value === 'string') return 'text';
426
+ if (value === null && /map|texture/i.test(key)) return 'texture';
427
+ return 'object';
428
+ }
429
+
430
+ function createFieldMetadata(group, key, value) {
431
+ const id = `${group.id}.${key}`;
432
+ const type = fieldTypeFor(group.id, key, value);
433
+ const options = SELECT_FIELD_OPTIONS[id] ?? null;
434
+ const optionLabels = SELECT_FIELD_OPTION_LABELS[id] ?? null;
435
+ const serializable = !NON_SERIALIZABLE_FIELDS.has(id) && type !== 'texture' && type !== 'object';
436
+ return Object.freeze({
437
+ defaultValue: cloneSerializableValue(value),
438
+ description: descriptionForField(group, key),
439
+ group: group.id,
440
+ id,
441
+ key,
442
+ label: labelFromFieldName(key),
443
+ optionLabels,
444
+ options,
445
+ range: type === 'number' ? rangeForNumberField(key, value, id) : null,
446
+ serializable,
447
+ type,
448
+ });
449
+ }
450
+
451
+ export const TOON_SETTING_FIELD_SCHEMA = Object.freeze(
452
+ Object.fromEntries(
453
+ TOON_SETTING_GROUPS.map((group) => [
454
+ group.id,
455
+ Object.freeze(
456
+ Object.fromEntries(
457
+ Object.entries(TOON_SETTING_DEFAULTS[group.id] ?? {})
458
+ .map(([key, value]) => [key, createFieldMetadata(group, key, value)]),
459
+ ),
460
+ ),
461
+ ]),
462
+ ),
463
+ );
464
+
465
+ const BUILT_IN_TOON_PRESETS = Object.freeze({
466
+ [TOON_PRESET_IDS.default]: Object.freeze({
467
+ description: 'Current stable library baseline. This intentionally mirrors the demo shader behavior today.',
468
+ label: 'Default',
469
+ settings: DEFAULT_TOON_PRESET_SETTINGS,
470
+ }),
471
+ [TOON_PRESET_IDS.callMeSensei]: Object.freeze({
472
+ description: 'Team-maintained house style preset. It starts identical to Default and can evolve separately.',
473
+ label: 'Call Me Sensei',
474
+ settings: { ...DEFAULT_TOON_PRESET_SETTINGS },
475
+ }),
476
+ [TOON_PRESET_IDS.showcase]: Object.freeze({
477
+ description: 'Every optional feature turned on with demo values — a one-click way to see the full feature set. Not a production look.',
478
+ label: 'Showcase (All Features)',
479
+ settings: Object.freeze({
480
+ // Default-on features stay at their defaults (depth rim, contact shadow,
481
+ // self shadow, head-space face, edge AA, measured average shadow);
482
+ // this preset flips on everything that is opt-in.
483
+ // Fur must read as strands, not per-pixel speckle: at full-body framing
484
+ // that means longer shells and coarser tufts. It still covers the whole
485
+ // 'costume' role (dress AND tights) because role-level opt-in is the
486
+ // only model-agnostic hook — production use should target specific
487
+ // materials via fur.materials patterns or userData.toonFur instead.
488
+ fur: {
489
+ density: 1.2,
490
+ enabled: true,
491
+ length: 0.03,
492
+ roles: ['costume'],
493
+ rootOffset: -0.35,
494
+ shellCount: 12,
495
+ },
496
+ glitter: {
497
+ density: 0.5,
498
+ enabled: true,
499
+ intensity: 0.7,
500
+ size: 1.4,
501
+ },
502
+ perspectiveRemoval: {
503
+ amount: 0.5,
504
+ enabled: true,
505
+ },
506
+ specular: {
507
+ directionMode: 'view',
508
+ },
509
+ }),
510
+ }),
511
+ });
512
+
513
+ const TOON_PRESET_ALIASES = Object.freeze({
514
+ '': TOON_PRESET_IDS.default,
515
+ baseline: TOON_PRESET_IDS.default,
516
+ current: TOON_PRESET_IDS.default,
517
+ default: TOON_PRESET_IDS.default,
518
+ demo: TOON_PRESET_IDS.default,
519
+ reference: TOON_PRESET_IDS.default,
520
+ callmesensei: TOON_PRESET_IDS.callMeSensei,
521
+ call_me_sensei: TOON_PRESET_IDS.callMeSensei,
522
+ cms: TOON_PRESET_IDS.callMeSensei,
523
+ sensei: TOON_PRESET_IDS.callMeSensei,
524
+ all: TOON_PRESET_IDS.showcase,
525
+ allfeatures: TOON_PRESET_IDS.showcase,
526
+ everything: TOON_PRESET_IDS.showcase,
527
+ showcase: TOON_PRESET_IDS.showcase,
528
+ });
529
+
530
+ const toonPresetRegistry = new Map(Object.entries(BUILT_IN_TOON_PRESETS));
531
+
532
+ function cleanObject(value) {
533
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
534
+ }
535
+
536
+ function isPlainObject(value) {
537
+ return value && typeof value === 'object' && !Array.isArray(value);
538
+ }
539
+
540
+ function cloneSerializableValue(value) {
541
+ if (value?.isColor) return [value.r, value.g, value.b];
542
+ if (value?.isVector2) return [value.x, value.y];
543
+ if (value?.isVector3) return [value.x, value.y, value.z];
544
+ if (value?.isVector4) return [value.x, value.y, value.z, value.w];
545
+ if (Array.isArray(value)) return value.map((entry) => cloneSerializableValue(entry));
546
+ if (isPlainObject(value)) {
547
+ return Object.fromEntries(
548
+ Object.entries(value)
549
+ .filter(([, entry]) => typeof entry !== 'function')
550
+ .map(([key, entry]) => [key, cloneSerializableValue(entry)]),
551
+ );
552
+ }
553
+ return value;
554
+ }
555
+
556
+ function normalizePresetKey(value) {
557
+ return String(value ?? '')
558
+ .trim()
559
+ .replace(/[\s-]+/g, '_')
560
+ .toLowerCase();
561
+ }
562
+
563
+ function normalizeAliasKey(value) {
564
+ return normalizePresetKey(value).replace(/_/g, '');
565
+ }
566
+
567
+ export function normalizeToonPresetName(value) {
568
+ const normalized = normalizePresetKey(value);
569
+ const alias = TOON_PRESET_ALIASES[normalized] ?? TOON_PRESET_ALIASES[normalizeAliasKey(value)];
570
+ if (alias && toonPresetRegistry.has(alias)) return alias;
571
+ if (toonPresetRegistry.has(normalized)) return normalized;
572
+ return TOON_PRESET_IDS.default;
573
+ }
574
+
575
+ export function getToonPresetIds() {
576
+ return Array.from(toonPresetRegistry.keys());
577
+ }
578
+
579
+ export function getToonPresetMetadata(id = TOON_PRESET_IDS.default) {
580
+ const presetId = normalizeToonPresetName(id);
581
+ const preset = toonPresetRegistry.get(presetId) ?? BUILT_IN_TOON_PRESETS[TOON_PRESET_IDS.default];
582
+ return {
583
+ description: preset.description ?? '',
584
+ id: presetId,
585
+ label: preset.label ?? presetId,
586
+ };
587
+ }
588
+
589
+ export function getToonPresetOptions() {
590
+ return getToonPresetIds().map((id) => getToonPresetMetadata(id));
591
+ }
592
+
593
+ export function getToonSettingGroupMetadata(id) {
594
+ return id ? TOON_SETTING_GROUP_METADATA[id] ?? null : TOON_SETTING_GROUPS;
595
+ }
596
+
597
+ export function getToonSettingFieldSchema(groupId = null) {
598
+ return groupId ? TOON_SETTING_FIELD_SCHEMA[groupId] ?? {} : TOON_SETTING_FIELD_SCHEMA;
599
+ }
600
+
601
+ function mergeGroupSettings(presetSettings, source) {
602
+ const merged = {};
603
+ for (const group of TOON_SETTING_GROUPS) {
604
+ const presetValue = presetSettings[group.id];
605
+ const sourceValue = source[group.id];
606
+ if (presetValue === undefined && sourceValue === undefined) continue;
607
+ merged[group.id] = sourceValue === undefined ? presetValue : sourceValue;
608
+ }
609
+ return merged;
610
+ }
611
+
612
+ function sourceGroupKeys(source) {
613
+ return Object.keys(cleanObject(source)).filter((key) => key in TOON_SETTING_GROUP_METADATA);
614
+ }
615
+
616
+ function numberFromValue(value, fallback) {
617
+ const number = Number(value);
618
+ return Number.isFinite(number) ? number : fallback;
619
+ }
620
+
621
+ function booleanFromValue(value, fallback) {
622
+ if (typeof value === 'boolean') return value;
623
+ if (typeof value === 'string') {
624
+ const normalized = value.trim().toLowerCase();
625
+ if (['1', 'true', 'on', 'yes'].includes(normalized)) return true;
626
+ if (['0', 'false', 'off', 'no'].includes(normalized)) return false;
627
+ }
628
+ if (Number.isFinite(value)) return value !== 0;
629
+ return fallback;
630
+ }
631
+
632
+ function colorFromValue(value, fallback) {
633
+ const source = cloneSerializableValue(value);
634
+ if (typeof source === 'string') {
635
+ const hex = source.trim().replace(/^#/, '');
636
+ if (/^[0-9a-f]{6}$/i.test(hex)) {
637
+ return [
638
+ parseInt(hex.slice(0, 2), 16) / 255,
639
+ parseInt(hex.slice(2, 4), 16) / 255,
640
+ parseInt(hex.slice(4, 6), 16) / 255,
641
+ ];
642
+ }
643
+ }
644
+ if (Array.isArray(source) && source.length >= 3) {
645
+ return [
646
+ numberFromValue(source[0], fallback[0] ?? 1),
647
+ numberFromValue(source[1], fallback[1] ?? 1),
648
+ numberFromValue(source[2], fallback[2] ?? 1),
649
+ ];
650
+ }
651
+ if (isPlainObject(source)) {
652
+ return [
653
+ numberFromValue(source.r ?? source.x, fallback[0] ?? 1),
654
+ numberFromValue(source.g ?? source.y, fallback[1] ?? 1),
655
+ numberFromValue(source.b ?? source.z, fallback[2] ?? 1),
656
+ ];
657
+ }
658
+ return cloneSerializableValue(fallback);
659
+ }
660
+
661
+ function vectorFromValue(value, fallback, size) {
662
+ const source = cloneSerializableValue(value);
663
+ const keys = ['x', 'y', 'z', 'w'];
664
+ return Array.from({ length: size }, (_, index) => {
665
+ if (Array.isArray(source)) return numberFromValue(source[index], fallback[index] ?? 0);
666
+ if (isPlainObject(source)) return numberFromValue(source[keys[index]], fallback[index] ?? 0);
667
+ return fallback[index] ?? 0;
668
+ });
669
+ }
670
+
671
+ function selectFromValue(value, fallback, options) {
672
+ if (!options?.length) return value;
673
+ const normalized = String(value ?? '').trim().toLowerCase();
674
+ const match = options.find((option) => String(option).toLowerCase() === normalized);
675
+ if (match !== undefined) return match;
676
+ return fallback;
677
+ }
678
+
679
+ function coerceFieldValue(value, field) {
680
+ const fallback = field.defaultValue;
681
+ switch (field.type) {
682
+ case 'boolean':
683
+ return booleanFromValue(value, fallback);
684
+ case 'number':
685
+ return numberFromValue(value, fallback);
686
+ case 'select':
687
+ return selectFromValue(value, fallback, field.options);
688
+ case 'color':
689
+ return colorFromValue(value, fallback);
690
+ case 'vector2':
691
+ return vectorFromValue(value, fallback, 2);
692
+ case 'vector3':
693
+ return vectorFromValue(value, fallback, 3);
694
+ case 'vector4':
695
+ return vectorFromValue(value, fallback, 4);
696
+ case 'text':
697
+ return String(value ?? fallback);
698
+ default:
699
+ return cloneSerializableValue(value);
700
+ }
701
+ }
702
+
703
+ export function sanitizeToonPresetSettings(settings = {}) {
704
+ const source = cleanObject(settings);
705
+ const sanitized = {};
706
+
707
+ for (const group of TOON_SETTING_GROUPS) {
708
+ const groupSource = cleanObject(source[group.id]);
709
+ if (Object.keys(groupSource).length === 0) continue;
710
+
711
+ const fields = TOON_SETTING_FIELD_SCHEMA[group.id] ?? {};
712
+ const groupSettings = {};
713
+ for (const [key, value] of Object.entries(groupSource)) {
714
+ const field = fields[key];
715
+ if (!field?.serializable) continue;
716
+ groupSettings[key] = coerceFieldValue(value, field);
717
+ }
718
+
719
+ if (Object.keys(groupSettings).length > 0) sanitized[group.id] = groupSettings;
720
+ }
721
+
722
+ return sanitized;
723
+ }
724
+
725
+ function collectUnknownSettingKeys(settings = {}) {
726
+ const source = cleanObject(settings);
727
+ const warnings = [];
728
+ for (const [groupId, groupValue] of Object.entries(source)) {
729
+ if (!(groupId in TOON_SETTING_GROUP_METADATA)) {
730
+ warnings.push(`Unknown settings group "${groupId}" was ignored.`);
731
+ continue;
732
+ }
733
+ const fields = TOON_SETTING_FIELD_SCHEMA[groupId] ?? {};
734
+ for (const key of Object.keys(cleanObject(groupValue))) {
735
+ if (!fields[key]) warnings.push(`Unknown setting "${groupId}.${key}" was ignored.`);
736
+ else if (!fields[key].serializable) warnings.push(`Setting "${groupId}.${key}" is runtime-only and was ignored.`);
737
+ }
738
+ }
739
+ return warnings;
740
+ }
741
+
742
+ function migrateToonPresetDocument(input) {
743
+ const source = cleanObject(input);
744
+ const version = Number.isFinite(source.version) ? Math.round(source.version) : 0;
745
+ const groupKeys = sourceGroupKeys(source);
746
+ const settings = cleanObject(source.settings);
747
+
748
+ if (version <= 1) {
749
+ return {
750
+ description: source.description ?? '',
751
+ id: source.id ?? source.name ?? source.preset ?? '',
752
+ label: source.label ?? source.title ?? source.name ?? source.id ?? '',
753
+ settings: Object.keys(settings).length > 0
754
+ ? settings
755
+ : Object.fromEntries(groupKeys.map((key) => [key, source[key]])),
756
+ type: source.type ?? TOON_PRESET_DOCUMENT_TYPE,
757
+ version: TOON_PRESET_SCHEMA_VERSION,
758
+ };
759
+ }
760
+
761
+ return source;
762
+ }
763
+
764
+ export function validateToonPresetDocument(input) {
765
+ return validateSettingsPresetDocument(input, {
766
+ collectWarnings: collectUnknownSettingKeys,
767
+ documentType: TOON_PRESET_DOCUMENT_TYPE,
768
+ migrateDocument: migrateToonPresetDocument,
769
+ normalizeId: normalizePresetKey,
770
+ sanitizeSettings: sanitizeToonPresetSettings,
771
+ schemaVersion: TOON_PRESET_SCHEMA_VERSION,
772
+ });
773
+ }
774
+
775
+ export function parseToonPresetDocument(input) {
776
+ return parsePresetDocument(input, validateToonPresetDocument);
777
+ }
778
+
779
+ export function createToonPresetDocument(id, definition = {}) {
780
+ return createSettingsPresetDocument(id, definition, {
781
+ collectSettings: (source) => source.settings
782
+ ?? Object.fromEntries(sourceGroupKeys(source).map((key) => [key, source[key]])),
783
+ documentType: TOON_PRESET_DOCUMENT_TYPE,
784
+ schemaVersion: TOON_PRESET_SCHEMA_VERSION,
785
+ validateDocument: validateToonPresetDocument,
786
+ });
787
+ }
788
+
789
+ export function serializeToonPreset(idOrDocument, definition = {}, { pretty = true } = {}) {
790
+ return serializePresetDocument(idOrDocument, definition, {
791
+ argumentCount: arguments.length,
792
+ createDocument: createToonPresetDocument,
793
+ pretty,
794
+ });
795
+ }
796
+
797
+ export function getToonPresetDefinition(id = TOON_PRESET_IDS.default) {
798
+ const presetId = normalizeToonPresetName(id);
799
+ const preset = toonPresetRegistry.get(presetId) ?? BUILT_IN_TOON_PRESETS[TOON_PRESET_IDS.default];
800
+ return createToonPresetDocument(presetId, preset);
801
+ }
802
+
803
+ export function registerToonPreset(id, definition = {}, { overwrite = false } = {}) {
804
+ const document = createToonPresetDocument(id, definition);
805
+ const presetId = document.id;
806
+ if (!presetId) throw new Error('A toon preset id is required.');
807
+ if (!overwrite && toonPresetRegistry.has(presetId)) {
808
+ throw new Error(`Toon preset "${presetId}" already exists.`);
809
+ }
810
+
811
+ toonPresetRegistry.set(presetId, {
812
+ description: document.description,
813
+ label: document.label,
814
+ settings: mergeGroupSettings(document.settings, {}),
815
+ version: document.version,
816
+ });
817
+ return getToonPresetMetadata(presetId);
818
+ }
819
+
820
+ export function registerSerializedToonPreset(input, options = {}) {
821
+ const result = parseToonPresetDocument(input);
822
+ if (!result.ok) throw new Error(result.errors.join(' '));
823
+ return registerToonPreset(result.value.id, result.value, { overwrite: options.overwrite ?? true });
824
+ }
825
+
826
+ export function createToonSettings(options = {}) {
827
+ const source = typeof options === 'string' ? { preset: options } : cleanObject(options);
828
+ const presetId = normalizeToonPresetName(source.preset ?? source.profile ?? source.style);
829
+ const preset = toonPresetRegistry.get(presetId) ?? BUILT_IN_TOON_PRESETS[TOON_PRESET_IDS.default];
830
+ const groupInput = mergeGroupSettings(cleanObject(preset.settings), source);
831
+
832
+ return {
833
+ alpha: createAlphaSettings(groupInput.alpha),
834
+ averageShadow: createAverageShadowSettings(groupInput.averageShadow),
835
+ baseTexture: createBaseTextureSettings(groupInput.baseTexture),
836
+ celShade: createCelShadeSettings(groupInput.celShade),
837
+ contactShadow: createContactShadowSettings(groupInput.contactShadow),
838
+ eyeHighlight: createEyeHighlightSettings(groupInput.eyeHighlight),
839
+ faceLighting: createFaceLightingSettings(groupInput.faceLighting),
840
+ fur: createFurSettings(groupInput.fur),
841
+ glitter: createGlitterSettings(groupInput.glitter),
842
+ hairHighlight: createHairHighlightSettings(groupInput.hairHighlight),
843
+ perspectiveRemoval: createPerspectiveRemovalSettings(groupInput.perspectiveRemoval),
844
+ sticker: createStickerSettings(groupInput.sticker),
845
+ indirectLight: createIndirectLightSettings(groupInput.indirectLight),
846
+ localLights: createLocalLightSettings(groupInput.localLights),
847
+ materialMaps: createMaterialMapSettings(groupInput.materialMaps),
848
+ materialRoles: groupInput.materialRoles ?? null,
849
+ outline: createOutlineSettings(groupInput.outline),
850
+ preset: presetId,
851
+ presetDescription: preset.description ?? '',
852
+ presetLabel: preset.label ?? presetId,
853
+ rimLight: createRimLightSettings(groupInput.rimLight),
854
+ sceneShadow: createSceneShadowSettings(groupInput.sceneShadow),
855
+ selfShadow: createSelfShadowSettings(groupInput.selfShadow),
856
+ shadowColor: createShadowColorSettings(groupInput.shadowColor),
857
+ skinTone: createSkinToneSettings(groupInput.skinTone),
858
+ specular: createSpecularSettings(groupInput.specular),
859
+ };
860
+ }