@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,238 @@
|
|
|
1
|
+
// Canonical settings and field metadata for the building grammar. Grouped
|
|
2
|
+
// settings (footprint / massing / roof / facade / palette) in the same shape
|
|
3
|
+
// the tree cluster uses, so the debug panel, the generated settings
|
|
4
|
+
// reference, and recipe serialization all come free.
|
|
5
|
+
|
|
6
|
+
export const BUILDING_TYPES = Object.freeze({
|
|
7
|
+
cottage: Object.freeze({
|
|
8
|
+
label: 'Cottage',
|
|
9
|
+
description: 'The Ghibli-adjacent staple: one-and-a-half floors, gable roof, timber facade, chimney.',
|
|
10
|
+
icon: '🏡',
|
|
11
|
+
}),
|
|
12
|
+
shed: Object.freeze({
|
|
13
|
+
label: 'Shed',
|
|
14
|
+
description: 'Small single-slope outbuilding with a wide door.',
|
|
15
|
+
icon: '🛖',
|
|
16
|
+
}),
|
|
17
|
+
farmhouse: Object.freeze({
|
|
18
|
+
label: 'Farmhouse',
|
|
19
|
+
description: 'L-footprint two-floor house — the composite-massing proof.',
|
|
20
|
+
icon: '🏠',
|
|
21
|
+
}),
|
|
22
|
+
watchtower: Object.freeze({
|
|
23
|
+
label: 'Watchtower',
|
|
24
|
+
description: 'Tall narrow tower, floors stepping inward, balcony under a capped roof.',
|
|
25
|
+
icon: '🗼',
|
|
26
|
+
}),
|
|
27
|
+
shrine: Object.freeze({
|
|
28
|
+
label: 'Shrine',
|
|
29
|
+
description: 'Open-fronted hall on a veranda plinth with a curved, deep-overhang roof.',
|
|
30
|
+
icon: '⛩️',
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const BUILDING_ROOF_KINDS = Object.freeze(['gable', 'hip', 'shed', 'pagoda']);
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_BUILDING_SETTINGS = Object.freeze({
|
|
37
|
+
footprint: Object.freeze({
|
|
38
|
+
kind: 'rect', // rect | L | T (farmhouse defaults to L)
|
|
39
|
+
width: 6.5,
|
|
40
|
+
depth: 5,
|
|
41
|
+
wingRatio: 0.55, // L/T wing size relative to the main rect
|
|
42
|
+
}),
|
|
43
|
+
massing: Object.freeze({
|
|
44
|
+
floors: 1,
|
|
45
|
+
floorHeight: 2.5,
|
|
46
|
+
atticRatio: 0.55, // gable-end attic half-floor height fraction
|
|
47
|
+
inset: 0, // per-floor inward step (towers)
|
|
48
|
+
wallLean: 0.012, // outward lean per meter of height — the hand-drawn read
|
|
49
|
+
}),
|
|
50
|
+
roof: Object.freeze({
|
|
51
|
+
kind: 'gable',
|
|
52
|
+
pitch: 0.85, // rise over half-span
|
|
53
|
+
overhang: 0.55,
|
|
54
|
+
curvature: 0, // upturned eaves (shrine roofs)
|
|
55
|
+
ridgeDecor: 0, // 0..1 ridge cap + finials
|
|
56
|
+
}),
|
|
57
|
+
facade: Object.freeze({
|
|
58
|
+
beams: 1, // 0..1 visible timber framing strength
|
|
59
|
+
bayWidth: 1.6,
|
|
60
|
+
windowChance: 0.75,
|
|
61
|
+
windowWidth: 0.75,
|
|
62
|
+
windowHeight: 0.95,
|
|
63
|
+
doorWidth: 1.0,
|
|
64
|
+
doorHeight: 2.0,
|
|
65
|
+
baseHeight: 0.35, // stone base course
|
|
66
|
+
}),
|
|
67
|
+
palette: Object.freeze({
|
|
68
|
+
wall: Object.freeze([0.82, 0.74, 0.6]),
|
|
69
|
+
beam: Object.freeze([0.32, 0.22, 0.14]),
|
|
70
|
+
roof: Object.freeze([0.42, 0.3, 0.24]),
|
|
71
|
+
trim: Object.freeze([0.45, 0.46, 0.44]),
|
|
72
|
+
door: Object.freeze([0.5, 0.3, 0.16]),
|
|
73
|
+
variation: 0.12,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Per-type defaults layered over the base (same mechanism as prop types).
|
|
78
|
+
export const BUILDING_TYPE_DEFAULTS = Object.freeze({
|
|
79
|
+
cottage: Object.freeze({
|
|
80
|
+
footprint: Object.freeze({ depth: 5, kind: 'rect', width: 6.5 }),
|
|
81
|
+
massing: Object.freeze({ atticRatio: 0.55, floors: 1 }),
|
|
82
|
+
roof: Object.freeze({ kind: 'gable', overhang: 0.55, pitch: 0.9 }),
|
|
83
|
+
}),
|
|
84
|
+
shed: Object.freeze({
|
|
85
|
+
facade: Object.freeze({ doorWidth: 1.6, windowChance: 0.25 }),
|
|
86
|
+
footprint: Object.freeze({ depth: 3.2, kind: 'rect', width: 4 }),
|
|
87
|
+
massing: Object.freeze({ atticRatio: 0, floors: 1 }),
|
|
88
|
+
roof: Object.freeze({ kind: 'shed', overhang: 0.4, pitch: 0.35 }),
|
|
89
|
+
palette: Object.freeze({ wall: [0.55, 0.42, 0.28] }),
|
|
90
|
+
}),
|
|
91
|
+
farmhouse: Object.freeze({
|
|
92
|
+
footprint: Object.freeze({ depth: 6, kind: 'L', width: 9, wingRatio: 0.6 }),
|
|
93
|
+
massing: Object.freeze({ atticRatio: 0.5, floors: 2 }),
|
|
94
|
+
roof: Object.freeze({ kind: 'gable', overhang: 0.6, pitch: 0.8 }),
|
|
95
|
+
}),
|
|
96
|
+
watchtower: Object.freeze({
|
|
97
|
+
facade: Object.freeze({ bayWidth: 1.3, windowChance: 0.5, windowHeight: 0.7, windowWidth: 0.55 }),
|
|
98
|
+
footprint: Object.freeze({ depth: 3.4, kind: 'rect', width: 3.4 }),
|
|
99
|
+
massing: Object.freeze({ atticRatio: 0, floors: 4, inset: 0.12 }),
|
|
100
|
+
roof: Object.freeze({ kind: 'hip', overhang: 0.7, pitch: 0.7, ridgeDecor: 0.6 }),
|
|
101
|
+
palette: Object.freeze({ wall: [0.6, 0.56, 0.5] }),
|
|
102
|
+
}),
|
|
103
|
+
shrine: Object.freeze({
|
|
104
|
+
facade: Object.freeze({ baseHeight: 0.8, beams: 1, windowChance: 0 }),
|
|
105
|
+
footprint: Object.freeze({ depth: 4.6, kind: 'rect', width: 6 }),
|
|
106
|
+
massing: Object.freeze({ atticRatio: 0, floorHeight: 3, floors: 1 }),
|
|
107
|
+
roof: Object.freeze({ curvature: 0.6, kind: 'pagoda', overhang: 1.1, pitch: 0.75, ridgeDecor: 0.8 }),
|
|
108
|
+
palette: Object.freeze({
|
|
109
|
+
beam: [0.62, 0.16, 0.1],
|
|
110
|
+
roof: [0.3, 0.37, 0.43],
|
|
111
|
+
wall: [0.9, 0.86, 0.78],
|
|
112
|
+
}),
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const BUILDING_SETTING_GROUPS = Object.freeze([
|
|
117
|
+
Object.freeze({ id: 'footprint', label: 'Footprint', description: 'Ground plan: rect, L, or T, in meters.' }),
|
|
118
|
+
Object.freeze({ id: 'massing', label: 'Massing', description: 'Floors, per-floor inset, and the slight outward wall lean that keeps facades hand-drawn.' }),
|
|
119
|
+
Object.freeze({ id: 'roof', label: 'Roof', description: 'Roof form: gable, hip, shed, or the curved pagoda-ish shrine roof. Roofs always overhang walls.' }),
|
|
120
|
+
Object.freeze({ id: 'facade', label: 'Facade', description: 'Timber framing, window rhythm (windows never intersect beams), and the door (always on an exterior wall).' }),
|
|
121
|
+
Object.freeze({ id: 'palette', label: 'Palette', description: 'Material role colors: wall, beam, roof, trim, door.' }),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const FIELD_DEFINITIONS = Object.freeze({
|
|
125
|
+
footprint: Object.freeze({
|
|
126
|
+
kind: {
|
|
127
|
+
label: 'Plan', description: 'Ground-plan shape.',
|
|
128
|
+
options: ['rect', 'L', 'T'], optionLabels: { L: 'L-shape', T: 'T-shape', rect: 'Rectangle' }, type: 'select',
|
|
129
|
+
},
|
|
130
|
+
width: { label: 'Width', description: 'Main rect width in meters.', range: { max: 14, min: 2.5, step: 0.1 } },
|
|
131
|
+
depth: { label: 'Depth', description: 'Main rect depth in meters.', range: { max: 12, min: 2.5, step: 0.1 } },
|
|
132
|
+
wingRatio: { label: 'Wing Size', description: 'L/T wing size relative to the main rect.', range: { max: 0.85, min: 0.3, step: 0.05 } },
|
|
133
|
+
}),
|
|
134
|
+
massing: Object.freeze({
|
|
135
|
+
floors: { label: 'Floors', description: 'Full floors (towers go tall).', range: { max: 5, min: 1, step: 1 } },
|
|
136
|
+
floorHeight: { label: 'Floor Height', description: 'Meters per floor.', range: { max: 3.4, min: 2.1, step: 0.05 } },
|
|
137
|
+
atticRatio: { label: 'Attic', description: 'Half-floor under a gable roof (0 = none).', range: { max: 0.8, min: 0, step: 0.05 } },
|
|
138
|
+
inset: { label: 'Floor Inset', description: 'Meters each floor steps inward — watchtower massing.', range: { max: 0.3, min: 0, step: 0.01 } },
|
|
139
|
+
wallLean: { label: 'Wall Lean', description: 'Outward lean per meter of height. Exaggerated proportions are settings, not bugs.', range: { max: 0.05, min: 0, step: 0.002 } },
|
|
140
|
+
}),
|
|
141
|
+
roof: Object.freeze({
|
|
142
|
+
kind: {
|
|
143
|
+
label: 'Kind', description: 'Roof construction.',
|
|
144
|
+
options: [...BUILDING_ROOF_KINDS], optionLabels: { gable: 'Gable', hip: 'Hip', pagoda: 'Pagoda-ish', shed: 'Shed' }, type: 'select',
|
|
145
|
+
},
|
|
146
|
+
pitch: { label: 'Pitch', description: 'Rise over half-span.', range: { max: 1.4, min: 0.25, step: 0.05 } },
|
|
147
|
+
overhang: { label: 'Overhang', description: 'Meters the roof reaches past the walls (invariant: > 0).', range: { max: 1.6, min: 0.25, step: 0.05 } },
|
|
148
|
+
curvature: { label: 'Curvature', description: 'Upturned eave sweep — the shrine-roof signature.', range: { max: 1, min: 0, step: 0.05 } },
|
|
149
|
+
ridgeDecor: { label: 'Ridge Decoration', description: 'Ridge cap beam and end finials.', range: { max: 1, min: 0, step: 0.05 } },
|
|
150
|
+
}),
|
|
151
|
+
facade: Object.freeze({
|
|
152
|
+
beams: { label: 'Timber Framing', description: 'Visible beam grid strength (0 hides framing).', range: { max: 1, min: 0, step: 0.05 } },
|
|
153
|
+
bayWidth: { label: 'Bay Width', description: 'Meters between beam columns; windows land mid-bay.', range: { max: 2.6, min: 1, step: 0.05 } },
|
|
154
|
+
windowChance: { label: 'Window Chance', description: 'Chance an eligible bay gets a window.', range: { max: 1, min: 0, step: 0.05 } },
|
|
155
|
+
windowWidth: { label: 'Window Width', description: 'Window width in meters (clamped inside its bay).', range: { max: 1.4, min: 0.4, step: 0.05 } },
|
|
156
|
+
windowHeight: { label: 'Window Height', description: 'Window height in meters.', range: { max: 1.6, min: 0.4, step: 0.05 } },
|
|
157
|
+
doorWidth: { label: 'Door Width', description: 'Door width in meters.', range: { max: 2.2, min: 0.7, step: 0.05 } },
|
|
158
|
+
doorHeight: { label: 'Door Height', description: 'Door height in meters.', range: { max: 2.4, min: 1.7, step: 0.05 } },
|
|
159
|
+
baseHeight: { label: 'Base Course', description: 'Stone base band height (shrines ride a full veranda plinth).', range: { max: 1.2, min: 0, step: 0.05 } },
|
|
160
|
+
}),
|
|
161
|
+
palette: Object.freeze({
|
|
162
|
+
wall: { label: 'Wall', description: 'Plaster / plank wall color.', type: 'color' },
|
|
163
|
+
beam: { label: 'Beam', description: 'Timber framing color.', type: 'color' },
|
|
164
|
+
roof: { label: 'Roof', description: 'Roof surface color.', type: 'color' },
|
|
165
|
+
trim: { label: 'Trim', description: 'Stone base, chimney, and sills.', type: 'color' },
|
|
166
|
+
door: { label: 'Door', description: 'Door color.', type: 'color' },
|
|
167
|
+
variation: { label: 'Variation', description: 'Per-vertex color drift.', range: { max: 0.4, min: 0, step: 0.01 } },
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
function createBuildingFieldMetadata(group, key, field) {
|
|
172
|
+
const defaultValue = DEFAULT_BUILDING_SETTINGS[group.id][key];
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
defaultValue: Array.isArray(defaultValue) ? [...defaultValue] : defaultValue,
|
|
175
|
+
description: field.description,
|
|
176
|
+
group: group.id,
|
|
177
|
+
id: `${group.id}.${key}`,
|
|
178
|
+
key,
|
|
179
|
+
label: field.label,
|
|
180
|
+
optionLabels: field.optionLabels ?? null,
|
|
181
|
+
options: field.options ?? null,
|
|
182
|
+
range: field.range ?? null,
|
|
183
|
+
serializable: true,
|
|
184
|
+
type: field.type ?? (typeof defaultValue === 'number' ? 'number' : 'text'),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const BUILDING_SETTING_FIELD_SCHEMA = Object.freeze(
|
|
189
|
+
Object.fromEntries(
|
|
190
|
+
BUILDING_SETTING_GROUPS.map((group) => [
|
|
191
|
+
group.id,
|
|
192
|
+
Object.freeze(
|
|
193
|
+
Object.fromEntries(
|
|
194
|
+
Object.entries(FIELD_DEFINITIONS[group.id] ?? {})
|
|
195
|
+
.map(([key, field]) => [key, createBuildingFieldMetadata(group, key, field)]),
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
]),
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
function cloneValue(value) {
|
|
203
|
+
if (Array.isArray(value)) return [...value];
|
|
204
|
+
if (value && typeof value === 'object') return Object.fromEntries(
|
|
205
|
+
Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]),
|
|
206
|
+
);
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function cloneBuildingSettings(settings = DEFAULT_BUILDING_SETTINGS) {
|
|
211
|
+
return cloneValue(settings);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Normalizes `{ type, seed, …group overrides }` over the defaults and the
|
|
216
|
+
* type's defaults. Type and seed ride alongside the groups (mirrors the
|
|
217
|
+
* tree editor's plant group).
|
|
218
|
+
*/
|
|
219
|
+
export function createBuildingSettings(overrides = {}) {
|
|
220
|
+
const type = BUILDING_TYPES[overrides.type] ? overrides.type : 'cottage';
|
|
221
|
+
const result = cloneBuildingSettings(DEFAULT_BUILDING_SETTINGS);
|
|
222
|
+
const typeDefaults = BUILDING_TYPE_DEFAULTS[type] ?? {};
|
|
223
|
+
for (const groupId of Object.keys(result)) {
|
|
224
|
+
if (typeDefaults[groupId]) Object.assign(result[groupId], cloneValue(typeDefaults[groupId]));
|
|
225
|
+
const group = overrides[groupId];
|
|
226
|
+
if (group && typeof group === 'object') {
|
|
227
|
+
for (const key of Object.keys(result[groupId])) {
|
|
228
|
+
if (group[key] !== undefined) result[groupId][key] = cloneValue(group[key]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
result.type = type;
|
|
233
|
+
result.seed = Math.max(0, Math.round(Number(overrides.seed) || 1)) >>> 0;
|
|
234
|
+
result.massing.floors = Math.min(6, Math.max(1, Math.round(result.massing.floors)));
|
|
235
|
+
if (!BUILDING_ROOF_KINDS.includes(result.roof.kind)) result.roof.kind = 'gable';
|
|
236
|
+
if (!['rect', 'L', 'T'].includes(result.footprint.kind)) result.footprint.kind = 'rect';
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { createCameraRig } from './cameraRig.js';
|
|
4
|
+
|
|
5
|
+
const clamp01 = (value) => Math.min(Math.max(value, 0), 1);
|
|
6
|
+
|
|
7
|
+
function smoothstep(value) {
|
|
8
|
+
const t = clamp01(value);
|
|
9
|
+
return t * t * (3 - 2 * t);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Selects the highest-priority enabled rig (or an explicit rig) and blends
|
|
14
|
+
* camera states. The director is optional: a CameraRig can be used directly.
|
|
15
|
+
*/
|
|
16
|
+
export function createCameraDirector(camera, { defaultBlendDuration = 0.45 } = {}) {
|
|
17
|
+
if (!camera?.isCamera) throw new Error('createCameraDirector requires a Three.js Camera.');
|
|
18
|
+
const entries = new Map();
|
|
19
|
+
const fromPosition = new THREE.Vector3();
|
|
20
|
+
const fromQuaternion = new THREE.Quaternion();
|
|
21
|
+
const toPosition = new THREE.Vector3();
|
|
22
|
+
const toQuaternion = new THREE.Quaternion();
|
|
23
|
+
const mixedPosition = new THREE.Vector3();
|
|
24
|
+
const mixedQuaternion = new THREE.Quaternion();
|
|
25
|
+
let disposed = false;
|
|
26
|
+
let requestedId = null;
|
|
27
|
+
let activeId = null;
|
|
28
|
+
let transitionElapsed = 0;
|
|
29
|
+
let transitionDuration = 0;
|
|
30
|
+
let fromFov = camera.fov ?? 50;
|
|
31
|
+
let fromNear = camera.near ?? 0.1;
|
|
32
|
+
let fromFar = camera.far ?? 1000;
|
|
33
|
+
let switches = 0;
|
|
34
|
+
let updates = 0;
|
|
35
|
+
|
|
36
|
+
function selectedEntry() {
|
|
37
|
+
if (requestedId && entries.get(requestedId)?.enabled) return entries.get(requestedId);
|
|
38
|
+
return [...entries.values()]
|
|
39
|
+
.filter((entry) => entry.enabled)
|
|
40
|
+
.sort((a, b) => b.priority - a.priority || a.id.localeCompare(b.id))[0] ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function beginTransition(next, duration = defaultBlendDuration) {
|
|
44
|
+
if (next?.id === activeId) return;
|
|
45
|
+
fromPosition.copy(camera.position);
|
|
46
|
+
fromQuaternion.copy(camera.quaternion);
|
|
47
|
+
fromFov = camera.fov ?? fromFov;
|
|
48
|
+
fromNear = camera.near ?? fromNear;
|
|
49
|
+
fromFar = camera.far ?? fromFar;
|
|
50
|
+
activeId = next?.id ?? null;
|
|
51
|
+
transitionElapsed = 0;
|
|
52
|
+
transitionDuration = Math.max(0, Number(duration) || 0);
|
|
53
|
+
next?.rig.syncFromCamera();
|
|
54
|
+
switches += 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function applySample(sample, amount) {
|
|
58
|
+
toPosition.fromArray(sample.position);
|
|
59
|
+
toQuaternion.fromArray(sample.quaternion);
|
|
60
|
+
mixedPosition.lerpVectors(fromPosition, toPosition, amount);
|
|
61
|
+
mixedQuaternion.slerpQuaternions(fromQuaternion, toQuaternion, amount);
|
|
62
|
+
camera.position.copy(mixedPosition);
|
|
63
|
+
camera.quaternion.copy(mixedQuaternion);
|
|
64
|
+
if (camera.isPerspectiveCamera) {
|
|
65
|
+
camera.fov = fromFov + (sample.fov - fromFov) * amount;
|
|
66
|
+
camera.near = fromNear + (sample.near - fromNear) * amount;
|
|
67
|
+
camera.far = fromFar + (sample.far - fromFar) * amount;
|
|
68
|
+
camera.updateProjectionMatrix();
|
|
69
|
+
}
|
|
70
|
+
camera.updateMatrixWorld();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const director = {
|
|
74
|
+
addRig(id, rigOrOptions, { enabled = true, owned = false, priority = 0 } = {}) {
|
|
75
|
+
if (disposed) throw new Error('Camera director is disposed.');
|
|
76
|
+
const cleanId = String(id ?? '').trim();
|
|
77
|
+
if (!cleanId) throw new Error('Camera director rig id is required.');
|
|
78
|
+
if (entries.has(cleanId)) throw new Error(`Camera director already has rig "${cleanId}".`);
|
|
79
|
+
const rig = rigOrOptions?.update && rigOrOptions?.setTarget
|
|
80
|
+
? rigOrOptions
|
|
81
|
+
: createCameraRig({ camera, ...(rigOrOptions ?? {}) });
|
|
82
|
+
entries.set(cleanId, { enabled: Boolean(enabled), id: cleanId, owned: owned || rig !== rigOrOptions, priority: Number(priority) || 0, rig });
|
|
83
|
+
return rig;
|
|
84
|
+
},
|
|
85
|
+
dispose() {
|
|
86
|
+
if (disposed) return;
|
|
87
|
+
disposed = true;
|
|
88
|
+
for (const entry of entries.values()) if (entry.owned) entry.rig.dispose();
|
|
89
|
+
entries.clear();
|
|
90
|
+
activeId = null;
|
|
91
|
+
requestedId = null;
|
|
92
|
+
},
|
|
93
|
+
getRig(id) {
|
|
94
|
+
return entries.get(String(id))?.rig ?? null;
|
|
95
|
+
},
|
|
96
|
+
removeRig(id, { dispose = null } = {}) {
|
|
97
|
+
const entry = entries.get(String(id));
|
|
98
|
+
if (!entry) return false;
|
|
99
|
+
if (dispose ?? entry.owned) entry.rig.dispose();
|
|
100
|
+
entries.delete(entry.id);
|
|
101
|
+
if (activeId === entry.id) activeId = null;
|
|
102
|
+
if (requestedId === entry.id) requestedId = null;
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
setActive(id, { duration = defaultBlendDuration } = {}) {
|
|
106
|
+
requestedId = id == null ? null : String(id);
|
|
107
|
+
if (requestedId && !entries.has(requestedId)) throw new Error(`Unknown camera rig "${requestedId}".`);
|
|
108
|
+
beginTransition(selectedEntry(), duration);
|
|
109
|
+
return director;
|
|
110
|
+
},
|
|
111
|
+
setEnabled(id, enabled, { duration = defaultBlendDuration } = {}) {
|
|
112
|
+
const entry = entries.get(String(id));
|
|
113
|
+
if (!entry) return false;
|
|
114
|
+
entry.enabled = Boolean(enabled);
|
|
115
|
+
beginTransition(selectedEntry(), duration);
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
setPriority(id, priority, { duration = defaultBlendDuration } = {}) {
|
|
119
|
+
const entry = entries.get(String(id));
|
|
120
|
+
if (!entry) return false;
|
|
121
|
+
entry.priority = Number(priority) || 0;
|
|
122
|
+
beginTransition(selectedEntry(), duration);
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
update(delta = 0) {
|
|
126
|
+
if (disposed) return null;
|
|
127
|
+
const selected = selectedEntry();
|
|
128
|
+
if (selected?.id !== activeId) beginTransition(selected);
|
|
129
|
+
if (!selected) return null;
|
|
130
|
+
const dt = Math.min(Math.max(Number(delta) || 0, 0), 0.25);
|
|
131
|
+
const sample = selected.rig.update(dt, { apply: false });
|
|
132
|
+
transitionElapsed += dt;
|
|
133
|
+
const amount = transitionDuration <= 0 ? 1 : smoothstep(transitionElapsed / transitionDuration);
|
|
134
|
+
applySample(sample, amount);
|
|
135
|
+
updates += 1;
|
|
136
|
+
return sample;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
Object.defineProperties(director, {
|
|
141
|
+
activeId: { get: () => activeId },
|
|
142
|
+
disposed: { get: () => disposed },
|
|
143
|
+
stats: {
|
|
144
|
+
get: () => ({
|
|
145
|
+
activeId,
|
|
146
|
+
disposed,
|
|
147
|
+
rigCount: entries.size,
|
|
148
|
+
switches,
|
|
149
|
+
transitionProgress: transitionDuration <= 0 ? 1 : clamp01(transitionElapsed / transitionDuration),
|
|
150
|
+
updates,
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return director;
|
|
156
|
+
}
|
|
157
|
+
|