@call-me-sensei/toonlab 0.2.0 → 0.3.0

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 (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +739 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,319 @@
1
+ import { createLightingLook, createLightingRecipe } from './lightingDocuments.js';
2
+ import { createLightDescriptor } from './lightDescriptors.js';
3
+ import {
4
+ clamp,
5
+ cloneJson,
6
+ deepFreeze,
7
+ finite,
8
+ isPlainObject,
9
+ mergePlain,
10
+ slug,
11
+ } from './utils.js';
12
+
13
+ const QUALITY_TYPES = Object.freeze([
14
+ 'ambient',
15
+ 'hemisphere',
16
+ 'directional',
17
+ 'point',
18
+ 'spot',
19
+ 'rectArea',
20
+ 'discArea',
21
+ 'tubeArea',
22
+ ]);
23
+
24
+ const DEFAULT_TYPE_CAPS = Object.freeze({
25
+ ambient: 2,
26
+ directional: 2,
27
+ discArea: 4,
28
+ hemisphere: 2,
29
+ point: 12,
30
+ rectArea: 4,
31
+ spot: 8,
32
+ tubeArea: 4,
33
+ });
34
+
35
+ /** Creates a complete runtime budget profile from a partial object. */
36
+ export function createLightingQualityProfile(options = {}) {
37
+ const source = isPlainObject(options) ? options : {};
38
+ const typeCaps = isPlainObject(source.maxLightsByType) ? source.maxLightsByType : {};
39
+ return {
40
+ allowAreaLights: source.allowAreaLights === undefined ? true : Boolean(source.allowAreaLights),
41
+ allowCookies: source.allowCookies === undefined ? true : Boolean(source.allowCookies),
42
+ description: String(source.description ?? 'Custom lighting quality profile.'),
43
+ id: slug(source.id ?? source.label, 'custom'),
44
+ label: String(source.label ?? 'Custom'),
45
+ maxDistance: Math.max(finite(source.maxDistance, 250), 0),
46
+ maxLights: Math.round(clamp(finite(source.maxLights, 24), 0, 1024)),
47
+ maxLightsByType: Object.fromEntries(QUALITY_TYPES.map((type) => [
48
+ type,
49
+ Math.round(clamp(finite(typeCaps[type], DEFAULT_TYPE_CAPS[type]), 0, 1024)),
50
+ ])),
51
+ maxShadowedLights: Math.round(clamp(finite(source.maxShadowedLights, 4), 0, 128)),
52
+ maxShadowMapPixels: Math.round(Math.max(finite(source.maxShadowMapPixels, 8_388_608), 0)),
53
+ shadowMapSizeScale: clamp(finite(source.shadowMapSizeScale, 1), 0.125, 4),
54
+ };
55
+ }
56
+
57
+ const QUALITY_DEFINITIONS = {
58
+ mobile: {
59
+ allowAreaLights: false,
60
+ allowCookies: false,
61
+ description: 'Conservative local-light and shadow caps for mobile and integrated GPUs.',
62
+ label: 'Mobile',
63
+ maxDistance: 55,
64
+ maxLights: 8,
65
+ maxLightsByType: { directional: 1, discArea: 0, point: 4, rectArea: 0, spot: 2, tubeArea: 0 },
66
+ maxShadowedLights: 1,
67
+ maxShadowMapPixels: 1_048_576,
68
+ shadowMapSizeScale: 0.5,
69
+ },
70
+ balanced: {
71
+ description: 'Portable default for WebGL2 and WebGPU game scenes.',
72
+ label: 'Balanced',
73
+ maxDistance: 120,
74
+ maxLights: 16,
75
+ maxLightsByType: { directional: 2, discArea: 2, point: 8, rectArea: 2, spot: 4, tubeArea: 2 },
76
+ maxShadowedLights: 3,
77
+ maxShadowMapPixels: 6_291_456,
78
+ shadowMapSizeScale: 1,
79
+ },
80
+ high: {
81
+ description: 'Expanded WebGPU-oriented budget for desktop game scenes.',
82
+ label: 'High',
83
+ maxDistance: 250,
84
+ maxLights: 40,
85
+ maxLightsByType: { directional: 2, discArea: 6, point: 24, rectArea: 6, spot: 12, tubeArea: 6 },
86
+ maxShadowedLights: 8,
87
+ maxShadowMapPixels: 25_165_824,
88
+ shadowMapSizeScale: 1,
89
+ },
90
+ cinematic: {
91
+ description: 'Large offline-lookdev budget; profile the target hardware before shipping.',
92
+ label: 'Cinematic',
93
+ maxDistance: 500,
94
+ maxLights: 96,
95
+ maxLightsByType: { directional: 4, discArea: 16, point: 64, rectArea: 16, spot: 32, tubeArea: 16 },
96
+ maxShadowedLights: 16,
97
+ maxShadowMapPixels: 67_108_864,
98
+ shadowMapSizeScale: 2,
99
+ },
100
+ };
101
+
102
+ export const LIGHTING_QUALITY_PRESETS = deepFreeze(Object.fromEntries(
103
+ Object.entries(QUALITY_DEFINITIONS).map(([id, definition]) => [
104
+ id,
105
+ createLightingQualityProfile({ ...definition, id }),
106
+ ]),
107
+ ));
108
+
109
+ const LUMINAIRE_DEFINITIONS = {
110
+ warm_bulb: {
111
+ description: 'A warm omnidirectional household bulb.',
112
+ label: 'Warm Bulb',
113
+ light: { color: { temperatureKelvin: 2700 }, distance: 16, intensity: { unit: 'lumens', value: 800 }, maxDistance: 24, type: 'point' },
114
+ },
115
+ paper_lantern: {
116
+ description: 'A warm, soft practical lantern for streets and interiors.',
117
+ label: 'Paper Lantern',
118
+ light: { artistic: { role: 'practical', shadowTint: [0.4, 0.25, 0.32] }, color: { temperatureKelvin: 2200 }, distance: 13, intensity: { unit: 'lumens', value: 600 }, maxDistance: 20, type: 'point' },
119
+ },
120
+ torch_spot: {
121
+ description: 'A focused warm source suitable for flame and magic emitters.',
122
+ label: 'Torch Spot',
123
+ light: { angle: 0.78, color: { temperatureKelvin: 1900 }, distance: 18, intensity: { unit: 'lumens', value: 1100 }, maxDistance: 28, penumbra: 0.65, type: 'spot' },
124
+ },
125
+ window_softbox: {
126
+ description: 'A broad cool window emitter represented by a rectangular area light.',
127
+ label: 'Window Softbox',
128
+ light: { color: { temperatureKelvin: 7600 }, height: 2.8, intensity: { unit: 'nits', value: 5 }, maxDistance: 22, type: 'rectArea', width: 1.8 },
129
+ },
130
+ neon_tube: {
131
+ description: 'A saturated tube source with a portable rect-area approximation.',
132
+ label: 'Neon Tube',
133
+ light: { color: [0.25, 0.72, 1], height: 0.08, intensity: { unit: 'nits', value: 15 }, maxDistance: 18, type: 'tubeArea', width: 2.4 },
134
+ },
135
+ disc_softbox: {
136
+ description: 'A circular studio emitter with a portable rect-area approximation.',
137
+ label: 'Disc Softbox',
138
+ light: { color: { temperatureKelvin: 5600 }, height: 1.4, intensity: { unit: 'nits', value: 8 }, maxDistance: 25, type: 'discArea', width: 1.4 },
139
+ },
140
+ };
141
+
142
+ export const LIGHTING_LUMINAIRE_PRESETS = deepFreeze(Object.fromEntries(
143
+ Object.entries(LUMINAIRE_DEFINITIONS).map(([id, definition]) => [id, {
144
+ description: definition.description,
145
+ id,
146
+ label: definition.label,
147
+ light: createLightDescriptor({ ...definition.light, id, name: definition.label }),
148
+ }]),
149
+ ));
150
+
151
+ function threePointLights() {
152
+ return [
153
+ { artistic: { role: 'ambient' }, color: [0.45, 0.55, 0.8], id: 'studio-ambient', intensity: 0.16, name: 'Studio Ambient', type: 'ambient' },
154
+ { angle: 0.66, artistic: { role: 'key' }, castShadow: true, color: { temperatureKelvin: 4800 }, id: 'key', intensity: { unit: 'lumens', value: 2200 }, name: 'Key', position: [3, 4.5, 4], priority: 10, shadow: { enabled: true, mapSize: 2048, priority: 10 }, target: [0, 1.4, 0], type: 'spot' },
155
+ { angle: 0.88, artistic: { role: 'fill' }, color: { temperatureKelvin: 7200 }, id: 'fill', intensity: { unit: 'lumens', value: 900 }, name: 'Fill', position: [-3, 2.8, 2], priority: 7, target: [0, 1.2, 0], type: 'spot' },
156
+ { angle: 0.6, artistic: { role: 'rim', rimInfluence: 1 }, color: [0.58, 0.72, 1], id: 'rim', intensity: { unit: 'lumens', value: 1500 }, name: 'Rim', position: [1.5, 3.8, -4], priority: 8, target: [0, 1.6, 0], type: 'spot' },
157
+ ];
158
+ }
159
+
160
+ function nightMarketLights() {
161
+ const colors = [[1, 0.3, 0.18], [1, 0.72, 0.2], [0.18, 0.65, 1], [0.9, 0.22, 0.72]];
162
+ const lights = [
163
+ { color: [0.08, 0.12, 0.24], id: 'night-ambient', intensity: 0.16, name: 'Night Ambient', type: 'ambient' },
164
+ { color: [0.2, 0.34, 0.65], groundColor: [0.04, 0.025, 0.05], id: 'night-sky', intensity: 0.32, name: 'Night Sky', type: 'hemisphere' },
165
+ ];
166
+ for (let index = 0; index < 12; index += 1) {
167
+ const side = index % 2 === 0 ? -1 : 1;
168
+ lights.push({
169
+ color: colors[index % colors.length],
170
+ distance: 12,
171
+ id: `market-lantern-${index + 1}`,
172
+ intensity: { unit: 'lumens', value: 500 + (index % 3) * 120 },
173
+ maxDistance: 38,
174
+ name: `Market Lantern ${index + 1}`,
175
+ position: [side * (3 + (index % 3) * 2.4), 2.6 + (index % 2) * 0.5, -9 + Math.floor(index / 2) * 3.4],
176
+ priority: 2,
177
+ type: 'point',
178
+ });
179
+ }
180
+ return lights;
181
+ }
182
+
183
+ const RIG_DEFINITIONS = {
184
+ outdoor_sun: {
185
+ description: 'Balanced sky fill and one aligned shadow-casting sun.',
186
+ label: 'Outdoor Sun',
187
+ lights: [
188
+ { artistic: { role: 'ambient' }, color: [0.55, 0.68, 0.92], id: 'outdoor-ambient', intensity: 0.22, name: 'Outdoor Ambient', type: 'ambient' },
189
+ { color: [0.62, 0.76, 1], groundColor: [0.22, 0.18, 0.16], id: 'sky-fill', intensity: 0.65, name: 'Sky Fill', type: 'hemisphere' },
190
+ { artistic: { role: 'key', shadowTint: [0.48, 0.55, 0.76] }, castShadow: true, color: { temperatureKelvin: 5600 }, id: 'sun', intensity: { unit: 'lux', value: 3.2 }, name: 'Sun', position: [-36, 64, 28], priority: 100, shadow: { enabled: true, far: 260, mapSize: 2048, priority: 100 }, target: [0, 0, 0], type: 'directional' },
191
+ ],
192
+ shadowPolicy: { maxShadowedLights: 1 },
193
+ },
194
+ three_point_character: {
195
+ description: 'Key, fill, rim, and ambient lights for character look development.',
196
+ label: 'Three-Point Character',
197
+ lights: threePointLights(),
198
+ shadowPolicy: { maxShadowedLights: 1 },
199
+ },
200
+ interior_warm: {
201
+ description: 'Cool window fill balanced by warm practical lights.',
202
+ label: 'Warm Interior',
203
+ lights: [
204
+ { color: [0.3, 0.34, 0.48], id: 'room-ambient', intensity: 0.18, name: 'Room Ambient', type: 'ambient' },
205
+ { color: { temperatureKelvin: 7200 }, height: 3, id: 'window', intensity: { unit: 'nits', value: 4 }, name: 'Window', position: [-4, 2.8, 0], priority: 7, target: [0, 1.2, 0], type: 'rectArea', width: 2.2 },
206
+ { color: { temperatureKelvin: 2400 }, distance: 14, id: 'table-lamp', intensity: { unit: 'lumens', value: 720 }, name: 'Table Lamp', position: [2.2, 1.7, 1], priority: 8, type: 'point' },
207
+ { color: { temperatureKelvin: 2100 }, distance: 16, id: 'hearth', intensity: { unit: 'lumens', value: 1000 }, name: 'Hearth', position: [-1.5, 1, -3], priority: 8, type: 'point' },
208
+ ],
209
+ shadowPolicy: { maxShadowedLights: 2 },
210
+ },
211
+ night_market: {
212
+ description: 'A many-practical-light stress rig for stylized night scenes.',
213
+ label: 'Night Market',
214
+ lights: nightMarketLights(),
215
+ shadowPolicy: { maxShadowedLights: 2 },
216
+ },
217
+ };
218
+
219
+ export const LIGHTING_RIG_PRESETS = deepFreeze(Object.fromEntries(
220
+ Object.entries(RIG_DEFINITIONS).map(([id, definition]) => [id, {
221
+ description: definition.description,
222
+ id,
223
+ label: definition.label,
224
+ lights: definition.lights.map((light) => createLightDescriptor(light)),
225
+ shadowPolicy: definition.shadowPolicy,
226
+ }]),
227
+ ));
228
+
229
+ const LOOK_DEFINITIONS = {
230
+ daylight: { description: 'Clear vivid daylight for outdoor toon scenes.', environment: { skyPreset: 'call_me_sensei', timeOfDay: 14, weatherPreset: 'clear' }, label: 'Daylight', post: { exposure: 1.08 }, quality: 'balanced', recipe: 'outdoor_sun' },
231
+ golden_hour: { description: 'Low warm key light with luminous cool sky fill.', environment: { skyPreset: 'sunset', timeOfDay: 18.2, weatherPreset: 'clear' }, label: 'Golden Hour', post: { exposure: 1.04, saturation: 1.12 }, quality: 'high', recipe: 'outdoor_sun' },
232
+ moonlit: { description: 'Cool low-intensity night foundation ready for practical lights.', environment: { skyPreset: 'night', timeOfDay: 23, weatherPreset: 'clear' }, label: 'Moonlit', post: { exposure: 0.92 }, quality: 'balanced', recipe: 'night_market' },
233
+ character_studio: { description: 'Neutral character look-development stage.', environment: { background: [0.07, 0.08, 0.11] }, label: 'Character Studio', post: { exposure: 1 }, quality: 'high', recipe: 'three_point_character' },
234
+ warm_interior: { description: 'Warm practicals with cool window contrast.', environment: { timeOfDay: 16 }, label: 'Warm Interior', post: { exposure: 1.02 }, quality: 'balanced', recipe: 'interior_warm' },
235
+ };
236
+
237
+ export const LIGHTING_LOOK_PRESETS = deepFreeze(Object.fromEntries(
238
+ Object.entries(LOOK_DEFINITIONS).map(([id, definition]) => [id, { ...definition, id }]),
239
+ ));
240
+
241
+ const PRESET_REGISTRIES = Object.freeze({
242
+ look: LIGHTING_LOOK_PRESETS,
243
+ luminaire: LIGHTING_LUMINAIRE_PRESETS,
244
+ quality: LIGHTING_QUALITY_PRESETS,
245
+ rig: LIGHTING_RIG_PRESETS,
246
+ });
247
+
248
+ function definitionOrThrow(kind, id) {
249
+ const definition = PRESET_REGISTRIES[kind]?.[id];
250
+ if (definition) return definition;
251
+ throw new Error(`Unknown lighting ${kind} preset "${id}".`);
252
+ }
253
+
254
+ /** Returns a new mutable descriptor for a built-in luminaire. */
255
+ export function resolveLuminairePreset(id = 'warm_bulb', overrides = {}) {
256
+ const definition = definitionOrThrow('luminaire', id);
257
+ return createLightDescriptor(mergePlain(definition.light, overrides));
258
+ }
259
+
260
+ /** Returns a new mutable LightingRecipe for a built-in rig. */
261
+ export function resolveLightingRigPreset(id = 'outdoor_sun', overrides = {}) {
262
+ const definition = definitionOrThrow('rig', id);
263
+ const base = {
264
+ id,
265
+ lights: definition.lights,
266
+ metadata: { preset: id },
267
+ name: definition.label,
268
+ shadowPolicy: definition.shadowPolicy,
269
+ };
270
+ return createLightingRecipe(mergePlain(base, overrides));
271
+ }
272
+
273
+ /** Returns a new mutable quality profile for a built-in profile id or inline profile. */
274
+ export function resolveLightingQualityPreset(idOrProfile = 'balanced', overrides = {}) {
275
+ const base = typeof idOrProfile === 'string'
276
+ ? definitionOrThrow('quality', idOrProfile)
277
+ : createLightingQualityProfile(idOrProfile);
278
+ return createLightingQualityProfile(mergePlain(base, overrides));
279
+ }
280
+
281
+ /** Returns a complete look document with inline recipe and quality objects. */
282
+ export function resolveLightingLookPreset(id = 'daylight', overrides = {}) {
283
+ const definition = definitionOrThrow('look', id);
284
+ const base = {
285
+ ...cloneJson(definition),
286
+ metadata: { preset: id },
287
+ recipe: resolveLightingRigPreset(definition.recipe),
288
+ quality: resolveLightingQualityPreset(definition.quality),
289
+ };
290
+ return createLightingLook(mergePlain(base, overrides));
291
+ }
292
+
293
+ /**
294
+ * Lists preset picker entries. With no kind all entries are returned in one
295
+ * flat array; pass `luminaire`, `rig`, `look`, or `quality` to filter.
296
+ */
297
+ export function getLightingPresetOptions(kind = null) {
298
+ if (kind !== null && !PRESET_REGISTRIES[kind]) {
299
+ throw new Error('Lighting preset kind must be luminaire, rig, look, or quality.');
300
+ }
301
+ const kinds = kind ? [kind] : Object.keys(PRESET_REGISTRIES);
302
+ return kinds.flatMap((entryKind) => Object.values(PRESET_REGISTRIES[entryKind]).map((definition) => ({
303
+ description: definition.description,
304
+ id: definition.id,
305
+ kind: entryKind,
306
+ label: definition.label,
307
+ })));
308
+ }
309
+
310
+ /** Generic resolver for UIs that keep the preset family as data. */
311
+ export function resolveLightingPreset(kind, id, overrides = {}) {
312
+ switch (kind) {
313
+ case 'luminaire': return resolveLuminairePreset(id, overrides);
314
+ case 'rig': return resolveLightingRigPreset(id, overrides);
315
+ case 'look': return resolveLightingLookPreset(id, overrides);
316
+ case 'quality': return resolveLightingQualityPreset(id, overrides);
317
+ default: throw new Error('Lighting preset kind must be luminaire, rig, look, or quality.');
318
+ }
319
+ }