@adatechnology/game-urban-three 0.0.0-stage → 0.2.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/dist/index.js ADDED
@@ -0,0 +1,953 @@
1
+ // src/draw.ts
2
+ import { shapeGeometries as shapeGeometries2 } from "@adatechnology/game-shapes-three";
3
+
4
+ // src/rounded-box.ts
5
+ import { shapeGeometries } from "@adatechnology/game-shapes-three";
6
+ function createRoundedBox(THREE, width, height, depth, radius) {
7
+ const T = shapeGeometries(THREE);
8
+ if (radius === 0) return new T.BoxGeometry(width, height, depth);
9
+ const geometry = new T.BoxGeometry(2, 2, 2, 4, 4, 4);
10
+ const position = geometry.attributes.position;
11
+ const normal = geometry.attributes.normal;
12
+ const half = [width / 2, height / 2, depth / 2];
13
+ const p = [0, 0, 0];
14
+ const q = [0, 0, 0];
15
+ const map = (value, halfSize) => {
16
+ const a = Math.abs(value);
17
+ return Math.sign(value) * (a <= 0.5 ? a * 2 * (halfSize - radius) : halfSize - radius + (a - 0.5) * 2 * radius);
18
+ };
19
+ for (let index = 0; index < position.count; index++) {
20
+ const source = [position.getX(index), position.getY(index), position.getZ(index)];
21
+ let length = 0;
22
+ for (let axis = 0; axis < 3; axis++) {
23
+ p[axis] = map(source[axis], half[axis]);
24
+ const clamped = Math.max(-(half[axis] - radius), Math.min(half[axis] - radius, p[axis]));
25
+ q[axis] = p[axis] - clamped;
26
+ p[axis] = clamped;
27
+ length += q[axis] ** 2;
28
+ }
29
+ length = Math.sqrt(length);
30
+ if (length < 1e-6) {
31
+ q[0] = normal.getX(index);
32
+ q[1] = normal.getY(index);
33
+ q[2] = normal.getZ(index);
34
+ } else for (let axis = 0; axis < 3; axis++) q[axis] = q[axis] / length;
35
+ position.setXYZ(index, p[0] + q[0] * radius, p[1] + q[1] * radius, p[2] + q[2] * radius);
36
+ normal.setXYZ(index, q[0], q[1], q[2]);
37
+ }
38
+ return geometry;
39
+ }
40
+ function createPrism(THREE, width, height, depth) {
41
+ const shape = new THREE.Shape();
42
+ shape.moveTo(-width / 2, 0);
43
+ shape.lineTo(width / 2, 0);
44
+ shape.lineTo(0, height);
45
+ shape.lineTo(-width / 2, 0);
46
+ const geometry = new (shapeGeometries(THREE)).ExtrudeGeometry(shape, { depth, bevelEnabled: false });
47
+ geometry.translate(0, 0, -depth / 2);
48
+ return geometry;
49
+ }
50
+
51
+ // src/text.ts
52
+ var FIT = 0.92;
53
+ function createTextTexture(THREE, document, lines, o) {
54
+ const canvas = document.createElement("canvas");
55
+ canvas.width = o.width;
56
+ canvas.height = o.height;
57
+ const g = canvas.getContext("2d");
58
+ g.fillStyle = o.background;
59
+ g.fillRect(0, 0, o.width, o.height);
60
+ g.fillStyle = o.foreground;
61
+ g.font = o.font;
62
+ g.textAlign = "center";
63
+ g.textBaseline = "middle";
64
+ const base = g.font;
65
+ lines.forEach((text, index) => {
66
+ g.font = base;
67
+ const measured = g.measureText(text).width;
68
+ if (measured > o.width * FIT) g.font = base.replace(/(\d+(?:\.\d+)?)px/, (_match, size) => (Number(size) * o.width * FIT / measured).toFixed(1) + "px");
69
+ g.fillText(text, o.width / 2, o.height / 2 + (index - (lines.length - 1) / 2) * (o.height / lines.length));
70
+ });
71
+ const texture = new THREE.CanvasTexture(canvas);
72
+ texture.anisotropy = 4;
73
+ return texture;
74
+ }
75
+
76
+ // src/draw.ts
77
+ var DEFAULT_ROUGHNESS = 0.9;
78
+ var DEFAULT_METALNESS = 0;
79
+ var ITEM_ROUNDNESS = 0.12;
80
+ var LIT_WINDOW = 16757854;
81
+ var DARK_WINDOW = { color: 856599, roughness: 0.3, metalness: 0.5 };
82
+ var SIGN_FONT = '700 56px "Arial Narrow", Arial, sans-serif';
83
+ var SIGN_CANVAS = { width: 512, height: 128 };
84
+ function createUrbanThree(THREE, options = {}) {
85
+ const T = shapeGeometries2(THREE);
86
+ const geometries = /* @__PURE__ */ new Map();
87
+ const materials = /* @__PURE__ */ new Map();
88
+ const disposables = [];
89
+ const standard = (color, style, flat = false) => {
90
+ const roughness = style?.roughness ?? DEFAULT_ROUGHNESS;
91
+ const metalness = style?.metalness ?? DEFAULT_METALNESS;
92
+ const key = `s|${color}|${roughness}|${metalness}|${flat ? 1 : 0}`;
93
+ let material = materials.get(key);
94
+ if (!material) {
95
+ material = new THREE.MeshStandardMaterial({ color, roughness, metalness, flatShading: flat });
96
+ materials.set(key, material);
97
+ }
98
+ return material;
99
+ };
100
+ const glowing = (color) => {
101
+ const key = `b|${color}`;
102
+ let material = materials.get(key);
103
+ if (!material) {
104
+ material = new THREE.MeshBasicMaterial({ color });
105
+ materials.set(key, material);
106
+ }
107
+ return material;
108
+ };
109
+ const roundedBox = (width, height, depth, radius) => {
110
+ const key = [width, height, depth, radius].map((value) => value.toFixed(3)).join("|");
111
+ let geometry = geometries.get(key);
112
+ if (!geometry) {
113
+ geometry = createRoundedBox(THREE, width, height, depth, radius);
114
+ geometries.set(key, geometry);
115
+ }
116
+ return geometry;
117
+ };
118
+ const shadows = (mesh, style, cast, receive) => {
119
+ mesh.castShadow = style?.castShadow ?? cast;
120
+ mesh.receiveShadow = style?.receiveShadow ?? receive;
121
+ };
122
+ const texture = (lines, width, height, background, foreground, font) => {
123
+ if (!options.document) throw new Error("createUrbanThree needs a document (option) to draw signs and labels");
124
+ const map = createTextTexture(THREE, options.document, lines, { width, height, background, foreground, font });
125
+ disposables.push(map);
126
+ return map;
127
+ };
128
+ const textPlane = (part, map) => {
129
+ const mesh = new THREE.Mesh(new T.PlaneGeometry(part.width, part.height), new THREE.MeshBasicMaterial({ map, transparent: true }));
130
+ mesh.userData.own = true;
131
+ mesh.position.set(part.x, part.y, part.z);
132
+ if (part.ry) mesh.rotation.y = part.ry;
133
+ disposables.push(mesh.geometry, mesh.material);
134
+ return mesh;
135
+ };
136
+ const drawPart = (part, animators, lights) => {
137
+ switch (part.shape) {
138
+ case "group": {
139
+ const group = new THREE.Group();
140
+ group.position.set(part.x, part.y, part.z);
141
+ if (part.ry) group.rotation.y = part.ry;
142
+ return [group];
143
+ }
144
+ case "box": {
145
+ const mesh = new THREE.Mesh(roundedBox(part.width, part.height, part.depth, part.radius), part.emissive ? glowing(part.color) : standard(part.color, part.style));
146
+ mesh.position.set(part.x, part.y, part.z);
147
+ if (part.ry) mesh.rotation.y = part.ry;
148
+ if (part.rz) mesh.rotation.z = part.rz;
149
+ shadows(mesh, part.style, true, true);
150
+ return [mesh];
151
+ }
152
+ case "prism": {
153
+ const geometry = createPrism(THREE, part.width, part.height, part.depth);
154
+ disposables.push(geometry);
155
+ const mesh = new THREE.Mesh(geometry, standard(part.color, part.style, true));
156
+ mesh.position.set(part.x, part.y, part.z);
157
+ if (part.ry) mesh.rotation.y = part.ry;
158
+ shadows(mesh, part.style, true, true);
159
+ return [mesh];
160
+ }
161
+ case "cylinder": {
162
+ const geometry = new T.CylinderGeometry(part.radiusTop, part.radiusBottom, part.height, part.segments);
163
+ disposables.push(geometry);
164
+ const mesh = new THREE.Mesh(geometry, standard(part.color, part.style));
165
+ mesh.position.set(part.x, part.y, part.z);
166
+ shadows(mesh, part.style, true, true);
167
+ return [mesh];
168
+ }
169
+ case "cone": {
170
+ const geometry = new T.ConeGeometry(part.radius, part.height, part.segments);
171
+ disposables.push(geometry);
172
+ const mesh = new THREE.Mesh(geometry, standard(part.color, part.style, true));
173
+ mesh.position.set(part.x, part.y, part.z);
174
+ shadows(mesh, part.style, true, true);
175
+ return [mesh];
176
+ }
177
+ case "crown": {
178
+ const geometry = new THREE.DodecahedronGeometry(part.radius, 0);
179
+ disposables.push(geometry);
180
+ const mesh = new THREE.Mesh(geometry, standard(part.color, part.style));
181
+ mesh.position.set(part.x, part.y, part.z);
182
+ shadows(mesh, part.style, true, false);
183
+ return [mesh];
184
+ }
185
+ case "terrain": {
186
+ const { grid } = part;
187
+ const geometry = new T.PlaneGeometry(grid.size, grid.size, grid.segments, grid.segments);
188
+ geometry.rotateX(-Math.PI / 2);
189
+ const position = geometry.attributes.position;
190
+ const colors = new Float32Array(position.count * 3);
191
+ const color = new THREE.Color();
192
+ for (let index = 0; index < position.count; index++) {
193
+ position.setXYZ(index, grid.x[index], grid.heights[index], grid.z[index]);
194
+ color.setHex(grid.colors[index]);
195
+ colors[index * 3] = color.r;
196
+ colors[index * 3 + 1] = color.g;
197
+ colors[index * 3 + 2] = color.b;
198
+ }
199
+ geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
200
+ geometry.computeVertexNormals();
201
+ disposables.push(geometry);
202
+ const material = new THREE.MeshStandardMaterial({ vertexColors: true, flatShading: true, roughness: 1 });
203
+ disposables.push(material);
204
+ const mesh = new THREE.Mesh(geometry, material);
205
+ mesh.userData.own = true;
206
+ shadows(mesh, part.style, true, true);
207
+ return [mesh];
208
+ }
209
+ case "vault": {
210
+ const geometry = new T.CylinderGeometry(part.radius, part.radius, part.length, part.segments, 1, false, 0, part.arc);
211
+ disposables.push(geometry);
212
+ const mesh = new THREE.Mesh(geometry, standard(part.color, part.style));
213
+ mesh.rotation.set(part.rotation.x, part.rotation.y, part.rotation.z);
214
+ mesh.position.set(part.x, part.y, part.z);
215
+ mesh.scale.set(part.scale.x, part.scale.y, part.scale.z);
216
+ shadows(mesh, part.style, true, false);
217
+ return [mesh];
218
+ }
219
+ case "plane": {
220
+ const geometry = new T.PlaneGeometry(part.width, part.depth).rotateX(-Math.PI / 2);
221
+ disposables.push(geometry);
222
+ const mesh = new THREE.Mesh(geometry, standard(part.color, part.style));
223
+ mesh.position.set(part.x, part.y, part.z);
224
+ shadows(mesh, part.style, false, true);
225
+ return [mesh];
226
+ }
227
+ case "line": {
228
+ const geometry = new THREE.BufferGeometry().setFromPoints(part.points.map(([x, y, z]) => new THREE.Vector3(x, y, z)));
229
+ const mesh = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: part.color }));
230
+ mesh.userData.own = true;
231
+ disposables.push(geometry, mesh.material);
232
+ return [mesh];
233
+ }
234
+ case "sign":
235
+ return [textPlane(part, texture([part.text], SIGN_CANVAS.width, SIGN_CANVAS.height, part.background, part.foreground, SIGN_FONT))];
236
+ case "label":
237
+ return [textPlane(part, texture(part.lines, part.canvas.width, part.canvas.height, part.background, part.foreground, part.font))];
238
+ case "placement": {
239
+ const holder = new THREE.Group();
240
+ holder.position.set(part.x, part.y, part.z);
241
+ if (part.ry) holder.rotation.y = part.ry;
242
+ if (part.scale !== 1) holder.scale.setScalar(part.scale);
243
+ const object = options.resolvers?.[part.kind]?.(part);
244
+ if (object) holder.add(object);
245
+ holder.userData.placement = { kind: part.kind, id: part.id };
246
+ return [holder];
247
+ }
248
+ case "light": {
249
+ if (part.kind === "point") {
250
+ const light2 = new THREE.PointLight(part.color, part.intensity, part.distance, part.decay);
251
+ light2.position.set(part.x, part.y, part.z);
252
+ lights.push(light2);
253
+ if (part.flicker) {
254
+ animators.push((t) => {
255
+ light2.intensity = part.intensity * (Math.sin(t * 23 + part.x) > 0.3 || Math.sin(t * 3.1) > 0.85 ? 0.15 : 1);
256
+ });
257
+ }
258
+ return [light2];
259
+ }
260
+ const light = new THREE.SpotLight(part.color, part.intensity, part.distance, part.angle, part.penumbra, part.decay);
261
+ light.position.set(part.x, part.y, part.z);
262
+ const target = new THREE.Object3D();
263
+ target.position.set(part.target.x, part.target.y, part.target.z);
264
+ light.target = target;
265
+ lights.push(light);
266
+ const { sweep } = part;
267
+ if (sweep) {
268
+ animators.push((t) => {
269
+ target.position.set(part.x + Math.sin(t * sweep.speed + part.x) * sweep.radius, 0, part.z + Math.cos(t * sweep.speed + part.x) * sweep.radius);
270
+ });
271
+ }
272
+ return [light, target];
273
+ }
274
+ }
275
+ };
276
+ const instancedItems = (list) => {
277
+ const mesh = new THREE.InstancedMesh(roundedBox(1, 1, 1, ITEM_ROUNDNESS), standard(16777215), list.length);
278
+ const matrix = new THREE.Matrix4();
279
+ const quaternion = new THREE.Quaternion();
280
+ const euler = new THREE.Euler();
281
+ const position = new THREE.Vector3();
282
+ const scale = new THREE.Vector3();
283
+ const color = new THREE.Color();
284
+ list.forEach((entry, index) => {
285
+ euler.set(entry.rx, entry.ry, entry.rz);
286
+ quaternion.setFromEuler(euler);
287
+ matrix.compose(position.set(entry.x, entry.y + entry.height / 2, entry.z), quaternion, scale.set(entry.width, entry.height, entry.depth));
288
+ mesh.setMatrixAt(index, matrix);
289
+ mesh.setColorAt(index, color.set(entry.color));
290
+ });
291
+ mesh.castShadow = true;
292
+ mesh.receiveShadow = true;
293
+ return mesh;
294
+ };
295
+ const instancedWindows = (list, material) => {
296
+ const geometry = new T.PlaneGeometry(1, 1);
297
+ disposables.push(geometry);
298
+ const mesh = new THREE.InstancedMesh(geometry, material, list.length);
299
+ const matrix = new THREE.Matrix4();
300
+ const quaternion = new THREE.Quaternion();
301
+ const axis = new THREE.Vector3(0, 1, 0);
302
+ const scale = new THREE.Vector3();
303
+ list.forEach((entry, index) => {
304
+ quaternion.setFromAxisAngle(axis, entry.ry);
305
+ matrix.compose(new THREE.Vector3(entry.x, entry.y, entry.z), quaternion, scale.set(entry.width, entry.height, 1));
306
+ mesh.setMatrixAt(index, matrix);
307
+ });
308
+ return mesh;
309
+ };
310
+ function draw(scene, drawOptions = {}) {
311
+ const root = drawOptions.root ?? new THREE.Group();
312
+ const animators = [];
313
+ const lights = [];
314
+ const objects = [];
315
+ for (const part of scene.parts) {
316
+ const made = drawPart(part, animators, lights);
317
+ objects.push(made);
318
+ const parent = part.parent < 0 ? root : objects[part.parent][0];
319
+ parent.add(...made);
320
+ }
321
+ if (scene.items.length) root.add(instancedItems(scene.items));
322
+ if (scene.windows.lit.length) root.add(instancedWindows(scene.windows.lit, glowing(LIT_WINDOW)));
323
+ if (scene.windows.dark.length) root.add(instancedWindows(scene.windows.dark, standard(DARK_WINDOW.color, DARK_WINDOW)));
324
+ return {
325
+ root,
326
+ lights,
327
+ animate: (t) => animators.forEach((step) => step(t)),
328
+ dispose: () => {
329
+ root.parent?.remove(root);
330
+ for (const item of disposables.splice(0)) item.dispose?.();
331
+ for (const geometry of geometries.values()) geometry.dispose();
332
+ for (const material of materials.values()) material.dispose();
333
+ geometries.clear();
334
+ materials.clear();
335
+ }
336
+ };
337
+ }
338
+ return { THREE, draw };
339
+ }
340
+
341
+ // src/resolvers.ts
342
+ import { generate } from "@adatechnology/game-vegetation";
343
+ import { createPlantObject, createVegetationMaterials } from "@adatechnology/game-vegetation/three";
344
+ import { toSpec } from "@adatechnology/game-vegetation-species";
345
+ var cssColor = (value) => `#${value.toString(16).padStart(6, "0")}`;
346
+ function createVehicleResolver(vehicles, options = {}) {
347
+ const quality = options.quality ?? "lite";
348
+ const built = /* @__PURE__ */ new Map();
349
+ const resolve = (part) => {
350
+ const key = `${part.id}|${part.color ?? ""}|${part.seed}`;
351
+ if (!built.has(key)) {
352
+ try {
353
+ built.set(key, vehicles.build(part.id, { quality, ...part.color === void 0 ? { seed: part.seed } : { color: cssColor(part.color) } }));
354
+ } catch {
355
+ built.set(key, null);
356
+ }
357
+ }
358
+ return built.get(key)?.group.clone() ?? null;
359
+ };
360
+ resolve.dispose = () => {
361
+ for (const entry of built.values()) entry?.dispose();
362
+ built.clear();
363
+ };
364
+ return resolve;
365
+ }
366
+ function createPlantResolver(THREE, options = {}) {
367
+ const materials = options.materials ?? createVegetationMaterials(THREE);
368
+ const made = /* @__PURE__ */ new Map();
369
+ const resolve = (part) => {
370
+ const key = `${part.id}|${part.seed}`;
371
+ if (!made.has(key)) {
372
+ try {
373
+ const mesh = generate(toSpec(part.id), { seed: part.seed, lod: options.lod ?? 1, condition: options.condition ?? "healthy", season: options.season ?? "spring", species: part.id });
374
+ made.set(key, createPlantObject(THREE, mesh, materials));
375
+ } catch {
376
+ made.set(key, null);
377
+ }
378
+ }
379
+ return made.get(key)?.clone() ?? null;
380
+ };
381
+ resolve.dispose = () => {
382
+ for (const object of made.values()) {
383
+ object?.traverse((o) => o.geometry?.dispose?.());
384
+ }
385
+ made.clear();
386
+ };
387
+ return resolve;
388
+ }
389
+
390
+ // src/test-area/module.ts
391
+ import { createPlantProvider as createPlantProvider2, createUrban as createUrban2, createVehicleProvider as createVehicleProvider2, SAMPLE_CELLS } from "@adatechnology/game-urban";
392
+ import { shapeGeometries as shapeGeometries3 } from "@adatechnology/game-shapes-three";
393
+
394
+ // src/test-area/scenarios.ts
395
+ import { createPlantProvider, createUrban, createVehicleProvider, PARKED_VEHICLE_IDS } from "@adatechnology/game-urban";
396
+
397
+ // src/test-area/text.ts
398
+ var ptBR = {
399
+ group: "Cidade",
400
+ name: "\xC1rea urbana",
401
+ items: {
402
+ pole_led_double: "Poste LED de bra\xE7o duplo",
403
+ pole_concrete_arm: "Poste de concreto com bra\xE7o",
404
+ pole_cast_iron: "Lampi\xE3o de ferro fundido",
405
+ pole_wood_crossarm: "Poste de madeira com cruzeta",
406
+ pole_flood_tower: "Torre de refletores",
407
+ street_grid: "Grade de ruas com quarteir\xE3o",
408
+ terrain_patch: "Trecho de terreno",
409
+ downtown: "Bairro: centro",
410
+ suburb: "Bairro: sub\xFArbio",
411
+ working_class: "Bairro: popular",
412
+ historic_street: "Bairro: rua hist\xF3rica",
413
+ industrial_port: "Bairro: industrial e porto",
414
+ railway_village: "Bairro: vila ferrovi\xE1ria",
415
+ mountain_resort: "Bairro: serra",
416
+ stilt_village: "Bairro: palafitas",
417
+ military: "Bairro: militar",
418
+ rural: "Bairro: rural"
419
+ },
420
+ controls: { pattern: "Bairro da bancada", seed: "Semente", lamps: "Lampi\xF5es", budget: "Luzes reais da cena", providers: "Ve\xEDculos e plantas" },
421
+ hints: {
422
+ pattern: "Padr\xE3o que as a\xE7\xF5es desenham na frente do jogador",
423
+ seed: "A mesma semente desenha a mesma rua",
424
+ lamps: "Quantos lampi\xF5es ficam acesos; o resto queima",
425
+ budget: "Cada lampi\xE3o aceso gasta uma; passando disso fica s\xF3 a lente brilhando",
426
+ providers: "Pede ao game-vehicles-fleet e ao game-vegetation-species os carros e as \xE1rvores"
427
+ },
428
+ options: {
429
+ lamps: { pattern: "Os do padr\xE3o", all: "Todos acesos", half: "Metade", none: "Nenhum" },
430
+ providers: { none: "Nenhum (padr\xE3o)", plants: "Plantas das esp\xE9cies", vehicles: "Ve\xEDculos da frota", both: "Os dois" }
431
+ },
432
+ actions: { compose: "Desenhar o bairro", clear: "Tirar o bairro" },
433
+ layers: { light_radius: "Alcance das luzes", bounds: "Caixas das amostras" },
434
+ metrics: { samples: "Amostras na sala", draw_calls: "Chamadas de desenho", triangles: "Tri\xE2ngulos", real_lights: "Luzes reais", instances: "Inst\xE2ncias", bench_objects: "Objetos da bancada", light_budget_left: "Or\xE7amento de luzes que sobra" },
435
+ log: { composed: "Bairro {0} desenhado com a semente {1}: {2} objetos, {3} luzes reais", cleared: "Bairro tirado", no_vehicles: "A sala n\xE3o recebeu um construtor de ve\xEDculos: os carros ficam de fora" },
436
+ scenarios: {
437
+ urban_pole_heights: "Os cinco postes t\xEAm a altura da tabela",
438
+ urban_light_budget: "O or\xE7amento de luzes: as primeiras acesas ganham luz real e o contador passa de zero",
439
+ urban_road_dashes: "A rua tem uma faixa a cada 5 m",
440
+ urban_block_heights: "Os pr\xE9dios altos s\xE3o mais raros",
441
+ urban_neighbourhood_seed: "A mesma semente desenha o mesmo bairro",
442
+ urban_lamps_burnt: "Um bairro com a fra\xE7\xE3o zero de lampi\xF5es acesos n\xE3o tem nenhum aceso",
443
+ urban_terrain: "O terreno tem a altura do v\xE9rtice, o leito do rio e o n\xEDvel do lago",
444
+ urban_providers: "Os provedores pedem ve\xEDculos da frota e esp\xE9cies da regi\xE3o onde o bairro estaciona e planta"
445
+ },
446
+ checks: {
447
+ pole_top: "o poste {0} chega a {1} m",
448
+ budget_real: "{0} l\xE2mpadas acesas, {1} luzes reais (o or\xE7amento \xE9 {2})",
449
+ budget_left: "o contador chegou a {0}",
450
+ dashes: "uma rua de 60 m tem {0} faixas, a primeira em x = {1}",
451
+ heights_in_range: "os pr\xE9dios ficam entre {0} e {1} m",
452
+ heights_median: "a mediana dos pr\xE9dios \xE9 {0} m, abaixo do meio {1} m",
453
+ same_seed: "a mesma semente repete as partes: {0}",
454
+ other_seed: "outra semente muda o bairro: {0}",
455
+ lamps_none: "com a fra\xE7\xE3o 0 nenhum dos {0} postes est\xE1 aceso",
456
+ lamps_all: "o centro mant\xE9m os {0} postes acesos",
457
+ vertex_height: "a altura do v\xE9rtice central \xE9 {0} m e a da malha {1} m",
458
+ river_bed: "o leito do rio est\xE1 {0} m abaixo do ch\xE3o ao lado",
459
+ lake_level: "o centro do lago est\xE1 a {0} m",
460
+ vehicles_placed: "{0} ve\xEDculos pedidos, {1} vagas de ve\xEDculo na cena",
461
+ vehicle_ids: "os ve\xEDculos s\xE3o da frota: {0}",
462
+ plants_placed: "{0} plantas pedidas, {1} vagas de planta na cena"
463
+ }
464
+ };
465
+ var en = {
466
+ group: "City",
467
+ name: "Urban area",
468
+ items: {
469
+ pole_led_double: "Double-arm LED pole",
470
+ pole_concrete_arm: "Concrete pole with an arm",
471
+ pole_cast_iron: "Cast-iron lantern",
472
+ pole_wood_crossarm: "Wooden pole with a crossarm",
473
+ pole_flood_tower: "Floodlight tower",
474
+ street_grid: "Street grid with a block",
475
+ terrain_patch: "Terrain patch",
476
+ downtown: "Neighbourhood: downtown",
477
+ suburb: "Neighbourhood: suburb",
478
+ working_class: "Neighbourhood: working class",
479
+ historic_street: "Neighbourhood: historic street",
480
+ industrial_port: "Neighbourhood: industrial and port",
481
+ railway_village: "Neighbourhood: railway village",
482
+ mountain_resort: "Neighbourhood: mountain resort",
483
+ stilt_village: "Neighbourhood: stilt village",
484
+ military: "Neighbourhood: military",
485
+ rural: "Neighbourhood: rural"
486
+ },
487
+ controls: { pattern: "Bench neighbourhood", seed: "Seed", lamps: "Street lamps", budget: "Real lights of the scene", providers: "Vehicles and plants" },
488
+ hints: {
489
+ pattern: "The pattern the actions draw in front of the player",
490
+ seed: "The same seed draws the same street",
491
+ lamps: "How many lamps are on; the rest are burnt out",
492
+ budget: "Every lamp that is on spends one; past that it keeps only its glowing lens",
493
+ providers: "Asks game-vehicles-fleet and game-vegetation-species for the cars and trees"
494
+ },
495
+ options: {
496
+ lamps: { pattern: "The pattern's own", all: "All on", half: "Half", none: "None" },
497
+ providers: { none: "None (default)", plants: "Plants of the species pack", vehicles: "Vehicles of the fleet", both: "Both" }
498
+ },
499
+ actions: { compose: "Draw the neighbourhood", clear: "Take the neighbourhood away" },
500
+ layers: { light_radius: "Light reach", bounds: "Sample boxes" },
501
+ metrics: { samples: "Samples in the room", draw_calls: "Draw calls", triangles: "Triangles", real_lights: "Real lights", instances: "Instances", bench_objects: "Bench objects", light_budget_left: "Light budget left" },
502
+ log: { composed: "Neighbourhood {0} drawn with seed {1}: {2} objects, {3} real lights", cleared: "Neighbourhood taken away", no_vehicles: "The room got no vehicle builder: the cars are left out" },
503
+ scenarios: {
504
+ urban_pole_heights: "The five poles stand at the height of the table",
505
+ urban_light_budget: "The light budget: the first lamps get real lights and the counter goes below zero",
506
+ urban_road_dashes: "A road has a dash every 5 m",
507
+ urban_block_heights: "Tall buildings are rarer",
508
+ urban_neighbourhood_seed: "The same seed draws the same neighbourhood",
509
+ urban_lamps_burnt: "A neighbourhood with no lamps lit has none on",
510
+ urban_terrain: "The terrain has the vertex height, the river bed and the lake level",
511
+ urban_providers: "The providers ask for fleet vehicles and species of the region where the neighbourhood parks and plants"
512
+ },
513
+ checks: {
514
+ pole_top: "pole {0} reaches {1} m",
515
+ budget_real: "{0} lamps on, {1} real lights (the budget is {2})",
516
+ budget_left: "the counter reached {0}",
517
+ dashes: "a 60 m road has {0} dashes, the first at x = {1}",
518
+ heights_in_range: "the buildings stand between {0} and {1} m",
519
+ heights_median: "the median building is {0} m, under the middle {1} m",
520
+ same_seed: "the same seed repeats the parts: {0}",
521
+ other_seed: "another seed changes the neighbourhood: {0}",
522
+ lamps_none: "with the share 0 none of the {0} poles is on",
523
+ lamps_all: "downtown keeps its {0} poles on",
524
+ vertex_height: "the height of the central vertex is {0} m and the mesh has {1} m",
525
+ river_bed: "the river bed is {0} m below the land beside it",
526
+ lake_level: "the centre of the lake is at {0} m",
527
+ vehicles_placed: "{0} vehicles asked, {1} vehicle spots in the scene",
528
+ vehicle_ids: "the vehicles are of the fleet: {0}",
529
+ plants_placed: "{0} plants asked, {1} plant spots in the scene"
530
+ }
531
+ };
532
+ var TEXT = { "pt-BR": ptBR, en };
533
+ var fill = (template, ...values) => template.replace(/\{(\d+)\}/g, (_match, index) => String(values[Number(index)] ?? ""));
534
+
535
+ // src/test-area/scenarios.ts
536
+ var POLE_TOPS = { led_double: 9, concrete_arm: 8, cast_iron: 4.8, wood_crossarm: 7, flood_tower: 12.55 };
537
+ var topOf = (parts) => Math.max(...parts.map((part) => "height" in part && "y" in part ? part.y + part.height / 2 : 0));
538
+ var placements = (parts, kind) => parts.filter((part) => part.shape === "placement" && part.kind === kind);
539
+ var round = (value, digits = 3) => Math.round(value * 10 ** digits) / 10 ** digits;
540
+ var createScenarios = (deps) => {
541
+ const { text } = deps;
542
+ const say = (id, ...values) => fill(text.checks[id], ...values);
543
+ const scenario = (id, run) => ({ id, name: text.scenarios[id] ?? id, run });
544
+ const kit = () => createUrban(deps.urban);
545
+ return [
546
+ scenario("urban_pole_heights", ({ expect }) => {
547
+ const urban = kit();
548
+ for (const id of urban.poles.ids()) {
549
+ const expected = POLE_TOPS[id];
550
+ if (expected === void 0) continue;
551
+ const b = urban.builder();
552
+ urban.poles.get(id).build(b, { x: 0, z: 0 });
553
+ const top = topOf(b.parts);
554
+ expect(Math.abs(top - expected) < 1e-6, say("pole_top", id, round(top)), { pole: id, top });
555
+ }
556
+ }),
557
+ scenario("urban_light_budget", ({ expect }) => {
558
+ const urban = kit();
559
+ const budget = 4;
560
+ const b = urban.builder({ budget });
561
+ for (let index = 0; index < 8; index++) urban.poles.get("led_double").build(b, { x: index * 8, z: 0 });
562
+ const real = b.parts.filter((part) => part.shape === "light").length;
563
+ expect(real === budget, say("budget_real", 8, real, budget), { real, budget });
564
+ expect(b.lights.left < 0, say("budget_left", b.lights.left), { left: b.lights.left });
565
+ }),
566
+ scenario("urban_road_dashes", ({ expect }) => {
567
+ const urban = kit();
568
+ const b = urban.builder();
569
+ urban.streets.road(b, 0, 0, 7, 60, "x");
570
+ expect(b.items.length === 12 && b.items[0]?.x === -28 && b.items[1]?.x === -23, say("dashes", b.items.length, b.items[0]?.x), { dashes: b.items.length });
571
+ }),
572
+ scenario("urban_block_heights", ({ expect }) => {
573
+ const urban = kit();
574
+ const b = urban.builder({ seed: 3 });
575
+ const placed = urban.block.fill(b, { cx: 0, cz: 0, size: 200, minHeight: 10, maxHeight: 50, k: 20, gap: 0.01 });
576
+ const heights = placed.map((building) => building.height).sort((a, c) => a - c);
577
+ const median = heights[Math.floor(heights.length / 2)];
578
+ expect(heights.length > 0 && heights[0] >= 10 && heights[heights.length - 1] <= 50, say("heights_in_range", 10, 50), { count: heights.length });
579
+ expect(median < 30, say("heights_median", round(median, 1), 30), { median });
580
+ }),
581
+ scenario("urban_neighbourhood_seed", ({ expect }) => {
582
+ const urban = kit();
583
+ const draw = (seed) => {
584
+ const b = urban.builder({ seed });
585
+ urban.neighbourhoods.get(urban.neighbourhoods.ids()[1]).compose(b, { x: 0, z: 0 });
586
+ return JSON.stringify(b.scene());
587
+ };
588
+ expect(draw(7) === draw(7), say("same_seed", 7));
589
+ expect(draw(7) !== draw(8), say("other_seed", 8));
590
+ }),
591
+ scenario("urban_lamps_burnt", ({ expect }) => {
592
+ const dark = createUrban({ ...deps.urban, patch: { ...deps.urban?.patch, neighbourhoods: { ...deps.urban?.patch?.neighbourhoods, suburb: { lit: 0 } } } });
593
+ const suburb = dark.neighbourhoods.get("suburb").compose(dark.builder(), { x: 0, z: 0 });
594
+ expect(suburb.poles.length > 0 && !suburb.poles.some((pole) => pole.on), say("lamps_none", suburb.poles.length), { poles: suburb.poles.length });
595
+ const lit = kit();
596
+ const downtown = lit.neighbourhoods.get("downtown").compose(lit.builder(), { x: 0, z: 0 });
597
+ expect(downtown.poles.length > 0 && downtown.poles.every((pole) => pole.on), say("lamps_all", downtown.poles.length), { poles: downtown.poles.length });
598
+ }),
599
+ scenario("urban_terrain", ({ expect }) => {
600
+ const urban = kit();
601
+ const terrain = urban.terrain(deps.terrain);
602
+ const grid = terrain.grid();
603
+ const mid = grid.segments / 2 * (grid.segments + 1) + grid.segments / 2;
604
+ const world = terrain.fromWorld(grid.x[mid], grid.z[mid]);
605
+ const height = terrain.heightAt(world[0], world[1]);
606
+ expect(Math.abs(height - grid.heights[mid]) < 1e-3, say("vertex_height", round(height), round(grid.heights[mid])), { height });
607
+ for (const river of deps.terrain.rivers) {
608
+ if (!("fixed" in river.bed)) continue;
609
+ const point = river.points[Math.floor(river.points.length / 2)];
610
+ const bed = terrain.heightAt(point[0], point[1]);
611
+ expect(Math.abs(bed - river.bed.fixed) < 1e-6, say("river_bed", round(river.bed.fixed < 0 ? -river.bed.fixed : river.bed.fixed)), { river: river.id, bed });
612
+ }
613
+ for (const lake of deps.terrain.lakes) {
614
+ const level = terrain.heightAt(lake.center[0], lake.center[1]);
615
+ expect(Math.abs(level - lake.level) < 1e-6, say("lake_level", round(level)), { lake: lake.id, level });
616
+ }
617
+ }),
618
+ scenario("urban_providers", ({ expect }) => {
619
+ const urban = kit();
620
+ const cars = urban.builder();
621
+ const downtown = urban.neighbourhoods.get("downtown").compose(cars, { x: 0, z: 0 }, { providers: { vehicle: createVehicleProvider() } });
622
+ const spots = placements(cars.parts, "vehicle");
623
+ expect(spots.length === downtown.vehicles.length && spots.length > 0, say("vehicles_placed", downtown.vehicles.length, spots.length));
624
+ expect(spots.every((spot) => PARKED_VEHICLE_IDS.includes(spot.id)), say("vehicle_ids", [...new Set(spots.map((spot) => spot.id))].join(", ")));
625
+ const trees = urban.builder();
626
+ const mountain = urban.neighbourhoods.get("mountain_resort").compose(trees, { x: 0, z: 0 }, { providers: { plant: createPlantProvider() } });
627
+ const planted = placements(trees.parts, "plant");
628
+ expect(planted.length === mountain.plants.length && planted.length > 0, say("plants_placed", mountain.plants.length, planted.length));
629
+ })
630
+ ];
631
+ };
632
+
633
+ // src/test-area/module.ts
634
+ var URBAN_AREA_ID = "urban";
635
+ var DEFAULT_TEST_TERRAIN = {
636
+ world: { size: 96, scale: 1, origin: -48, segments: 48 },
637
+ base: {
638
+ level: 1.2,
639
+ waves: [
640
+ { kind: "product", x: ["sin", 0.09], y: ["cos", 0.07], amplitude: 1.1 },
641
+ { kind: "diagonal", kx: 0.05, ky: 0.04, amplitude: 0.5 }
642
+ ]
643
+ },
644
+ layers: [],
645
+ flatten: [],
646
+ rivers: [{ id: "river", points: [[-48, -30], [-20, -24], [10, -30], [48, -22]], width: [3, 9], bed: { fixed: -1.2 } }],
647
+ lakes: [{ id: "lake", center: [24, 24], radius: [12, 12], level: -0.9, edge: [0.55, 1], reach: 1.4 }],
648
+ forest: { zones: [{ mask: [{ axis: "x", from: -48, to: -20, fullUntil: -40 }] }] },
649
+ colors: [
650
+ { kind: "below", height: 0.1, color: 7043674 },
651
+ { kind: "forest", above: 0.5, color: 4086325 }
652
+ ],
653
+ fields: { colors: [8031317, 9083486, 7308368], cell: [8, 8] },
654
+ water: { level: -0.35, color: 2835538, roughness: 0.3, metalness: 0.25 }
655
+ };
656
+ var FLOOR_Y = 0;
657
+ var POLE_ROW_Z = -26;
658
+ var POLE_GAP = 6;
659
+ var SHEET_SHIFT_Z = -200;
660
+ var VIEW_FAR = 600;
661
+ var GRID = { block: 36, count: 2, width: 10 };
662
+ var LAMP_SHARES = { pattern: void 0, all: 1, half: 0.5, none: 0 };
663
+ function createUrbanTestModule(options = {}) {
664
+ const locale = (options.locale ?? "pt-BR").toLowerCase().startsWith("en") ? "en" : "pt-BR";
665
+ const text = TEXT[locale];
666
+ const terrain = options.terrain ?? DEFAULT_TEST_TERRAIN;
667
+ const seed = options.seed ?? 7;
668
+ const benchOrigin = options.benchOrigin ?? [0, -60];
669
+ const settings = { pattern: "downtown", seed, lamps: "pattern", budget: 16, providers: "none" };
670
+ let host = null;
671
+ let three = null;
672
+ let clock = 0;
673
+ let helpersShown = "";
674
+ let bench = null;
675
+ const placed = /* @__PURE__ */ new Map();
676
+ let helpers = [];
677
+ const resolvers = {};
678
+ const baseKit = createUrban2(options.urban);
679
+ const poleIds = baseKit.poles.ids();
680
+ const patternIds = baseKit.neighbourhoods.ids();
681
+ const itemIds = [...poleIds.map((id) => `pole_${id}`), "street_grid", "terrain_patch", ...patternIds];
682
+ const kitWith = (overrides = {}) => {
683
+ const share = LAMP_SHARES[overrides.lamps ?? "pattern"];
684
+ const urban = options.urban ?? {};
685
+ const patch = share === void 0 ? urban.patch : { ...urban.patch, neighbourhoods: { ...urban.patch?.neighbourhoods, ...Object.fromEntries(patternIds.map((id) => [id, { ...urban.patch?.neighbourhoods?.[id] ?? {}, lit: share }])) } };
686
+ return createUrban2({ ...urban, ...patch ? { patch } : {}, lights: { budget: overrides.budget ?? urban.lights?.budget } });
687
+ };
688
+ const resolversFor = (THREE) => {
689
+ if (!resolvers.plant) resolvers.plant = createPlantResolver(THREE, { lod: 1 });
690
+ if (!resolvers.vehicle && options.vehicles) resolvers.vehicle = createVehicleResolver(options.vehicles(THREE), { quality: "lite" });
691
+ return resolvers;
692
+ };
693
+ const providersOf = (choice) => {
694
+ const providers = {};
695
+ if (choice === "plants" || choice === "both") providers.plant = createPlantProvider2();
696
+ if ((choice === "vehicles" || choice === "both") && options.vehicles) providers.vehicle = createVehicleProvider2();
697
+ return providers;
698
+ };
699
+ const draw = (THREE, build, kit, buildSeed) => {
700
+ const b = kit.builder({ seed: buildSeed });
701
+ build(b, kit);
702
+ const document = options.document ?? globalThis.document;
703
+ const drawn = createUrbanThree(THREE, { ...document ? { document } : {}, resolvers: resolversFor(THREE) }).draw(b.scene());
704
+ return { root: drawn.root, drawn };
705
+ };
706
+ const sample = (THREE, key) => {
707
+ const kit = kitWith();
708
+ if (key.startsWith("pole_")) return draw(THREE, (b, k) => k.poles.get(key.slice(5)).build(b, { x: 0, z: 0 }), kit, seed);
709
+ if (key === "street_grid") {
710
+ return draw(THREE, (b, k) => {
711
+ k.streets.grid(b, GRID);
712
+ const inner = GRID.block - GRID.width;
713
+ k.block.fill(b, { cx: GRID.block / 2, cz: GRID.block / 2, size: inner - 4, minHeight: 10, maxHeight: 40, k: 3 });
714
+ }, kit, seed);
715
+ }
716
+ if (key === "terrain_patch") return draw(THREE, (b, k) => k.drawTerrain(b, k.terrain(terrain)), kit, seed);
717
+ return draw(THREE, (b, k) => {
718
+ k.neighbourhoods.get(key).compose(b, { x: 0, z: 0 }, { locale });
719
+ }, kit, seed);
720
+ };
721
+ const item = (id) => ({
722
+ id,
723
+ name: text.items[id] ?? id,
724
+ movable: false,
725
+ build: (THREE) => {
726
+ three = THREE;
727
+ const made = sample(THREE, id);
728
+ made.root.userData.urbanItem = id;
729
+ placed.set(made.root.uuid, made);
730
+ return made.root;
731
+ },
732
+ spec: (root) => {
733
+ const THREE_ = three;
734
+ const box = new THREE_.Box3().setFromObject(root);
735
+ const size = box.getSize(new THREE_.Vector3());
736
+ const centre = box.getCenter(new THREE_.Vector3());
737
+ return { name: id, tags: ["structure"], parts: [{ id: "ground", material: "concrete", shape: { kind: "box", size: [Math.max(size.x, 0.1), Math.max(size.y, 0.1), Math.max(size.z, 0.1)] }, position: [centre.x, centre.y, centre.z], anchored: true }] };
738
+ }
739
+ });
740
+ const clearBench = () => {
741
+ if (!bench) return;
742
+ bench.drawn.dispose();
743
+ bench = null;
744
+ };
745
+ const countObjects = (root) => {
746
+ let n = 0;
747
+ root.traverse(() => n++);
748
+ return n;
749
+ };
750
+ const composeBench = () => {
751
+ if (!host || !three) return;
752
+ clearBench();
753
+ const kit = kitWith({ lamps: settings.lamps, budget: settings.budget });
754
+ if ((settings.providers === "vehicles" || settings.providers === "both") && !options.vehicles) host.log(text.log.no_vehicles);
755
+ const b = kit.builder({ seed: settings.seed });
756
+ const made = kit.neighbourhoods.get(settings.pattern).compose(b, { x: 0, z: 0 }, { locale, providers: providersOf(settings.providers) });
757
+ const document = options.document ?? globalThis.document;
758
+ const drawn = createUrbanThree(three, { ...document ? { document } : {}, resolvers: resolversFor(three) }).draw(b.scene());
759
+ drawn.root.position.set(benchOrigin[0], FLOOR_Y, benchOrigin[1]);
760
+ host.scene.add(drawn.root);
761
+ bench = { root: drawn.root, drawn, plan: made };
762
+ lightsLeft = b.lights.left;
763
+ host.log(fill(text.log.composed, settings.pattern, settings.seed, countObjects(drawn.root), drawn.lights.length), { pattern: settings.pattern, seed: settings.seed });
764
+ helpersShown = "";
765
+ };
766
+ let lightsLeft = settings.budget;
767
+ const onChange = (change) => {
768
+ if (change.kind === "layer") return refreshLayers();
769
+ if (change.kind === "action") {
770
+ if (change.id === "compose") composeBench();
771
+ else if (change.id === "clear") {
772
+ clearBench();
773
+ host?.log(text.log.cleared);
774
+ }
775
+ return;
776
+ }
777
+ switch (change.id) {
778
+ case "pattern":
779
+ settings.pattern = String(change.value);
780
+ if (bench) composeBench();
781
+ break;
782
+ case "seed":
783
+ settings.seed = Number(change.value);
784
+ if (bench) composeBench();
785
+ break;
786
+ case "lamps":
787
+ settings.lamps = String(change.value);
788
+ if (bench) composeBench();
789
+ break;
790
+ case "budget":
791
+ settings.budget = Number(change.value);
792
+ if (bench) composeBench();
793
+ break;
794
+ case "providers":
795
+ settings.providers = String(change.value);
796
+ if (bench) composeBench();
797
+ break;
798
+ default:
799
+ break;
800
+ }
801
+ };
802
+ const roots = () => [...[...placed.values()].filter((entry) => entry.root.parent), ...bench ? [bench] : []];
803
+ const clearHelpers = () => {
804
+ for (const helper of helpers) helper.parent?.remove(helper);
805
+ helpers = [];
806
+ helpersShown = "";
807
+ };
808
+ const boxKey = () => `${host?.bench?.layer("light_radius")}|${host?.bench?.layer("bounds")}|${roots().length}|${bench ? bench.root.uuid : ""}`;
809
+ const refreshLayers = () => {
810
+ if (!host?.bench || !three) return;
811
+ clearHelpers();
812
+ const mark = (parent, helper) => {
813
+ helper.userData.helper = true;
814
+ parent.add(helper);
815
+ helpers.push(helper);
816
+ };
817
+ for (const entry of roots()) {
818
+ if (!entry.root.parent) continue;
819
+ entry.root.updateMatrixWorld(true);
820
+ if (host.bench.layer("bounds")) {
821
+ const box = new three.Box3().setFromObject(entry.root);
822
+ const inverse = new three.Matrix4().copy(entry.root.matrixWorld).invert();
823
+ box.applyMatrix4(inverse);
824
+ mark(entry.root, new three.Box3Helper(box, 6737151));
825
+ }
826
+ if (host.bench.layer("light_radius")) {
827
+ for (const light of entry.drawn.lights) {
828
+ if (!light.isPointLight && !light.isSpotLight) continue;
829
+ const reach = new three.Mesh(new (shapeGeometries3(three)).SphereGeometry(Math.min(light.distance || 10, 60), 14, 8), new three.MeshBasicMaterial({ color: light.color ?? 16764006, wireframe: true, transparent: true, opacity: 0.25 }));
830
+ reach.position.copy(light.position);
831
+ mark(light.parent ?? entry.root, reach);
832
+ }
833
+ }
834
+ }
835
+ helpersShown = boxKey();
836
+ };
837
+ const frame = (dt) => {
838
+ clock += dt;
839
+ for (const entry of roots()) entry.drawn.animate(clock);
840
+ for (const [id, entry] of placed) if (!entry.root.parent) placed.delete(id);
841
+ if (host?.bench && boxKey() !== helpersShown && (host.bench.layer("light_radius") || host.bench.layer("bounds") || helpersShown !== "")) refreshLayers();
842
+ };
843
+ const count = () => {
844
+ let drawCalls = 0;
845
+ let triangles = 0;
846
+ let lights = 0;
847
+ let instances = 0;
848
+ for (const entry of roots()) {
849
+ entry.root.traverse((o) => {
850
+ if (o.isLight) lights += 1;
851
+ if (!o.isMesh || o.userData.helper) return;
852
+ for (let p = o; p; p = p.parent) if (p.visible === false) return;
853
+ drawCalls += 1;
854
+ if (o.isInstancedMesh) instances += o.count;
855
+ const g = o.geometry;
856
+ const perInstance = (g.index ? g.index.count : g.attributes.position.count) / 3;
857
+ triangles += perInstance * (o.isInstancedMesh ? o.count : 1);
858
+ });
859
+ }
860
+ return { drawCalls, triangles: Math.round(triangles), lights, instances };
861
+ };
862
+ const area = () => {
863
+ const placements2 = [];
864
+ poleIds.forEach((id, index) => placements2.push({ item: `pole_${id}`, position: [(index - (poleIds.length - 1) / 2) * POLE_GAP, 0, POLE_ROW_Z], rotationY: 0 }));
865
+ placements2.push({ item: "street_grid", position: [-230, 0, -200], rotationY: 0 });
866
+ placements2.push({ item: "terrain_patch", position: [270, 0, -150], rotationY: 0 });
867
+ for (const id of patternIds) {
868
+ const cell = SAMPLE_CELLS[id];
869
+ if (cell) placements2.push({ item: id, position: [cell.x, 0, cell.z + SHEET_SHIFT_Z], rotationY: 0 });
870
+ }
871
+ const extra = patternIds.filter((id) => !SAMPLE_CELLS[id]);
872
+ extra.forEach((id, index) => placements2.push({ item: id, position: [-80 + index * 80, 0, SHEET_SHIFT_Z - 112], rotationY: 0 }));
873
+ return { placements: placements2, view: { eye: [20, 190, -20], target: [20, 0, -190] } };
874
+ };
875
+ return {
876
+ id: URBAN_AREA_ID,
877
+ name: text.name,
878
+ group: text.group,
879
+ get placed() {
880
+ return placed;
881
+ },
882
+ itemIds,
883
+ tick: (t) => {
884
+ for (const entry of roots()) entry.drawn.animate(t);
885
+ },
886
+ items: () => itemIds.map(item),
887
+ scenarios: () => createScenarios({ text: TEXT[locale], urban: options.urban, terrain }),
888
+ area,
889
+ controls: () => [
890
+ { kind: "choice", id: "pattern", name: text.controls.pattern, hint: text.hints.pattern, options: patternIds.map((id) => ({ id, name: text.items[id] ?? id })), default: patternIds.includes("downtown") ? "downtown" : patternIds[0] },
891
+ { kind: "number", id: "seed", name: text.controls.seed, hint: text.hints.seed, min: 0, max: 99999, step: 1, default: seed },
892
+ { kind: "choice", id: "lamps", name: text.controls.lamps, hint: text.hints.lamps, options: Object.keys(LAMP_SHARES).map((id) => ({ id, name: text.options.lamps[id] })), default: "pattern" },
893
+ { kind: "number", id: "budget", name: text.controls.budget, hint: text.hints.budget, min: 0, max: 64, step: 1, default: 16 },
894
+ { kind: "choice", id: "providers", name: text.controls.providers, hint: text.hints.providers, options: ["none", "plants", "vehicles", "both"].map((id) => ({ id, name: text.options.providers[id] })), default: "none" },
895
+ { kind: "action", id: "compose", name: text.actions.compose },
896
+ { kind: "action", id: "clear", name: text.actions.clear }
897
+ ],
898
+ layers: () => ["light_radius", "bounds"].map((id) => ({ id, name: text.layers[id], default: false })),
899
+ metrics: () => [
900
+ { id: "samples", name: text.metrics.samples, read: () => [...placed.values()].filter((entry) => entry.root.parent).length },
901
+ { id: "draw_calls", name: text.metrics.draw_calls, read: () => count().drawCalls },
902
+ { id: "triangles", name: text.metrics.triangles, read: () => count().triangles },
903
+ { id: "real_lights", name: text.metrics.real_lights, read: () => count().lights },
904
+ { id: "instances", name: text.metrics.instances, read: () => count().instances },
905
+ { id: "bench_objects", name: text.metrics.bench_objects, read: () => bench ? countObjects(bench.root) : 0 },
906
+ { id: "light_budget_left", name: text.metrics.light_budget_left, read: () => bench ? lightsLeft : settings.budget }
907
+ ],
908
+ attach(lent) {
909
+ host = lent;
910
+ three = lent.THREE;
911
+ const camera = lent.camera;
912
+ const farBefore = camera?.far;
913
+ if (camera && camera.far < VIEW_FAR) {
914
+ camera.far = VIEW_FAR;
915
+ camera.updateProjectionMatrix();
916
+ }
917
+ const stopChange = lent.bench?.onChange(onChange);
918
+ const stopFrame = lent.onFrame(frame);
919
+ return () => {
920
+ stopChange?.();
921
+ stopFrame();
922
+ clearHelpers();
923
+ clearBench();
924
+ if (camera && farBefore !== void 0) {
925
+ camera.far = farBefore;
926
+ camera.updateProjectionMatrix();
927
+ }
928
+ resolvers.plant?.dispose?.();
929
+ resolvers.vehicle?.dispose?.();
930
+ host = null;
931
+ };
932
+ }
933
+ };
934
+ }
935
+ function loadUrbanTestModule(room, options = {}) {
936
+ return room.loadModule(createUrbanTestModule(options));
937
+ }
938
+ export {
939
+ DEFAULT_TEST_TERRAIN,
940
+ POLE_TOPS,
941
+ TEXT,
942
+ URBAN_AREA_ID,
943
+ createPlantResolver,
944
+ createPrism,
945
+ createRoundedBox,
946
+ createScenarios,
947
+ createTextTexture,
948
+ createUrbanTestModule,
949
+ createUrbanThree,
950
+ createVehicleResolver,
951
+ loadUrbanTestModule
952
+ };
953
+ //# sourceMappingURL=index.js.map