@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,44 @@
|
|
|
1
|
+
// Buildings as PropAssets: a building is "just" a big prop with a
|
|
2
|
+
// multi-circle footprint — which is exactly why the placement pipeline,
|
|
3
|
+
// villages, and the catalog need no building-specific code paths.
|
|
4
|
+
|
|
5
|
+
import { createBuildingSettings } from './buildingSettings.js';
|
|
6
|
+
import { createBuildingFromRecipe, buildingRecipeFromSettings, BUILDING_RECIPE_SCHEMA } from './buildingRecipe.js';
|
|
7
|
+
|
|
8
|
+
const LOD_DISTANCE = { cottage: 140, farmhouse: 160, shed: 90, shrine: 180, watchtower: 220 };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `buildingAsset(recipeOrSettings)` → the 02 PropAsset contract.
|
|
12
|
+
* `build(seed)` re-rolls the seed only (window rhythm, chimney position);
|
|
13
|
+
* the recipe's proportions stay fixed — villages instance cousins, not
|
|
14
|
+
* clones.
|
|
15
|
+
*/
|
|
16
|
+
export function buildingAsset(recipeOrSettings = {}) {
|
|
17
|
+
const settings = recipeOrSettings?.schema === BUILDING_RECIPE_SCHEMA
|
|
18
|
+
? createBuildingSettings({ ...recipeOrSettings.options, type: recipeOrSettings.type })
|
|
19
|
+
: createBuildingSettings(recipeOrSettings ?? {});
|
|
20
|
+
return {
|
|
21
|
+
build(seed = settings.seed) {
|
|
22
|
+
const buildSettings = { ...settings, seed };
|
|
23
|
+
const hi = createBuildingFromRecipe(buildSettings, { detail: 'hi' });
|
|
24
|
+
const lo = createBuildingFromRecipe(buildSettings, { detail: 'lo' });
|
|
25
|
+
return {
|
|
26
|
+
anchor: 0, // grammar origin is ground contact; the skirt is buried below
|
|
27
|
+
door: hi.plan.door,
|
|
28
|
+
footprint: { circles: hi.plan.footprintCircles },
|
|
29
|
+
lod: {
|
|
30
|
+
distance: LOD_DISTANCE[settings.type] ?? 150,
|
|
31
|
+
far: lo.object3D,
|
|
32
|
+
},
|
|
33
|
+
object3D: hi.object3D,
|
|
34
|
+
plan: hi.plan,
|
|
35
|
+
stats: hi.stats,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
linear: false,
|
|
39
|
+
recipe: buildingRecipeFromSettings(settings),
|
|
40
|
+
settings,
|
|
41
|
+
type: settings.type,
|
|
42
|
+
variant: settings.type,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// The shape grammar: settings + seed → a PLAN (pure data, no THREE). Every
|
|
2
|
+
// stage is deterministic, and the grammar invariants (door reachable and on
|
|
3
|
+
// an exterior wall, windows never intersecting beams, roofs always
|
|
4
|
+
// overhanging walls) are properties of the plan — which is why the 1000-seed
|
|
5
|
+
// invariant suite runs in milliseconds without building any geometry.
|
|
6
|
+
|
|
7
|
+
function mulberry32(seed) {
|
|
8
|
+
let state = (Math.trunc(seed) || 1) >>> 0;
|
|
9
|
+
return () => {
|
|
10
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
11
|
+
let t = state;
|
|
12
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
13
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
14
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Footprint construction: axis-aligned rect union whose perimeter we can
|
|
19
|
+
// walk explicitly (general polygon union is overkill for village-grade
|
|
20
|
+
// buildings). CCW viewed from above; outward normal of a CCW segment
|
|
21
|
+
// (dx, dz) is (dz, -dx).
|
|
22
|
+
function buildFootprint({ kind, width, depth, wingRatio }, random) {
|
|
23
|
+
const w = width;
|
|
24
|
+
const d = depth;
|
|
25
|
+
const rects = [{ x0: -w / 2, x1: w / 2, z0: -d / 2, z1: d / 2 }];
|
|
26
|
+
let perimeter;
|
|
27
|
+
if (kind === 'L') {
|
|
28
|
+
const ww = Math.max(w * wingRatio, 2);
|
|
29
|
+
const wd = Math.max(d * wingRatio, 2);
|
|
30
|
+
rects.push({ x0: w / 2 - ww, x1: w / 2, z0: d / 2, z1: d / 2 + wd });
|
|
31
|
+
perimeter = [
|
|
32
|
+
[-w / 2, -d / 2], [w / 2, -d / 2],
|
|
33
|
+
[w / 2, d / 2 + wd], [w / 2 - ww, d / 2 + wd],
|
|
34
|
+
[w / 2 - ww, d / 2], [-w / 2, d / 2],
|
|
35
|
+
];
|
|
36
|
+
} else if (kind === 'T') {
|
|
37
|
+
const ww = Math.max(w * wingRatio * 0.8, 2);
|
|
38
|
+
const wd = Math.max(d * wingRatio, 2);
|
|
39
|
+
const cx = (random() - 0.5) * (w - ww) * 0.4; // wing wanders off-center a bit
|
|
40
|
+
rects.push({ x0: cx - ww / 2, x1: cx + ww / 2, z0: d / 2, z1: d / 2 + wd });
|
|
41
|
+
perimeter = [
|
|
42
|
+
[-w / 2, -d / 2], [w / 2, -d / 2],
|
|
43
|
+
[w / 2, d / 2], [cx + ww / 2, d / 2],
|
|
44
|
+
[cx + ww / 2, d / 2 + wd], [cx - ww / 2, d / 2 + wd],
|
|
45
|
+
[cx - ww / 2, d / 2], [-w / 2, d / 2],
|
|
46
|
+
];
|
|
47
|
+
} else {
|
|
48
|
+
perimeter = [
|
|
49
|
+
[-w / 2, -d / 2], [w / 2, -d / 2],
|
|
50
|
+
[w / 2, d / 2], [-w / 2, d / 2],
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
const walls = perimeter.map((point, index) => {
|
|
54
|
+
const next = perimeter[(index + 1) % perimeter.length];
|
|
55
|
+
const dx = next[0] - point[0];
|
|
56
|
+
const dz = next[1] - point[1];
|
|
57
|
+
const length = Math.hypot(dx, dz);
|
|
58
|
+
return {
|
|
59
|
+
ax: point[0],
|
|
60
|
+
az: point[1],
|
|
61
|
+
bx: next[0],
|
|
62
|
+
bz: next[1],
|
|
63
|
+
length,
|
|
64
|
+
nx: dz / (length || 1),
|
|
65
|
+
nz: -dx / (length || 1),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
return { perimeter, rects, walls };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Bay layout along one wall: beam columns at bay edges, windows strictly
|
|
72
|
+
// inside bays with a margin — the "windows never intersect beams" invariant
|
|
73
|
+
// holds by construction.
|
|
74
|
+
function layoutBays(wall, settings, random, { door = null, floor = 0 }) {
|
|
75
|
+
const { facade } = settings;
|
|
76
|
+
const bayCount = Math.max(Math.round(wall.length / facade.bayWidth), 1);
|
|
77
|
+
const bays = [];
|
|
78
|
+
for (let index = 0; index < bayCount; index += 1) {
|
|
79
|
+
const u0 = index / bayCount;
|
|
80
|
+
const u1 = (index + 1) / bayCount;
|
|
81
|
+
const bayLength = (u1 - u0) * wall.length;
|
|
82
|
+
const isDoorBay = door !== null && index === door.bayIndex;
|
|
83
|
+
let window = null;
|
|
84
|
+
const roll = random();
|
|
85
|
+
if (!isDoorBay && roll < facade.windowChance) {
|
|
86
|
+
const margin = 0.16; // beam clearance in meters
|
|
87
|
+
const maxWidth = bayLength - margin * 2;
|
|
88
|
+
const width = Math.min(facade.windowWidth, maxWidth * 0.8);
|
|
89
|
+
if (width > 0.3) {
|
|
90
|
+
const y0 = floor === 0 ? 0.95 : 0.75;
|
|
91
|
+
window = {
|
|
92
|
+
height: Math.min(facade.windowHeight, settings.massing.floorHeight - y0 - 0.45),
|
|
93
|
+
u: (u0 + u1) / 2,
|
|
94
|
+
width,
|
|
95
|
+
y0,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
bays.push({ door: isDoorBay ? door : null, u0, u1, window });
|
|
100
|
+
}
|
|
101
|
+
return bays;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* settings (+ its seed) → plan. See module header; the plan is everything
|
|
106
|
+
* the mesher and the invariant tests need.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveBuildingPlan(settings) {
|
|
109
|
+
const random = mulberry32(settings.seed * 2654435761 + 17);
|
|
110
|
+
const footprint = buildFootprint(settings.footprint, random);
|
|
111
|
+
const { massing, roof } = settings;
|
|
112
|
+
|
|
113
|
+
// Floors: inset shrinks the working rects; clamp so the top floor never
|
|
114
|
+
// degenerates (invariant 4).
|
|
115
|
+
const minSpan = Math.min(
|
|
116
|
+
settings.footprint.width,
|
|
117
|
+
settings.footprint.depth,
|
|
118
|
+
);
|
|
119
|
+
const maxInset = (minSpan * 0.3) / Math.max(massing.floors, 1);
|
|
120
|
+
const inset = Math.min(massing.inset, maxInset);
|
|
121
|
+
const floors = [];
|
|
122
|
+
for (let level = 0; level < massing.floors; level += 1) {
|
|
123
|
+
floors.push({
|
|
124
|
+
inset: inset * level,
|
|
125
|
+
level,
|
|
126
|
+
y0: level * massing.floorHeight,
|
|
127
|
+
y1: (level + 1) * massing.floorHeight,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const wallTop = floors[floors.length - 1].y1;
|
|
131
|
+
const topInset = inset * (massing.floors - 1);
|
|
132
|
+
|
|
133
|
+
// Door: the longest ground-floor wall; centered in its middle bay. The
|
|
134
|
+
// wall is exterior by construction (every perimeter segment is).
|
|
135
|
+
let doorWallIndex = 0;
|
|
136
|
+
for (let index = 1; index < footprint.walls.length; index += 1) {
|
|
137
|
+
if (footprint.walls[index].length > footprint.walls[doorWallIndex].length) {
|
|
138
|
+
doorWallIndex = index;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const doorWall = footprint.walls[doorWallIndex];
|
|
142
|
+
const doorBayCount = Math.max(Math.round(doorWall.length / settings.facade.bayWidth), 1);
|
|
143
|
+
const door = {
|
|
144
|
+
bayIndex: Math.floor(doorBayCount / 2),
|
|
145
|
+
height: Math.min(settings.facade.doorHeight, massing.floorHeight - 0.3),
|
|
146
|
+
u: (Math.floor(doorBayCount / 2) + 0.5) / doorBayCount,
|
|
147
|
+
width: Math.min(settings.facade.doorWidth, doorWall.length * 0.5),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Facades: per floor per wall. Upper floors of inset massing reuse the
|
|
151
|
+
// same wall directions with shrunk extents (the mesher applies inset).
|
|
152
|
+
const facades = [];
|
|
153
|
+
for (const floor of floors) {
|
|
154
|
+
footprint.walls.forEach((wall, wallIndex) => {
|
|
155
|
+
facades.push({
|
|
156
|
+
bays: layoutBays(wall, settings, random, {
|
|
157
|
+
door: floor.level === 0 && wallIndex === doorWallIndex ? door : null,
|
|
158
|
+
floor: floor.level,
|
|
159
|
+
}),
|
|
160
|
+
floor: floor.level,
|
|
161
|
+
wallIndex,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Roof per rect: the main rect carries the ridge along its longer axis;
|
|
167
|
+
// wings get their own smaller roof of the same kind.
|
|
168
|
+
const roofs = footprint.rects.map((rect, index) => {
|
|
169
|
+
const spanX = rect.x1 - rect.x0;
|
|
170
|
+
const spanZ = rect.z1 - rect.z0;
|
|
171
|
+
const ridgeAxis = spanX >= spanZ ? 'x' : 'z';
|
|
172
|
+
const halfSpan = (ridgeAxis === 'x' ? spanZ : spanX) / 2;
|
|
173
|
+
// seeded proportion jitter: cousins, not clones — and the only seed
|
|
174
|
+
// variation some spartan types (shrine) have
|
|
175
|
+
const riseJitter = 0.95 + random() * 0.1;
|
|
176
|
+
return {
|
|
177
|
+
curvature: roof.curvature,
|
|
178
|
+
kind: roof.kind,
|
|
179
|
+
overhang: Math.max(roof.overhang * (0.97 + random() * 0.06), 0.25), // invariant 3: floor applied last
|
|
180
|
+
rect,
|
|
181
|
+
ridgeAxis,
|
|
182
|
+
ridgeDecor: roof.ridgeDecor,
|
|
183
|
+
rise: Math.max((halfSpan + roof.overhang) * roof.pitch, 0.4) * riseJitter,
|
|
184
|
+
wing: index > 0,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Trim: chimney near the main ridge end for homely types.
|
|
189
|
+
const wantsChimney = settings.type === 'cottage' || settings.type === 'farmhouse';
|
|
190
|
+
const mainRect = footprint.rects[0];
|
|
191
|
+
const chimney = wantsChimney
|
|
192
|
+
? {
|
|
193
|
+
x: roofs[0].ridgeAxis === 'x'
|
|
194
|
+
? mainRect.x0 + (mainRect.x1 - mainRect.x0) * (0.22 + random() * 0.12)
|
|
195
|
+
: (mainRect.x0 + mainRect.x1) / 2 + (random() - 0.5) * 0.6,
|
|
196
|
+
z: roofs[0].ridgeAxis === 'z'
|
|
197
|
+
? mainRect.z0 + (mainRect.z1 - mainRect.z0) * (0.22 + random() * 0.12)
|
|
198
|
+
: (mainRect.z0 + mainRect.z1) / 2 + (random() - 0.5) * 0.6,
|
|
199
|
+
}
|
|
200
|
+
: null;
|
|
201
|
+
|
|
202
|
+
// Collision: one circle per footprint rect (two for long rects).
|
|
203
|
+
const footprintCircles = [];
|
|
204
|
+
for (const rect of footprint.rects) {
|
|
205
|
+
const cx = (rect.x0 + rect.x1) / 2;
|
|
206
|
+
const cz = (rect.z0 + rect.z1) / 2;
|
|
207
|
+
const spanX = rect.x1 - rect.x0;
|
|
208
|
+
const spanZ = rect.z1 - rect.z0;
|
|
209
|
+
const radius = Math.min(spanX, spanZ) * 0.62;
|
|
210
|
+
if (Math.max(spanX, spanZ) > Math.min(spanX, spanZ) * 1.5) {
|
|
211
|
+
const along = spanX >= spanZ ? 'x' : 'z';
|
|
212
|
+
const reach = (Math.max(spanX, spanZ) / 2 - radius * 0.7);
|
|
213
|
+
for (const side of [-1, 1]) {
|
|
214
|
+
footprintCircles.push({
|
|
215
|
+
radius,
|
|
216
|
+
x: cx + (along === 'x' ? side * reach : 0),
|
|
217
|
+
z: cz + (along === 'z' ? side * reach : 0),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
footprintCircles.push({ radius, x: cx, z: cz });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const bounds = {
|
|
226
|
+
maxX: Math.max(...footprint.rects.map((rect) => rect.x1)),
|
|
227
|
+
maxZ: Math.max(...footprint.rects.map((rect) => rect.z1)),
|
|
228
|
+
minX: Math.min(...footprint.rects.map((rect) => rect.x0)),
|
|
229
|
+
minZ: Math.min(...footprint.rects.map((rect) => rect.z0)),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
bounds,
|
|
234
|
+
chimney,
|
|
235
|
+
door: {
|
|
236
|
+
...door,
|
|
237
|
+
// world-frame door anchor for villages: position + outward normal
|
|
238
|
+
nx: doorWall.nx,
|
|
239
|
+
nz: doorWall.nz,
|
|
240
|
+
wallIndex: doorWallIndex,
|
|
241
|
+
x: doorWall.ax + (doorWall.bx - doorWall.ax) * door.u,
|
|
242
|
+
z: doorWall.az + (doorWall.bz - doorWall.az) * door.u,
|
|
243
|
+
},
|
|
244
|
+
facades,
|
|
245
|
+
floors,
|
|
246
|
+
footprint,
|
|
247
|
+
footprintCircles,
|
|
248
|
+
roofs,
|
|
249
|
+
seed: settings.seed,
|
|
250
|
+
settings,
|
|
251
|
+
topInset,
|
|
252
|
+
type: settings.type,
|
|
253
|
+
wallTop,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Grammar invariant checks — run across 1000 seeds in the verify script.
|
|
259
|
+
* Returns a list of violation strings (empty = clean).
|
|
260
|
+
*/
|
|
261
|
+
export function checkPlanInvariants(plan) {
|
|
262
|
+
const violations = [];
|
|
263
|
+
const { door, facades, footprint, roofs, settings } = plan;
|
|
264
|
+
|
|
265
|
+
// 1. Door exists on an exterior wall, fits it, sits on the ground floor.
|
|
266
|
+
const doorWall = footprint.walls[door.wallIndex];
|
|
267
|
+
if (!doorWall) violations.push('door wall missing');
|
|
268
|
+
else if (door.width > doorWall.length * 0.8) violations.push('door wider than its wall');
|
|
269
|
+
const doorFacade = facades.find((facade) => facade.floor === 0 && facade.wallIndex === door.wallIndex);
|
|
270
|
+
if (!doorFacade || !doorFacade.bays.some((bay) => bay.door)) {
|
|
271
|
+
violations.push('door bay missing from ground facade');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 2. Windows never intersect beams: window extent ⊂ its bay minus margin.
|
|
275
|
+
for (const facade of facades) {
|
|
276
|
+
const wall = footprint.walls[facade.wallIndex];
|
|
277
|
+
for (const bay of facade.bays) {
|
|
278
|
+
if (!bay.window) continue;
|
|
279
|
+
const bayLength = (bay.u1 - bay.u0) * wall.length;
|
|
280
|
+
const windowU0 = bay.window.u - (bay.window.width / 2) / wall.length;
|
|
281
|
+
const windowU1 = bay.window.u + (bay.window.width / 2) / wall.length;
|
|
282
|
+
const marginU = 0.1 / wall.length;
|
|
283
|
+
if (windowU0 < bay.u0 + marginU || windowU1 > bay.u1 - marginU) {
|
|
284
|
+
violations.push(`window escapes its bay on wall ${facade.wallIndex} floor ${facade.floor}`);
|
|
285
|
+
}
|
|
286
|
+
if (bay.window.y0 + bay.window.height > settings.massing.floorHeight - 0.2) {
|
|
287
|
+
violations.push('window intersects the floor beam above');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 3. Roofs always overhang walls.
|
|
293
|
+
for (const roofSpec of roofs) {
|
|
294
|
+
if (roofSpec.overhang < 0.25 - 1e-9) violations.push('roof overhang collapsed');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 4. Massing sanity: top floor keeps positive span after inset.
|
|
298
|
+
const minSpan = Math.min(settings.footprint.width, settings.footprint.depth);
|
|
299
|
+
if (plan.topInset * 2 > minSpan * 0.8) violations.push('floor inset degenerates the top floor');
|
|
300
|
+
|
|
301
|
+
// 5. Collision covers every rect center.
|
|
302
|
+
for (const rect of footprint.rects) {
|
|
303
|
+
const cx = (rect.x0 + rect.x1) / 2;
|
|
304
|
+
const cz = (rect.z0 + rect.z1) / 2;
|
|
305
|
+
const covered = plan.footprintCircles.some(
|
|
306
|
+
(circle) => (circle.x - cx) ** 2 + (circle.z - cz) ** 2 <= circle.radius ** 2,
|
|
307
|
+
);
|
|
308
|
+
if (!covered) violations.push('footprint circle misses a rect center');
|
|
309
|
+
}
|
|
310
|
+
return violations;
|
|
311
|
+
}
|