@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,222 @@
1
+ // Runtime-safe motion style settings. These settings deliberately describe
2
+ // playback and presentation, not a catalog of animations: behavior topology
3
+ // and clip-slot bindings live in separate serializable documents.
4
+
5
+ export const MOTION_CADENCE_MODES = Object.freeze(['smooth', 'stepped']);
6
+ export const MOTION_ROOT_POLICIES = Object.freeze(['ignore', 'inPlace', 'extract', 'apply']);
7
+ export const MOTION_EASING_MODES = Object.freeze(['linear', 'smoothstep', 'easeInOutCubic']);
8
+
9
+ export const DEFAULT_MOTION_SETTINGS = Object.freeze({
10
+ playback: Object.freeze({
11
+ cadence: 'smooth',
12
+ sampleRate: 24,
13
+ speed: 1,
14
+ maxDelta: 0.1,
15
+ }),
16
+ transitions: Object.freeze({
17
+ duration: 0.18,
18
+ easing: 'smoothstep',
19
+ allowInterrupt: true,
20
+ syncPhase: true,
21
+ }),
22
+ lean: Object.freeze({
23
+ enabled: true,
24
+ maxAngle: 0.2,
25
+ response: 10,
26
+ turnParameter: 'turn',
27
+ forwardParameter: 'speed',
28
+ }),
29
+ bob: Object.freeze({
30
+ enabled: true,
31
+ amplitude: 0.035,
32
+ frequency: 1.8,
33
+ lateral: 0.012,
34
+ speedParameter: 'speed',
35
+ }),
36
+ squash: Object.freeze({
37
+ enabled: true,
38
+ amount: 0.08,
39
+ response: 14,
40
+ verticalParameter: 'verticalSpeed',
41
+ }),
42
+ rootMotion: Object.freeze({
43
+ policy: 'inPlace',
44
+ axes: Object.freeze([true, false, true]),
45
+ scale: 1,
46
+ applyYaw: false,
47
+ }),
48
+ events: Object.freeze({
49
+ maxPerUpdate: 32,
50
+ fireOnLoop: true,
51
+ minimumWeight: 0.05,
52
+ }),
53
+ });
54
+
55
+ export const MOTION_SETTING_GROUPS = Object.freeze([
56
+ Object.freeze({ id: 'playback', label: 'Playback', description: 'Global timing and frame cadence. Stepped cadence creates deliberately held animation frames without slowing simulation.' }),
57
+ Object.freeze({ id: 'transitions', label: 'Transitions', description: 'Default cross-fade behavior for graph edges that do not provide their own values.' }),
58
+ Object.freeze({ id: 'lean', label: 'Lean', description: 'Procedural directional lean driven by arbitrary named graph parameters.' }),
59
+ Object.freeze({ id: 'bob', label: 'Bob', description: 'A subtle locomotion presentation layer applied after clip and graph blending.' }),
60
+ Object.freeze({ id: 'squash', label: 'Squash', description: 'Velocity-driven squash and stretch with volume-preserving horizontal compensation.' }),
61
+ Object.freeze({ id: 'rootMotion', label: 'Root Motion', description: 'Whether authored root travel is ignored, removed in place, extracted for gameplay, or applied through the rig adapter.' }),
62
+ Object.freeze({ id: 'events', label: 'Events', description: 'Per-frame safety limits and filtering for arbitrary clip event tracks.' }),
63
+ ]);
64
+
65
+ const FIELDS = Object.freeze({
66
+ playback: Object.freeze({
67
+ cadence: { label: 'Cadence', type: 'select', options: MOTION_CADENCE_MODES },
68
+ sampleRate: { label: 'Held-frame rate', range: { min: 1, max: 120, step: 1 } },
69
+ speed: { label: 'Global speed', range: { min: 0, max: 4, step: 0.01 } },
70
+ maxDelta: { label: 'Maximum delta', range: { min: 0.016, max: 0.5, step: 0.001 } },
71
+ }),
72
+ transitions: Object.freeze({
73
+ duration: { label: 'Default duration', range: { min: 0, max: 2, step: 0.01 } },
74
+ easing: { label: 'Easing', type: 'select', options: MOTION_EASING_MODES },
75
+ allowInterrupt: { label: 'Allow interruption', type: 'boolean' },
76
+ syncPhase: { label: 'Synchronize phase', type: 'boolean' },
77
+ }),
78
+ lean: Object.freeze({
79
+ enabled: { label: 'Enabled', type: 'boolean' },
80
+ maxAngle: { label: 'Maximum angle', range: { min: 0, max: 0.8, step: 0.01 } },
81
+ response: { label: 'Response', range: { min: 0, max: 40, step: 0.1 } },
82
+ turnParameter: { label: 'Turn parameter', type: 'text' },
83
+ forwardParameter: { label: 'Forward parameter', type: 'text' },
84
+ }),
85
+ bob: Object.freeze({
86
+ enabled: { label: 'Enabled', type: 'boolean' },
87
+ amplitude: { label: 'Vertical amplitude', range: { min: 0, max: 0.4, step: 0.001 } },
88
+ frequency: { label: 'Frequency', range: { min: 0, max: 8, step: 0.01 } },
89
+ lateral: { label: 'Lateral amplitude', range: { min: 0, max: 0.25, step: 0.001 } },
90
+ speedParameter: { label: 'Speed parameter', type: 'text' },
91
+ }),
92
+ squash: Object.freeze({
93
+ enabled: { label: 'Enabled', type: 'boolean' },
94
+ amount: { label: 'Amount', range: { min: 0, max: 0.45, step: 0.001 } },
95
+ response: { label: 'Response', range: { min: 0, max: 40, step: 0.1 } },
96
+ verticalParameter: { label: 'Vertical parameter', type: 'text' },
97
+ }),
98
+ rootMotion: Object.freeze({
99
+ policy: { label: 'Policy', type: 'select', options: MOTION_ROOT_POLICIES },
100
+ axes: { label: 'Translation axes', type: 'boolean3' },
101
+ scale: { label: 'Scale', range: { min: 0, max: 8, step: 0.01 } },
102
+ applyYaw: { label: 'Apply yaw', type: 'boolean' },
103
+ }),
104
+ events: Object.freeze({
105
+ maxPerUpdate: { label: 'Maximum events/update', range: { min: 1, max: 512, step: 1 } },
106
+ fireOnLoop: { label: 'Fire across loops', type: 'boolean' },
107
+ minimumWeight: { label: 'Minimum blend weight', range: { min: 0, max: 1, step: 0.01 } },
108
+ }),
109
+ });
110
+
111
+ export const MOTION_SETTING_FIELD_SCHEMA = Object.freeze(Object.fromEntries(
112
+ MOTION_SETTING_GROUPS.map((group) => [group.id, Object.freeze(Object.fromEntries(
113
+ Object.entries(FIELDS[group.id]).map(([key, field]) => [key, Object.freeze({
114
+ ...field,
115
+ defaultValue: DEFAULT_MOTION_SETTINGS[group.id][key],
116
+ group: group.id,
117
+ id: `${group.id}.${key}`,
118
+ key,
119
+ type: field.type ?? 'number',
120
+ })]),
121
+ ))]),
122
+ ));
123
+
124
+ const plain = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
125
+ const finite = (value, fallback) => Number.isFinite(Number(value)) ? Number(value) : fallback;
126
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
127
+ const text = (value, fallback) => typeof value === 'string' && value.trim() ? value.trim() : fallback;
128
+
129
+ function sourceGroup(options, id) {
130
+ return plain(options?.[id]) ? options[id] : {};
131
+ }
132
+
133
+ function boolean3(value, fallback) {
134
+ return Array.isArray(value) && value.length >= 3
135
+ ? value.slice(0, 3).map(Boolean)
136
+ : [...fallback];
137
+ }
138
+
139
+ /** Normalizes partial style settings into a complete mutable object. */
140
+ export function createMotionSettings(options = {}) {
141
+ const p = sourceGroup(options, 'playback');
142
+ const t = sourceGroup(options, 'transitions');
143
+ const l = sourceGroup(options, 'lean');
144
+ const b = sourceGroup(options, 'bob');
145
+ const s = sourceGroup(options, 'squash');
146
+ const r = sourceGroup(options, 'rootMotion');
147
+ const e = sourceGroup(options, 'events');
148
+ const d = DEFAULT_MOTION_SETTINGS;
149
+ return {
150
+ playback: {
151
+ cadence: MOTION_CADENCE_MODES.includes(p.cadence) ? p.cadence : d.playback.cadence,
152
+ sampleRate: clamp(finite(p.sampleRate, d.playback.sampleRate), 1, 240),
153
+ speed: clamp(finite(p.speed, d.playback.speed), 0, 8),
154
+ maxDelta: clamp(finite(p.maxDelta, d.playback.maxDelta), 0.001, 1),
155
+ },
156
+ transitions: {
157
+ duration: clamp(finite(t.duration, d.transitions.duration), 0, 8),
158
+ easing: MOTION_EASING_MODES.includes(t.easing) ? t.easing : d.transitions.easing,
159
+ allowInterrupt: t.allowInterrupt === undefined ? d.transitions.allowInterrupt : Boolean(t.allowInterrupt),
160
+ syncPhase: t.syncPhase === undefined ? d.transitions.syncPhase : Boolean(t.syncPhase),
161
+ },
162
+ lean: {
163
+ enabled: l.enabled === undefined ? d.lean.enabled : Boolean(l.enabled),
164
+ maxAngle: clamp(finite(l.maxAngle, d.lean.maxAngle), 0, Math.PI / 2),
165
+ response: clamp(finite(l.response, d.lean.response), 0, 100),
166
+ turnParameter: text(l.turnParameter, d.lean.turnParameter),
167
+ forwardParameter: text(l.forwardParameter, d.lean.forwardParameter),
168
+ },
169
+ bob: {
170
+ enabled: b.enabled === undefined ? d.bob.enabled : Boolean(b.enabled),
171
+ amplitude: clamp(finite(b.amplitude, d.bob.amplitude), 0, 1),
172
+ frequency: clamp(finite(b.frequency, d.bob.frequency), 0, 20),
173
+ lateral: clamp(finite(b.lateral, d.bob.lateral), 0, 1),
174
+ speedParameter: text(b.speedParameter, d.bob.speedParameter),
175
+ },
176
+ squash: {
177
+ enabled: s.enabled === undefined ? d.squash.enabled : Boolean(s.enabled),
178
+ amount: clamp(finite(s.amount, d.squash.amount), 0, 0.8),
179
+ response: clamp(finite(s.response, d.squash.response), 0, 100),
180
+ verticalParameter: text(s.verticalParameter, d.squash.verticalParameter),
181
+ },
182
+ rootMotion: {
183
+ policy: MOTION_ROOT_POLICIES.includes(r.policy) ? r.policy : d.rootMotion.policy,
184
+ axes: boolean3(r.axes, d.rootMotion.axes),
185
+ scale: clamp(finite(r.scale, d.rootMotion.scale), 0, 20),
186
+ applyYaw: r.applyYaw === undefined ? d.rootMotion.applyYaw : Boolean(r.applyYaw),
187
+ },
188
+ events: {
189
+ maxPerUpdate: Math.round(clamp(finite(e.maxPerUpdate, d.events.maxPerUpdate), 1, 2048)),
190
+ fireOnLoop: e.fireOnLoop === undefined ? d.events.fireOnLoop : Boolean(e.fireOnLoop),
191
+ minimumWeight: clamp(finite(e.minimumWeight, d.events.minimumWeight), 0, 1),
192
+ },
193
+ };
194
+ }
195
+
196
+ /** Deep-merges grouped overrides and re-normalizes them. */
197
+ export function mergeMotionSettings(base = {}, overrides = {}) {
198
+ const current = createMotionSettings(base);
199
+ return createMotionSettings(Object.fromEntries(
200
+ MOTION_SETTING_GROUPS.map(({ id }) => [id, {
201
+ ...current[id],
202
+ ...(plain(overrides?.[id]) ? overrides[id] : {}),
203
+ }]),
204
+ ));
205
+ }
206
+
207
+ export function interpolateMotionSettings(from, to, amount) {
208
+ const a = createMotionSettings(from);
209
+ const b = createMotionSettings(to);
210
+ const t = clamp(finite(amount, 0), 0, 1);
211
+ const output = {};
212
+ for (const { id } of MOTION_SETTING_GROUPS) {
213
+ output[id] = {};
214
+ for (const key of Object.keys(a[id])) {
215
+ const av = a[id][key];
216
+ const bv = b[id][key];
217
+ if (typeof av === 'number' && typeof bv === 'number') output[id][key] = av + (bv - av) * t;
218
+ else output[id][key] = t < 0.5 ? av : bv;
219
+ }
220
+ }
221
+ return createMotionSettings(output);
222
+ }
@@ -0,0 +1,7 @@
1
+ export * from './pathSettings.js';
2
+ export * from './pathRouter.js';
3
+ export * from './pathRibbon.js';
4
+ export * from './pathBridge.js';
5
+ export * from './pathTextures.js';
6
+ export * from './pathPresets.js';
7
+ export * from './stylizedPaths.js';
@@ -0,0 +1,232 @@
1
+ // Parametric plank bridges for water crossings. Generated like a debrisgen
2
+ // piece: deterministic, vertex-colored wood, one merged geometry per bridge
3
+ // (a single draw call each), rails registered as collision circles so a
4
+ // character crosses the deck and never strolls off the side.
5
+
6
+ import * as THREE from 'three';
7
+
8
+ import { hashCombine } from '../rockgen/noise/prng.js';
9
+ import { valueNoise3 } from '../rockgen/noise/valueNoise3.js';
10
+ import { mergePathGeometries } from './pathRibbon.js';
11
+
12
+ const WOOD = Object.freeze({
13
+ dark: Object.freeze([0.3, 0.2, 0.12]),
14
+ base: Object.freeze([0.44, 0.3, 0.18]),
15
+ light: Object.freeze([0.56, 0.4, 0.24]),
16
+ });
17
+
18
+ function paintBox(geometry, rgb, shade) {
19
+ const count = geometry.attributes.position.count;
20
+ const colors = new Float32Array(count * 3);
21
+ for (let index = 0; index < count; index += 1) {
22
+ colors[index * 3] = Math.min(rgb[0] * shade, 1);
23
+ colors[index * 3 + 1] = Math.min(rgb[1] * shade, 1);
24
+ colors[index * 3 + 2] = Math.min(rgb[2] * shade, 1);
25
+ }
26
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
27
+ return geometry;
28
+ }
29
+
30
+ /**
31
+ * Builds one bridge over a crossing (bank sample to bank sample).
32
+ *
33
+ * @param {Object} options
34
+ * @param {Array} options.samples Route samples (from buildRouteProfile).
35
+ * @param {{startIndex: number, endIndex: number, span: number}} options.crossing
36
+ * @param {Object} options.settings Path settings (bridge group used).
37
+ * @param {Function} [options.heightAt] Bed sampler for pier depth.
38
+ * @param {number} [options.waterLevel]
39
+ * @param {number} [options.seed]
40
+ * @returns {{ geometry: THREE.BufferGeometry, blockers: Array<{x,z,radius}>,
41
+ * deckWidth: number, span: number }}
42
+ */
43
+ export function buildBridge({
44
+ samples,
45
+ crossing,
46
+ settings,
47
+ heightAt = null,
48
+ waterLevel = 0,
49
+ seed = 1,
50
+ }) {
51
+ const { bridge } = settings;
52
+ const deck = samples.slice(crossing.startIndex, crossing.endIndex + 1);
53
+ const span = crossing.span;
54
+ const halfWidth = deck[0].half + 0.12;
55
+ const random = (value, salt = 0) => valueNoise3(hashCombine(seed, 419 + salt), value * 5.13, 0.3, 0.7);
56
+
57
+ const geometries = [];
58
+ const unitBox = new THREE.BoxGeometry(1, 1, 1);
59
+ const matrix = new THREE.Matrix4();
60
+ const quaternion = new THREE.Quaternion();
61
+ const scale = new THREE.Vector3();
62
+ const position = new THREE.Vector3();
63
+ const euler = new THREE.Euler();
64
+
65
+ // Deck position/heading at arclength t along the crossing.
66
+ const atLength = (target) => {
67
+ let index = 0;
68
+ while (index < deck.length - 2 && deck[index + 1].s - deck[0].s < target) index += 1;
69
+ const a = deck[index];
70
+ const b = deck[Math.min(index + 1, deck.length - 1)];
71
+ const t = (target - (a.s - deck[0].s)) / Math.max(b.s - a.s, 1e-6);
72
+ const clamped = Math.min(Math.max(t, 0), 1);
73
+ return {
74
+ heading: Math.atan2(b.x - a.x, b.z - a.z),
75
+ pitch: Math.atan2(b.profile - a.profile, Math.max(b.s - a.s, 1e-6)),
76
+ sideX: a.sideX + (b.sideX - a.sideX) * clamped,
77
+ sideZ: a.sideZ + (b.sideZ - a.sideZ) * clamped,
78
+ x: a.x + (b.x - a.x) * clamped,
79
+ y: a.profile + (b.profile - a.profile) * clamped,
80
+ z: a.z + (b.z - a.z) * clamped,
81
+ };
82
+ };
83
+
84
+ const addBox = (rgb, shade, px, py, pz, sx, sy, sz, yaw, pitch = 0) => {
85
+ const geometry = unitBox.clone();
86
+ euler.set(pitch, yaw, 0, 'YXZ');
87
+ quaternion.setFromEuler(euler);
88
+ position.set(px, py, pz);
89
+ scale.set(sx, sy, sz);
90
+ matrix.compose(position, quaternion, scale);
91
+ geometry.applyMatrix4(matrix);
92
+ paintBox(geometry, rgb, shade);
93
+ geometries.push(geometry);
94
+ };
95
+
96
+ // Planks across the walking direction, following the arc.
97
+ const plankPitchGap = 0.42;
98
+ const plankCount = Math.max(Math.round(span / plankPitchGap), 3);
99
+ for (let index = 0; index < plankCount; index += 1) {
100
+ const along = (index + 0.5) * (span / plankCount);
101
+ const at = atLength(along);
102
+ addBox(
103
+ WOOD.base, 0.9 + random(index) * 0.18,
104
+ at.x, at.y + 0.045, at.z,
105
+ halfWidth * 2, 0.09, span / plankCount * 0.82,
106
+ at.heading, -at.pitch,
107
+ );
108
+ }
109
+
110
+ // Two stringer beams under the planks, segment per deck sample pair.
111
+ for (let index = 0; index < deck.length - 1; index += 1) {
112
+ const a = deck[index];
113
+ const b = deck[index + 1];
114
+ const length = Math.hypot(b.x - a.x, b.z - a.z, b.profile - a.profile);
115
+ const heading = Math.atan2(b.x - a.x, b.z - a.z);
116
+ const pitch = Math.atan2(b.profile - a.profile, Math.hypot(b.x - a.x, b.z - a.z));
117
+ for (const side of [-1, 1]) {
118
+ const offset = halfWidth * 0.56 * side;
119
+ addBox(
120
+ WOOD.dark, 0.9,
121
+ (a.x + b.x) / 2 + a.sideX * offset,
122
+ (a.profile + b.profile) / 2 - 0.09,
123
+ (a.z + b.z) / 2 + a.sideZ * offset,
124
+ 0.14, 0.16, length + 0.05,
125
+ heading, -pitch,
126
+ );
127
+ }
128
+ }
129
+
130
+ // Rails: posts + top rail (and a mid beam for the 'beams' style).
131
+ const blockers = [];
132
+ if (bridge.railStyle !== 'none') {
133
+ const postCount = Math.max(Math.round(span / bridge.postSpacing), 2);
134
+ const railHeight = 0.98;
135
+ for (const side of [-1, 1]) {
136
+ const posts = [];
137
+ for (let index = 0; index <= postCount; index += 1) {
138
+ const along = index * (span / postCount);
139
+ const at = atLength(along);
140
+ const px = at.x + at.sideX * (halfWidth - 0.05) * side;
141
+ const pz = at.z + at.sideZ * (halfWidth - 0.05) * side;
142
+ addBox(
143
+ WOOD.dark, 0.86 + random(index, side + 2) * 0.14,
144
+ px, at.y + railHeight / 2, pz,
145
+ 0.11, railHeight, 0.11,
146
+ at.heading,
147
+ );
148
+ posts.push({ x: px, y: at.y, z: pz });
149
+ }
150
+ for (let index = 0; index < posts.length - 1; index += 1) {
151
+ const a = posts[index];
152
+ const b = posts[index + 1];
153
+ const length = Math.hypot(b.x - a.x, b.z - a.z, b.y - a.y);
154
+ const heading = Math.atan2(b.x - a.x, b.z - a.z);
155
+ const pitch = Math.atan2(b.y - a.y, Math.hypot(b.x - a.x, b.z - a.z));
156
+ addBox(
157
+ WOOD.light, 0.95,
158
+ (a.x + b.x) / 2, (a.y + b.y) / 2 + railHeight - 0.06, (a.z + b.z) / 2,
159
+ 0.09, 0.09, length + 0.06,
160
+ heading, -pitch,
161
+ );
162
+ if (bridge.railStyle === 'beams') {
163
+ addBox(
164
+ WOOD.base, 0.92,
165
+ (a.x + b.x) / 2, (a.y + b.y) / 2 + railHeight * 0.5, (a.z + b.z) / 2,
166
+ 0.07, 0.07, length + 0.06,
167
+ heading, -pitch,
168
+ );
169
+ }
170
+ }
171
+ // Collision: a fence of circles along the rail line keeps the
172
+ // character on the deck without blocking the deck itself.
173
+ const blockerSpacing = 0.7;
174
+ const blockerCount = Math.max(Math.round(span / blockerSpacing), 2);
175
+ for (let index = 0; index <= blockerCount; index += 1) {
176
+ const at = atLength(index * (span / blockerCount));
177
+ blockers.push({
178
+ radius: 0.3,
179
+ x: at.x + at.sideX * (halfWidth + 0.18) * side,
180
+ z: at.z + at.sideZ * (halfWidth + 0.18) * side,
181
+ });
182
+ }
183
+ }
184
+ }
185
+
186
+ // Abutments: a grounded block under each deck end, so the first planks
187
+ // never hover over the bank.
188
+ for (const endIndex of [0, deck.length - 1]) {
189
+ const at = deck[endIndex];
190
+ const bankY = typeof heightAt === 'function'
191
+ ? Number(heightAt(at.x, at.z)) || at.profile
192
+ : at.profile;
193
+ const top = at.profile + 0.02;
194
+ const bottom = Math.min(bankY, top) - 0.55;
195
+ addBox(
196
+ WOOD.dark, 0.85,
197
+ at.x, (top + bottom) / 2, at.z,
198
+ halfWidth * 2 - 0.06, top - bottom, 1.15,
199
+ Math.atan2(
200
+ deck[Math.min(endIndex + 1, deck.length - 1)].x - deck[Math.max(endIndex - 1, 0)].x,
201
+ deck[Math.min(endIndex + 1, deck.length - 1)].z - deck[Math.max(endIndex - 1, 0)].z,
202
+ ),
203
+ );
204
+ }
205
+
206
+ // Piers on long crossings: post pairs down to the bed.
207
+ if (span > bridge.pierSpacing * 1.4) {
208
+ const pierCount = Math.floor(span / bridge.pierSpacing);
209
+ const bed = typeof heightAt === 'function' ? heightAt : () => waterLevel - 2;
210
+ for (let index = 1; index <= pierCount; index += 1) {
211
+ const along = index * (span / (pierCount + 1));
212
+ const at = atLength(along);
213
+ for (const side of [-1, 1]) {
214
+ const px = at.x + at.sideX * halfWidth * 0.56 * side;
215
+ const pz = at.z + at.sideZ * halfWidth * 0.56 * side;
216
+ const bedY = Math.min(Number(bed(px, pz)) || waterLevel - 2, waterLevel - 0.2);
217
+ const top = at.y - 0.12;
218
+ addBox(
219
+ WOOD.dark, 0.82 + random(index, side + 7) * 0.1,
220
+ px, (top + bedY - 0.5) / 2, pz,
221
+ 0.16, top - bedY + 0.5, 0.16,
222
+ at.heading,
223
+ );
224
+ }
225
+ }
226
+ }
227
+
228
+ unitBox.dispose();
229
+ const geometry = mergePathGeometries(geometries);
230
+ for (const piece of geometries) piece.dispose();
231
+ return { blockers, deckWidth: halfWidth * 2, geometry, span };
232
+ }
@@ -0,0 +1,35 @@
1
+ // Built-in path network presets. Same convention as every cluster:
2
+ // `default` is the neutral starting point, `call_me_sensei` is the
3
+ // studio-managed look used across the reference worlds.
4
+
5
+ import { createPathRecipeDocument } from './pathSettings.js';
6
+
7
+ export const BUILT_IN_PATH_PRESETS = Object.freeze([
8
+ Object.freeze({
9
+ description: 'Neutral dirt trail network with plank bridges.',
10
+ id: 'default',
11
+ label: 'Default',
12
+ recipe: Object.freeze(createPathRecipeDocument({
13
+ auto: { count: 4, styles: ['dirt'] },
14
+ seed: 1,
15
+ })),
16
+ }),
17
+ Object.freeze({
18
+ description: 'The studio look: wandering dirt trails, a stone spine road, arched bridges with rails, stairs on the climbs.',
19
+ id: 'call_me_sensei',
20
+ label: 'Call Me Sensei',
21
+ recipe: Object.freeze(createPathRecipeDocument({
22
+ auto: { count: 5, styles: ['dirt', 'dirt', 'stone'] },
23
+ seed: 7,
24
+ settings: {
25
+ bridge: { arc: 0.11, railStyle: 'posts' },
26
+ ribbon: { edgeFade: 1.8, width: 2.8, widthWobble: 0.28 },
27
+ routing: { reuseBonus: 0.5, slopeCost: 30 },
28
+ },
29
+ })),
30
+ }),
31
+ ]);
32
+
33
+ export function findPathPreset(id) {
34
+ return BUILT_IN_PATH_PRESETS.find((preset) => preset.id === id) ?? null;
35
+ }