@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,367 @@
1
+ import {
2
+ cloneSerializable,
3
+ createGeneratorRecipeDocument,
4
+ createSeededRandom,
5
+ deepMerge,
6
+ generateDomainValues,
7
+ hashSeed,
8
+ parseGeneratorRecipeDocument,
9
+ stableStringify,
10
+ validateGeneratorRecipeDocument,
11
+ } from '../core/generation.js';
12
+ import { createCameraSettings } from './cameraSettings.js';
13
+
14
+ export const CAMERA_PRESET_SCHEMA_VERSION = 1;
15
+ export const CAMERA_PRESET_DOCUMENT_TYPE = 'toonlab/camera-preset';
16
+ export const CAMERA_GENERATOR_DOMAIN = 'camera';
17
+
18
+ export const DEFAULT_CAMERA_OPERATORS = Object.freeze([
19
+ Object.freeze({ enabled: true, id: 'follow', order: 100, settings: Object.freeze({}), type: 'follow' }),
20
+ Object.freeze({ enabled: true, id: 'look-ahead', order: 200, settings: Object.freeze({}), type: 'lookAhead' }),
21
+ Object.freeze({ enabled: true, id: 'framing', order: 300, settings: Object.freeze({}), type: 'framing' }),
22
+ Object.freeze({ enabled: true, id: 'collision', order: 400, settings: Object.freeze({}), type: 'collision' }),
23
+ Object.freeze({ enabled: true, id: 'damping', order: 500, settings: Object.freeze({}), type: 'damping' }),
24
+ Object.freeze({ enabled: true, id: 'lens', order: 600, settings: Object.freeze({}), type: 'lens' }),
25
+ Object.freeze({ enabled: true, id: 'noise', order: 650, settings: Object.freeze({}), type: 'noise' }),
26
+ Object.freeze({ enabled: true, id: 'impulse', order: 700, settings: Object.freeze({}), type: 'impulse' }),
27
+ ]);
28
+
29
+ const plainObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
30
+ const cleanId = (value) => String(value ?? '').trim().replace(/[^a-zA-Z0-9._/-]+/g, '_');
31
+ const finite = (value, fallback) => Number.isFinite(Number(value)) ? Number(value) : fallback;
32
+
33
+ export function normalizeCameraOperators(operators = DEFAULT_CAMERA_OPERATORS) {
34
+ if (!Array.isArray(operators)) return DEFAULT_CAMERA_OPERATORS.map((operator) => ({ ...operator, settings: {} }));
35
+ const used = new Set();
36
+ return operators.map((source, index) => {
37
+ const entry = plainObject(source) ? source : {};
38
+ const type = cleanId(entry.type || `operator_${index}`);
39
+ let id = cleanId(entry.id || `${type}_${index}`);
40
+ while (used.has(id)) id = `${id}_${index}`;
41
+ used.add(id);
42
+ return {
43
+ enabled: entry.enabled === undefined ? true : Boolean(entry.enabled),
44
+ id,
45
+ order: finite(entry.order, (index + 1) * 100),
46
+ settings: plainObject(entry.settings) ? cloneSerializable(entry.settings) : {},
47
+ type,
48
+ };
49
+ }).filter((operator) => operator.type);
50
+ }
51
+
52
+ function sanitizeGeneratorConfiguration(value = {}) {
53
+ const source = plainObject(value) ? value : {};
54
+ const parameters = plainObject(source.parameters) ? source.parameters : {};
55
+ return {
56
+ operators: normalizeCameraOperators(source.operators),
57
+ parameters: {
58
+ aimDepth: finite(parameters.aimDepth, 0),
59
+ aimHeight: finite(parameters.aimHeight, 1.45),
60
+ aimLateral: finite(parameters.aimLateral, 0),
61
+ distance: Math.max(0.01, finite(parameters.distance, 6.4)),
62
+ height: finite(parameters.height, 2.8),
63
+ shoulder: finite(parameters.shoulder, 1.15),
64
+ },
65
+ settings: createCameraSettings(source.settings),
66
+ };
67
+ }
68
+
69
+ const BASE_DOMAINS = Object.freeze({
70
+ parameters: Object.freeze({
71
+ aimDepth: Object.freeze({ $type: 'range', min: -1.2, max: 2.2, step: 0.01 }),
72
+ aimHeight: Object.freeze({ $type: 'range', min: 0.6, max: 3.2, step: 0.01, distribution: 'normal' }),
73
+ aimLateral: Object.freeze({ $type: 'range', min: -1.6, max: 1.6, step: 0.01 }),
74
+ distance: Object.freeze({ $type: 'range', min: 3.2, max: 11.5, step: 0.01, distribution: 'normal' }),
75
+ height: Object.freeze({ $type: 'range', min: 1.4, max: 6.5, step: 0.01, distribution: 'normal' }),
76
+ shoulder: Object.freeze({ $type: 'range', min: -2.1, max: 2.1, step: 0.01 }),
77
+ }),
78
+ settings: Object.freeze({
79
+ collision: Object.freeze({
80
+ minimumDistance: Object.freeze({ $type: 'range', min: 0.5, max: 1.8, step: 0.01 }),
81
+ radius: Object.freeze({ $type: 'range', min: 0.16, max: 0.52, step: 0.01 }),
82
+ recoveryDamping: Object.freeze({ $type: 'range', min: 3, max: 12, step: 0.1 }),
83
+ }),
84
+ damping: Object.freeze({
85
+ aim: Object.freeze({ $type: 'range', min: 5, max: 18, step: 0.1 }),
86
+ lens: Object.freeze({ $type: 'range', min: 4, max: 15, step: 0.1 }),
87
+ position: Object.freeze({ $type: 'range', min: 3.5, max: 14, step: 0.1 }),
88
+ }),
89
+ framing: Object.freeze({
90
+ horizontalScale: Object.freeze({ $type: 'range', min: 0.25, max: 1.3, step: 0.01 }),
91
+ screenX: Object.freeze({ $type: 'range', min: 0.38, max: 0.62, step: 0.01 }),
92
+ screenY: Object.freeze({ $type: 'range', min: 0.42, max: 0.67, step: 0.01 }),
93
+ verticalScale: Object.freeze({ $type: 'range', min: 0.2, max: 1.15, step: 0.01 }),
94
+ }),
95
+ impulse: Object.freeze({
96
+ decay: Object.freeze({ $type: 'range', min: 7, max: 22, step: 0.1 }),
97
+ positionScale: Object.freeze({ $type: 'range', min: 0.4, max: 1.8, step: 0.01 }),
98
+ rotationScale: Object.freeze({ $type: 'range', min: 0.35, max: 1.8, step: 0.01 }),
99
+ }),
100
+ lens: Object.freeze({
101
+ fov: Object.freeze({ $type: 'range', min: 32, max: 72, step: 0.1, distribution: 'normal' }),
102
+ speedFov: Object.freeze({ $type: 'range', min: 0, max: 10, step: 0.1 }),
103
+ speedReference: Object.freeze({ $type: 'range', min: 4, max: 18, step: 0.1 }),
104
+ }),
105
+ lookAhead: Object.freeze({
106
+ maxDistance: Object.freeze({ $type: 'range', min: 0.6, max: 7, step: 0.05 }),
107
+ smoothing: Object.freeze({ $type: 'range', min: 3, max: 16, step: 0.1 }),
108
+ time: Object.freeze({ $type: 'range', min: 0.08, max: 0.9, step: 0.01 }),
109
+ }),
110
+ noise: Object.freeze({
111
+ enabled: Object.freeze({ $type: 'choice', options: Object.freeze([
112
+ Object.freeze({ value: false, weight: 3 }),
113
+ Object.freeze({ value: true, weight: 1 }),
114
+ ]) }),
115
+ frequency: Object.freeze({ $type: 'range', min: 0.18, max: 2.4, step: 0.01 }),
116
+ positionAmplitude: Object.freeze({ $type: 'range', min: 0.005, max: 0.06, step: 0.001 }),
117
+ rotationAmplitude: Object.freeze({ $type: 'range', min: 0.001, max: 0.012, step: 0.001 }),
118
+ octaves: Object.freeze({ $type: 'range', min: 1, max: 4, integer: true }),
119
+ persistence: Object.freeze({ $type: 'range', min: 0.35, max: 0.72, step: 0.01 }),
120
+ lacunarity: Object.freeze({ $type: 'range', min: 1.5, max: 2.6, step: 0.01 }),
121
+ seed: Object.freeze({ $type: 'range', min: 1, max: 1000000, integer: true }),
122
+ }),
123
+ }),
124
+ });
125
+
126
+ const archetypeRegistry = new Map();
127
+
128
+ function registerBuiltInArchetypes() {
129
+ registerCameraGeneratorArchetype('adventure', {
130
+ description: 'Responsive over-shoulder seed family for traversal and lightweight combat.',
131
+ label: 'Adventure Seed',
132
+ configuration: sanitizeGeneratorConfiguration({}),
133
+ domains: BASE_DOMAINS,
134
+ });
135
+ registerCameraGeneratorArchetype('close_action', {
136
+ description: 'Closer, faster response family with stronger impacts. Every field remains generative.',
137
+ label: 'Close Action Seed',
138
+ configuration: sanitizeGeneratorConfiguration({
139
+ parameters: { distance: 4.5, height: 2.1, shoulder: 0.85, aimHeight: 1.25 },
140
+ settings: {
141
+ damping: { position: 12, aim: 16 },
142
+ lens: { fov: 56, speedFov: 7 },
143
+ impulse: { positionScale: 1.35, rotationScale: 1.25 },
144
+ },
145
+ }),
146
+ domains: deepMerge(BASE_DOMAINS, {
147
+ parameters: {
148
+ distance: { $type: 'range', min: 2.6, max: 6.2, step: 0.01, distribution: 'normal' },
149
+ height: { $type: 'range', min: 1.2, max: 3.6, step: 0.01 },
150
+ },
151
+ settings: { lens: { fov: { $type: 'range', min: 44, max: 72, step: 0.1 } } },
152
+ }),
153
+ });
154
+ registerCameraGeneratorArchetype('wide_exploration', {
155
+ description: 'Wide scenic framing family for world exploration and traversal reveals.',
156
+ label: 'Wide Exploration Seed',
157
+ configuration: sanitizeGeneratorConfiguration({
158
+ parameters: { distance: 10.5, height: 5.1, shoulder: 0.2, aimHeight: 2 },
159
+ settings: { damping: { position: 4.8, aim: 6 }, lens: { fov: 40, speedFov: 2 } },
160
+ }),
161
+ domains: deepMerge(BASE_DOMAINS, {
162
+ parameters: {
163
+ distance: { $type: 'range', min: 7, max: 18, step: 0.01, distribution: 'normal' },
164
+ height: { $type: 'range', min: 3.2, max: 10, step: 0.01, distribution: 'normal' },
165
+ shoulder: { $type: 'range', min: -1, max: 1, step: 0.01 },
166
+ },
167
+ settings: { lens: { fov: { $type: 'range', min: 25, max: 52, step: 0.1 } } },
168
+ }),
169
+ });
170
+ }
171
+
172
+ export function registerCameraGeneratorArchetype(id, definition = {}, { overwrite = false } = {}) {
173
+ const clean = cleanId(id);
174
+ if (!clean) throw new Error('Camera generator archetype id is required.');
175
+ if (!overwrite && archetypeRegistry.has(clean)) throw new Error(`Camera generator archetype "${clean}" already exists.`);
176
+ const source = plainObject(definition) ? definition : {};
177
+ archetypeRegistry.set(clean, {
178
+ configuration: sanitizeGeneratorConfiguration(source.configuration),
179
+ description: String(source.description ?? ''),
180
+ domains: cloneSerializable(plainObject(source.domains) ? source.domains : {}),
181
+ label: String(source.label || clean),
182
+ });
183
+ return { id: clean, label: String(source.label || clean) };
184
+ }
185
+
186
+ export function getCameraGeneratorArchetypeOptions() {
187
+ return [...archetypeRegistry].map(([id, entry]) => ({ description: entry.description, id, label: entry.label }));
188
+ }
189
+
190
+ export function createCameraGeneratorRecipeDocument(id = 'camera_recipe', definition = {}) {
191
+ const source = plainObject(definition) ? definition : {};
192
+ const archetype = archetypeRegistry.get(source.basePreset) ?? archetypeRegistry.get('adventure');
193
+ return createGeneratorRecipeDocument(CAMERA_GENERATOR_DOMAIN, id, {
194
+ ...source,
195
+ basePreset: source.basePreset ?? 'adventure',
196
+ configuration: deepMerge(archetype?.configuration, source.configuration),
197
+ domains: source.domains ?? archetype?.domains ?? BASE_DOMAINS,
198
+ }, { sanitizeConfiguration: sanitizeGeneratorConfiguration });
199
+ }
200
+
201
+ export function validateCameraGeneratorRecipeDocument(input) {
202
+ return validateGeneratorRecipeDocument(input, {
203
+ domain: CAMERA_GENERATOR_DOMAIN,
204
+ sanitizeConfiguration: sanitizeGeneratorConfiguration,
205
+ });
206
+ }
207
+
208
+ export function parseCameraGeneratorRecipeDocument(input) {
209
+ return parseGeneratorRecipeDocument(input, {
210
+ domain: CAMERA_GENERATOR_DOMAIN,
211
+ sanitizeConfiguration: sanitizeGeneratorConfiguration,
212
+ });
213
+ }
214
+
215
+ export function serializeCameraGeneratorRecipeDocument(idOrDocument, definition = {}, { pretty = true } = {}) {
216
+ const document = plainObject(idOrDocument)
217
+ ? createCameraGeneratorRecipeDocument(idOrDocument.id, idOrDocument)
218
+ : createCameraGeneratorRecipeDocument(idOrDocument, definition);
219
+ return stableStringify(document, pretty ? 2 : 0);
220
+ }
221
+
222
+ function composeSettings(configuration) {
223
+ const clean = sanitizeGeneratorConfiguration(configuration);
224
+ const p = clean.parameters;
225
+ return createCameraSettings(deepMerge(clean.settings, {
226
+ follow: {
227
+ offset: [p.shoulder, p.height, p.distance],
228
+ targetOffset: [p.aimLateral, p.aimHeight, p.aimDepth],
229
+ },
230
+ }));
231
+ }
232
+
233
+ function isPathLocked(locks, path) {
234
+ const id = path.join('.');
235
+ return locks.some((lock) => id === lock || id.startsWith(`${lock}.`));
236
+ }
237
+
238
+ function blendMutation(current, candidate, amount, locks, random, path = []) {
239
+ if (isPathLocked(locks, path)) return cloneSerializable(current ?? candidate);
240
+ if (typeof current === 'number' && typeof candidate === 'number') {
241
+ return current + (candidate - current) * amount;
242
+ }
243
+ if (Array.isArray(current) && Array.isArray(candidate)) {
244
+ return candidate.map((value, index) => blendMutation(current[index], value, amount, locks, random, [...path, index]));
245
+ }
246
+ if (plainObject(current) && plainObject(candidate)) {
247
+ const keys = new Set([...Object.keys(current), ...Object.keys(candidate)]);
248
+ return Object.fromEntries([...keys].map((key) => [key, blendMutation(current[key], candidate[key], amount, locks, random, [...path, key])]));
249
+ }
250
+ return random.fork(path.join('.')).bool(amount) ? cloneSerializable(candidate) : cloneSerializable(current ?? candidate);
251
+ }
252
+
253
+ export function validateCameraPresetDocument(input) {
254
+ const errors = [];
255
+ const warnings = [];
256
+ if (!plainObject(input)) return { errors: ['Camera preset must be a JSON object.'], ok: false, value: null, warnings };
257
+ if (input.type !== CAMERA_PRESET_DOCUMENT_TYPE) errors.push(`Camera preset type must be "${CAMERA_PRESET_DOCUMENT_TYPE}".`);
258
+ const version = Number(input.version ?? 1);
259
+ if (!Number.isInteger(version) || version < 1) errors.push('Camera preset version must be a positive integer.');
260
+ if (version > CAMERA_PRESET_SCHEMA_VERSION) errors.push(`Camera preset version ${version} is newer than supported version ${CAMERA_PRESET_SCHEMA_VERSION}.`);
261
+ const id = cleanId(input.id);
262
+ if (!id) errors.push('Camera preset id is required.');
263
+ const operators = normalizeCameraOperators(input.operators);
264
+ if (operators.length === 0) warnings.push('Camera preset has no operators and will not move the camera.');
265
+ return {
266
+ errors,
267
+ ok: errors.length === 0,
268
+ value: errors.length ? null : {
269
+ description: String(input.description ?? ''),
270
+ id,
271
+ label: String(input.label || id),
272
+ operators,
273
+ seed: hashSeed(input.seed ?? 1),
274
+ settings: createCameraSettings(input.settings),
275
+ type: CAMERA_PRESET_DOCUMENT_TYPE,
276
+ version: CAMERA_PRESET_SCHEMA_VERSION,
277
+ },
278
+ warnings,
279
+ };
280
+ }
281
+
282
+ export function createCameraPresetDocument(id = 'camera_preset', definition = {}) {
283
+ const source = plainObject(definition) ? definition : {};
284
+ const result = validateCameraPresetDocument({
285
+ description: source.description ?? '',
286
+ id: id ?? source.id,
287
+ label: source.label ?? source.name ?? id,
288
+ operators: source.operators,
289
+ seed: source.seed ?? 1,
290
+ settings: source.settings,
291
+ type: CAMERA_PRESET_DOCUMENT_TYPE,
292
+ version: CAMERA_PRESET_SCHEMA_VERSION,
293
+ });
294
+ if (!result.ok) throw new Error(result.errors.join(' '));
295
+ return result.value;
296
+ }
297
+
298
+ export function parseCameraPresetDocument(input) {
299
+ try {
300
+ return validateCameraPresetDocument(typeof input === 'string' ? JSON.parse(input) : input);
301
+ } catch (error) {
302
+ return { errors: [`Invalid camera preset JSON: ${error.message}`], ok: false, value: null, warnings: [] };
303
+ }
304
+ }
305
+
306
+ export function serializeCameraPresetDocument(idOrDocument, definition = {}, { pretty = true } = {}) {
307
+ const document = plainObject(idOrDocument)
308
+ ? createCameraPresetDocument(idOrDocument.id, idOrDocument)
309
+ : createCameraPresetDocument(idOrDocument, definition);
310
+ return stableStringify(document, pretty ? 2 : 0);
311
+ }
312
+
313
+ /**
314
+ * Generates a deterministic resolved preset. `mutation` blends continuous
315
+ * values around the current candidate; locks are dot paths into the recipe's
316
+ * configuration (for example `settings.lens.fov` or `parameters.distance`).
317
+ */
318
+ export function generateCameraPreset(recipeInput, {
319
+ current = null,
320
+ locks = null,
321
+ mutation = 1,
322
+ seed = null,
323
+ } = {}) {
324
+ const result = validateCameraGeneratorRecipeDocument(recipeInput);
325
+ if (!result.ok) throw new Error(result.errors.join(' '));
326
+ const recipe = result.value;
327
+ const resolvedSeed = hashSeed(seed ?? recipe.seed);
328
+ const lockPaths = Array.isArray(locks) ? locks.map(String) : recipe.locks;
329
+ const archetype = archetypeRegistry.get(recipe.basePreset);
330
+ const base = deepMerge(archetype?.configuration, recipe.configuration);
331
+ const currentConfiguration = plainObject(current?.generatorConfiguration)
332
+ ? sanitizeGeneratorConfiguration(current.generatorConfiguration)
333
+ : base;
334
+ const sampled = generateDomainValues(recipe.domains, {
335
+ current: currentConfiguration,
336
+ locks: lockPaths,
337
+ seed: resolvedSeed,
338
+ });
339
+ let configuration = deepMerge(base, sampled);
340
+ const amount = Math.min(Math.max(finite(mutation, 1), 0), 1);
341
+ if (current && amount < 1) {
342
+ configuration = blendMutation(
343
+ currentConfiguration,
344
+ configuration,
345
+ amount,
346
+ lockPaths,
347
+ createSeededRandom(resolvedSeed, 'camera-mutation'),
348
+ );
349
+ }
350
+ const preset = createCameraPresetDocument(`${recipe.id}_${resolvedSeed}`, {
351
+ description: `Generated from ${recipe.label}.`,
352
+ label: `${recipe.label} ${resolvedSeed}`,
353
+ operators: configuration.operators,
354
+ seed: resolvedSeed,
355
+ settings: composeSettings(configuration),
356
+ });
357
+ // Non-enumerable authoring provenance keeps the shipping preset flat while
358
+ // allowing design-time mutation to preserve locked source parameters.
359
+ Object.defineProperty(preset, 'generatorConfiguration', {
360
+ configurable: true,
361
+ enumerable: false,
362
+ value: sanitizeGeneratorConfiguration(configuration),
363
+ });
364
+ return preset;
365
+ }
366
+
367
+ registerBuiltInArchetypes();