@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.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// Canonical settings and field metadata for the path/road/bridge generator.
|
|
2
|
+
// Follows the library-wide convention: DEFAULT_PATH_SETTINGS holds the
|
|
3
|
+
// grouped values, PATH_SETTING_GROUPS + PATH_SETTING_FIELD_SCHEMA drive the
|
|
4
|
+
// debug panel and the generated settings reference, and the recipe document
|
|
5
|
+
// ({ schema, version, seed, routes, auto, settings }) rebuilds the identical
|
|
6
|
+
// network forever.
|
|
7
|
+
|
|
8
|
+
export const PATH_STYLES = Object.freeze([
|
|
9
|
+
Object.freeze({ id: 'dirt', label: 'Dirt' }),
|
|
10
|
+
Object.freeze({ id: 'stone', label: 'Stone' }),
|
|
11
|
+
Object.freeze({ id: 'planks', label: 'Planks' }),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export const PATH_STYLE_IDS = Object.freeze(PATH_STYLES.map((entry) => entry.id));
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_PATH_SETTINGS = Object.freeze({
|
|
17
|
+
routing: Object.freeze({
|
|
18
|
+
pointCount: 4,
|
|
19
|
+
slopeCost: 26,
|
|
20
|
+
waterCost: 14,
|
|
21
|
+
reuseBonus: 0.45,
|
|
22
|
+
gridStep: 8,
|
|
23
|
+
shoreMargin: 0.6,
|
|
24
|
+
loopChance: 0.35,
|
|
25
|
+
}),
|
|
26
|
+
ribbon: Object.freeze({
|
|
27
|
+
width: 2.6,
|
|
28
|
+
widthWobble: 0.22,
|
|
29
|
+
edgeSkirt: 1.1,
|
|
30
|
+
lift: 0.07,
|
|
31
|
+
smoothing: 16,
|
|
32
|
+
stepLength: 2,
|
|
33
|
+
edgeFade: 1.4,
|
|
34
|
+
}),
|
|
35
|
+
bridge: Object.freeze({
|
|
36
|
+
arc: 0.1,
|
|
37
|
+
railStyle: 'posts',
|
|
38
|
+
postSpacing: 2.2,
|
|
39
|
+
minSpan: 4,
|
|
40
|
+
pierSpacing: 7,
|
|
41
|
+
deckClearance: 1.1,
|
|
42
|
+
}),
|
|
43
|
+
stairs: Object.freeze({
|
|
44
|
+
slopeThreshold: 0.45,
|
|
45
|
+
stepHeight: 0.19,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const PATH_SETTING_GROUPS = Object.freeze([
|
|
50
|
+
Object.freeze({
|
|
51
|
+
id: 'routing',
|
|
52
|
+
label: 'Routing',
|
|
53
|
+
description: 'Cost-field router: how strongly slope and water repel routes, and how much existing paths attract reuse (forks and junctions).',
|
|
54
|
+
}),
|
|
55
|
+
Object.freeze({
|
|
56
|
+
id: 'ribbon',
|
|
57
|
+
label: 'Ribbon',
|
|
58
|
+
description: 'The walkable strip: width, hand-drawn wobble, edge skirts that tuck into the terrain, and the height-profile smoothing that flattens the walk.',
|
|
59
|
+
}),
|
|
60
|
+
Object.freeze({
|
|
61
|
+
id: 'bridge',
|
|
62
|
+
label: 'Bridges',
|
|
63
|
+
description: 'Arched plank bridges generated where a route crosses open water.',
|
|
64
|
+
}),
|
|
65
|
+
Object.freeze({
|
|
66
|
+
id: 'stairs',
|
|
67
|
+
label: 'Stairs',
|
|
68
|
+
description: 'Stepped stone segments swapped in where the route climbs steeply. Visual only — paths.heightAt stays a smooth ramp.',
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const FIELD_DEFINITIONS = Object.freeze({
|
|
73
|
+
routing: Object.freeze({
|
|
74
|
+
pointCount: {
|
|
75
|
+
label: 'Points of Interest',
|
|
76
|
+
description: 'Auto mode: number of destinations probed from the terrain and connected into a network.',
|
|
77
|
+
range: { min: 2, max: 8, step: 1 },
|
|
78
|
+
},
|
|
79
|
+
slopeCost: {
|
|
80
|
+
label: 'Slope Cost',
|
|
81
|
+
description: 'How expensive climbing is for the router. Higher values hug contours and produce switchbacks instead of straight climbs.',
|
|
82
|
+
range: { min: 0, max: 80, step: 1 },
|
|
83
|
+
},
|
|
84
|
+
waterCost: {
|
|
85
|
+
label: 'Water Cost',
|
|
86
|
+
description: 'Cost multiplier for crossing water. High enough that routes only cross where a bridge is worth it, low enough that crossings still happen.',
|
|
87
|
+
range: { min: 2, max: 60, step: 1 },
|
|
88
|
+
},
|
|
89
|
+
reuseBonus: {
|
|
90
|
+
label: 'Reuse Bonus',
|
|
91
|
+
description: 'Cost discount (0..1) on cells an earlier route already walks — the source of natural forks and shared trunk roads.',
|
|
92
|
+
range: { min: 0, max: 0.9, step: 0.05 },
|
|
93
|
+
},
|
|
94
|
+
gridStep: {
|
|
95
|
+
label: 'Grid Step',
|
|
96
|
+
description: 'Router grid resolution in meters. Smaller steps find finer detours and cost more to solve.',
|
|
97
|
+
range: { min: 3, max: 24, step: 1 },
|
|
98
|
+
},
|
|
99
|
+
shoreMargin: {
|
|
100
|
+
label: 'Shore Margin',
|
|
101
|
+
description: 'Meters above the waterline a cell must be to count as dry land.',
|
|
102
|
+
range: { min: 0, max: 2, step: 0.1 },
|
|
103
|
+
},
|
|
104
|
+
loopChance: {
|
|
105
|
+
label: 'Loop Chance',
|
|
106
|
+
description: 'Auto mode: chance to add one extra ring road beyond the spanning network.',
|
|
107
|
+
range: { min: 0, max: 1, step: 0.05 },
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
ribbon: Object.freeze({
|
|
111
|
+
width: {
|
|
112
|
+
label: 'Width',
|
|
113
|
+
description: 'Walkable ribbon width in meters (dirt trail 2–3, stone road 3–4).',
|
|
114
|
+
range: { min: 1, max: 6, step: 0.1 },
|
|
115
|
+
},
|
|
116
|
+
widthWobble: {
|
|
117
|
+
label: 'Width Wobble',
|
|
118
|
+
description: 'Low-frequency width variation (0..1) for the hand-drawn look. 0 is a survey-straight road.',
|
|
119
|
+
range: { min: 0, max: 0.6, step: 0.02 },
|
|
120
|
+
},
|
|
121
|
+
edgeSkirt: {
|
|
122
|
+
label: 'Edge Skirt',
|
|
123
|
+
description: 'Extra meters each side that slope down and tuck under the terrain so the ribbon never floats on side slopes.',
|
|
124
|
+
range: { min: 0.2, max: 2.5, step: 0.05 },
|
|
125
|
+
},
|
|
126
|
+
lift: {
|
|
127
|
+
label: 'Lift',
|
|
128
|
+
description: 'Meters the ribbon rides above the height profile — the true-overlay offset that prevents z-fighting.',
|
|
129
|
+
range: { min: 0.02, max: 0.25, step: 0.01 },
|
|
130
|
+
},
|
|
131
|
+
smoothing: {
|
|
132
|
+
label: 'Profile Smoothing',
|
|
133
|
+
description: 'Moving-average window in meters applied to the terrain height along the route; the flattened profile is what paths.heightAt reports.',
|
|
134
|
+
range: { min: 0, max: 40, step: 1 },
|
|
135
|
+
},
|
|
136
|
+
stepLength: {
|
|
137
|
+
label: 'Step Length',
|
|
138
|
+
description: 'Meters between ribbon cross-sections. Smaller steps follow curves tighter and spend more triangles.',
|
|
139
|
+
range: { min: 1, max: 5, step: 0.25 },
|
|
140
|
+
},
|
|
141
|
+
edgeFade: {
|
|
142
|
+
label: 'Edge Fade',
|
|
143
|
+
description: 'Meters past the ribbon edge over which maskAt falls from 1 to 0 — the band where grass and flowers thin out.',
|
|
144
|
+
range: { min: 0.2, max: 4, step: 0.1 },
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
bridge: Object.freeze({
|
|
148
|
+
arc: {
|
|
149
|
+
label: 'Arch',
|
|
150
|
+
description: 'Deck rise as a fraction of span length. 0 is a flat causeway, 0.14 a strong arched footbridge.',
|
|
151
|
+
range: { min: 0, max: 0.18, step: 0.01 },
|
|
152
|
+
},
|
|
153
|
+
railStyle: {
|
|
154
|
+
label: 'Rail Style',
|
|
155
|
+
description: 'Bridge railing construction.',
|
|
156
|
+
type: 'select',
|
|
157
|
+
options: ['posts', 'beams', 'none'],
|
|
158
|
+
optionLabels: { posts: 'Posts + top rail', beams: 'Posts + double beams', none: 'No rails' },
|
|
159
|
+
},
|
|
160
|
+
postSpacing: {
|
|
161
|
+
label: 'Post Spacing',
|
|
162
|
+
description: 'Meters between railing posts.',
|
|
163
|
+
range: { min: 1.2, max: 4, step: 0.1 },
|
|
164
|
+
},
|
|
165
|
+
minSpan: {
|
|
166
|
+
label: 'Minimum Span',
|
|
167
|
+
description: 'Meters of open water a route must cross before a bridge is generated (shorter crossings ford instead).',
|
|
168
|
+
range: { min: 2, max: 12, step: 0.5 },
|
|
169
|
+
},
|
|
170
|
+
pierSpacing: {
|
|
171
|
+
label: 'Pier Spacing',
|
|
172
|
+
description: 'Long crossings get support piers to the bed every this many meters.',
|
|
173
|
+
range: { min: 4, max: 16, step: 0.5 },
|
|
174
|
+
},
|
|
175
|
+
deckClearance: {
|
|
176
|
+
label: 'Deck Clearance',
|
|
177
|
+
description: 'Minimum meters between the water level and the deck at mid-span.',
|
|
178
|
+
range: { min: 0.3, max: 3, step: 0.1 },
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
stairs: Object.freeze({
|
|
182
|
+
slopeThreshold: {
|
|
183
|
+
label: 'Slope Threshold',
|
|
184
|
+
description: 'Rise-over-run along the route beyond which the ribbon switches to stepped stone segments.',
|
|
185
|
+
range: { min: 0.2, max: 0.9, step: 0.05 },
|
|
186
|
+
},
|
|
187
|
+
stepHeight: {
|
|
188
|
+
label: 'Step Height',
|
|
189
|
+
description: 'Riser height of generated steps in meters.',
|
|
190
|
+
range: { min: 0.12, max: 0.3, step: 0.01 },
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
function createPathFieldMetadata(group, key, field) {
|
|
196
|
+
const defaultValue = DEFAULT_PATH_SETTINGS[group.id][key];
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
defaultValue: Array.isArray(defaultValue) ? [...defaultValue] : defaultValue,
|
|
199
|
+
description: field.description,
|
|
200
|
+
group: group.id,
|
|
201
|
+
id: `${group.id}.${key}`,
|
|
202
|
+
key,
|
|
203
|
+
label: field.label,
|
|
204
|
+
optionLabels: field.optionLabels ?? null,
|
|
205
|
+
options: field.options ?? null,
|
|
206
|
+
range: field.range ?? null,
|
|
207
|
+
serializable: true,
|
|
208
|
+
type: field.type ?? (typeof defaultValue === 'boolean' ? 'boolean'
|
|
209
|
+
: typeof defaultValue === 'number' ? 'number' : 'text'),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const PATH_SETTING_FIELD_SCHEMA = Object.freeze(
|
|
214
|
+
Object.fromEntries(
|
|
215
|
+
PATH_SETTING_GROUPS.map((group) => [
|
|
216
|
+
group.id,
|
|
217
|
+
Object.freeze(
|
|
218
|
+
Object.fromEntries(
|
|
219
|
+
Object.entries(FIELD_DEFINITIONS[group.id] ?? {})
|
|
220
|
+
.map(([key, field]) => [key, createPathFieldMetadata(group, key, field)]),
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
]),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
function cloneValue(value) {
|
|
228
|
+
if (Array.isArray(value)) return value.map(cloneValue);
|
|
229
|
+
if (value && typeof value === 'object') return Object.fromEntries(
|
|
230
|
+
Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]),
|
|
231
|
+
);
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function clonePathSettings(settings = DEFAULT_PATH_SETTINGS) {
|
|
236
|
+
return cloneValue(settings);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Normalizes partial overrides over the defaults; unknown keys are dropped. */
|
|
240
|
+
export function createPathSettings(overrides = {}) {
|
|
241
|
+
const result = clonePathSettings(DEFAULT_PATH_SETTINGS);
|
|
242
|
+
for (const groupId of Object.keys(result)) {
|
|
243
|
+
const group = overrides?.[groupId];
|
|
244
|
+
if (!group || typeof group !== 'object') continue;
|
|
245
|
+
for (const key of Object.keys(result[groupId])) {
|
|
246
|
+
if (group[key] === undefined) continue;
|
|
247
|
+
result[groupId][key] = cloneValue(group[key]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const routing = result.routing;
|
|
251
|
+
routing.pointCount = Math.min(12, Math.max(2, Math.round(Number(routing.pointCount) || 4)));
|
|
252
|
+
routing.gridStep = Math.min(32, Math.max(2, Number(routing.gridStep) || 8));
|
|
253
|
+
const ribbon = result.ribbon;
|
|
254
|
+
ribbon.width = Math.min(8, Math.max(0.8, Number(ribbon.width) || 2.6));
|
|
255
|
+
ribbon.stepLength = Math.min(6, Math.max(0.75, Number(ribbon.stepLength) || 2));
|
|
256
|
+
if (!['posts', 'beams', 'none'].includes(result.bridge.railStyle)) {
|
|
257
|
+
result.bridge.railStyle = 'posts';
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const PATH_RECIPE_SCHEMA = 'pathRecipe';
|
|
263
|
+
export const PATH_RECIPE_VERSION = 1;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Normalizes a route spec: either `{ from: [x, z], to: [x, z], style }`
|
|
267
|
+
* (cost-field routed) or `{ points: [[x, z], …], style }` (explicit
|
|
268
|
+
* centerline — village streets want exact control, not a router's opinion).
|
|
269
|
+
*/
|
|
270
|
+
export function normalizeRouteSpec(route) {
|
|
271
|
+
const point = (value) => (Array.isArray(value) && value.length >= 2
|
|
272
|
+
? [Number(value[0]) || 0, Number(value[1]) || 0]
|
|
273
|
+
: null);
|
|
274
|
+
const style = PATH_STYLE_IDS.includes(route?.style) ? route.style : 'dirt';
|
|
275
|
+
if (Array.isArray(route?.points)) {
|
|
276
|
+
const points = route.points.map(point).filter(Boolean);
|
|
277
|
+
if (points.length < 2) return null;
|
|
278
|
+
return { points, style, wander: route.wander !== false };
|
|
279
|
+
}
|
|
280
|
+
const from = point(route?.from);
|
|
281
|
+
const to = point(route?.to);
|
|
282
|
+
if (!from || !to) return null;
|
|
283
|
+
return { from, style, to };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Serializable recipe: everything needed to rebuild the identical network
|
|
288
|
+
* given the same terrain (`heightAt` itself is the host's, not serialized).
|
|
289
|
+
*/
|
|
290
|
+
export function createPathRecipeDocument({ seed = 1, routes = null, auto = null, settings = {} } = {}) {
|
|
291
|
+
const document = {
|
|
292
|
+
schema: PATH_RECIPE_SCHEMA,
|
|
293
|
+
seed: Math.round(Number(seed) || 1),
|
|
294
|
+
settings: createPathSettings(settings),
|
|
295
|
+
version: PATH_RECIPE_VERSION,
|
|
296
|
+
};
|
|
297
|
+
if (Array.isArray(routes) && routes.length > 0) {
|
|
298
|
+
document.routes = routes.map(normalizeRouteSpec).filter(Boolean);
|
|
299
|
+
}
|
|
300
|
+
if (auto && typeof auto === 'object') {
|
|
301
|
+
document.auto = {
|
|
302
|
+
count: Math.min(12, Math.max(2, Math.round(Number(auto.count) || 4))),
|
|
303
|
+
styles: Array.isArray(auto.styles) && auto.styles.length > 0
|
|
304
|
+
? auto.styles.filter((style) => PATH_STYLE_IDS.includes(style))
|
|
305
|
+
: ['dirt'],
|
|
306
|
+
};
|
|
307
|
+
if (document.auto.styles.length === 0) document.auto.styles = ['dirt'];
|
|
308
|
+
}
|
|
309
|
+
return document;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function validatePathRecipeDocument(input) {
|
|
313
|
+
const errors = [];
|
|
314
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
315
|
+
return { errors: ['Recipe document must be an object.'], ok: false };
|
|
316
|
+
}
|
|
317
|
+
if (input.schema !== PATH_RECIPE_SCHEMA) errors.push(`schema must be "${PATH_RECIPE_SCHEMA}".`);
|
|
318
|
+
if (!Number.isInteger(input.version) || input.version < 1 || input.version > PATH_RECIPE_VERSION) {
|
|
319
|
+
errors.push(`version must be an integer between 1 and ${PATH_RECIPE_VERSION}.`);
|
|
320
|
+
}
|
|
321
|
+
if (!Number.isFinite(Number(input.seed))) errors.push('seed must be a number.');
|
|
322
|
+
if (input.routes !== undefined) {
|
|
323
|
+
if (!Array.isArray(input.routes)) errors.push('routes must be an array when present.');
|
|
324
|
+
else if (input.routes.some((route) => !normalizeRouteSpec(route))) {
|
|
325
|
+
errors.push('every route needs { from: [x, z], to: [x, z] }.');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (input.auto !== undefined && (typeof input.auto !== 'object' || input.auto === null)) {
|
|
329
|
+
errors.push('auto must be an object when present.');
|
|
330
|
+
}
|
|
331
|
+
if (!input.routes?.length && !input.auto) {
|
|
332
|
+
errors.push('recipe needs routes, auto, or both.');
|
|
333
|
+
}
|
|
334
|
+
return { errors, ok: errors.length === 0 };
|
|
335
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Procedural detail textures for path ribbons — the same deterministic
|
|
2
|
+
// DataTexture approach as debrisgen (no canvas/DOM, headless-safe). Maps are
|
|
3
|
+
// near-white multipliers over baked vertex colors: the palette stays in the
|
|
4
|
+
// ribbon painter, the map adds the material read (trodden earth, cobble
|
|
5
|
+
// cells with grout, plank runs) that survives the environment toon
|
|
6
|
+
// conversion.
|
|
7
|
+
|
|
8
|
+
import * as THREE from 'three';
|
|
9
|
+
|
|
10
|
+
import { hashCombine } from '../rockgen/noise/prng.js';
|
|
11
|
+
import { fbm3, valueNoise3 } from '../rockgen/noise/valueNoise3.js';
|
|
12
|
+
import { periodicCellular2 } from '../texgen/noise2.js';
|
|
13
|
+
|
|
14
|
+
const SIZE = 256;
|
|
15
|
+
const cache = new Map();
|
|
16
|
+
|
|
17
|
+
const clamp01 = (value) => Math.min(Math.max(value, 0), 1);
|
|
18
|
+
|
|
19
|
+
function bakeTexture(shade) {
|
|
20
|
+
const data = new Uint8Array(SIZE * SIZE * 4);
|
|
21
|
+
for (let y = 0; y < SIZE; y += 1) {
|
|
22
|
+
for (let x = 0; x < SIZE; x += 1) {
|
|
23
|
+
const [r, g, b] = shade(x / SIZE, y / SIZE);
|
|
24
|
+
const offset = (y * SIZE + x) * 4;
|
|
25
|
+
data[offset] = Math.round(clamp01(r) * 255);
|
|
26
|
+
data[offset + 1] = Math.round(clamp01(g) * 255);
|
|
27
|
+
data[offset + 2] = Math.round(clamp01(b) * 255);
|
|
28
|
+
data[offset + 3] = 255;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const texture = new THREE.DataTexture(data, SIZE, SIZE, THREE.RGBAFormat);
|
|
32
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
33
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
34
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
35
|
+
texture.generateMipmaps = true;
|
|
36
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
37
|
+
texture.magFilter = THREE.LinearFilter;
|
|
38
|
+
texture.needsUpdate = true;
|
|
39
|
+
return texture;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Tileable fbm via torus-blend of four phase-shifted reads (same trick as
|
|
43
|
+
// debrisTextures — exact periodic noise is overkill for detail maps).
|
|
44
|
+
function tileNoise(seed, u, v, scaleU, scaleV, octaves = 3) {
|
|
45
|
+
const a = fbm3(seed, u * scaleU, v * scaleV, 0.37, octaves, 2, 0.5);
|
|
46
|
+
const b = fbm3(seed, (u - 1) * scaleU, v * scaleV, 0.37, octaves, 2, 0.5);
|
|
47
|
+
const c = fbm3(seed, u * scaleU, (v - 1) * scaleV, 0.37, octaves, 2, 0.5);
|
|
48
|
+
const d = fbm3(seed, (u - 1) * scaleU, (v - 1) * scaleV, 0.37, octaves, 2, 0.5);
|
|
49
|
+
return a * (1 - u) * (1 - v) + b * u * (1 - v) + c * (1 - u) * v + d * u * v;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Dirt: broad trodden mottle, faint wheel-rut darkening along V, sparse
|
|
53
|
+
// pebble specks.
|
|
54
|
+
function shadeDirt(seed) {
|
|
55
|
+
return (u, v) => {
|
|
56
|
+
const mottle = tileNoise(seed, u, v, 5, 5, 3);
|
|
57
|
+
const speck = valueNoise3(hashCombine(seed, 7), u * 70, v * 70, 0.25);
|
|
58
|
+
let value = 0.9 + mottle * 0.09;
|
|
59
|
+
// Two soft ruts at u ≈ 0.3 / 0.7 — reads as wear, not stripes.
|
|
60
|
+
const rut = Math.min(Math.abs(u - 0.3), Math.abs(u - 0.7));
|
|
61
|
+
value -= Math.max(0, 0.05 - rut * 0.4) * 0.7;
|
|
62
|
+
if (speck > 0.66) value += (speck - 0.66) * 0.65;
|
|
63
|
+
return [value, value * 0.985, value * 0.955];
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Stone: cobble cells from periodic cellular noise — cell interiors bright
|
|
68
|
+
// with per-cell tone drift, grout lines dark. The painterly cobble read.
|
|
69
|
+
function shadeStone(seed) {
|
|
70
|
+
const cellsU = 6;
|
|
71
|
+
const cellsV = 6;
|
|
72
|
+
return (u, v) => {
|
|
73
|
+
const { f1, f2, id } = periodicCellular2(seed, u * cellsU, v * cellsV, cellsU, cellsV, 0.85);
|
|
74
|
+
const edge = f2 - f1; // small near cell borders
|
|
75
|
+
const grout = 1 - clamp01((edge - 0.02) / 0.1);
|
|
76
|
+
const tone = 0.9 + ((id % 97) / 97 - 0.5) * 0.12;
|
|
77
|
+
const mottle = tileNoise(hashCombine(seed, 3), u, v, 7, 7, 2) * 0.05;
|
|
78
|
+
let value = tone + mottle - grout * 0.34;
|
|
79
|
+
// Painterly top-left cell highlight, like the cliff edge treatment.
|
|
80
|
+
value += (1 - grout) * 0.04;
|
|
81
|
+
return [value, value, value * 0.99];
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Planks: boards running across the path (breaks along V every board
|
|
86
|
+
// length), wood grain along U, darker seams.
|
|
87
|
+
function shadePlanks(seed) {
|
|
88
|
+
const boards = 5; // across (U)
|
|
89
|
+
return (u, v) => {
|
|
90
|
+
const board = Math.floor(u * boards);
|
|
91
|
+
// Stagger board ends per column so seams don't align.
|
|
92
|
+
const phase = valueNoise3(hashCombine(seed, board), board * 13.7, 0.5, 0.9) * 0.5;
|
|
93
|
+
const along = (v * 3 + phase) % 1;
|
|
94
|
+
const grain = tileNoise(hashCombine(seed, 5 + board), u * boards - board, v, 2, 18, 2);
|
|
95
|
+
let value = 0.88 + grain * 0.1;
|
|
96
|
+
const seamU = Math.abs(u * boards - board - 0.5);
|
|
97
|
+
if (seamU > 0.44) value -= (seamU - 0.44) * 3.2;
|
|
98
|
+
if (along < 0.035) value -= (1 - along / 0.035) * 0.22;
|
|
99
|
+
return [value, value * 0.97, value * 0.92];
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const SHADERS = { dirt: shadeDirt, planks: shadePlanks, stone: shadeStone };
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Cached deterministic detail texture per style + seed. Cache-owned:
|
|
107
|
+
* dispose of ribbon materials, never of `material.map`.
|
|
108
|
+
*/
|
|
109
|
+
export function getPathDetailTexture(style, seed = 1) {
|
|
110
|
+
const key = `${style}:${seed >>> 0}`;
|
|
111
|
+
let texture = cache.get(key);
|
|
112
|
+
if (!texture) {
|
|
113
|
+
const shade = SHADERS[style] ?? shadeDirt;
|
|
114
|
+
texture = bakeTexture(shade(hashCombine(seed >>> 0, 131)));
|
|
115
|
+
cache.set(key, texture);
|
|
116
|
+
if (cache.size > 12) {
|
|
117
|
+
const oldest = cache.keys().next().value;
|
|
118
|
+
cache.get(oldest)?.dispose();
|
|
119
|
+
cache.delete(oldest);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return texture;
|
|
123
|
+
}
|