@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,462 @@
1
+ // Wave-2 (rural set) and wave-3 (garden/urban) prop generators. Same
2
+ // contract as wave 1: { main, glow, footprint } with origin at ground
3
+ // contact.
4
+
5
+ import { PartsBuilder } from './propParts.js';
6
+
7
+ const TAU = Math.PI * 2;
8
+
9
+ // --- well ---------------------------------------------------------------------
10
+
11
+ export function generateWell(settings, random, detail = 'hi') {
12
+ const { shape } = settings;
13
+ const hi = detail === 'hi';
14
+ const builder = new PartsBuilder(settings.surface, random);
15
+ const radius = shape.radius;
16
+ const wall = shape.wallHeight;
17
+
18
+ // stone ring: lathe with a lip
19
+ builder.lathe([
20
+ [radius + 0.16, 0], [radius + 0.14, wall * 0.82], [radius + 0.2, wall * 0.86],
21
+ [radius + 0.2, wall], [radius - 0.04, wall], [radius - 0.04, wall * 0.35],
22
+ ], { segments: hi ? 12 : 8, slot: 0 });
23
+ {
24
+ // individual coping stones for the hand-built read (values drawn on
25
+ // both detail levels so the structure stream stays aligned)
26
+ const stones = 8;
27
+ for (let index = 0; index < stones; index += 1) {
28
+ const angle = (index / stones) * TAU + random() * 0.1;
29
+ const roll = (random() - 0.5) * 0.06;
30
+ const shade = 0.88 + random() * 0.2;
31
+ if (!hi) continue;
32
+ builder.box(0.32, 0.12, 0.24, {
33
+ position: [Math.cos(angle) * (radius + 0.02), wall + 0.05, Math.sin(angle) * (radius + 0.02)],
34
+ rotation: [0, -angle + Math.PI / 2, roll],
35
+ shade,
36
+ slot: 0,
37
+ });
38
+ }
39
+ }
40
+
41
+ if (settings.asset.variant === 'roofed') {
42
+ const roofY = shape.roofHeight;
43
+ for (const side of [-1, 1]) {
44
+ builder.box(0.09, roofY, 0.09, {
45
+ position: [side * (radius + 0.05), roofY / 2, 0], shade: 0.95, slot: 1,
46
+ });
47
+ }
48
+ // winch beam + rope + bucket
49
+ builder.cylinder(0.04, 0.04, (radius + 0.2) * 2, {
50
+ position: [0, roofY * 0.82, 0],
51
+ rotation: [0, 0, Math.PI / 2],
52
+ segments: hi ? 7 : 5,
53
+ shade: 0.85,
54
+ slot: 1,
55
+ });
56
+ if (shape.bucket >= 0.5) {
57
+ const ropeLength = roofY * 0.82 - wall - 0.3;
58
+ builder.cylinder(0.012, 0.012, ropeLength, {
59
+ position: [0, roofY * 0.82 - ropeLength / 2, 0], segments: 5, shade: 0.7, slot: 2,
60
+ });
61
+ builder.lathe([
62
+ [0.09, 0], [0.12, 0.16], [0.115, 0.18],
63
+ ], { position: [0, wall + 0.12, 0], segments: hi ? 9 : 6, shade: 0.9, slot: 1 });
64
+ }
65
+ // gable roof: two slabs + ridge
66
+ const roofSpan = radius + 0.55;
67
+ for (const side of [-1, 1]) {
68
+ builder.box(roofSpan, 0.05, roofSpan * 1.15, {
69
+ position: [side * roofSpan * 0.33, roofY + roofSpan * 0.2, 0],
70
+ rotation: [0, 0, side * -0.62],
71
+ shade: 0.9,
72
+ slot: 1,
73
+ });
74
+ }
75
+ builder.box(0.1, 0.08, roofSpan * 1.18, {
76
+ position: [0, roofY + roofSpan * 0.37, 0], shade: 0.8, slot: 1,
77
+ });
78
+ }
79
+ return { footprint: { radius: radius + 0.32 }, ...builder.build() };
80
+ }
81
+
82
+ // --- crates & barrels ----------------------------------------------------------
83
+
84
+ export function generateCrateStack(settings, random, detail = 'hi') {
85
+ const { shape } = settings;
86
+ const hi = detail === 'hi';
87
+ const builder = new PartsBuilder(settings.surface, random);
88
+ const size = shape.size;
89
+ const count = Math.round(shape.count);
90
+ const placed = [];
91
+
92
+ for (let index = 0; index < count; index += 1) {
93
+ const isBarrel = settings.asset.variant === 'barrels'
94
+ || (settings.asset.variant === 'mixed' && random() < 0.45);
95
+ const pieceSize = size * (0.85 + random() * 0.3);
96
+ // stack on an earlier piece, or spread on the ground
97
+ const stackOn = placed.length > 0 && random() < shape.stackiness
98
+ ? placed[Math.floor(random() * placed.length)]
99
+ : null;
100
+ const x = stackOn ? stackOn.x + (random() - 0.5) * 0.1 : (random() - 0.5) * shape.jitter * size * 2.6;
101
+ const z = stackOn ? stackOn.z + (random() - 0.5) * 0.1 : (random() - 0.5) * shape.jitter * size * 2.6;
102
+ const y = stackOn ? stackOn.top : 0;
103
+ const yaw = random() * TAU * shape.jitter * 0.5;
104
+ if (isBarrel) {
105
+ const height = pieceSize * 1.15;
106
+ builder.lathe([
107
+ [pieceSize * 0.34, 0], [pieceSize * 0.42, height * 0.5], [pieceSize * 0.34, height],
108
+ ], { position: [x, y, z], rotation: [0, yaw, 0], segments: hi ? 11 : 7, shade: 0.92 + random() * 0.14, slot: 0 });
109
+ if (hi) {
110
+ for (const bandY of [height * 0.18, height * 0.82]) {
111
+ builder.cylinder(pieceSize * 0.395, pieceSize * 0.395, 0.035, {
112
+ position: [x, y + bandY, z], segments: 11, shade: 0.8, slot: 2,
113
+ });
114
+ }
115
+ }
116
+ placed.push({ top: y + height, x, z });
117
+ } else {
118
+ const height = pieceSize * 0.92;
119
+ builder.box(pieceSize, height, pieceSize, {
120
+ position: [x, y + height / 2, z],
121
+ rotation: [0, yaw, 0],
122
+ shade: 0.9 + random() * 0.18,
123
+ slot: random() < 0.3 ? 1 : 0,
124
+ });
125
+ if (hi) {
126
+ // lid frame reads "crate" from any distance
127
+ builder.box(pieceSize * 1.04, 0.045, pieceSize * 1.04, {
128
+ position: [x, y + height - 0.02, z], rotation: [0, yaw, 0], shade: 0.78, slot: 1,
129
+ });
130
+ }
131
+ placed.push({ top: y + height, x, z });
132
+ }
133
+ }
134
+ const spread = size * (1 + shape.jitter);
135
+ return { footprint: { radius: Math.max(spread, size * 0.8) + 0.15 }, ...builder.build() };
136
+ }
137
+
138
+ // --- firewood pile ---------------------------------------------------------------
139
+
140
+ export function generateFirewood(settings, random, detail = 'hi') {
141
+ const { shape } = settings;
142
+ const hi = detail === 'hi';
143
+ const builder = new PartsBuilder(settings.surface, random);
144
+ const loose = settings.asset.variant === 'loose';
145
+ const logRadius = shape.logRadius;
146
+ const rows = Math.max(Math.round(shape.height / (logRadius * 2)), 2);
147
+ const logsPerRow = Math.max(Math.round(shape.length / (logRadius * 2.2)), 4);
148
+ const logLength = 0.45;
149
+
150
+ if (!hi) {
151
+ // far LOD: one box per row keeps the silhouette at ~2% of the triangles
152
+ for (let row = 0; row < rows; row += 1) {
153
+ const rowCount = loose ? Math.max(logsPerRow - row, 2) : logsPerRow - (row % 2);
154
+ builder.box(rowCount * logRadius * 2.15, logRadius * 2, logLength, {
155
+ position: [0, logRadius + row * logRadius * (loose ? 1.55 : 1.8), 0],
156
+ shade: 0.9,
157
+ slot: 0,
158
+ });
159
+ }
160
+ return {
161
+ footprint: {
162
+ circles: [
163
+ { radius: 0.42, x: -shape.length * 0.25, z: 0 },
164
+ { radius: 0.42, x: shape.length * 0.25, z: 0 },
165
+ ],
166
+ },
167
+ ...builder.build(),
168
+ };
169
+ }
170
+ for (let row = 0; row < rows; row += 1) {
171
+ const rowCount = loose ? Math.max(logsPerRow - row, 2) : logsPerRow - (row % 2);
172
+ for (let index = 0; index < rowCount; index += 1) {
173
+ if (loose && random() < 0.2) continue;
174
+ const x = (index - rowCount / 2 + 0.5) * logRadius * 2.15
175
+ + (loose ? (random() - 0.5) * logRadius * 1.6 : (random() - 0.5) * logRadius * 0.3);
176
+ const y = logRadius + row * logRadius * (loose ? 1.55 : 1.8);
177
+ const z = (loose ? (random() - 0.5) * logRadius * 3 : (row % 2) * logRadius * 0.2);
178
+ const roll = (random() - 0.5) * (loose ? 0.5 : 0.08);
179
+ builder.cylinder(logRadius, logRadius, logLength, {
180
+ position: [x, y, z],
181
+ rotation: [Math.PI / 2 + roll, 0, 0],
182
+ segments: hi ? 7 : 5,
183
+ shade: 0.85 + random() * 0.25,
184
+ slot: 0,
185
+ });
186
+ if (hi) {
187
+ // pale cut face on the near end
188
+ builder.cylinder(logRadius * 0.92, logRadius * 0.92, 0.012, {
189
+ position: [x, y, z + logLength / 2],
190
+ rotation: [Math.PI / 2 + roll, 0, 0],
191
+ segments: 7,
192
+ shade: 1,
193
+ slot: 2,
194
+ });
195
+ }
196
+ }
197
+ }
198
+ return {
199
+ footprint: {
200
+ circles: [
201
+ { radius: 0.42, x: -shape.length * 0.25, z: 0 },
202
+ { radius: 0.42, x: shape.length * 0.25, z: 0 },
203
+ ],
204
+ },
205
+ ...builder.build(),
206
+ };
207
+ }
208
+
209
+ // --- torii gate -------------------------------------------------------------------
210
+
211
+ export function generateTorii(settings, random, detail = 'hi') {
212
+ const { shape } = settings;
213
+ const hi = detail === 'hi';
214
+ const builder = new PartsBuilder(settings.surface, random);
215
+ const height = shape.height * (0.97 + random() * 0.06);
216
+ const halfSpan = height * 0.5 * shape.width * (0.97 + random() * 0.06);
217
+ const pillarR = height * shape.pillarRadius;
218
+ const lean = 0.05 + random() * 0.02; // classic inward pillar lean
219
+
220
+ for (const side of [-1, 1]) {
221
+ builder.cylinder(pillarR * 0.85, pillarR, height * 0.92, {
222
+ position: [side * halfSpan, height * 0.46, 0],
223
+ rotation: [0, 0, -side * lean],
224
+ segments: hi ? 10 : 6,
225
+ slot: 0,
226
+ });
227
+ // base stone
228
+ builder.cylinder(pillarR * 1.5, pillarR * 1.7, height * 0.05, {
229
+ position: [side * halfSpan, height * 0.025, 0],
230
+ segments: hi ? 10 : 6,
231
+ shade: 0.85,
232
+ slot: 1,
233
+ });
234
+ }
235
+ // nuki (tie beam through the pillars)
236
+ builder.box(halfSpan * 2 + pillarR * 3, height * 0.055, pillarR * 1.1, {
237
+ position: [0, height * 0.72, 0], slot: 0, shade: 0.96,
238
+ });
239
+ // gakuzuka (center strut)
240
+ builder.box(pillarR * 1.2, height * 0.14, pillarR * 0.9, {
241
+ position: [0, height * 0.82, 0], slot: 0, shade: 0.9,
242
+ });
243
+ // shimaki + kasagi (double top lintel, curved for myōjin)
244
+ const curve = settings.asset.variant === 'shinmei' ? 0 : shape.curvature;
245
+ const lintelSegments = curve > 0.05 && hi ? 5 : 1;
246
+ const lintelSpan = halfSpan * 2 + pillarR * 5;
247
+ for (const [thickness, yBase, lengthScale, shadeValue, slot] of [
248
+ [height * 0.05, height * 0.885, 0.94, 0.92, 0], // shimaki: vermilion
249
+ [height * 0.075, height * 0.95, 1, 1, 2], // kasagi: black cap
250
+ ]) {
251
+ for (let segment = 0; segment < lintelSegments; segment += 1) {
252
+ const t0 = segment / lintelSegments - 0.5;
253
+ const t1 = (segment + 1) / lintelSegments - 0.5;
254
+ const x0 = t0 * lintelSpan * lengthScale;
255
+ const x1 = t1 * lintelSpan * lengthScale;
256
+ const lift = (t) => Math.abs(t * 2) ** 2 * curve * height * 0.09;
257
+ const y0 = yBase + lift(t0);
258
+ const y1 = yBase + lift(t1);
259
+ const segLength = Math.hypot(x1 - x0, y1 - y0);
260
+ builder.box(segLength + 0.02, thickness, pillarR * 2.4, {
261
+ position: [(x0 + x1) / 2, (y0 + y1) / 2, 0],
262
+ rotation: [0, 0, Math.atan2(y1 - y0, x1 - x0)],
263
+ shade: shadeValue,
264
+ slot,
265
+ });
266
+ }
267
+ }
268
+ return {
269
+ footprint: {
270
+ circles: [
271
+ { radius: pillarR * 2, x: -halfSpan, z: 0 },
272
+ { radius: pillarR * 2, x: halfSpan, z: 0 },
273
+ ],
274
+ },
275
+ ...builder.build(),
276
+ };
277
+ }
278
+
279
+ // --- pier ---------------------------------------------------------------------------
280
+
281
+ export function generatePier(settings, random, detail = 'hi') {
282
+ const { shape } = settings;
283
+ const hi = detail === 'hi';
284
+ const builder = new PartsBuilder(settings.surface, random);
285
+ const width = shape.width;
286
+ const deckY = shape.deckHeight;
287
+ const plankPitch = 0.34;
288
+
289
+ const buildRun = (fromZ, toZ, offsetX = 0, runWidth = width) => {
290
+ const length = toZ - fromZ;
291
+ const planks = Math.max(Math.round(length / plankPitch), 3);
292
+ for (let index = 0; index < planks; index += 1) {
293
+ const z = fromZ + (index + 0.5) * (length / planks);
294
+ builder.box(runWidth, 0.07, length / planks * 0.8, {
295
+ position: [offsetX, deckY, z],
296
+ rotation: [0, (random() - 0.5) * 0.02, 0],
297
+ shade: 0.88 + random() * 0.2,
298
+ slot: 0,
299
+ });
300
+ }
301
+ // stringers + posts
302
+ for (const side of [-1, 1]) {
303
+ builder.box(0.12, 0.14, length, {
304
+ position: [offsetX + side * runWidth * 0.42, deckY - 0.1, fromZ + length / 2],
305
+ shade: 0.8,
306
+ slot: 1,
307
+ });
308
+ }
309
+ const bays = Math.max(Math.round(length / 2.4), 1);
310
+ for (let bay = 0; bay <= bays; bay += 1) {
311
+ const z = fromZ + (bay / bays) * length;
312
+ for (const side of [-1, 1]) {
313
+ builder.cylinder(0.07, 0.085, deckY + 1.3, {
314
+ position: [offsetX + side * runWidth * 0.42, (deckY - 1.3) / 2 + 0.65 - 0.65, z],
315
+ segments: hi ? 7 : 5,
316
+ shade: 0.75 + random() * 0.15,
317
+ slot: 1,
318
+ });
319
+ }
320
+ }
321
+ };
322
+
323
+ buildRun(0, shape.length);
324
+ if (settings.asset.variant === 'tShape') {
325
+ const headWidth = shape.length * 0.45;
326
+ buildRun(shape.length, shape.length + width, 0, headWidth * 2);
327
+ }
328
+ if (shape.rails >= 1) {
329
+ const sides = shape.rails >= 2 ? [-1, 1] : [1];
330
+ for (const side of sides) {
331
+ const posts = Math.max(Math.round(shape.length / 1.8), 2);
332
+ for (let index = 0; index <= posts; index += 1) {
333
+ const z = (index / posts) * shape.length;
334
+ builder.box(0.07, 0.85, 0.07, {
335
+ position: [side * width * 0.46, deckY + 0.42, z], shade: 0.82, slot: 1,
336
+ });
337
+ }
338
+ builder.box(0.06, 0.06, shape.length, {
339
+ position: [side * width * 0.46, deckY + 0.85, shape.length / 2], shade: 0.95, slot: 0,
340
+ });
341
+ }
342
+ }
343
+ return {
344
+ footprint: {
345
+ circles: [
346
+ { radius: width * 0.35, x: -width * 0.42, z: shape.length * 0.5 },
347
+ { radius: width * 0.35, x: width * 0.42, z: shape.length * 0.5 },
348
+ ],
349
+ },
350
+ ...builder.build(),
351
+ };
352
+ }
353
+
354
+ // --- stone wall (linear) --------------------------------------------------------------
355
+
356
+ export function generateStoneWallAlong(settings, random, points, detail = 'hi') {
357
+ const { shape } = settings;
358
+ const hi = detail === 'hi';
359
+ const builder = new PartsBuilder(settings.surface, random);
360
+ const courses = Math.max(Math.round(shape.height / (0.22 * shape.stoneSize)), 2);
361
+ const stoneH = shape.height / courses;
362
+
363
+ for (let index = 0; index < points.length - 1; index += 1) {
364
+ const a = points[index];
365
+ const b = points[index + 1];
366
+ const span = Math.hypot(b.x - a.x, b.z - a.z);
367
+ const heading = Math.atan2(b.x - a.x, b.z - a.z);
368
+ const stones = Math.max(Math.round(span / (0.5 * shape.stoneSize)), 1);
369
+ for (let course = 0; course < courses; course += 1) {
370
+ for (let stone = 0; stone < stones; stone += 1) {
371
+ const t = (stone + 0.5 + (course % 2) * 0.5) / stones;
372
+ if (t > 1) continue;
373
+ const x = a.x + (b.x - a.x) * t;
374
+ const z = a.z + (b.z - a.z) * t;
375
+ const y = a.y + (b.y - a.y) * t;
376
+ const mossRoll = random();
377
+ builder.box(
378
+ (span / stones) * (0.94 + random() * 0.1),
379
+ stoneH * (0.9 + random() * 0.18),
380
+ shape.thickness * (0.9 + random() * 0.2),
381
+ {
382
+ position: [x, y + (course + 0.5) * stoneH, z],
383
+ rotation: [(random() - 0.5) * 0.04, heading + Math.PI / 2 + (random() - 0.5) * 0.05, (random() - 0.5) * 0.04],
384
+ shade: 0.85 + random() * 0.28,
385
+ slot: hi && course === 0 && mossRoll < 0.35 ? 2 : 0, // mossy base stones
386
+ },
387
+ );
388
+ }
389
+ }
390
+ if (shape.topCourse >= 0.5) {
391
+ const copings = Math.max(Math.round(span / 0.32), 1);
392
+ for (let coping = 0; coping < copings; coping += 1) {
393
+ const t = (coping + 0.5) / copings;
394
+ const rollA = (random() - 0.5) * 0.1;
395
+ const rollB = (random() - 0.5) * 0.12;
396
+ const shade = 0.9 + random() * 0.16;
397
+ if (!hi) continue;
398
+ const x = a.x + (b.x - a.x) * t;
399
+ const z = a.z + (b.z - a.z) * t;
400
+ const y = a.y + (b.y - a.y) * t;
401
+ builder.box(0.24, 0.26, shape.thickness * 0.7, {
402
+ position: [x, y + shape.height + 0.1, z],
403
+ rotation: [rollA, heading + Math.PI / 2, rollB],
404
+ shade,
405
+ slot: 1,
406
+ });
407
+ }
408
+ }
409
+ }
410
+
411
+ const footprints = [];
412
+ for (let index = 0; index < points.length - 1; index += 1) {
413
+ const a = points[index];
414
+ const b = points[index + 1];
415
+ const span = Math.hypot(b.x - a.x, b.z - a.z);
416
+ const steps = Math.max(Math.round(span / 0.5), 1);
417
+ for (let step = 0; step <= steps; step += 1) {
418
+ const t = step / steps;
419
+ footprints.push({
420
+ radius: shape.thickness * 0.75,
421
+ x: a.x + (b.x - a.x) * t,
422
+ z: a.z + (b.z - a.z) * t,
423
+ });
424
+ }
425
+ }
426
+ return { footprints, ...builder.build() };
427
+ }
428
+
429
+ // --- bench ------------------------------------------------------------------------------
430
+
431
+ export function generateBench(settings, random, detail = 'hi') {
432
+ const { shape } = settings;
433
+ const builder = new PartsBuilder(settings.surface, random);
434
+ const isLog = settings.asset.variant === 'log';
435
+ const length = shape.length * (0.95 + random() * 0.1);
436
+ if (isLog) {
437
+ builder.cylinder(0.16, 0.16, length, {
438
+ position: [0, shape.height - 0.08, 0],
439
+ rotation: [0, 0, Math.PI / 2],
440
+ scale: [1, 1, 0.7],
441
+ segments: detail === 'hi' ? 9 : 6,
442
+ slot: 0,
443
+ });
444
+ } else {
445
+ builder.box(length, 0.06, 0.38, {
446
+ position: [0, shape.height, 0], slot: 0,
447
+ });
448
+ }
449
+ for (const side of [-1, 1]) {
450
+ builder.box(0.08, shape.height, isLog ? 0.3 : 0.34, {
451
+ position: [side * (length * 0.42), shape.height / 2, 0],
452
+ shade: 0.85,
453
+ slot: 1,
454
+ });
455
+ }
456
+ if (!isLog && shape.backrest >= 0.5) {
457
+ builder.box(length, 0.3, 0.05, {
458
+ position: [0, shape.height + 0.32, -0.17], rotation: [-0.16, 0, 0], shade: 0.95, slot: 0,
459
+ });
460
+ }
461
+ return { footprint: { radius: Math.max(length * 0.5, 0.4) }, ...builder.build() };
462
+ }
@@ -0,0 +1,5 @@
1
+ export * from './propSettings.js';
2
+ export * from './propAsset.js';
3
+ export * from './propPlacement.js';
4
+ export * from './propPresets.js';
5
+ export { PartsBuilder, createPropRandom, mergePainted } from './propParts.js';