@jgengine/shell 0.18.0 → 0.18.1
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/CHANGELOG.md +114 -2
- package/dist/GamePlayerShell.js +10 -4
- package/dist/JoinGate.d.ts +17 -0
- package/dist/JoinGate.js +14 -0
- package/dist/Shell3dPresentation.d.ts +8 -1
- package/dist/Shell3dPresentation.js +34 -5
- package/dist/ShellChrome.d.ts +2 -0
- package/dist/ShellChrome.js +6 -0
- package/dist/TerritoryOverlay.d.ts +21 -0
- package/dist/TerritoryOverlay.js +9 -0
- package/dist/audio/AudioComponents.js +8 -1
- package/dist/audio/audioEngine.d.ts +7 -1
- package/dist/audio/audioEngine.js +67 -5
- package/dist/camera/cameraRigs.js +7 -0
- package/dist/defineGame.js +21 -6
- package/dist/devtools/KeysPanel.js +3 -3
- package/dist/drivers/FrameDriver.d.ts +5 -1
- package/dist/drivers/FrameDriver.js +61 -24
- package/dist/drivers/HudOnlyDriver.js +9 -4
- package/dist/environment/EnvironmentScene.js +1 -1
- package/dist/input/gamepadMerge.d.ts +10 -0
- package/dist/input/gamepadMerge.js +12 -0
- package/dist/input/gamepadSource.d.ts +12 -0
- package/dist/input/gamepadSource.js +107 -0
- package/dist/materialOverride.d.ts +4 -1
- package/dist/materialOverride.js +10 -1
- package/dist/postfx/PostProcessing.d.ts +10 -7
- package/dist/postfx/PostProcessing.js +33 -15
- package/dist/postfx/gradeShader.js +13 -0
- package/dist/render/CascadedShadows.d.ts +11 -0
- package/dist/render/CascadedShadows.js +19 -8
- package/dist/render/EnvironmentLighting.d.ts +4 -1
- package/dist/render/EnvironmentLighting.js +45 -9
- package/dist/render/SceneLighting.d.ts +16 -2
- package/dist/render/SceneLighting.js +56 -6
- package/dist/render/SceneModels.js +45 -9
- package/dist/render/modelLoad.d.ts +10 -0
- package/dist/render/modelLoad.js +28 -0
- package/dist/render/modelRender.d.ts +3 -0
- package/dist/render/modelRender.js +6 -2
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +15 -0
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +30 -0
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +13 -0
- package/dist/render/testFixtures/proceduralTileLayer.js +30 -0
- package/dist/render/useFootIk.d.ts +9 -0
- package/dist/render/useFootIk.js +89 -0
- package/dist/render/useModelAnimation.d.ts +2 -0
- package/dist/render/useModelAnimation.js +163 -107
- package/dist/scene/CityRenderer.js +661 -236
- package/dist/scene/cityWindowMaterial.d.ts +8 -6
- package/dist/scene/cityWindowMaterial.js +80 -21
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +5 -2
- package/dist/terrain/terrainDetailMaterial.js +7 -3
- package/dist/useShellMultiplayerSync.d.ts +8 -1
- package/dist/useShellMultiplayerSync.js +25 -5
- package/dist/visibility/CullingProvider.d.ts +2 -1
- package/dist/visibility/CullingProvider.js +7 -2
- package/dist/world/GridWorldScene.js +4 -0
- package/dist/world/TileLayerRenderer.d.ts +9 -0
- package/dist/world/TileLayerRenderer.js +64 -0
- package/dist/world/WorldParticles.d.ts +31 -0
- package/dist/world/WorldParticles.js +128 -0
- package/dist/world/WorldScene.d.ts +2 -1
- package/dist/world/WorldScene.js +46 -4
- package/dist/world/entityPose.d.ts +4 -0
- package/dist/world/entityPose.js +9 -0
- package/dist/world/tileLayerInstances.d.ts +11 -0
- package/dist/world/tileLayerInstances.js +28 -0
- package/dist/worldSync.d.ts +5 -1
- package/dist/worldSync.js +25 -4
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +94 -19
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-ui/SKILL.md +8 -0
- package/skills/jgengine-ui/api.md +127 -16
- package/skills/jgengine-ui/capabilities.md +21 -0
- package/skills/jgengine-ui/reference.md +3 -0
- package/skills/jgengine-world/SKILL.md +6 -0
- package/skills/jgengine-world/api.md +312 -12
- package/skills/jgengine-world/capabilities.md +102 -0
- package/skills/level-design/SKILL.md +1 -1
- package/skills/jgengine-ui/references/visual-scorecard.md +0 -73
|
@@ -6,7 +6,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
6
6
|
* sidewalks; styled bridges with railings, piers and abutments; massing pieces as five instanced
|
|
7
7
|
* primitive meshes (plain walls, window-banded walls via `createCityWindowMaterial`, gable roofs,
|
|
8
8
|
* cylinders, domes) with palette-role coloring and per-lot jitter; species-mixed instanced trees
|
|
9
|
-
* (shared scatter proxies), street lights, estate hedges, driveways, parking pads, and
|
|
9
|
+
* (shared scatter proxies), street lights, estate hedges, driveways, parking pads, curbside and
|
|
10
|
+
* pad-stall parked vehicles, mast-arm junction signals with sign posts, and
|
|
10
11
|
* vertex-colored parks with crop-row fields. Lots near the camera and flat enough swap their
|
|
11
12
|
* massing for full `generateBuilding` facade kits (windows, awnings, storefronts) batched per part
|
|
12
13
|
* kind — a bounded LOD that re-picks only when the camera crosses a coarse cell, never per frame.
|
|
@@ -16,52 +17,85 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
16
17
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
17
18
|
import { useFrame } from "@react-three/fiber";
|
|
18
19
|
import * as THREE from "three";
|
|
19
|
-
import { roundPathCorners,
|
|
20
|
+
import { roundPathCorners, buildIntersectionMarkings, buildRoadRibbon, buildTrimmedIntersections, dashSegments, GROUND_DECAL_LAYERS, } from "@jgengine/core/world/roads";
|
|
20
21
|
import { offsetPath } from "@jgengine/core/world/streets";
|
|
21
22
|
import { triangulatePolygon, triangulateRingBand } from "@jgengine/core/world/cityGeometry";
|
|
22
23
|
import { resolveCityObject, CITY_KIND, CITY_ZONE_KIND, } from "@jgengine/core/world/cityKind";
|
|
23
|
-
import {
|
|
24
|
+
import { zoneBand, zoneMetric } from "@jgengine/core/world/cityContent";
|
|
25
|
+
import { generateBuilding, resolveBuildingPalette, resolveBuildingWallTones, } from "@jgengine/core/world/buildings";
|
|
26
|
+
import { hashString } from "@jgengine/core/random/rng";
|
|
24
27
|
import { buildScatterProxy } from "../scatter/scatterProxies.js";
|
|
25
28
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
26
29
|
import { getBuildingKit } from "../structures/buildingKitRegistry.js";
|
|
27
30
|
import { createCityWindowMaterial } from "./cityWindowMaterial.js";
|
|
28
31
|
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
29
|
-
const ASPHALT_COLOR = "#
|
|
32
|
+
const ASPHALT_COLOR = "#3a3b3d";
|
|
30
33
|
const GRAVEL_COLOR = "#93815f";
|
|
31
|
-
const SIDEWALK_COLOR = "#
|
|
32
|
-
const MARKING_COLOR = "#
|
|
34
|
+
const SIDEWALK_COLOR = "#867f74";
|
|
35
|
+
const MARKING_COLOR = "#ddd9cb";
|
|
33
36
|
const MEDIAN_COLOR = "#4d7a40";
|
|
34
37
|
const PAVEMENT_COLOR = "#a8a49a";
|
|
35
38
|
const CONCRETE_COLOR = "#b0aba1";
|
|
36
39
|
const STEEL_COLOR = "#4c505a";
|
|
37
40
|
const TUNNEL_COLOR = "#5b5754";
|
|
38
|
-
const CURB_COLOR = "#
|
|
41
|
+
const CURB_COLOR = "#b8b4aa";
|
|
39
42
|
const HEDGE_COLOR = "#3d6631";
|
|
40
|
-
const
|
|
43
|
+
const VEHICLE_PAINTS = ["#b9bdc3", "#2c2f33", "#79838f", "#8d3330", "#27446e", "#eceae1", "#3c5c48", "#a8792f"];
|
|
44
|
+
const VEHICLE_GLASS_COLOR = "#23262b";
|
|
45
|
+
const SIGNAL_POST_COLOR = "#3b4046";
|
|
46
|
+
const SIGNAL_HOUSING_COLOR = "#1e2226";
|
|
47
|
+
const SIGN_FACE_COLOR = "#c8ccd2";
|
|
48
|
+
const SIGNAL_LAMP_COLORS = ["#d43a2c", "#dfa32b", "#33a558"];
|
|
49
|
+
const KERB_RED = new THREE.Color("#d23a34");
|
|
50
|
+
const KERB_WHITE = new THREE.Color("#e8e8ea");
|
|
51
|
+
const CHECK_DARK = new THREE.Color("#101015");
|
|
52
|
+
const CHECK_LIGHT = new THREE.Color("#e8e8ea");
|
|
53
|
+
const RACE_ASPHALT = new THREE.Color("#1a1c20");
|
|
54
|
+
const RACE_EDGE = new THREE.Color("#d8d4c8");
|
|
55
|
+
const PARK_COLORS = { plaza: "#b3ada0", green: "#4e7c41", meadow: "#6d8a4c", field: "#94805a", courtyard: "#6a655c", buffer: "#63734a" };
|
|
41
56
|
const CROP_COLORS = ["#5d7a35", "#7a6b41", "#6f7f3a"];
|
|
57
|
+
/** Block-interior ground by zone band. A dense district keeps hard surface all the way to its rim;
|
|
58
|
+
* only a low-density (large-block) district lets the edge band go back to lawn. */
|
|
59
|
+
const BLOCK_SURFACE_COLORS = { core: "#5b5850", mid: "#6c6456", edge: "#78765c" };
|
|
60
|
+
const BLOCK_EDGE_GREEN = "#6a7f4a";
|
|
61
|
+
/** The developed ground fades into this at its outer reach so the fabric has no stamped rim. */
|
|
62
|
+
const FRINGE_COLOR = "#5f7444";
|
|
63
|
+
/** Non-buildable block land takes its open-space colour so a park block never shows raw terrain. */
|
|
64
|
+
const BLOCK_OPEN_COLORS = { park: PARK_COLORS.green, plaza: PARK_COLORS.plaza, field: PARK_COLORS.field, buffer: PARK_COLORS.buffer };
|
|
42
65
|
/**
|
|
43
66
|
* City ground-decal stack. The single-owner {@link GROUND_DECAL_LAYERS} table fixes the authored-road
|
|
44
67
|
* heights, but a generated district drapes many more surface classes (sidewalk/park/median/curb) and
|
|
45
68
|
* wants more clearance over relief, so it keeps its own baseline while mirroring the table's ordering
|
|
46
69
|
* (terrain < road == junction < marking) and its two hard contracts: the welded junction surface
|
|
47
70
|
* shares the road layer (seam-welded, never stacked), and markings ride above the road with
|
|
48
|
-
* `polygonOffset`/`renderOrder` on the material.
|
|
71
|
+
* `polygonOffset`/`renderOrder` on the material. Everything behind the curb (block land, sidewalk,
|
|
72
|
+
* parks) rides a curb-height step ABOVE the carriageway, and the curb face is real vertical
|
|
73
|
+
* geometry between the two — a flat sidewalk under the road plane cannot read as a street.
|
|
49
74
|
*/
|
|
50
|
-
const
|
|
51
|
-
const PARK_LAYER = 0.12;
|
|
52
|
-
const DRIVEWAY_LAYER = 0.12;
|
|
53
|
-
const BRIDGE_DECK_LIFT = 0.12;
|
|
54
|
-
const PARKING_LAYER = 0.13;
|
|
55
|
-
const PLAZA_LAYER = 0.14;
|
|
75
|
+
const URBAN_LAYER = 0.06; // street-derived developed ground, under everything else
|
|
56
76
|
const ROAD_LAYER = 0.16; // asphalt/gravel carriageway AND the welded junction surface (seam-shared)
|
|
57
|
-
const
|
|
58
|
-
const CURB_LAYER = 0.24;
|
|
59
|
-
const MARKING_LAYER = 0.26;
|
|
77
|
+
const MARKING_LAYER = 0.24;
|
|
60
78
|
const MEDIAN_LAYER = 0.3;
|
|
61
|
-
|
|
62
|
-
const
|
|
79
|
+
const BLOCK_LAND_LAYER = 0.3;
|
|
80
|
+
const CURB_LAYER = 0.32;
|
|
81
|
+
const SIDEWALK_LAYER = 0.32;
|
|
82
|
+
const PARK_LAYER = 0.34;
|
|
83
|
+
const DRIVEWAY_LAYER = 0.34;
|
|
84
|
+
const PARKING_LAYER = 0.35;
|
|
85
|
+
const PLAZA_LAYER = 0.36;
|
|
86
|
+
const CROP_LAYER = 0.42;
|
|
87
|
+
const RACE_SURFACE_LAYER = 0.28;
|
|
88
|
+
const RACE_PAINT_LAYER = 0.3;
|
|
89
|
+
const BRIDGE_DECK_LIFT = 0.12;
|
|
90
|
+
/** Compact carriageway-union — same seam as StreetGeometryPreview and playground `cityScene`. */
|
|
91
|
+
const JUNCTION_OPTS = { curbReturnRadius: 2, apronMargin: 0.25, filletSegments: 6, elevation: ROAD_LAYER, maxSegmentLength: 2 };
|
|
63
92
|
const SPECIES_PROXY = { broadleaf: "oak", conifer: "pine", palm: "palm", cypress: "cypress" };
|
|
64
93
|
const SPECIES_SCALE = { broadleaf: 2.7, conifer: 2.5, palm: 2.6, cypress: 2.3 };
|
|
94
|
+
const VEHICLE_SHAPES = {
|
|
95
|
+
car: { body: [1.82, 0.72, 4.42], bodyY: 0.62, cabin: [1.62, 0.62, 2.24], cabinY: 1.24, cabinAlong: -0.24, roof: [1.34, 0.12, 1.86], roofY: 1.58, roofAlong: -0.24 },
|
|
96
|
+
van: { body: [2.02, 1.0, 5.2], bodyY: 0.76, cabin: [1.82, 0.74, 3.2], cabinY: 1.58, cabinAlong: -0.42, roof: [1.56, 0.12, 2.8], roofY: 1.99, roofAlong: -0.42 },
|
|
97
|
+
truck: { body: [2.32, 1.46, 6.8], bodyY: 0.99, cabin: [2.1, 0.62, 0.54], cabinY: 1.32, cabinAlong: 3.18, roof: [1.94, 0.14, 6.1], roofY: 1.79, roofAlong: -0.2 },
|
|
98
|
+
};
|
|
65
99
|
/** Accumulates draped ribbons, patches, discs, and quads into one merged (optionally colored) buffer. */
|
|
66
100
|
class MeshAccumulator {
|
|
67
101
|
positions = [];
|
|
@@ -162,6 +196,22 @@ class MeshAccumulator {
|
|
|
162
196
|
this.indices.push(index + base);
|
|
163
197
|
this.pushColor(positions.length, color);
|
|
164
198
|
}
|
|
199
|
+
/** A vertical skirt around a closed ring — the curb face between carriageway and sidewalk. */
|
|
200
|
+
addRingWall(ring, sample, bottom, top, color = null) {
|
|
201
|
+
if (ring.length < 3)
|
|
202
|
+
return;
|
|
203
|
+
const base = this.positions.length / 3;
|
|
204
|
+
for (const [x, z] of ring) {
|
|
205
|
+
const y = sample(x, z);
|
|
206
|
+
this.positions.push(x, y + bottom, z, x, y + top, z);
|
|
207
|
+
}
|
|
208
|
+
for (let i = 0; i < ring.length; i += 1) {
|
|
209
|
+
const a = base + i * 2;
|
|
210
|
+
const b = base + ((i + 1) % ring.length) * 2;
|
|
211
|
+
this.indices.push(a, a + 1, b, b, a + 1, b + 1);
|
|
212
|
+
}
|
|
213
|
+
this.pushColor(ring.length * 2, color);
|
|
214
|
+
}
|
|
165
215
|
build() {
|
|
166
216
|
if (this.indices.length === 0)
|
|
167
217
|
return null;
|
|
@@ -173,6 +223,48 @@ class MeshAccumulator {
|
|
|
173
223
|
geometry.computeVertexNormals();
|
|
174
224
|
return geometry;
|
|
175
225
|
}
|
|
226
|
+
/** Flat draped quad in XZ, already lifted: corners in order around the face. */
|
|
227
|
+
addQuad(corners, color) {
|
|
228
|
+
if (this.colors === null)
|
|
229
|
+
return;
|
|
230
|
+
const base = this.positions.length / 3;
|
|
231
|
+
for (const corner of corners)
|
|
232
|
+
this.positions.push(corner[0], corner[1], corner[2]);
|
|
233
|
+
this.indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
234
|
+
this.pushColor(4, color);
|
|
235
|
+
}
|
|
236
|
+
/** Standing barrier brick: a box extruded along `across` and `forward`, striped by the caller. */
|
|
237
|
+
addSealBlock(x, z, y, height, halfAcross, halfDepth, across, forward, color) {
|
|
238
|
+
if (this.colors === null)
|
|
239
|
+
return;
|
|
240
|
+
const ax = across[0];
|
|
241
|
+
const az = across[1];
|
|
242
|
+
const fx = forward[0];
|
|
243
|
+
const fz = forward[1];
|
|
244
|
+
const corners = [
|
|
245
|
+
[x - ax * halfAcross - fx * halfDepth, y, z - az * halfAcross - fz * halfDepth],
|
|
246
|
+
[x + ax * halfAcross - fx * halfDepth, y, z + az * halfAcross - fz * halfDepth],
|
|
247
|
+
[x + ax * halfAcross + fx * halfDepth, y, z + az * halfAcross + fz * halfDepth],
|
|
248
|
+
[x - ax * halfAcross + fx * halfDepth, y, z - az * halfAcross + fz * halfDepth],
|
|
249
|
+
[x - ax * halfAcross - fx * halfDepth, y + height, z - az * halfAcross - fz * halfDepth],
|
|
250
|
+
[x + ax * halfAcross - fx * halfDepth, y + height, z + az * halfAcross - fz * halfDepth],
|
|
251
|
+
[x + ax * halfAcross + fx * halfDepth, y + height, z + az * halfAcross + fz * halfDepth],
|
|
252
|
+
[x - ax * halfAcross + fx * halfDepth, y + height, z - az * halfAcross + fz * halfDepth],
|
|
253
|
+
];
|
|
254
|
+
const base = this.positions.length / 3;
|
|
255
|
+
for (const corner of corners)
|
|
256
|
+
this.positions.push(corner[0], corner[1], corner[2]);
|
|
257
|
+
const faces = [
|
|
258
|
+
[0, 1, 5, 4],
|
|
259
|
+
[1, 2, 6, 5],
|
|
260
|
+
[2, 3, 7, 6],
|
|
261
|
+
[3, 0, 4, 7],
|
|
262
|
+
[4, 5, 6, 7],
|
|
263
|
+
];
|
|
264
|
+
for (const [a, b, c, d] of faces)
|
|
265
|
+
this.indices.push(base + a, base + b, base + c, base + a, base + c, base + d);
|
|
266
|
+
this.pushColor(8, color);
|
|
267
|
+
}
|
|
176
268
|
}
|
|
177
269
|
/** Split a polyline into runs that keep `pad` clearance from every intersection disc. */
|
|
178
270
|
function clipPolylineAtIntersections(points, intersections, pad) {
|
|
@@ -210,86 +302,6 @@ function clipPolylineAtIntersections(points, intersections, pad) {
|
|
|
210
302
|
runs.push(current);
|
|
211
303
|
return runs;
|
|
212
304
|
}
|
|
213
|
-
/** Andrew's monotone-chain convex hull of XZ points (CCW), degenerate-safe. */
|
|
214
|
-
function convexHull(points) {
|
|
215
|
-
const pts = points.map((p) => [p[0], p[1]]).sort((a, b) => (a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]));
|
|
216
|
-
if (pts.length < 3)
|
|
217
|
-
return pts;
|
|
218
|
-
const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
219
|
-
const lower = [];
|
|
220
|
-
for (const p of pts) {
|
|
221
|
-
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
|
|
222
|
-
lower.pop();
|
|
223
|
-
lower.push(p);
|
|
224
|
-
}
|
|
225
|
-
const upper = [];
|
|
226
|
-
for (let i = pts.length - 1; i >= 0; i -= 1) {
|
|
227
|
-
const p = pts[i];
|
|
228
|
-
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
|
|
229
|
-
upper.pop();
|
|
230
|
-
upper.push(p);
|
|
231
|
-
}
|
|
232
|
-
lower.pop();
|
|
233
|
-
upper.pop();
|
|
234
|
-
return lower.concat(upper);
|
|
235
|
-
}
|
|
236
|
-
/** Round every vertex of a CLOSED ring into a short arc — the curb-return radius that turns a hard
|
|
237
|
-
* polygon corner into a real street corner. Radius is clamped to each corner's shorter leg. */
|
|
238
|
-
function roundRing(ring, radius, segments = 5) {
|
|
239
|
-
if (ring.length < 3 || radius <= 0)
|
|
240
|
-
return ring.map((p) => [p[0], p[1]]);
|
|
241
|
-
const n = ring.length;
|
|
242
|
-
const out = [];
|
|
243
|
-
for (let i = 0; i < n; i += 1) {
|
|
244
|
-
const a = ring[(i - 1 + n) % n];
|
|
245
|
-
const v = ring[i];
|
|
246
|
-
const b = ring[(i + 1) % n];
|
|
247
|
-
const ax = a[0] - v[0];
|
|
248
|
-
const az = a[1] - v[1];
|
|
249
|
-
const bx = b[0] - v[0];
|
|
250
|
-
const bz = b[1] - v[1];
|
|
251
|
-
const la = Math.hypot(ax, az) || 1;
|
|
252
|
-
const lb = Math.hypot(bx, bz) || 1;
|
|
253
|
-
const r = Math.min(radius, la / 2, lb / 2);
|
|
254
|
-
const p0 = [v[0] + (ax / la) * r, v[1] + (az / la) * r];
|
|
255
|
-
const p1 = [v[0] + (bx / lb) * r, v[1] + (bz / lb) * r];
|
|
256
|
-
out.push(p0);
|
|
257
|
-
for (let s = 1; s < segments; s += 1) {
|
|
258
|
-
const t = s / segments;
|
|
259
|
-
const u = 1 - t;
|
|
260
|
-
out.push([u * u * p0[0] + 2 * u * t * v[0] + t * t * p1[0], u * u * p0[1] + 2 * u * t * v[1] + t * t * p1[1]]);
|
|
261
|
-
}
|
|
262
|
-
out.push(p1);
|
|
263
|
-
}
|
|
264
|
-
return out;
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* The pavement outline of a junction: each arm's roadway extended to the crossing, hulled into one
|
|
268
|
-
* convex area, then corner-rounded. This reads as a real intersection (a squared crossing with
|
|
269
|
-
* rounded curb corners) instead of the old stamped circle. Returns `null` for a degenerate junction.
|
|
270
|
-
*/
|
|
271
|
-
function junctionOutline(cross, reachScale = 1.15, expand = 0) {
|
|
272
|
-
if (cross.arms.length < 2)
|
|
273
|
-
return null;
|
|
274
|
-
let maxHalf = 0;
|
|
275
|
-
for (const arm of cross.arms)
|
|
276
|
-
maxHalf = Math.max(maxHalf, arm.width / 2);
|
|
277
|
-
const reach = maxHalf * reachScale + expand;
|
|
278
|
-
const mouth = [];
|
|
279
|
-
for (const arm of cross.arms) {
|
|
280
|
-
const dir = [Math.sin(arm.angle), Math.cos(arm.angle)];
|
|
281
|
-
const perp = [-dir[1], dir[0]];
|
|
282
|
-
const hw = arm.width / 2 + expand;
|
|
283
|
-
mouth.push([cross.x + dir[0] * reach + perp[0] * hw, cross.z + dir[1] * reach + perp[1] * hw]);
|
|
284
|
-
mouth.push([cross.x + dir[0] * reach - perp[0] * hw, cross.z + dir[1] * reach - perp[1] * hw]);
|
|
285
|
-
}
|
|
286
|
-
const hull = convexHull(mouth);
|
|
287
|
-
if (hull.length < 3)
|
|
288
|
-
return null;
|
|
289
|
-
// Curb-return radius: a real corner, capped so a tight junction never over-rounds into a blob.
|
|
290
|
-
const cornerR = Math.min(maxHalf * 0.9 + expand, 5.5);
|
|
291
|
-
return roundRing(hull, cornerR, 6);
|
|
292
|
-
}
|
|
293
305
|
let unitGeometryCache = null;
|
|
294
306
|
/** Unit massing primitives shared by every district: box, gable prism (ridge along X), cylinder, dome. */
|
|
295
307
|
function unitGeometries() {
|
|
@@ -436,18 +448,20 @@ function OneCity({ object, context }) {
|
|
|
436
448
|
const units = unitGeometries();
|
|
437
449
|
const buckets = {
|
|
438
450
|
box: { matrices: [], colors: [] },
|
|
439
|
-
banded: { matrices: [], colors: [], groundOffsets: [] },
|
|
451
|
+
banded: { matrices: [], colors: [], groundOffsets: [], facades: [] },
|
|
440
452
|
gable: { matrices: [], colors: [] },
|
|
441
453
|
cylinder: { matrices: [], colors: [] },
|
|
442
454
|
dome: { matrices: [], colors: [] },
|
|
443
455
|
};
|
|
444
456
|
const color = new THREE.Color();
|
|
457
|
+
const wallTones = resolveBuildingWallTones(resolved.rules.style).map((tone) => new THREE.Color(tone));
|
|
445
458
|
const roleBase = {
|
|
446
459
|
wall: new THREE.Color(roleColor(palette, "wall")),
|
|
447
460
|
roof: new THREE.Color(roleColor(palette, "roof")),
|
|
448
461
|
trim: new THREE.Color(roleColor(palette, "trim")),
|
|
449
462
|
accent: new THREE.Color(roleColor(palette, "accent")),
|
|
450
463
|
};
|
|
464
|
+
const lotWall = new THREE.Color();
|
|
451
465
|
for (const lot of resolved.lots) {
|
|
452
466
|
if (detail.ids.has(lot.id))
|
|
453
467
|
continue;
|
|
@@ -471,6 +485,17 @@ function OneCity({ object, context }) {
|
|
|
471
485
|
const foundationBase = minY - 0.5;
|
|
472
486
|
// Edge-of-town lots vary more; downtown stays composed.
|
|
473
487
|
const jitterAmp = lot.zone === "edge" ? 0.4 : lot.zone === "mid" ? 0.28 : 0.18;
|
|
488
|
+
// Each building draws its wall colour from the style's tone family, then shifts value/hue on
|
|
489
|
+
// top — one flat hue per style is what makes a whole skyline read as a single paint bucket.
|
|
490
|
+
const idHash = hashString(lot.id);
|
|
491
|
+
lotWall.copy(wallTones[idHash % wallTones.length]);
|
|
492
|
+
const shade = ((idHash >>> 8) % 1024) / 1024;
|
|
493
|
+
const hue = ((idHash >>> 19) % 512) / 512;
|
|
494
|
+
lotWall.offsetHSL((hue - 0.5) * 0.07, (hue - 0.5) * 0.24, (shade - 0.5) * 0.2);
|
|
495
|
+
const facadeSeed = ((idHash >>> 17) % 997) / 997;
|
|
496
|
+
// Parking structures are the one class whose "windows" are open deck slots, and the resolver
|
|
497
|
+
// can seat an interior-fill class here that `CityLotClass` does not name.
|
|
498
|
+
const facadeKind = lot.class === "garage" ? 2 : lot.floors >= 8 ? 1 : 0;
|
|
474
499
|
for (const piece of lot.pieces) {
|
|
475
500
|
const px = lot.center[0] + piece.offset[0] * c + piece.offset[2] * s;
|
|
476
501
|
const pz = lot.center[1] - piece.offset[0] * s + piece.offset[2] * c;
|
|
@@ -480,19 +505,23 @@ function OneCity({ object, context }) {
|
|
|
480
505
|
const groundOffset = grounded ? grade - foundationBase : -piece.offset[1];
|
|
481
506
|
const matrix = new THREE.Matrix4().compose(new THREE.Vector3(px, piece.shape === "gable" || piece.shape === "cylinder" || piece.shape === "dome" ? baseY : baseY + height / 2, pz), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), lot.rotationY + piece.rotationY), new THREE.Vector3(Math.max(0.05, piece.size[0]), Math.max(0.05, height), Math.max(0.05, piece.size[2])));
|
|
482
507
|
const amp = jitterAmp * ROLE_JITTER[piece.role];
|
|
483
|
-
color.copy(roleBase[piece.role]);
|
|
508
|
+
color.copy(piece.role === "wall" ? lotWall : roleBase[piece.role]);
|
|
509
|
+
// Trim/roof/accent stay in the lot's own family so a building reads as one object, not as
|
|
510
|
+
// borrowed parts: pull each partway toward its wall tone before the role jitter.
|
|
511
|
+
if (piece.role === "trim")
|
|
512
|
+
color.lerp(lotWall, 0.45);
|
|
513
|
+
else if (piece.role === "roof")
|
|
514
|
+
color.lerp(lotWall, 0.2);
|
|
484
515
|
// Accent trim (awnings/cornices) is a saturated storefront color that reads as a stray red/pink
|
|
485
516
|
// strip at district scale — pull it partway to the wall so it stays a tasteful accent.
|
|
486
|
-
if (piece.role === "accent")
|
|
487
|
-
color.lerp(
|
|
517
|
+
else if (piece.role === "accent")
|
|
518
|
+
color.lerp(lotWall, 0.5);
|
|
488
519
|
color.multiplyScalar(1 - amp / 2 + lot.jitter * amp);
|
|
489
|
-
// A whisper of hue shift so rows of houses never read as one paint bucket.
|
|
490
|
-
color.r *= 1 + (lot.jitter - 0.5) * 0.08;
|
|
491
|
-
color.b *= 1 + (0.5 - lot.jitter) * 0.08;
|
|
492
520
|
const bucket = piece.shape === "box" ? (piece.banded ? buckets.banded : buckets.box) : buckets[piece.shape];
|
|
493
521
|
bucket.matrices.push(matrix);
|
|
494
522
|
bucket.colors.push(color.clone());
|
|
495
523
|
bucket.groundOffsets?.push(groundOffset);
|
|
524
|
+
bucket.facades?.push(facadeSeed, facadeKind);
|
|
496
525
|
}
|
|
497
526
|
}
|
|
498
527
|
// Cylinders/gables/domes scale from their base (geometry roots at y=0), boxes from center —
|
|
@@ -504,9 +533,10 @@ function OneCity({ object, context }) {
|
|
|
504
533
|
const mesh = new THREE.InstancedMesh(geometry, material, bucket.matrices.length);
|
|
505
534
|
bucket.matrices.forEach((matrix, i) => mesh.setMatrixAt(i, matrix));
|
|
506
535
|
bucket.colors.forEach((tint, i) => mesh.setColorAt(i, tint));
|
|
507
|
-
if (bucket.groundOffsets !== undefined) {
|
|
536
|
+
if (bucket.groundOffsets !== undefined && bucket.facades !== undefined) {
|
|
508
537
|
mesh.geometry = geometry.clone();
|
|
509
538
|
mesh.geometry.setAttribute("instanceGroundOffset", new THREE.InstancedBufferAttribute(new Float32Array(bucket.groundOffsets), 1));
|
|
539
|
+
mesh.geometry.setAttribute("instanceFacade", new THREE.InstancedBufferAttribute(new Float32Array(bucket.facades), 2));
|
|
510
540
|
}
|
|
511
541
|
mesh.instanceMatrix.needsUpdate = true;
|
|
512
542
|
if (mesh.instanceColor !== null)
|
|
@@ -540,11 +570,13 @@ function OneCity({ object, context }) {
|
|
|
540
570
|
const ground = useMemo(() => {
|
|
541
571
|
if (resolved === null || resolved.streets.length === 0)
|
|
542
572
|
return null;
|
|
543
|
-
const asphalt = new MeshAccumulator();
|
|
573
|
+
const asphalt = new MeshAccumulator(true);
|
|
544
574
|
const gravel = new MeshAccumulator();
|
|
545
575
|
const sidewalks = new MeshAccumulator();
|
|
546
|
-
const
|
|
547
|
-
const
|
|
576
|
+
const urban = new MeshAccumulator(true);
|
|
577
|
+
const land = new MeshAccumulator(true);
|
|
578
|
+
const patches = new MeshAccumulator(true);
|
|
579
|
+
const markings = new MeshAccumulator(true);
|
|
548
580
|
const curbs = new MeshAccumulator();
|
|
549
581
|
const medians = new MeshAccumulator();
|
|
550
582
|
const pavementDrives = new MeshAccumulator();
|
|
@@ -552,9 +584,14 @@ function OneCity({ object, context }) {
|
|
|
552
584
|
const parks = new MeshAccumulator(true);
|
|
553
585
|
const crops = new MeshAccumulator(true);
|
|
554
586
|
const parkColor = new THREE.Color();
|
|
587
|
+
const wearColor = new THREE.Color();
|
|
588
|
+
const markColor = new THREE.Color();
|
|
589
|
+
const LANE_TINT = new THREE.Color(0.78, 0.77, 0.72);
|
|
590
|
+
const STALL_TINT = new THREE.Color(0.55, 0.54, 0.5);
|
|
555
591
|
// Sidewalks now come from block curb→land bands; only pre-block documents fall back to the old
|
|
556
592
|
// per-street widened ribbon.
|
|
557
593
|
const useBlockSidewalks = resolved.blocks.length > 0;
|
|
594
|
+
const sidewalkW = Math.max(0, resolved.rules.sidewalkWidth);
|
|
558
595
|
// Feature-aware height: because streets stay continuous across water and through ridges, the road
|
|
559
596
|
// must ride UP onto a bridge deck and hold FLAT through a tunnel bore rather than dive under the
|
|
560
597
|
// river or climb over the hill. Points off any feature fall back to the terrain sampler.
|
|
@@ -605,7 +642,7 @@ function OneCity({ object, context }) {
|
|
|
605
642
|
}
|
|
606
643
|
return sample(x, z);
|
|
607
644
|
};
|
|
608
|
-
const addMarkQuad = (center, along, length, width) => {
|
|
645
|
+
const addMarkQuad = (center, along, length, width, tint) => {
|
|
609
646
|
const perp = [-along[1], along[0]];
|
|
610
647
|
const base = markings.positions.length / 3;
|
|
611
648
|
for (const [sa, sp] of [
|
|
@@ -617,106 +654,123 @@ function OneCity({ object, context }) {
|
|
|
617
654
|
const x = center[0] + along[0] * length * sa + perp[0] * width * sp;
|
|
618
655
|
const z = center[1] + along[1] * length * sa + perp[1] * width * sp;
|
|
619
656
|
markings.positions.push(x, deckSampler(x, z) + MARKING_LAYER, z);
|
|
657
|
+
markings.colors?.push(tint.r, tint.g, tint.b);
|
|
620
658
|
}
|
|
621
659
|
markings.indices.push(base, base + 2, base + 1, base + 1, base + 2, base + 3);
|
|
622
660
|
};
|
|
623
|
-
// Trim carriageways
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
// approach (e.g. sliced past the intersection cap) falls back to the old rounded outline below.
|
|
661
|
+
// Trim carriageways and weld one compact surface per crossing — the same
|
|
662
|
+
// `buildTrimmedIntersections` seam StreetGeometryPreview and playground cityScene consume.
|
|
663
|
+
// Do not stack plaza discs, hull aprons, or fat zebra paint on top of that union.
|
|
664
|
+
const roundedPaths = resolved.streets.map((street) => street.points.length < 2 ? street.points : roundPathCorners(street.points, Math.max(2.5, street.width * 1.15), 9));
|
|
628
665
|
const intersectionInputs = resolved.intersections.map((cross) => ({
|
|
629
666
|
x: cross.x,
|
|
630
667
|
z: cross.z,
|
|
631
668
|
arms: cross.arms.map((arm) => ({ angle: arm.angle, width: arm.width })),
|
|
632
669
|
}));
|
|
633
|
-
const
|
|
634
|
-
|
|
670
|
+
const intersectionStreets = resolved.streets.map((street, streetIndex) => {
|
|
671
|
+
const path = roundedPaths[streetIndex];
|
|
672
|
+
// Always declare sidewalk widths when sidewalks are on so junction curb-return aprons weld;
|
|
673
|
+
// per-street sidewalk ribbons are skipped below when block curb rings already own the walk.
|
|
674
|
+
const wantSidewalk = street.sidewalk && sidewalkW > 0 && resolved.rules.sidewalks;
|
|
675
|
+
const arterial = street.level === "boulevard" || street.level === "avenue";
|
|
676
|
+
return {
|
|
677
|
+
path,
|
|
678
|
+
width: street.width,
|
|
679
|
+
...(wantSidewalk ? { sidewalks: { left: sidewalkW, right: sidewalkW } } : {}),
|
|
680
|
+
...(arterial && street.surface === "asphalt"
|
|
681
|
+
? {
|
|
682
|
+
markings: {
|
|
683
|
+
lines: [{ offset: 0, width: 0.16 }],
|
|
684
|
+
stopLine: true,
|
|
685
|
+
},
|
|
686
|
+
}
|
|
687
|
+
: {}),
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
const trimmed = buildTrimmedIntersections(intersectionStreets, intersectionInputs, deckSampler, JUNCTION_OPTS);
|
|
691
|
+
for (let i = 0; i < trimmed.ribbons.length; i += 1) {
|
|
692
|
+
const streetIndex = trimmed.trimmedStreetIndices[i];
|
|
693
|
+
const street = resolved.streets[streetIndex];
|
|
694
|
+
const wear = (hashString(`surface:${streetIndex}`) % 1024) / 1024;
|
|
695
|
+
wearColor.setRGB(0.86 + wear * 0.3, 0.87 + wear * 0.28, 0.9 + wear * 0.24);
|
|
696
|
+
const target = street.surface === "gravel" ? gravel : asphalt;
|
|
697
|
+
target.addMesh(trimmed.ribbons[i], street.surface === "gravel" ? null : wearColor);
|
|
698
|
+
}
|
|
699
|
+
if (!useBlockSidewalks) {
|
|
700
|
+
for (const sidewalk of trimmed.sidewalks)
|
|
701
|
+
sidewalks.addMesh(sidewalk);
|
|
702
|
+
for (const apron of trimmed.sidewalkAprons)
|
|
703
|
+
sidewalks.addMesh(apron);
|
|
704
|
+
}
|
|
705
|
+
for (let j = 0; j < trimmed.junctions.length; j += 1) {
|
|
706
|
+
const crossIndex = trimmed.junctionIndices[j];
|
|
707
|
+
const scuff = (hashString(`cross:${crossIndex}`) % 512) / 512;
|
|
708
|
+
wearColor.setRGB(0.76 + scuff * 0.18, 0.77 + scuff * 0.17, 0.8 + scuff * 0.16);
|
|
709
|
+
patches.addMesh(trimmed.junctions[j], wearColor);
|
|
710
|
+
}
|
|
711
|
+
for (const paint of buildIntersectionMarkings(trimmed, deckSampler, {
|
|
712
|
+
mouthClearance: 1.25,
|
|
713
|
+
dashLength: 2.6,
|
|
714
|
+
dashGap: 3.6,
|
|
715
|
+
elevation: MARKING_LAYER,
|
|
716
|
+
maxSegmentLength: 2,
|
|
717
|
+
})) {
|
|
718
|
+
markColor.copy(LANE_TINT);
|
|
719
|
+
markings.addMesh(paint, markColor);
|
|
720
|
+
}
|
|
721
|
+
resolved.streets.forEach((street, streetIndex) => {
|
|
635
722
|
if (street.points.length < 2)
|
|
636
|
-
|
|
637
|
-
const rounded =
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
for (const cut of sub.cuts) {
|
|
642
|
-
const approach = {
|
|
643
|
-
center: cut.center,
|
|
644
|
-
left: [cut.left[0], deckSampler(cut.left[0], cut.left[1]) + ROAD_LAYER, cut.left[1]],
|
|
645
|
-
right: [cut.right[0], deckSampler(cut.right[0], cut.right[1]) + ROAD_LAYER, cut.right[1]],
|
|
646
|
-
};
|
|
647
|
-
const list = approachesByCross.get(cut.junctionIndex);
|
|
648
|
-
if (list)
|
|
649
|
-
list.push(approach);
|
|
650
|
-
else
|
|
651
|
-
approachesByCross.set(cut.junctionIndex, [approach]);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
if (street.sidewalk && !useBlockSidewalks)
|
|
723
|
+
return;
|
|
724
|
+
const rounded = roundedPaths[streetIndex];
|
|
725
|
+
const wear = (hashString(`surface:${streetIndex}`) % 1024) / 1024;
|
|
726
|
+
wearColor.setRGB(0.86 + wear * 0.3, 0.87 + wear * 0.28, 0.9 + wear * 0.24);
|
|
727
|
+
if (street.sidewalk && !useBlockSidewalks && sidewalkW <= 0) {
|
|
655
728
|
sidewalks.addRibbon(rounded, street.width + 3.6, sample, SIDEWALK_LAYER);
|
|
656
|
-
|
|
657
|
-
// junctions (the crossing owns its own pavement), so clip the centerline first.
|
|
729
|
+
}
|
|
658
730
|
if (street.surface === "asphalt") {
|
|
659
731
|
const half = street.width / 2;
|
|
660
732
|
for (const run of clipPolylineAtIntersections(rounded, resolved.intersections, half + 0.5)) {
|
|
661
733
|
if (run.length < 2)
|
|
662
734
|
continue;
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (street.level
|
|
668
|
-
|
|
669
|
-
markings.addRibbon(offsetPath(run, half - 0.
|
|
670
|
-
markings.addRibbon(offsetPath(run, -(half - 0.
|
|
671
|
-
// Avenue+ get dashed lane dividers either side of the centerline.
|
|
735
|
+
if (!useBlockSidewalks) {
|
|
736
|
+
curbs.addRibbon(offsetPath(run, half), 0.55, deckSampler, CURB_LAYER);
|
|
737
|
+
curbs.addRibbon(offsetPath(run, -half), 0.55, deckSampler, CURB_LAYER);
|
|
738
|
+
}
|
|
739
|
+
if (street.level === "boulevard" || street.level === "avenue") {
|
|
740
|
+
markColor.copy(LANE_TINT);
|
|
741
|
+
markings.addRibbon(offsetPath(run, half - 0.8), 0.12, deckSampler, MARKING_LAYER, markColor);
|
|
742
|
+
markings.addRibbon(offsetPath(run, -(half - 0.8)), 0.12, deckSampler, MARKING_LAYER, markColor);
|
|
672
743
|
if (street.level === "avenue") {
|
|
673
744
|
for (const dash of dashSegments(offsetPath(run, half * 0.5), 2.4, 3.2))
|
|
674
|
-
markings.addRibbon(dash, 0.
|
|
745
|
+
markings.addRibbon(dash, 0.12, deckSampler, MARKING_LAYER, markColor);
|
|
675
746
|
for (const dash of dashSegments(offsetPath(run, -half * 0.5), 2.4, 3.2))
|
|
676
|
-
markings.addRibbon(dash, 0.
|
|
747
|
+
markings.addRibbon(dash, 0.12, deckSampler, MARKING_LAYER, markColor);
|
|
677
748
|
}
|
|
678
749
|
}
|
|
679
750
|
}
|
|
680
751
|
}
|
|
681
752
|
if (street.bulb !== undefined) {
|
|
682
|
-
(street.surface === "gravel"
|
|
753
|
+
if (street.surface === "gravel")
|
|
754
|
+
gravel.addDisc(street.bulb, street.width * 1.15, sample, ROAD_LAYER);
|
|
755
|
+
else
|
|
756
|
+
asphalt.addDisc(street.bulb, street.width * 1.15, sample, ROAD_LAYER, wearColor);
|
|
683
757
|
if (street.sidewalk)
|
|
684
758
|
sidewalks.addDisc(street.bulb, street.width * 1.15 + 1.8, sample, SIDEWALK_LAYER);
|
|
685
759
|
}
|
|
686
|
-
if (street.surface === "asphalt" && street.level
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const width = Math.max(1.3, street.width * 0.14);
|
|
698
|
-
medians.addRibbon(run, width, deckSampler, MEDIAN_LAYER);
|
|
699
|
-
medians.addDisc(run[0], width / 2, deckSampler, MEDIAN_LAYER, null, 8);
|
|
700
|
-
medians.addDisc(run[run.length - 1], width / 2, deckSampler, MEDIAN_LAYER, null, 8);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
else {
|
|
704
|
-
for (const dash of dashSegments(rounded, 2.6, 3.6)) {
|
|
705
|
-
const [a, b] = [dash[0], dash[dash.length - 1]];
|
|
706
|
-
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
707
|
-
let inCross = false;
|
|
708
|
-
for (const cross of resolved.intersections) {
|
|
709
|
-
if (Math.hypot(cross.x - mid[0], cross.z - mid[1]) < cross.radius + 0.8) {
|
|
710
|
-
inCross = true;
|
|
711
|
-
break;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
if (!inCross)
|
|
715
|
-
markings.addRibbon(dash, 0.18, deckSampler, MARKING_LAYER);
|
|
716
|
-
}
|
|
760
|
+
if (street.surface === "asphalt" && street.level === "boulevard") {
|
|
761
|
+
for (const run of clipPolylineAtIntersections(rounded, resolved.intersections, 2.6)) {
|
|
762
|
+
let runLength = 0;
|
|
763
|
+
for (let i = 0; i + 1 < run.length; i += 1)
|
|
764
|
+
runLength += Math.hypot(run[i + 1][0] - run[i][0], run[i + 1][1] - run[i][1]);
|
|
765
|
+
if (runLength < 7)
|
|
766
|
+
continue;
|
|
767
|
+
const width = Math.max(1.3, street.width * 0.14);
|
|
768
|
+
medians.addRibbon(run, width, deckSampler, MEDIAN_LAYER);
|
|
769
|
+
medians.addDisc(run[0], width / 2, deckSampler, MEDIAN_LAYER, null, 8);
|
|
770
|
+
medians.addDisc(run[run.length - 1], width / 2, deckSampler, MEDIAN_LAYER, null, 8);
|
|
717
771
|
}
|
|
718
772
|
}
|
|
719
|
-
}
|
|
773
|
+
});
|
|
720
774
|
// NOTE: streetGenerator's `Street.sidewalks` offset polylines are NOT surfaced here — this renderer
|
|
721
775
|
// consumes `resolveCityObject`'s `CityStreet` (which carries only a `sidewalk` boolean), and its
|
|
722
776
|
// sidewalks come from richer block curb→land bands below. Wiring the raw offset polylines would be a
|
|
@@ -724,57 +778,133 @@ function OneCity({ object, context }) {
|
|
|
724
778
|
// Sidewalk band per block: the region between the curb ring (outer) and the land ring (inner).
|
|
725
779
|
// Blocks whose two rings coincide (no sidewalk) drop out — every band triangle is degenerate,
|
|
726
780
|
// so `addRingBand` emits nothing.
|
|
781
|
+
const hx = Math.max(1e-6, resolved.size[0] / 2);
|
|
782
|
+
const hz = Math.max(1e-6, resolved.size[1] / 2);
|
|
783
|
+
const cosR = Math.cos(resolved.rotationY);
|
|
784
|
+
const sinR = Math.sin(resolved.rotationY);
|
|
785
|
+
const bandAt = (x, z, roll) => {
|
|
786
|
+
const dx = x - resolved.center[0];
|
|
787
|
+
const dz = z - resolved.center[2];
|
|
788
|
+
return zoneBand(zoneMetric(dx * cosR - dz * sinR, dx * sinR + dz * cosR, hx, hz), resolved.rules.profile, resolved.rules.coreExtent, resolved.rules.midExtent, roll);
|
|
789
|
+
};
|
|
790
|
+
const lawnEdge = !resolved.rules.sidewalks || resolved.rules.blockSize > 64;
|
|
791
|
+
// Developed ground. Where the street graph yields real block faces the land ring behind the
|
|
792
|
+
// sidewalk is filled exactly; but block extraction routinely recovers only a fraction of a
|
|
793
|
+
// wandered district's faces, and every face it misses is terrain grass under downtown. So the
|
|
794
|
+
// fabric is also stamped as a street-distance field: any cell within reach of a centerline is
|
|
795
|
+
// developed, shaded pavement near the kerb and yard/service surface deeper into the block.
|
|
796
|
+
if (resolved.rules.fabric) {
|
|
797
|
+
// Reach and paved band are measured OUTWARD FROM THE KERB, not from the centreline: a
|
|
798
|
+
// 20 m boulevard and a 5 m lane must not lay the same width of pavement beside them.
|
|
799
|
+
const reach = Math.min(40, Math.max(10, resolved.rules.blockSize * 0.55));
|
|
800
|
+
const pavedTo = Math.max(1.2, resolved.rules.sidewalkWidth + 0.3);
|
|
801
|
+
const cell = 2.5;
|
|
802
|
+
let minX = Infinity;
|
|
803
|
+
let minZ = Infinity;
|
|
804
|
+
let maxX = -Infinity;
|
|
805
|
+
let maxZ = -Infinity;
|
|
806
|
+
let widest = 0;
|
|
807
|
+
for (const street of resolved.streets) {
|
|
808
|
+
widest = Math.max(widest, street.width);
|
|
809
|
+
for (const [x, z] of street.points) {
|
|
810
|
+
minX = Math.min(minX, x);
|
|
811
|
+
maxX = Math.max(maxX, x);
|
|
812
|
+
minZ = Math.min(minZ, z);
|
|
813
|
+
maxZ = Math.max(maxZ, z);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const pad = reach + widest / 2;
|
|
817
|
+
const originX = minX - pad;
|
|
818
|
+
const originZ = minZ - pad;
|
|
819
|
+
const nx = Math.ceil((maxX + pad - originX) / cell) + 1;
|
|
820
|
+
const nz = Math.ceil((maxZ + pad - originZ) / cell) + 1;
|
|
821
|
+
if (Number.isFinite(minX) && nx > 1 && nz > 1 && nx * nz <= 160_000) {
|
|
822
|
+
const field = new Float32Array(nx * nz).fill(Infinity);
|
|
823
|
+
for (const street of resolved.streets) {
|
|
824
|
+
const half = street.width / 2;
|
|
825
|
+
for (let s = 0; s + 1 < street.points.length; s += 1) {
|
|
826
|
+
const [ax, az] = street.points[s];
|
|
827
|
+
const [bx, bz] = street.points[s + 1];
|
|
828
|
+
const vx = bx - ax;
|
|
829
|
+
const vz = bz - az;
|
|
830
|
+
const len2 = Math.max(1e-6, vx * vx + vz * vz);
|
|
831
|
+
const span = reach + half;
|
|
832
|
+
const i0 = Math.max(0, Math.floor((Math.min(ax, bx) - span - originX) / cell));
|
|
833
|
+
const i1 = Math.min(nx - 1, Math.ceil((Math.max(ax, bx) + span - originX) / cell));
|
|
834
|
+
const j0 = Math.max(0, Math.floor((Math.min(az, bz) - span - originZ) / cell));
|
|
835
|
+
const j1 = Math.min(nz - 1, Math.ceil((Math.max(az, bz) + span - originZ) / cell));
|
|
836
|
+
for (let j = j0; j <= j1; j += 1) {
|
|
837
|
+
const pz = originZ + j * cell;
|
|
838
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
839
|
+
const px = originX + i * cell;
|
|
840
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * vx + (pz - az) * vz) / len2));
|
|
841
|
+
const d = Math.hypot(px - (ax + vx * t), pz - (az + vz * t)) - half;
|
|
842
|
+
const idx = j * nx + i;
|
|
843
|
+
if (d < field[idx])
|
|
844
|
+
field[idx] = d;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const fringe = new THREE.Color(FRINGE_COLOR);
|
|
850
|
+
const fabricColor = new THREE.Color();
|
|
851
|
+
for (let j = 0; j + 1 < nz; j += 1) {
|
|
852
|
+
for (let i = 0; i + 1 < nx; i += 1) {
|
|
853
|
+
const d = (field[j * nx + i] + field[j * nx + i + 1] + field[(j + 1) * nx + i] + field[(j + 1) * nx + i + 1]) / 4;
|
|
854
|
+
if (d > reach)
|
|
855
|
+
continue;
|
|
856
|
+
const x = originX + (i + 0.5) * cell;
|
|
857
|
+
const z = originZ + (j + 0.5) * cell;
|
|
858
|
+
const tone = (hashString(`fabric:${i}:${j}`) % 256) / 256;
|
|
859
|
+
if (d < pavedTo) {
|
|
860
|
+
fabricColor.set(SIDEWALK_COLOR);
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
const band = bandAt(x, z, tone);
|
|
864
|
+
fabricColor.set(band === "edge" && lawnEdge ? BLOCK_EDGE_GREEN : BLOCK_SURFACE_COLORS[band]);
|
|
865
|
+
// A block-sized patch tone over the fine per-cell grain: yards, courts and lots,
|
|
866
|
+
// not one poured apron.
|
|
867
|
+
fabricColor.multiplyScalar(0.8 + ((hashString(`yard:${Math.floor(x / 22)}:${Math.floor(z / 22)}`) % 256) / 256) * 0.42);
|
|
868
|
+
}
|
|
869
|
+
fabricColor.multiplyScalar(0.95 + tone * 0.1);
|
|
870
|
+
if (d > reach - 7)
|
|
871
|
+
fabricColor.lerp(fringe, Math.min(1, (d - (reach - 7)) / 7));
|
|
872
|
+
urban.addPatch([x, z], [cell, cell], 0, sample, URBAN_LAYER, fabricColor);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// The land ring behind the sidewalk is the block INTERIOR — fill it with a surface read from
|
|
878
|
+
// the block's use and its zone band, varied per block so a district reads as many yards and
|
|
879
|
+
// service courts rather than one sheet.
|
|
727
880
|
if (useBlockSidewalks) {
|
|
881
|
+
const landColor = new THREE.Color();
|
|
728
882
|
for (const block of resolved.blocks) {
|
|
883
|
+
let cx = 0;
|
|
884
|
+
let cz = 0;
|
|
885
|
+
for (const [x, z] of block.polygon) {
|
|
886
|
+
cx += x;
|
|
887
|
+
cz += z;
|
|
888
|
+
}
|
|
889
|
+
cx /= Math.max(1, block.polygon.length);
|
|
890
|
+
cz /= Math.max(1, block.polygon.length);
|
|
891
|
+
const tone = (hashString(block.id) % 512) / 512;
|
|
892
|
+
const band = bandAt(cx, cz, tone);
|
|
893
|
+
landColor.set(block.kind !== "buildable"
|
|
894
|
+
? BLOCK_OPEN_COLORS[block.kind]
|
|
895
|
+
: band === "edge" && lawnEdge
|
|
896
|
+
? BLOCK_EDGE_GREEN
|
|
897
|
+
: BLOCK_SURFACE_COLORS[band]);
|
|
898
|
+
landColor.multiplyScalar(0.88 + tone * 0.24);
|
|
899
|
+
landColor.r *= 1 + (tone - 0.5) * 0.1;
|
|
900
|
+
landColor.b *= 1 + (0.5 - tone) * 0.12;
|
|
901
|
+
land.addPolygon(block.polygon, sample, BLOCK_LAND_LAYER, landColor);
|
|
729
902
|
sidewalks.addRingBand(block.curb, block.polygon, sample, SIDEWALK_LAYER);
|
|
903
|
+
curbs.addRingWall(block.curb, sample, ROAD_LAYER - 0.02, CURB_LAYER);
|
|
730
904
|
}
|
|
731
905
|
}
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
// it. Avenue-and-up arms (or a boulevard/avenue junction) get zebra stripes and the apron;
|
|
735
|
-
// small residential crossings get just the asphalt patch.
|
|
736
|
-
resolved.intersections.forEach((cross, crossIndex) => {
|
|
737
|
-
const bigLevel = cross.level === "boulevard" || cross.level === "avenue";
|
|
738
|
-
const armZebra = (width) => bigLevel || width >= resolved.rules.streetWidth * 1.4;
|
|
739
|
-
const paved = resolved.rules.sidewalks && cross.level !== "lane";
|
|
740
|
-
// A slightly larger apron underneath the crossing reads as the wrapped sidewalk/curb.
|
|
741
|
-
if (paved) {
|
|
742
|
-
const apron = junctionOutline(cross, 1.15, resolved.rules.sidewalkWidth + 0.4);
|
|
743
|
-
if (apron !== null)
|
|
744
|
-
sidewalks.addPolygon(apron, sample, SIDEWALK_LAYER);
|
|
745
|
-
}
|
|
746
|
-
// The junction pavement is welded onto the trimmed ribbon ends (seam-shared, no floating disc).
|
|
747
|
-
// Crossings that got no trimmed approach fall back to the rounded outline / disc, so coverage
|
|
748
|
-
// never regresses.
|
|
749
|
-
const approaches = approachesByCross.get(crossIndex);
|
|
750
|
-
if (approaches !== undefined && approaches.length > 0) {
|
|
751
|
-
patches.addMesh(buildJunctionSurface({ x: cross.x, z: cross.z }, approaches, deckSampler, JUNCTION_OPTS));
|
|
752
|
-
}
|
|
753
|
-
else {
|
|
754
|
-
const outline = junctionOutline(cross);
|
|
755
|
-
if (outline !== null)
|
|
756
|
-
patches.addPolygon(outline, deckSampler, ROAD_LAYER);
|
|
757
|
-
else
|
|
758
|
-
patches.addDisc([cross.x, cross.z], cross.radius, deckSampler, ROAD_LAYER);
|
|
759
|
-
}
|
|
760
|
-
if (paved) {
|
|
761
|
-
const maxHalf = Math.max(...cross.arms.map((arm) => arm.width / 2));
|
|
762
|
-
for (const arm of cross.arms) {
|
|
763
|
-
if (!armZebra(arm.width))
|
|
764
|
-
continue;
|
|
765
|
-
const dir = [Math.sin(arm.angle), Math.cos(arm.angle)];
|
|
766
|
-
const perp = [-dir[1], dir[0]];
|
|
767
|
-
// Stop line across the approach, just outside the crossing box.
|
|
768
|
-
const stopDist = maxHalf * 1.15 + 0.55;
|
|
769
|
-
addMarkQuad([cross.x + dir[0] * stopDist, cross.z + dir[1] * stopDist], perp, arm.width * 0.82, 0.5);
|
|
770
|
-
for (let stripe = 0; stripe < 4; stripe += 1) {
|
|
771
|
-
const dist = maxHalf * 1.15 + 1.5 + stripe * 0.85;
|
|
772
|
-
const center = [cross.x + dir[0] * dist, cross.z + dir[1] * dist];
|
|
773
|
-
addMarkQuad(center, perp, arm.width * 0.8, 0.44);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
});
|
|
906
|
+
// Junction pavement + curb-return sidewalk aprons already came from `buildTrimmedIntersections`
|
|
907
|
+
// above. No plaza discs, hull aprons, or fat zebra ladders on top of that union.
|
|
778
908
|
for (const drive of resolved.driveways) {
|
|
779
909
|
(drive.surface === "gravel" ? gravelDrives : pavementDrives).addRibbon(drive.points, drive.width, sample, DRIVEWAY_LAYER);
|
|
780
910
|
}
|
|
@@ -788,7 +918,7 @@ function OneCity({ object, context }) {
|
|
|
788
918
|
const lx = (i / stalls - 0.5) * pad.size[0];
|
|
789
919
|
const cx = pad.center[0] + lx * cos + (pad.size[1] * 0.24) * sin;
|
|
790
920
|
const cz = pad.center[1] - lx * sin + (pad.size[1] * 0.24) * cos;
|
|
791
|
-
addMarkQuad([cx, cz], [sin, cos], pad.size[1] * 0.42, 0.
|
|
921
|
+
addMarkQuad([cx, cz], [sin, cos], pad.size[1] * 0.42, 0.1, STALL_TINT);
|
|
792
922
|
}
|
|
793
923
|
}
|
|
794
924
|
for (const park of resolved.parks) {
|
|
@@ -811,6 +941,8 @@ function OneCity({ object, context }) {
|
|
|
811
941
|
asphalt: asphalt.build(),
|
|
812
942
|
gravel: gravel.build(),
|
|
813
943
|
sidewalks: sidewalks.build(),
|
|
944
|
+
urban: urban.build(),
|
|
945
|
+
land: land.build(),
|
|
814
946
|
patches: patches.build(),
|
|
815
947
|
markings: markings.build(),
|
|
816
948
|
curbs: curbs.build(),
|
|
@@ -946,7 +1078,7 @@ function OneCity({ object, context }) {
|
|
|
946
1078
|
return { roof: roofs.build(), stone };
|
|
947
1079
|
}, [resolved, sample]);
|
|
948
1080
|
useEffect(() => () => tunnels?.roof?.dispose(), [tunnels]);
|
|
949
|
-
// --- Instanced furniture: trees per species, street lights, hedges. ---
|
|
1081
|
+
// --- Instanced furniture: trees per species, street lights, hedges, parked cars, junction signals. ---
|
|
950
1082
|
const furniture = useMemo(() => {
|
|
951
1083
|
if (resolved === null)
|
|
952
1084
|
return null;
|
|
@@ -967,10 +1099,14 @@ function OneCity({ object, context }) {
|
|
|
967
1099
|
const mesh = new THREE.InstancedMesh(geometry, material, trees.length);
|
|
968
1100
|
const matrix = new THREE.Matrix4();
|
|
969
1101
|
trees.forEach((tree, i) => {
|
|
970
|
-
|
|
971
|
-
|
|
1102
|
+
// A second stream off the planting position: `tree.jitter` alone left every canopy the same
|
|
1103
|
+
// ball on a stick, so age (girth vs height) and leaf tone vary independently of it.
|
|
1104
|
+
const age = (hashString(`tree:${tree.x.toFixed(2)}:${tree.z.toFixed(2)}`) % 1024) / 1024;
|
|
1105
|
+
const scale = tree.scale * SPECIES_SCALE[species] * (0.66 + age * 0.78);
|
|
1106
|
+
const spread = 0.86 + tree.jitter * 0.34;
|
|
1107
|
+
matrix.compose(new THREE.Vector3(tree.x, sample(tree.x, tree.z) - 0.05, tree.z), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), tree.jitter * Math.PI * 2), new THREE.Vector3(scale * spread, scale * (0.8 + age * 0.55), scale * spread));
|
|
972
1108
|
mesh.setMatrixAt(i, matrix);
|
|
973
|
-
color.
|
|
1109
|
+
color.setRGB(0.74 + age * 0.42, 0.84 + tree.jitter * 0.3, 0.68 + ((age * 5) % 1) * 0.4);
|
|
974
1110
|
mesh.setColorAt(i, color);
|
|
975
1111
|
});
|
|
976
1112
|
mesh.instanceMatrix.needsUpdate = true;
|
|
@@ -1036,6 +1172,108 @@ function OneCity({ object, context }) {
|
|
|
1036
1172
|
meshes.push(hedges);
|
|
1037
1173
|
disposables.push(hedgeMaterial);
|
|
1038
1174
|
}
|
|
1175
|
+
if (resolved.vehicles.length > 0) {
|
|
1176
|
+
const units = unitGeometries();
|
|
1177
|
+
const paintMaterial = new THREE.MeshStandardMaterial({ roughness: 0.42, metalness: 0.34 });
|
|
1178
|
+
const glassMaterial = new THREE.MeshPhysicalMaterial({ color: VEHICLE_GLASS_COLOR, roughness: 0.16, metalness: 0.2 });
|
|
1179
|
+
const bodies = new THREE.InstancedMesh(units.box, paintMaterial, resolved.vehicles.length);
|
|
1180
|
+
const cabins = new THREE.InstancedMesh(units.box, glassMaterial, resolved.vehicles.length);
|
|
1181
|
+
const roofs = new THREE.InstancedMesh(units.box, paintMaterial, resolved.vehicles.length);
|
|
1182
|
+
const matrix = new THREE.Matrix4();
|
|
1183
|
+
const quat = new THREE.Quaternion();
|
|
1184
|
+
const upAxis = new THREE.Vector3(0, 1, 0);
|
|
1185
|
+
const paint = new THREE.Color();
|
|
1186
|
+
resolved.vehicles.forEach((vehicle, i) => {
|
|
1187
|
+
const shape = VEHICLE_SHAPES[vehicle.kind];
|
|
1188
|
+
const base = sample(vehicle.x, vehicle.z) + (vehicle.stall === "lot" ? PARKING_LAYER : ROAD_LAYER);
|
|
1189
|
+
const along = [Math.sin(vehicle.rotationY), Math.cos(vehicle.rotationY)];
|
|
1190
|
+
quat.setFromAxisAngle(upAxis, vehicle.rotationY);
|
|
1191
|
+
const place = (mesh, size, y, offset) => {
|
|
1192
|
+
matrix.compose(new THREE.Vector3(vehicle.x + along[0] * offset, base + y, vehicle.z + along[1] * offset), quat, new THREE.Vector3(size[0], size[1], size[2]));
|
|
1193
|
+
mesh.setMatrixAt(i, matrix);
|
|
1194
|
+
};
|
|
1195
|
+
place(bodies, shape.body, shape.bodyY, 0);
|
|
1196
|
+
place(cabins, shape.cabin, shape.cabinY, shape.cabinAlong);
|
|
1197
|
+
place(roofs, shape.roof, shape.roofY, shape.roofAlong);
|
|
1198
|
+
paint.set(VEHICLE_PAINTS[vehicle.paint % VEHICLE_PAINTS.length]);
|
|
1199
|
+
paint.multiplyScalar(0.88 + ((hashString(`car:${vehicle.x.toFixed(2)}:${vehicle.z.toFixed(2)}`) % 256) / 256) * 0.26);
|
|
1200
|
+
bodies.setColorAt(i, paint);
|
|
1201
|
+
roofs.setColorAt(i, paint);
|
|
1202
|
+
});
|
|
1203
|
+
for (const mesh of [bodies, cabins, roofs]) {
|
|
1204
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
1205
|
+
if (mesh.instanceColor !== null)
|
|
1206
|
+
mesh.instanceColor.needsUpdate = true;
|
|
1207
|
+
mesh.castShadow = true;
|
|
1208
|
+
mesh.receiveShadow = true;
|
|
1209
|
+
mesh.frustumCulled = false;
|
|
1210
|
+
meshes.push(mesh);
|
|
1211
|
+
}
|
|
1212
|
+
disposables.push(paintMaterial, glassMaterial);
|
|
1213
|
+
}
|
|
1214
|
+
if (resolved.signals.length > 0) {
|
|
1215
|
+
const units = unitGeometries();
|
|
1216
|
+
const mastCount = resolved.signals.filter((signal) => signal.kind === "signal").length;
|
|
1217
|
+
const postMaterial = new THREE.MeshStandardMaterial({ color: SIGNAL_POST_COLOR, roughness: 0.55, metalness: 0.55 });
|
|
1218
|
+
const headMaterial = new THREE.MeshStandardMaterial({ roughness: 0.62, metalness: 0.15 });
|
|
1219
|
+
const lampMaterial = new THREE.MeshBasicMaterial({ toneMapped: false });
|
|
1220
|
+
const posts = new THREE.InstancedMesh(units.cylinder, postMaterial, resolved.signals.length);
|
|
1221
|
+
const heads = new THREE.InstancedMesh(units.box, headMaterial, resolved.signals.length);
|
|
1222
|
+
const arms = mastCount > 0 ? new THREE.InstancedMesh(units.box, postMaterial, mastCount) : null;
|
|
1223
|
+
const lamps = mastCount > 0 ? new THREE.InstancedMesh(units.box, lampMaterial, mastCount * 3) : null;
|
|
1224
|
+
const matrix = new THREE.Matrix4();
|
|
1225
|
+
const quat = new THREE.Quaternion();
|
|
1226
|
+
const armQuat = new THREE.Quaternion();
|
|
1227
|
+
const upAxis = new THREE.Vector3(0, 1, 0);
|
|
1228
|
+
const tint = new THREE.Color();
|
|
1229
|
+
let mast = 0;
|
|
1230
|
+
resolved.signals.forEach((signal, i) => {
|
|
1231
|
+
const y = sample(signal.x, signal.z);
|
|
1232
|
+
const facing = [Math.sin(signal.heading), Math.cos(signal.heading)];
|
|
1233
|
+
// The mast arm reaches to the right of the facing direction — out over the lanes it controls.
|
|
1234
|
+
const overRoad = [facing[1], -facing[0]];
|
|
1235
|
+
quat.setFromAxisAngle(upAxis, signal.heading);
|
|
1236
|
+
const isSignal = signal.kind === "signal";
|
|
1237
|
+
const postHeight = isSignal ? 5.3 : 2.5;
|
|
1238
|
+
const thickness = isSignal ? 0.2 : 0.13;
|
|
1239
|
+
matrix.compose(new THREE.Vector3(signal.x, y, signal.z), quat, new THREE.Vector3(thickness, postHeight, thickness));
|
|
1240
|
+
posts.setMatrixAt(i, matrix);
|
|
1241
|
+
if (!isSignal) {
|
|
1242
|
+
matrix.compose(new THREE.Vector3(signal.x, y + postHeight - 0.2, signal.z), quat, new THREE.Vector3(0.72, 0.72, 0.07));
|
|
1243
|
+
heads.setMatrixAt(i, matrix);
|
|
1244
|
+
tint.set(SIGN_FACE_COLOR);
|
|
1245
|
+
heads.setColorAt(i, tint);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const hx = signal.x + overRoad[0] * signal.reach;
|
|
1249
|
+
const hz = signal.z + overRoad[1] * signal.reach;
|
|
1250
|
+
armQuat.setFromAxisAngle(upAxis, signal.heading + Math.PI / 2);
|
|
1251
|
+
matrix.compose(new THREE.Vector3(signal.x + overRoad[0] * signal.reach * 0.5, y + postHeight - 0.15, signal.z + overRoad[1] * signal.reach * 0.5), armQuat, new THREE.Vector3(0.14, 0.14, signal.reach));
|
|
1252
|
+
arms.setMatrixAt(mast, matrix);
|
|
1253
|
+
matrix.compose(new THREE.Vector3(hx, y + postHeight - 0.72, hz), quat, new THREE.Vector3(0.4, 1.14, 0.3));
|
|
1254
|
+
heads.setMatrixAt(i, matrix);
|
|
1255
|
+
tint.set(SIGNAL_HOUSING_COLOR);
|
|
1256
|
+
heads.setColorAt(i, tint);
|
|
1257
|
+
for (let lamp = 0; lamp < 3; lamp += 1) {
|
|
1258
|
+
matrix.compose(new THREE.Vector3(hx + facing[0] * 0.17, y + postHeight - 0.36 - lamp * 0.36, hz + facing[1] * 0.17), quat, new THREE.Vector3(0.22, 0.22, 0.08));
|
|
1259
|
+
lamps.setMatrixAt(mast * 3 + lamp, matrix);
|
|
1260
|
+
tint.set(SIGNAL_LAMP_COLORS[lamp]);
|
|
1261
|
+
lamps.setColorAt(mast * 3 + lamp, tint);
|
|
1262
|
+
}
|
|
1263
|
+
mast += 1;
|
|
1264
|
+
});
|
|
1265
|
+
for (const mesh of [posts, heads, arms, lamps]) {
|
|
1266
|
+
if (mesh === null)
|
|
1267
|
+
continue;
|
|
1268
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
1269
|
+
if (mesh.instanceColor !== null)
|
|
1270
|
+
mesh.instanceColor.needsUpdate = true;
|
|
1271
|
+
mesh.castShadow = mesh !== lamps;
|
|
1272
|
+
mesh.frustumCulled = false;
|
|
1273
|
+
meshes.push(mesh);
|
|
1274
|
+
}
|
|
1275
|
+
disposables.push(postMaterial, headMaterial, lampMaterial);
|
|
1276
|
+
}
|
|
1039
1277
|
return { meshes, disposables };
|
|
1040
1278
|
}, [resolved, sample]);
|
|
1041
1279
|
useEffect(() => () => {
|
|
@@ -1044,9 +1282,196 @@ function OneCity({ object, context }) {
|
|
|
1044
1282
|
for (const disposable of furniture.disposables)
|
|
1045
1283
|
disposable.dispose();
|
|
1046
1284
|
}, [furniture]);
|
|
1285
|
+
// Race dressing: resurface the lap, stripe its kerbs, paint start/finish + grid, and stand a
|
|
1286
|
+
// barrier across every sealed side street — the visual proof the race is a section of this city.
|
|
1287
|
+
const race = useMemo(() => {
|
|
1288
|
+
if (resolved?.race === undefined)
|
|
1289
|
+
return null;
|
|
1290
|
+
const route = resolved.race;
|
|
1291
|
+
const points = route.centerline;
|
|
1292
|
+
const n = points.length;
|
|
1293
|
+
if (n < 8)
|
|
1294
|
+
return null;
|
|
1295
|
+
const decals = new MeshAccumulator(true);
|
|
1296
|
+
const barriers = new MeshAccumulator(true);
|
|
1297
|
+
const normals = points.map((_, i) => {
|
|
1298
|
+
const before = points[(i - 1 + n) % n];
|
|
1299
|
+
const after = points[(i + 1) % n];
|
|
1300
|
+
const dx = after[0] - before[0];
|
|
1301
|
+
const dz = after[1] - before[1];
|
|
1302
|
+
const len = Math.hypot(dx, dz) || 1;
|
|
1303
|
+
return [dz / len, -dx / len];
|
|
1304
|
+
});
|
|
1305
|
+
const half = points.map((_, i) => Math.max(1.5, (route.widths[i] ?? 9) * 0.5 - 0.3));
|
|
1306
|
+
const surfaceY0 = points.map((p, i) => route.heights?.[i] ?? sample(p[0], p[1]));
|
|
1307
|
+
const rim = (i, off, lift) => [
|
|
1308
|
+
points[i][0] + normals[i][0] * off,
|
|
1309
|
+
surfaceY0[i] + lift,
|
|
1310
|
+
points[i][1] + normals[i][1] * off,
|
|
1311
|
+
];
|
|
1312
|
+
for (let i = 0; i < n; i += 1) {
|
|
1313
|
+
const j = (i + 1) % n;
|
|
1314
|
+
const midX = (points[i][0] + points[j][0]) * 0.5;
|
|
1315
|
+
const midZ = (points[i][1] + points[j][1]) * 0.5;
|
|
1316
|
+
// Leave welded junction pavement visible — a continuous race ribbon over the mouth undoes the seam.
|
|
1317
|
+
let overJunction = false;
|
|
1318
|
+
for (const cross of resolved.intersections) {
|
|
1319
|
+
if (Math.hypot(cross.x - midX, cross.z - midZ) < cross.radius + 0.6) {
|
|
1320
|
+
overJunction = true;
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (overJunction)
|
|
1325
|
+
continue;
|
|
1326
|
+
decals.addQuad([rim(i, -half[i], RACE_SURFACE_LAYER), rim(j, -half[j], RACE_SURFACE_LAYER), rim(j, half[j], RACE_SURFACE_LAYER), rim(i, half[i], RACE_SURFACE_LAYER)], RACE_ASPHALT);
|
|
1327
|
+
for (const side of [-1, 1]) {
|
|
1328
|
+
decals.addQuad([
|
|
1329
|
+
rim(i, side * half[i], RACE_PAINT_LAYER),
|
|
1330
|
+
rim(j, side * half[j], RACE_PAINT_LAYER),
|
|
1331
|
+
rim(j, side * (half[j] - 0.34), RACE_PAINT_LAYER),
|
|
1332
|
+
rim(i, side * (half[i] - 0.34), RACE_PAINT_LAYER),
|
|
1333
|
+
], RACE_EDGE);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
for (const kerb of route.kerbs) {
|
|
1337
|
+
const run = kerb.points;
|
|
1338
|
+
if (run.length < 2)
|
|
1339
|
+
continue;
|
|
1340
|
+
const outward = run.map((_, i) => {
|
|
1341
|
+
const before = run[Math.max(0, i - 1)];
|
|
1342
|
+
const after = run[Math.min(run.length - 1, i + 1)];
|
|
1343
|
+
const dx = after[0] - before[0];
|
|
1344
|
+
const dz = after[1] - before[1];
|
|
1345
|
+
const len = Math.hypot(dx, dz) || 1;
|
|
1346
|
+
return [(dz / len) * kerb.side, (-dx / len) * kerb.side];
|
|
1347
|
+
});
|
|
1348
|
+
let travelled = 0;
|
|
1349
|
+
for (let i = 0; i + 1 < run.length; i += 1) {
|
|
1350
|
+
const a = run[i];
|
|
1351
|
+
const b = run[i + 1];
|
|
1352
|
+
const seg = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
1353
|
+
if (seg < 1e-6)
|
|
1354
|
+
continue;
|
|
1355
|
+
const midX = (a[0] + b[0]) * 0.5;
|
|
1356
|
+
const midZ = (a[1] + b[1]) * 0.5;
|
|
1357
|
+
let overJunction = false;
|
|
1358
|
+
for (const cross of resolved.intersections) {
|
|
1359
|
+
if (Math.hypot(cross.x - midX, cross.z - midZ) < cross.radius + 0.4) {
|
|
1360
|
+
overJunction = true;
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (overJunction) {
|
|
1365
|
+
travelled += seg;
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const oa = outward[i];
|
|
1369
|
+
const ob = outward[i + 1];
|
|
1370
|
+
let t = 0;
|
|
1371
|
+
while (t < 1 - 1e-6) {
|
|
1372
|
+
const here = travelled + t * seg;
|
|
1373
|
+
const band = Math.floor(here / kerb.stripe + 1e-6);
|
|
1374
|
+
const next = Math.min(1, Math.max(t + 1e-3, t + ((band + 1) * kerb.stripe - here) / seg));
|
|
1375
|
+
const along = (u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
|
|
1376
|
+
const away = (u) => [oa[0] + (ob[0] - oa[0]) * u, oa[1] + (ob[1] - oa[1]) * u];
|
|
1377
|
+
const [x0, z0] = along(t);
|
|
1378
|
+
const [x1, z1] = along(next);
|
|
1379
|
+
const [ox0, oz0] = away(t);
|
|
1380
|
+
const [ox1, oz1] = away(next);
|
|
1381
|
+
const drape = (x, z) => [x, sample(x, z) + RACE_PAINT_LAYER, z];
|
|
1382
|
+
decals.addQuad([drape(x0, z0), drape(x1, z1), drape(x1 + ox1 * kerb.width, z1 + oz1 * kerb.width), drape(x0 + ox0 * kerb.width, z0 + oz0 * kerb.width)], band % 2 === 0 ? KERB_RED : KERB_WHITE);
|
|
1383
|
+
t = next;
|
|
1384
|
+
}
|
|
1385
|
+
travelled += seg;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const [lineA, lineB] = route.start.line;
|
|
1389
|
+
const acrossX = lineB[0] - lineA[0];
|
|
1390
|
+
const acrossZ = lineB[1] - lineA[1];
|
|
1391
|
+
const travelX = Math.sin(route.start.heading);
|
|
1392
|
+
const travelZ = Math.cos(route.start.heading);
|
|
1393
|
+
for (let r = 0; r < 2; r += 1) {
|
|
1394
|
+
for (let c = 0; c < 10; c += 1) {
|
|
1395
|
+
const cell = (u, v) => {
|
|
1396
|
+
const x = lineA[0] + acrossX * u + travelX * v;
|
|
1397
|
+
const z = lineA[1] + acrossZ * u + travelZ * v;
|
|
1398
|
+
return [x, sample(x, z) + RACE_PAINT_LAYER, z];
|
|
1399
|
+
};
|
|
1400
|
+
const v0 = -2.5 + (r / 2) * 5;
|
|
1401
|
+
const v1 = -2.5 + ((r + 1) / 2) * 5;
|
|
1402
|
+
decals.addQuad([cell(c / 10, v0), cell((c + 1) / 10, v0), cell((c + 1) / 10, v1), cell(c / 10, v1)], (c + r) % 2 === 0 ? CHECK_DARK : CHECK_LIGHT);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
for (const slot of route.grid) {
|
|
1406
|
+
const fx = Math.sin(slot.heading);
|
|
1407
|
+
const fz = Math.cos(slot.heading);
|
|
1408
|
+
const ax = Math.cos(slot.heading);
|
|
1409
|
+
const az = -Math.sin(slot.heading);
|
|
1410
|
+
const corner = (u, v) => {
|
|
1411
|
+
const x = slot.x + ax * u + fx * v;
|
|
1412
|
+
const z = slot.z + az * u + fz * v;
|
|
1413
|
+
return [x, sample(x, z) + RACE_PAINT_LAYER, z];
|
|
1414
|
+
};
|
|
1415
|
+
for (const [u0, u1, v0, v1] of [
|
|
1416
|
+
[-1.3, -1.12, -2.6, 2.6],
|
|
1417
|
+
[1.12, 1.3, -2.6, 2.6],
|
|
1418
|
+
[-1.3, 1.3, 2.42, 2.6],
|
|
1419
|
+
]) {
|
|
1420
|
+
decals.addQuad([corner(u0, v0), corner(u1, v0), corner(u1, v1), corner(u0, v1)], RACE_EDGE);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
for (const seal of route.seals) {
|
|
1424
|
+
const fx = Math.sin(seal.heading);
|
|
1425
|
+
const fz = Math.cos(seal.heading);
|
|
1426
|
+
const ax = Math.cos(seal.heading);
|
|
1427
|
+
const az = -Math.sin(seal.heading);
|
|
1428
|
+
const span = seal.width + 3;
|
|
1429
|
+
const blocks = Math.max(3, Math.round(span / 1.4));
|
|
1430
|
+
const halfRun = span / blocks / 2;
|
|
1431
|
+
for (let s = 0; s < blocks; s += 1) {
|
|
1432
|
+
const u = -span / 2 + ((s + 0.5) / blocks) * span;
|
|
1433
|
+
const cx = seal.x + ax * u;
|
|
1434
|
+
const cz = seal.z + az * u;
|
|
1435
|
+
barriers.addSealBlock(cx, cz, sample(cx, cz), 1.05, halfRun, 0.28, [ax, az], [fx, fz], s % 2 === 0 ? KERB_RED : KERB_WHITE);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const geometries = [];
|
|
1439
|
+
const meshes = [];
|
|
1440
|
+
const surface = decals.build();
|
|
1441
|
+
if (surface !== null) {
|
|
1442
|
+
geometries.push(surface);
|
|
1443
|
+
const mesh = new THREE.Mesh(surface, new THREE.MeshBasicMaterial({
|
|
1444
|
+
vertexColors: true,
|
|
1445
|
+
side: THREE.DoubleSide,
|
|
1446
|
+
polygonOffset: true,
|
|
1447
|
+
polygonOffsetFactor: -4,
|
|
1448
|
+
polygonOffsetUnits: -4,
|
|
1449
|
+
}));
|
|
1450
|
+
mesh.renderOrder = 4;
|
|
1451
|
+
mesh.frustumCulled = false;
|
|
1452
|
+
meshes.push(mesh);
|
|
1453
|
+
}
|
|
1454
|
+
const sealGeom = barriers.build();
|
|
1455
|
+
if (sealGeom !== null) {
|
|
1456
|
+
geometries.push(sealGeom);
|
|
1457
|
+
const mesh = new THREE.Mesh(sealGeom, new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.72, metalness: 0.04, side: THREE.DoubleSide }));
|
|
1458
|
+
mesh.castShadow = true;
|
|
1459
|
+
mesh.frustumCulled = false;
|
|
1460
|
+
meshes.push(mesh);
|
|
1461
|
+
}
|
|
1462
|
+
return { meshes, geometries, materials: meshes.map((mesh) => mesh.material) };
|
|
1463
|
+
}, [resolved, sample]);
|
|
1464
|
+
useEffect(() => () => {
|
|
1465
|
+
if (race === null)
|
|
1466
|
+
return;
|
|
1467
|
+
for (const geometry of race.geometries)
|
|
1468
|
+
geometry.dispose();
|
|
1469
|
+
for (const material of race.materials)
|
|
1470
|
+
material.dispose();
|
|
1471
|
+
}, [race]);
|
|
1047
1472
|
if (resolved === null)
|
|
1048
1473
|
return null;
|
|
1049
|
-
return (_jsxs("group", { children: [ground?.asphalt != null ? (_jsx("mesh", { geometry: ground.asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravel != null ? (_jsx("mesh", { geometry: ground.gravel, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.sidewalks != null ? (_jsx("mesh", { geometry: ground.sidewalks, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: SIDEWALK_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.patches != null ? (_jsx("mesh", { geometry: ground.patches, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.curbs != null ? (_jsx("mesh", { geometry: ground.curbs, receiveShadow: true, castShadow: true, children: _jsx("meshStandardMaterial", { color: CURB_COLOR, roughness: 0.9, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.markings != null ? (_jsx("mesh", { geometry: ground.markings, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: MARKING_COLOR, roughness: 0.8, metalness: 0, side: THREE.DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, ground?.medians != null ? (_jsx("mesh", { geometry: ground.medians, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: MEDIAN_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.pavementDrives != null ? (_jsx("mesh", { geometry: ground.pavementDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: PAVEMENT_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravelDrives != null ? (_jsx("mesh", { geometry: ground.gravelDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.parks != null ? (_jsx("mesh", { geometry: ground.parks, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.crops != null ? (_jsx("mesh", { geometry: ground.crops, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, bridges?.deck != null ? (_jsx("mesh", { geometry: bridges.deck, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: CONCRETE_COLOR, roughness: 0.85, metalness: 0.05, side: THREE.DoubleSide }) })) : null, bridges !== null && bridges.concrete.length > 0 ? (_jsx(InstancedBoxes, { matrices: bridges.concrete, color: CONCRETE_COLOR })) : null, bridges !== null && bridges.steel.length > 0 ? _jsx(InstancedBoxes, { matrices: bridges.steel, color: STEEL_COLOR }) : null, tunnels?.roof != null ? (_jsx("mesh", { geometry: tunnels.roof, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: TUNNEL_COLOR, roughness: 0.95, metalness: 0, side: THREE.DoubleSide }) })) : null, tunnels !== null && tunnels.stone.length > 0 ? _jsx(InstancedBoxes, { matrices: tunnels.stone, color: TUNNEL_COLOR }) : null, massing !== null ? massing.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `massing:${i}`)) : null, furniture !== null ? furniture.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `furniture:${i}`)) : null, _jsx(DetailBuildings, { buildings: detail.buildings, palette: palette, kitName: resolved?.rules.buildingKit ?? "" })] }));
|
|
1474
|
+
return (_jsxs("group", { children: [ground?.urban != null ? (_jsx("mesh", { geometry: ground.urban, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.land != null ? (_jsx("mesh", { geometry: ground.land, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.asphalt != null ? (_jsx("mesh", { geometry: ground.asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, vertexColors: true, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravel != null ? (_jsx("mesh", { geometry: ground.gravel, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.sidewalks != null ? (_jsx("mesh", { geometry: ground.sidewalks, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: SIDEWALK_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.patches != null ? (_jsx("mesh", { geometry: ground.patches, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, vertexColors: true, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.curbs != null ? (_jsx("mesh", { geometry: ground.curbs, receiveShadow: true, castShadow: true, children: _jsx("meshStandardMaterial", { color: CURB_COLOR, roughness: 0.9, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.markings != null ? (_jsx("mesh", { geometry: ground.markings, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: MARKING_COLOR, vertexColors: true, roughness: 0.8, metalness: 0, side: THREE.DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, ground?.medians != null ? (_jsx("mesh", { geometry: ground.medians, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: MEDIAN_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.pavementDrives != null ? (_jsx("mesh", { geometry: ground.pavementDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: PAVEMENT_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravelDrives != null ? (_jsx("mesh", { geometry: ground.gravelDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.parks != null ? (_jsx("mesh", { geometry: ground.parks, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.crops != null ? (_jsx("mesh", { geometry: ground.crops, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, bridges?.deck != null ? (_jsx("mesh", { geometry: bridges.deck, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: CONCRETE_COLOR, roughness: 0.85, metalness: 0.05, side: THREE.DoubleSide }) })) : null, bridges !== null && bridges.concrete.length > 0 ? (_jsx(InstancedBoxes, { matrices: bridges.concrete, color: CONCRETE_COLOR })) : null, bridges !== null && bridges.steel.length > 0 ? _jsx(InstancedBoxes, { matrices: bridges.steel, color: STEEL_COLOR }) : null, tunnels?.roof != null ? (_jsx("mesh", { geometry: tunnels.roof, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: TUNNEL_COLOR, roughness: 0.95, metalness: 0, side: THREE.DoubleSide }) })) : null, tunnels !== null && tunnels.stone.length > 0 ? _jsx(InstancedBoxes, { matrices: tunnels.stone, color: TUNNEL_COLOR }) : null, massing !== null ? massing.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `massing:${i}`)) : null, furniture !== null ? furniture.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `furniture:${i}`)) : null, race !== null ? race.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `race:${i}`)) : null, _jsx(DetailBuildings, { buildings: detail.buildings, palette: palette, kitName: resolved?.rules.buildingKit ?? "" })] }));
|
|
1050
1475
|
}
|
|
1051
1476
|
/** One instanced unit-box mesh from precomposed matrices (bridge parts). */
|
|
1052
1477
|
function InstancedBoxes({ matrices, color }) {
|