@unson/brainbase-mcp 0.15.5 → 0.15.6
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/package.json +2 -2
- package/ui/world/world-placement.js +40 -0
- package/ui/world/world-view.js +392 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.6",
|
|
4
4
|
"description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -358,5 +358,5 @@
|
|
|
358
358
|
"engines": {
|
|
359
359
|
"node": ">=20"
|
|
360
360
|
},
|
|
361
|
-
"gitHead": "
|
|
361
|
+
"gitHead": "94fabc0ef66f6296d9f68fb5e4e54d87a63fc65b"
|
|
362
362
|
}
|
|
@@ -93,3 +93,43 @@ export function cityMeasures(business, { isFinished, judgments = [], now = Date.
|
|
|
93
93
|
lit: recent > 0,
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The sky for an hour of the day (scenery only; it represents no data). `mode` 'auto' follows the hour;
|
|
99
|
+
* 'day', 'dusk' and 'night' fix it. At night lit windows and street lamps glow the most, so the cities
|
|
100
|
+
* that moved lately read best then.
|
|
101
|
+
*/
|
|
102
|
+
export function skyAt(hour, mode = 'auto') {
|
|
103
|
+
const phase = mode !== 'auto' ? mode
|
|
104
|
+
: hour >= 5 && hour < 7 ? 'dawn'
|
|
105
|
+
: hour >= 7 && hour < 17 ? 'day'
|
|
106
|
+
: hour >= 17 && hour < 19 ? 'dusk'
|
|
107
|
+
: 'night';
|
|
108
|
+
const skies = {
|
|
109
|
+
dawn: { top: '#c9d6ef', middle: '#f3d9c4', bottom: '#f6ead8', fog: 0xf1e2d2, sun: 0xffd2a8, sunIntensity: 1.6, hemi: 0.95, exposure: 1.0, glow: 1.1, lamps: true, stars: false },
|
|
110
|
+
day: { top: '#bfdcf0', middle: '#e3f0f2', bottom: '#f4f1e6', fog: 0xe9f1ee, sun: 0xfff1dc, sunIntensity: 2.7, hemi: 1.25, exposure: 1.05, glow: 0.6, lamps: false, stars: false },
|
|
111
|
+
dusk: { top: '#5d6f9e', middle: '#e7a77e', bottom: '#f3cfa4', fog: 0xe6bf9c, sun: 0xffb27a, sunIntensity: 1.3, hemi: 0.75, exposure: 0.95, glow: 1.5, lamps: true, stars: false },
|
|
112
|
+
night: { top: '#0d1730', middle: '#1c2b4c', bottom: '#33456a', fog: 0x2a3a5a, sun: 0x9fb4ff, sunIntensity: 0.35, hemi: 0.35, exposure: 0.9, glow: 2.4, lamps: true, stars: true },
|
|
113
|
+
};
|
|
114
|
+
return { phase, ...skies[phase] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What moved since the viewer last opened the world: cities whose newest decision or a placed judgment
|
|
119
|
+
* is newer than `lastVisit`, and plaza buildings whose newest judgment is. Null on a first visit.
|
|
120
|
+
*/
|
|
121
|
+
export function changesSince(lastVisit, businesses, judgmentsByBusiness, rows) {
|
|
122
|
+
const since = Date.parse(lastVisit ?? '');
|
|
123
|
+
if (!Number.isFinite(since)) return null;
|
|
124
|
+
const newer = (at) => {
|
|
125
|
+
const time = Date.parse(at ?? '');
|
|
126
|
+
return Number.isFinite(time) && time > since;
|
|
127
|
+
};
|
|
128
|
+
const cities = [];
|
|
129
|
+
for (const business of businesses ?? []) {
|
|
130
|
+
const judgments = judgmentsByBusiness?.get?.(business.code) ?? [];
|
|
131
|
+
if (newer(business.activity?.latest_decision_at) || judgments.some((entry) => newer(entry?.item?.proof?.recorded_at))) cities.push(business.code);
|
|
132
|
+
}
|
|
133
|
+
const plaza = (rows ?? []).filter((row) => newer(row.latest_recorded_at)).map((row) => row.key);
|
|
134
|
+
return { since: new Date(since).toISOString(), cities, plaza };
|
|
135
|
+
}
|
package/ui/world/world-view.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { THREE, MapControls } from './world-vendor.js';
|
|
18
|
-
import { cityMeasures, districtLots, groupJudgmentPlaces, UNPLACED_REASON_TEXT } from './world-placement.js';
|
|
18
|
+
import { changesSince, cityMeasures, districtLots, groupJudgmentPlaces, skyAt, UNPLACED_REASON_TEXT } from './world-placement.js';
|
|
19
19
|
import {
|
|
20
20
|
makeWorkspaceElement as el,
|
|
21
21
|
workspacePageHeader,
|
|
@@ -242,15 +242,29 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
242
242
|
stage.prepend(renderer.domElement);
|
|
243
243
|
|
|
244
244
|
const scene = new THREE.Scene();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
245
|
+
// Scenery (sky, sea, land, mountains, trees, lamps) represents no data: it only sets the place.
|
|
246
|
+
let skyMode = 'auto';
|
|
247
|
+
let sky = skyAt(new Date().getHours(), skyMode);
|
|
248
|
+
function skyTexture(current) {
|
|
249
|
+
return canvasTexture(doc, 256, 256, (ctx, w, h) => {
|
|
250
|
+
const gradient = ctx.createLinearGradient(0, 0, 0, h);
|
|
251
|
+
gradient.addColorStop(0, current.top);
|
|
252
|
+
gradient.addColorStop(0.55, current.middle);
|
|
253
|
+
gradient.addColorStop(1, current.bottom);
|
|
254
|
+
ctx.fillStyle = gradient;
|
|
255
|
+
ctx.fillRect(0, 0, w, h);
|
|
256
|
+
if (current.stars) {
|
|
257
|
+
ctx.fillStyle = 'rgba(255,255,255,0.85)';
|
|
258
|
+
for (let i = 0; i < 90; i += 1) {
|
|
259
|
+
const sx = hashUnit(`star${i}x`) * w;
|
|
260
|
+
const sy = hashUnit(`star${i}y`) * h * 0.55;
|
|
261
|
+
ctx.fillRect(sx, sy, hashUnit(`star${i}s`) > 0.85 ? 1.6 : 0.9, hashUnit(`star${i}s`) > 0.85 ? 1.6 : 0.9);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
scene.background = skyTexture(sky);
|
|
267
|
+
scene.fog = new THREE.Fog(sky.fog, 170, 340);
|
|
254
268
|
|
|
255
269
|
const camera = new THREE.OrthographicCamera(-50, 50, 50, -50, 0.1, 1000);
|
|
256
270
|
const home = { position: new THREE.Vector3(90, 95, 90), target: new THREE.Vector3(0, 0, 0), zoom: 1 };
|
|
@@ -267,8 +281,9 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
267
281
|
controls.minPolarAngle = Math.PI / 6;
|
|
268
282
|
controls.target.copy(home.target);
|
|
269
283
|
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
const hemisphere = new THREE.HemisphereLight(0xf4f8ff, 0xd8ccb2, sky.hemi);
|
|
285
|
+
scene.add(hemisphere);
|
|
286
|
+
const sun = new THREE.DirectionalLight(sky.sun, sky.sunIntensity);
|
|
272
287
|
sun.position.set(70, 110, 40);
|
|
273
288
|
sun.castShadow = true;
|
|
274
289
|
sun.shadow.mapSize.set(2048, 2048);
|
|
@@ -280,11 +295,10 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
280
295
|
fill.position.set(-80, 60, -60);
|
|
281
296
|
scene.add(fill);
|
|
282
297
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
scene.add(ground);
|
|
298
|
+
// Lit windows and street lamps follow the sky (brightest at night); their meaning does not change.
|
|
299
|
+
const litMaterials = new Set();
|
|
300
|
+
const lampMaterials = [];
|
|
301
|
+
const animated = { water: null, jets: [], sparkles: [] };
|
|
288
302
|
|
|
289
303
|
const textures = { lit: windowTexture(doc, true), unlit: windowTexture(doc, false), glow: windowTexture(doc, true, true) };
|
|
290
304
|
const pickables = [];
|
|
@@ -324,7 +338,8 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
324
338
|
glow.repeat.copy(map.repeat);
|
|
325
339
|
material.emissive = new THREE.Color(0xffc867);
|
|
326
340
|
material.emissiveMap = glow;
|
|
327
|
-
material.emissiveIntensity =
|
|
341
|
+
material.emissiveIntensity = sky.glow;
|
|
342
|
+
litMaterials.add(material);
|
|
328
343
|
}
|
|
329
344
|
return mesh(new THREE.BoxGeometry(w, h, d), material, { y: h / 2 });
|
|
330
345
|
}
|
|
@@ -350,14 +365,215 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
350
365
|
scene.add(line);
|
|
351
366
|
}
|
|
352
367
|
|
|
368
|
+
// Trees are many; they are drawn as two instanced meshes (trunks, crowns) once the world is built.
|
|
369
|
+
const treePlacements = [];
|
|
353
370
|
function tree(x, z, scale = 1) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
371
|
+
treePlacements.push([x, z, scale]);
|
|
372
|
+
}
|
|
373
|
+
function plantTrees() {
|
|
374
|
+
if (treePlacements.length === 0) return;
|
|
375
|
+
const trunks = new THREE.InstancedMesh(new THREE.CylinderGeometry(0.12, 0.16, 0.7, 6), standard(0x8a6d4e), treePlacements.length);
|
|
376
|
+
const crowns = new THREE.InstancedMesh(new THREE.ConeGeometry(0.7, 1.6, 7), standard(0x6f9a72, { roughness: 0.95 }), treePlacements.length);
|
|
377
|
+
const matrix = new THREE.Matrix4();
|
|
378
|
+
const tint = new THREE.Color();
|
|
379
|
+
treePlacements.forEach(([x, z, scale], index) => {
|
|
380
|
+
matrix.makeScale(scale, scale, scale).setPosition(x, 0.35 * scale, z);
|
|
381
|
+
trunks.setMatrixAt(index, matrix);
|
|
382
|
+
matrix.makeScale(scale, scale, scale).setPosition(x, 1.4 * scale, z);
|
|
383
|
+
crowns.setMatrixAt(index, matrix);
|
|
384
|
+
crowns.setColorAt(index, tint.setHSL(0.33 + (hashUnit(`${x},${z}`) - 0.5) * 0.05, 0.22, 0.45 + (hashUnit(`${z},${x}`) - 0.5) * 0.1));
|
|
385
|
+
});
|
|
386
|
+
for (const instanced of [trunks, crowns]) {
|
|
387
|
+
instanced.castShadow = true;
|
|
388
|
+
instanced.receiveShadow = true;
|
|
389
|
+
scene.add(instanced);
|
|
390
|
+
}
|
|
391
|
+
treePlacements.length = 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// --- scenery (no data) -----------------------------------------------------
|
|
395
|
+
function grassTexture() {
|
|
396
|
+
const texture = canvasTexture(doc, 256, 256, (ctx, w, h) => {
|
|
397
|
+
ctx.fillStyle = '#cfe0c2';
|
|
398
|
+
ctx.fillRect(0, 0, w, h);
|
|
399
|
+
for (let i = 0; i < 900; i += 1) {
|
|
400
|
+
const tone = hashUnit(`g${i}t`);
|
|
401
|
+
ctx.fillStyle = tone > 0.66 ? 'rgba(150,186,132,0.35)' : tone > 0.33 ? 'rgba(214,230,196,0.45)' : 'rgba(176,204,152,0.3)';
|
|
402
|
+
ctx.beginPath();
|
|
403
|
+
ctx.arc(hashUnit(`g${i}x`) * w, hashUnit(`g${i}y`) * h, 2 + hashUnit(`g${i}r`) * 7, 0, Math.PI * 2);
|
|
404
|
+
ctx.fill();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
408
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
409
|
+
texture.repeat.set(18, 18);
|
|
410
|
+
return texture;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function waterTexture() {
|
|
414
|
+
const texture = canvasTexture(doc, 128, 128, (ctx, w, h) => {
|
|
415
|
+
ctx.fillStyle = '#8fc0d2';
|
|
416
|
+
ctx.fillRect(0, 0, w, h);
|
|
417
|
+
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
|
|
418
|
+
ctx.lineWidth = 1.2;
|
|
419
|
+
for (let i = 0; i < 26; i += 1) {
|
|
420
|
+
const x = hashUnit(`w${i}x`) * w;
|
|
421
|
+
const y = hashUnit(`w${i}y`) * h;
|
|
422
|
+
ctx.beginPath();
|
|
423
|
+
ctx.moveTo(x, y);
|
|
424
|
+
ctx.quadraticCurveTo(x + 6, y - 3, x + 12, y);
|
|
425
|
+
ctx.stroke();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
429
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
430
|
+
texture.repeat.set(40, 40);
|
|
431
|
+
return texture;
|
|
359
432
|
}
|
|
360
433
|
|
|
434
|
+
function pavingTexture() {
|
|
435
|
+
return canvasTexture(doc, 128, 128, (ctx, w, h) => {
|
|
436
|
+
ctx.fillStyle = '#f1ece1';
|
|
437
|
+
ctx.fillRect(0, 0, w, h);
|
|
438
|
+
ctx.strokeStyle = 'rgba(176,164,140,0.55)';
|
|
439
|
+
ctx.lineWidth = 1;
|
|
440
|
+
for (let r = 8; r < w / 2; r += 9) {
|
|
441
|
+
ctx.beginPath();
|
|
442
|
+
ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);
|
|
443
|
+
ctx.stroke();
|
|
444
|
+
}
|
|
445
|
+
for (let i = 0; i < 24; i += 1) {
|
|
446
|
+
const a = (i / 24) * Math.PI * 2;
|
|
447
|
+
ctx.beginPath();
|
|
448
|
+
ctx.moveTo(w / 2 + Math.cos(a) * 8, h / 2 + Math.sin(a) * 8);
|
|
449
|
+
ctx.lineTo(w / 2 + Math.cos(a) * w / 2, h / 2 + Math.sin(a) * h / 2);
|
|
450
|
+
ctx.stroke();
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Land, beach, sea and a far shore of mountains around the cities; scenery only. */
|
|
456
|
+
function buildTerrain(extent) {
|
|
457
|
+
const landRadius = extent + 22;
|
|
458
|
+
const land = mesh(new THREE.CircleGeometry(landRadius, 96), standard(0xffffff, { map: grassTexture(), roughness: 1 }), { shadow: false });
|
|
459
|
+
land.rotation.x = -Math.PI / 2;
|
|
460
|
+
land.receiveShadow = true;
|
|
461
|
+
scene.add(land);
|
|
462
|
+
const beach = mesh(new THREE.RingGeometry(landRadius, landRadius + 4.5, 96), new THREE.MeshLambertMaterial({ color: 0xeadfc4 }), { y: -0.05, shadow: false });
|
|
463
|
+
beach.rotation.x = -Math.PI / 2;
|
|
464
|
+
beach.receiveShadow = false;
|
|
465
|
+
scene.add(beach);
|
|
466
|
+
const water = waterTexture();
|
|
467
|
+
const sea = mesh(new THREE.PlaneGeometry(900, 900), new THREE.MeshLambertMaterial({ map: water }), { y: -0.35, shadow: false });
|
|
468
|
+
sea.rotation.x = -Math.PI / 2;
|
|
469
|
+
sea.receiveShadow = false;
|
|
470
|
+
scene.add(sea);
|
|
471
|
+
animated.water = water;
|
|
472
|
+
// A far shore: low mountains across the water, faded by the fog.
|
|
473
|
+
for (let i = 0; i < 22; i += 1) {
|
|
474
|
+
const angle = Math.PI * 0.85 + (i / 21) * Math.PI * 0.95 + (hashUnit(`m${i}a`) - 0.5) * 0.08;
|
|
475
|
+
const distance = landRadius + 110 + hashUnit(`m${i}d`) * 50;
|
|
476
|
+
const peak = 10 + hashUnit(`m${i}h`) * 16;
|
|
477
|
+
const radius = 10 + hashUnit(`m${i}r`) * 9;
|
|
478
|
+
const mountain = mesh(new THREE.ConeGeometry(radius, peak, 7), new THREE.MeshLambertMaterial({ color: 0xb4c4bc, flatShading: true }), { x: Math.cos(angle) * distance, y: peak / 2 - 0.4, z: Math.sin(angle) * distance, shadow: false });
|
|
479
|
+
mountain.rotation.y = hashUnit(`m${i}y`) * Math.PI;
|
|
480
|
+
mountain.receiveShadow = false;
|
|
481
|
+
scene.add(mountain);
|
|
482
|
+
if (peak > 21) {
|
|
483
|
+
const cap = mesh(new THREE.ConeGeometry(radius * 0.32, peak * 0.3, 7), new THREE.MeshLambertMaterial({ color: 0xf4f6f6, flatShading: true }), { x: mountain.position.x, y: peak - 0.4 - peak * 0.15, z: mountain.position.z, shadow: false });
|
|
484
|
+
cap.rotation.y = mountain.rotation.y;
|
|
485
|
+
scene.add(cap);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return landRadius;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const lampPlacements = [];
|
|
492
|
+
function streetLamp(x, z) {
|
|
493
|
+
lampPlacements.push([x, z]);
|
|
494
|
+
}
|
|
495
|
+
function placeLamps() {
|
|
496
|
+
if (lampPlacements.length === 0) return;
|
|
497
|
+
const poles = new THREE.InstancedMesh(new THREE.CylinderGeometry(0.05, 0.07, 1.6, 6), new THREE.MeshLambertMaterial({ color: 0x4b5550 }), lampPlacements.length);
|
|
498
|
+
const bulbMaterial = standard(0xfff3d6, { emissive: new THREE.Color(0xffd27a), emissiveIntensity: sky.lamps ? 1.6 : 0 });
|
|
499
|
+
lampMaterials.push(bulbMaterial);
|
|
500
|
+
const bulbs = new THREE.InstancedMesh(new THREE.SphereGeometry(0.16, 10, 8), bulbMaterial, lampPlacements.length);
|
|
501
|
+
const matrix = new THREE.Matrix4();
|
|
502
|
+
lampPlacements.forEach(([x, z], index) => {
|
|
503
|
+
poles.setMatrixAt(index, matrix.makeTranslation(x, 0.8, z));
|
|
504
|
+
bulbs.setMatrixAt(index, matrix.makeTranslation(x, 1.68, z));
|
|
505
|
+
});
|
|
506
|
+
scene.add(poles, bulbs);
|
|
507
|
+
lampPlacements.length = 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Trees and lamps along a road, kept off the plaza and the city plates. */
|
|
511
|
+
function avenue(from, to, keepOut) {
|
|
512
|
+
const length = from.distanceTo(to);
|
|
513
|
+
const dir = to.clone().sub(from).normalize();
|
|
514
|
+
const side = new THREE.Vector3(-dir.z, 0, dir.x);
|
|
515
|
+
for (let d = 3; d < length - 2; d += 4.2) {
|
|
516
|
+
const point = from.clone().addScaledVector(dir, d);
|
|
517
|
+
if (keepOut(point)) continue;
|
|
518
|
+
const index = Math.round(d / 4.2);
|
|
519
|
+
for (const sign of [-1, 1]) {
|
|
520
|
+
const at = point.clone().addScaledVector(side, sign * 1.9);
|
|
521
|
+
if (index % 3 === 0 && sign === 1) streetLamp(at.x, at.z);
|
|
522
|
+
else tree(at.x, at.z, 0.55 + hashUnit(`${from.x}${d}${sign}`) * 0.2);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Groves and fields between the cities; scenery only (no houses, which could read as engagements). */
|
|
528
|
+
function countryside(landRadius, cities, roads) {
|
|
529
|
+
const blocked = (x, z, margin) => {
|
|
530
|
+
if (Math.hypot(x, z) < PLAZA_RADIUS + 8) return true;
|
|
531
|
+
if (Math.hypot(x, z) > landRadius - 4) return true;
|
|
532
|
+
if (cities.some((city) => Math.hypot(x - city.x, z - city.z) < city.size * 0.8 + margin)) return true;
|
|
533
|
+
return roads.some(([a, b]) => {
|
|
534
|
+
const ab = b.clone().sub(a);
|
|
535
|
+
const t = Math.max(0, Math.min(1, new THREE.Vector3(x, 0, z).sub(a).dot(ab) / ab.lengthSq()));
|
|
536
|
+
return a.clone().addScaledVector(ab, t).distanceTo(new THREE.Vector3(x, 0, z)) < margin;
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
let groves = 0;
|
|
540
|
+
for (let i = 0; i < 90 && groves < 26; i += 1) {
|
|
541
|
+
const angle = hashUnit(`grove${i}a`) * Math.PI * 2;
|
|
542
|
+
const radius = PLAZA_RADIUS + 10 + hashUnit(`grove${i}r`) * (landRadius - PLAZA_RADIUS - 14);
|
|
543
|
+
const x = Math.cos(angle) * radius;
|
|
544
|
+
const z = Math.sin(angle) * radius;
|
|
545
|
+
if (blocked(x, z, 4)) continue;
|
|
546
|
+
groves += 1;
|
|
547
|
+
if (hashUnit(`grove${i}k`) > 0.7) {
|
|
548
|
+
// A field: a flat patch in a crop colour.
|
|
549
|
+
const field = mesh(new THREE.PlaneGeometry(6 + hashUnit(`f${i}w`) * 5, 4 + hashUnit(`f${i}h`) * 4), new THREE.MeshLambertMaterial({ color: hashUnit(`f${i}c`) > 0.5 ? 0xe7dca0 : 0xbfd69b }), { x, y: 0.02, z, shadow: false });
|
|
550
|
+
field.rotation.set(-Math.PI / 2, 0, hashUnit(`f${i}r`) * Math.PI);
|
|
551
|
+
scene.add(field);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const count = 4 + Math.floor(hashUnit(`grove${i}n`) * 6);
|
|
555
|
+
for (let t = 0; t < count; t += 1) {
|
|
556
|
+
const a = hashUnit(`grove${i}t${t}a`) * Math.PI * 2;
|
|
557
|
+
const r = hashUnit(`grove${i}t${t}r`) * 3.2;
|
|
558
|
+
tree(x + Math.cos(a) * r, z + Math.sin(a) * r, 0.6 + hashUnit(`grove${i}t${t}s`) * 0.5);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function applySky() {
|
|
564
|
+
sky = skyAt(new Date().getHours(), skyMode);
|
|
565
|
+
scene.background?.dispose?.();
|
|
566
|
+
scene.background = skyTexture(sky);
|
|
567
|
+
scene.fog.color.setHex(sky.fog);
|
|
568
|
+
sun.color.setHex(sky.sun);
|
|
569
|
+
sun.intensity = sky.sunIntensity;
|
|
570
|
+
hemisphere.intensity = sky.hemi;
|
|
571
|
+
renderer.toneMappingExposure = sky.exposure;
|
|
572
|
+
for (const material of litMaterials) if (!highlightedMaterials.has(material)) material.emissiveIntensity = sky.glow;
|
|
573
|
+
for (const material of lampMaterials) material.emissiveIntensity = sky.lamps ? 1.6 : 0;
|
|
574
|
+
}
|
|
575
|
+
const highlightedMaterials = new Set();
|
|
576
|
+
|
|
361
577
|
// --- plaza ---------------------------------------------------------------
|
|
362
578
|
function buildPlaza(rows, rowItems, waitingOf, now) {
|
|
363
579
|
const plaza = new THREE.Group();
|
|
@@ -365,6 +581,20 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
365
581
|
plaza.add(mesh(new THREE.CylinderGeometry(PLAZA_RADIUS, PLAZA_RADIUS + 0.4, 0.7, 64), standard(0xf3efe6, { roughness: 0.9 }), { y: 0.35 }));
|
|
366
582
|
plaza.add(mesh(new THREE.TorusGeometry(PLAZA_RADIUS - 0.3, 0.08, 6, 64), standard(0xd8cfbd), { y: 0.72 }));
|
|
367
583
|
plaza.children[2].rotation.x = Math.PI / 2;
|
|
584
|
+
const paving = mesh(new THREE.CircleGeometry(PLAZA_RADIUS - 0.05, 64), standard(0xffffff, { map: pavingTexture(), roughness: 0.95 }), { y: 0.705, shadow: false });
|
|
585
|
+
paving.rotation.x = -Math.PI / 2;
|
|
586
|
+
plaza.add(paving);
|
|
587
|
+
if (rows.length !== 1) {
|
|
588
|
+
// A fountain in the middle of the plaza (scenery; the kinds of judgment stand around it).
|
|
589
|
+
plaza.add(mesh(new THREE.CylinderGeometry(1.5, 1.7, 0.45, 32), standard(0xd9d1c1), { y: 0.92 }));
|
|
590
|
+
const pool = mesh(new THREE.CircleGeometry(1.3, 32), standard(0x9fd0de, { roughness: 0.2, metalness: 0.1 }), { y: 1.15, shadow: false });
|
|
591
|
+
pool.rotation.x = -Math.PI / 2;
|
|
592
|
+
plaza.add(pool);
|
|
593
|
+
plaza.add(mesh(new THREE.CylinderGeometry(0.18, 0.24, 0.9, 12), standard(0xd9d1c1), { y: 1.5 }));
|
|
594
|
+
const jet = mesh(new THREE.CylinderGeometry(0.05, 0.16, 1.2, 10), new THREE.MeshBasicMaterial({ color: 0xe8f6fb, transparent: true, opacity: 0.7 }), { y: 2.5, shadow: false });
|
|
595
|
+
plaza.add(jet);
|
|
596
|
+
animated.jets.push(jet);
|
|
597
|
+
}
|
|
368
598
|
scene.add(plaza);
|
|
369
599
|
register(plaza, { kind: 'plaza' });
|
|
370
600
|
addLabel('広場(判断)', new THREE.Vector3(0, 0.8, PLAZA_RADIUS + 2.8), 'is-plaza', { priority: LABEL_PRIORITY.plaza });
|
|
@@ -393,6 +623,7 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
393
623
|
scene.add(group);
|
|
394
624
|
register(group, { kind: 'judgment', row, items: rowItems.get(row.key) ?? [] });
|
|
395
625
|
const top = 0.7 + 0.25 + height + 0.7;
|
|
626
|
+
markers.plaza.set(row.key, new THREE.Vector3(x, top, z));
|
|
396
627
|
const waiting = waitingOf(row);
|
|
397
628
|
if (waiting > 0) {
|
|
398
629
|
const beam = mesh(new THREE.CylinderGeometry(0.22, 0.22, 16, 12), new THREE.MeshBasicMaterial({ color: 0xd92335, transparent: true, opacity: 0.55 }), { x, y: top + 8, z, shadow: false });
|
|
@@ -454,8 +685,11 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
454
685
|
const status = engagement.status;
|
|
455
686
|
const phase = statusPhase(status);
|
|
456
687
|
if (phase === 'finished') {
|
|
457
|
-
// A finished engagement
|
|
458
|
-
group.add(mesh(new THREE.BoxGeometry(1.8, 0.
|
|
688
|
+
// A finished engagement becomes a small memorial park: a lawn, a stone and two bushes.
|
|
689
|
+
group.add(mesh(new THREE.BoxGeometry(1.8, 0.16, 1.8), standard(0xa9c99a, { roughness: 1 }), { y: 0.08 }));
|
|
690
|
+
group.add(mesh(new THREE.BoxGeometry(0.28, 0.9, 0.28), standard(0xdcd5c6), { y: 0.61 }));
|
|
691
|
+
group.add(mesh(new THREE.ConeGeometry(0.2, 0.25, 4), standard(0xdcd5c6), { y: 1.18 }));
|
|
692
|
+
for (const [bx, bz] of [[-0.55, 0.45], [0.55, -0.4]]) group.add(mesh(new THREE.SphereGeometry(0.28, 8, 6), standard(0x7fa877, { roughness: 1 }), { x: bx, y: 0.3, z: bz }));
|
|
459
693
|
group.userData.finished = true;
|
|
460
694
|
return group;
|
|
461
695
|
}
|
|
@@ -483,8 +717,13 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
483
717
|
return group;
|
|
484
718
|
}
|
|
485
719
|
|
|
720
|
+
const markers = { cities: new Map(), plaza: new Map() };
|
|
486
721
|
function buildCities({ cities, sectors, extent }, judgmentsByBusiness = new Map()) {
|
|
487
722
|
fitExtent(extent);
|
|
723
|
+
const landRadius = buildTerrain(extent);
|
|
724
|
+
const roads = [];
|
|
725
|
+
const keepOut = (point) => Math.hypot(point.x, point.z) < PLAZA_RADIUS + 5
|
|
726
|
+
|| cities.some((city) => Math.hypot(point.x - city.x, point.z - city.z) < city.size * 0.62 + 1.2);
|
|
488
727
|
for (let i = 0; i < 12; i += 1) {
|
|
489
728
|
const angle = (i / 12) * Math.PI * 2;
|
|
490
729
|
road(new THREE.Vector3(Math.cos(angle) * (PLAZA_RADIUS + 4), 0, Math.sin(angle) * (PLAZA_RADIUS + 4)), new THREE.Vector3(Math.cos(angle + Math.PI / 6) * (PLAZA_RADIUS + 4), 0, Math.sin(angle + Math.PI / 6) * (PLAZA_RADIUS + 4)), 1.5);
|
|
@@ -495,6 +734,8 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
495
734
|
const color = kindColor(kind);
|
|
496
735
|
const start = new THREE.Vector3(x, 0, z).setLength(PLAZA_RADIUS + 4);
|
|
497
736
|
road(start, new THREE.Vector3(x, 0, z));
|
|
737
|
+
roads.push([start, new THREE.Vector3(x, 0, z)]);
|
|
738
|
+
avenue(start, new THREE.Vector3(x, 0, z), keepOut);
|
|
498
739
|
const group = new THREE.Group();
|
|
499
740
|
group.position.set(x, 0, z);
|
|
500
741
|
const plateColor = new THREE.Color(0xf7f8f4).lerp(new THREE.Color(color), 0.1);
|
|
@@ -505,6 +746,7 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
505
746
|
scene.add(group);
|
|
506
747
|
register(group, { kind: 'city', business });
|
|
507
748
|
const top = entry.form === 'tower' ? towerHeight * 1.45 + 1.8 : towerHeight + 1;
|
|
749
|
+
markers.cities.set(business.code, new THREE.Vector3(x, top, z));
|
|
508
750
|
const cityLabel = addLabel(statusPhase(business.status) === 'concept' ? `${business.name}(${statusText(business.status)})` : business.name, new THREE.Vector3(x, top, z), 'is-city', { priority: LABEL_PRIORITY.city, owner: group });
|
|
509
751
|
cityLabel.node.style.borderColor = `${entry.color}80`;
|
|
510
752
|
cityLabel.business = business;
|
|
@@ -543,6 +785,9 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
543
785
|
tree(x + Math.cos(a) * size * 0.42, z + Math.sin(a) * size * 0.42, 0.8);
|
|
544
786
|
}
|
|
545
787
|
}
|
|
788
|
+
countryside(landRadius, cities, roads);
|
|
789
|
+
plantTrees();
|
|
790
|
+
placeLamps();
|
|
546
791
|
for (const { kind, angle } of sectors) {
|
|
547
792
|
addLabel(kindEntry(kind).label, new THREE.Vector3(Math.cos(angle) * 21, 0.2, Math.sin(angle) * 21), 'is-sector', { priority: LABEL_PRIORITY.sector });
|
|
548
793
|
}
|
|
@@ -559,11 +804,13 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
559
804
|
function setHighlight(group, on) {
|
|
560
805
|
for (const entry of highlightable.get(group) ?? []) {
|
|
561
806
|
if (on) {
|
|
807
|
+
highlightedMaterials.add(entry.material);
|
|
562
808
|
entry.material.emissive.set(0x3d8f74);
|
|
563
809
|
entry.material.emissiveIntensity = 0.35;
|
|
564
810
|
} else {
|
|
811
|
+
highlightedMaterials.delete(entry.material);
|
|
565
812
|
entry.material.emissive.copy(entry.base);
|
|
566
|
-
entry.material.emissiveIntensity = entry.intensity;
|
|
813
|
+
entry.material.emissiveIntensity = litMaterials.has(entry.material) ? sky.glow : entry.intensity;
|
|
567
814
|
}
|
|
568
815
|
}
|
|
569
816
|
}
|
|
@@ -705,6 +952,34 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
705
952
|
}
|
|
706
953
|
}
|
|
707
954
|
|
|
955
|
+
/** A short burst of rising lights over what moved since the last visit. */
|
|
956
|
+
function markChanges(changes) {
|
|
957
|
+
const points = [
|
|
958
|
+
...(changes?.cities ?? []).map((code) => markers.cities.get(code)),
|
|
959
|
+
...(changes?.plaza ?? []).map((key) => markers.plaza.get(key)),
|
|
960
|
+
].filter(Boolean);
|
|
961
|
+
for (const point of points) {
|
|
962
|
+
for (let i = 0; i < 10; i += 1) {
|
|
963
|
+
// Around the building and from its middle, so the labels above it never hide the sparks.
|
|
964
|
+
const angle = (i / 10) * Math.PI * 2;
|
|
965
|
+
const spark = mesh(new THREE.SphereGeometry(0.6, 10, 8), new THREE.MeshBasicMaterial({ color: 0xffb21a, transparent: true, opacity: 0.95, fog: false }), { x: point.x + Math.cos(angle) * 2.6, y: point.y * 0.35, z: point.z + Math.sin(angle) * 2.6, shadow: false });
|
|
966
|
+
spark.userData = { base: Math.max(1.2, point.y * 0.35), rise: Math.max(4, point.y * 0.8), delay: i * 220 };
|
|
967
|
+
scene.add(spark);
|
|
968
|
+
animated.sparkles.push(spark);
|
|
969
|
+
}
|
|
970
|
+
// A ring on the ground that pulses with the sparks.
|
|
971
|
+
const ring = mesh(new THREE.RingGeometry(2.8, 3.5, 48), new THREE.MeshBasicMaterial({ color: 0xffb21a, transparent: true, opacity: 0.8, side: THREE.DoubleSide, depthTest: false, fog: false }), { x: point.x, y: 0.9, z: point.z, shadow: false });
|
|
972
|
+
ring.rotation.x = -Math.PI / 2;
|
|
973
|
+
ring.renderOrder = 2;
|
|
974
|
+
ring.userData = { ring: true, base: 0.9, delay: 0 };
|
|
975
|
+
scene.add(ring);
|
|
976
|
+
animated.sparkles.push(ring);
|
|
977
|
+
}
|
|
978
|
+
animated.sparkleStart = null;
|
|
979
|
+
return points.length;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
let lastSkyMinute = -1;
|
|
708
983
|
let running = true;
|
|
709
984
|
function frame(time) {
|
|
710
985
|
if (!running) return;
|
|
@@ -735,7 +1010,35 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
735
1010
|
renderer.domElement.style.cursor = hovered ? 'pointer' : '';
|
|
736
1011
|
}
|
|
737
1012
|
}
|
|
1013
|
+
const minute = Math.floor(Date.now() / 60000);
|
|
1014
|
+
if (skyMode === 'auto' && minute !== lastSkyMinute) {
|
|
1015
|
+
if (lastSkyMinute !== -1 && skyAt(new Date().getHours(), 'auto').phase !== sky.phase) applySky();
|
|
1016
|
+
lastSkyMinute = minute;
|
|
1017
|
+
}
|
|
1018
|
+
if (animated.sparkles.length) {
|
|
1019
|
+
// Timed by the clock, not the frame timestamp, which can lag behind on a slow renderer.
|
|
1020
|
+
const clock = performance.now();
|
|
1021
|
+
animated.sparkleStart ??= clock;
|
|
1022
|
+
const elapsed = clock - animated.sparkleStart;
|
|
1023
|
+
for (const spark of animated.sparkles) {
|
|
1024
|
+
const t = Math.max(0, elapsed - spark.userData.delay) / 2600;
|
|
1025
|
+
const cycle = t % 1;
|
|
1026
|
+
if (spark.userData.ring) {
|
|
1027
|
+
spark.scale.setScalar(reducedMotion ? 1 : 1 + cycle * 0.6);
|
|
1028
|
+
spark.material.opacity = elapsed > 9000 ? Math.max(0, 0.8 - (elapsed - 9000) / 1500) : reducedMotion ? 0.8 : 0.8 * (1 - cycle);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
spark.position.y = spark.userData.base + (reducedMotion ? 0.6 : cycle * spark.userData.rise);
|
|
1032
|
+
spark.material.opacity = elapsed > 9000 ? Math.max(0, 1 - (elapsed - 9000) / 1500) : reducedMotion ? 0.9 : 0.95 * (1 - cycle);
|
|
1033
|
+
}
|
|
1034
|
+
if (elapsed > 10500) {
|
|
1035
|
+
for (const spark of animated.sparkles) scene.remove(spark);
|
|
1036
|
+
animated.sparkles = [];
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
738
1039
|
if (!reducedMotion) {
|
|
1040
|
+
if (animated.water) animated.water.offset.set((time / 90000) % 1, (time / 140000) % 1);
|
|
1041
|
+
for (const jet of animated.jets) jet.scale.y = 0.85 + 0.2 * Math.sin(time / 260);
|
|
739
1042
|
const wave = (1 + Math.sin(time / 380)) / 2;
|
|
740
1043
|
for (const beam of beacons) beam.material.opacity = 0.35 + 0.3 * wave;
|
|
741
1044
|
for (const ring of glows) {
|
|
@@ -753,6 +1056,13 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
753
1056
|
return {
|
|
754
1057
|
buildPlaza,
|
|
755
1058
|
buildCities,
|
|
1059
|
+
markChanges,
|
|
1060
|
+
/** The sky: 'auto' follows the hour; 'day', 'dusk' or 'night' fix it. Returns the phase shown. */
|
|
1061
|
+
setSky(mode) {
|
|
1062
|
+
skyMode = mode;
|
|
1063
|
+
applySky();
|
|
1064
|
+
return sky.phase;
|
|
1065
|
+
},
|
|
756
1066
|
flyTo,
|
|
757
1067
|
resetView,
|
|
758
1068
|
clearSelection() {
|
|
@@ -799,12 +1109,26 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
799
1109
|
root.append(wrap);
|
|
800
1110
|
let scene = null;
|
|
801
1111
|
|
|
1112
|
+
// The sky follows the hour by default; the viewer can fix it to see the lit windows at night.
|
|
1113
|
+
const SKY_MODES = [['auto', '空:時刻'], ['day', '空:昼'], ['dusk', '空:夕方'], ['night', '空:夜']];
|
|
1114
|
+
let skyIndex = 0;
|
|
1115
|
+
const skyButton = workspaceButton(doc, {
|
|
1116
|
+
text: SKY_MODES[0][1],
|
|
1117
|
+
onClick: () => {
|
|
1118
|
+
skyIndex = (skyIndex + 1) % SKY_MODES.length;
|
|
1119
|
+
skyButton.textContent = SKY_MODES[skyIndex][1];
|
|
1120
|
+
scene?.setSky(SKY_MODES[skyIndex][0]);
|
|
1121
|
+
},
|
|
1122
|
+
});
|
|
802
1123
|
const header = workspacePageHeader(doc, {
|
|
803
1124
|
crumbs: page?.crumbs ?? ['あなたのBrainbase', '世界'],
|
|
804
1125
|
title: '世界',
|
|
805
1126
|
lead: '事業を都市、案件を区画、判断の種類を広場の建物として描いています。見るための画面で、ここからは何も書き換えません。',
|
|
806
1127
|
source: page?.source ?? null,
|
|
807
|
-
actions: [
|
|
1128
|
+
actions: [
|
|
1129
|
+
skyButton,
|
|
1130
|
+
workspaceButton(doc, { text: '全体に戻る', onClick: () => { scene?.clearSelection(); showEmptyRail(); scene?.resetView(); } }),
|
|
1131
|
+
],
|
|
808
1132
|
});
|
|
809
1133
|
const notices = el(doc, 'div', { className: 'bb-world-hud', attrs: { 'aria-live': 'polite' } });
|
|
810
1134
|
const legend = el(doc, 'div', { className: 'bb-world-legend', attrs: { 'aria-label': '凡例' } });
|
|
@@ -916,7 +1240,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
916
1240
|
['分類', kindText],
|
|
917
1241
|
['状態', statusText(business.status)],
|
|
918
1242
|
['コード', business.code],
|
|
919
|
-
['進行中の案件(都市の広さ)', `${measures.open}件${measures.finished ? `(完了・終了${measures.finished}
|
|
1243
|
+
['進行中の案件(都市の広さ)', `${measures.open}件${measures.finished ? `(完了・終了${measures.finished}件は記念公園)` : ''}`],
|
|
920
1244
|
['最近30日の動き(高さ・明かり)', `決定${measures.decisions}件${judgmentText}`],
|
|
921
1245
|
]) }));
|
|
922
1246
|
const ul = el(doc, 'ul', { className: 'bb-world-rail-list' });
|
|
@@ -1072,6 +1396,46 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1072
1396
|
scene = createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear: showEmptyRail });
|
|
1073
1397
|
scene.buildPlaza(rows, rowItems, (row) => (rowItems.get(row.key) ?? []).filter((ref) => isWaiting(proofs.get(ref.decision_attempt_id))).length, Date.now());
|
|
1074
1398
|
scene.buildCities(layoutWorld(businesses), placement.byBusiness);
|
|
1399
|
+
showChangesSinceLastVisit(note, businesses, rows);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* What moved since this viewer last opened the world (a per-viewer convenience kept in this browser;
|
|
1404
|
+
* nothing is lost if the browser forgets it).
|
|
1405
|
+
*/
|
|
1406
|
+
function showChangesSinceLastVisit(note, businesses, rows) {
|
|
1407
|
+
const key = 'brainbase.world.lastVisit';
|
|
1408
|
+
let lastVisit = null;
|
|
1409
|
+
try {
|
|
1410
|
+
lastVisit = globalThis.localStorage?.getItem(key) ?? null;
|
|
1411
|
+
} catch {
|
|
1412
|
+
lastVisit = null;
|
|
1413
|
+
}
|
|
1414
|
+
const changes = changesSince(lastVisit, businesses, placement.byBusiness, rows);
|
|
1415
|
+
try {
|
|
1416
|
+
globalThis.localStorage?.setItem(key, new Date().toISOString());
|
|
1417
|
+
} catch {
|
|
1418
|
+
// Without storage the world simply shows no marks next time.
|
|
1419
|
+
}
|
|
1420
|
+
if (!changes) {
|
|
1421
|
+
note('前回から', '初めての表示です。次に開いたときから、前回から動きがあった所に光の印を出します。');
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const names = new Map(businesses.map((business) => [business.code, business.name]));
|
|
1425
|
+
const cityNames = changes.cities.map((code) => names.get(code) ?? code);
|
|
1426
|
+
const last = new Date(changes.since);
|
|
1427
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
1428
|
+
const when = `${pad(last.getMonth() + 1)}/${pad(last.getDate())} ${pad(last.getHours())}:${pad(last.getMinutes())}`;
|
|
1429
|
+
if (cityNames.length === 0 && changes.plaza.length === 0) {
|
|
1430
|
+
note('前回から', `前回(${when})から、新しい決定や判断はありません。`);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const parts = [
|
|
1434
|
+
cityNames.length ? `都市:${cityNames.join('・')}` : null,
|
|
1435
|
+
changes.plaza.length ? `広場の判断の種類${changes.plaza.length}つ` : null,
|
|
1436
|
+
].filter(Boolean);
|
|
1437
|
+
note('前回から', `前回(${when})から動きがあった所に光の印を出しています。${parts.join('、')}。`, 'attention');
|
|
1438
|
+
scene?.markChanges(changes);
|
|
1075
1439
|
}
|
|
1076
1440
|
|
|
1077
1441
|
void load();
|