@unson/brainbase-mcp 0.21.1 → 0.22.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.
Files changed (37) hide show
  1. package/dist/local-web-fetch-bridge.js +1 -1
  2. package/dist/local-web-host.d.ts +59 -0
  3. package/dist/local-web-host.js +171 -4
  4. package/dist/meeting-minutes-http.d.ts +22 -0
  5. package/dist/meeting-minutes-http.js +252 -0
  6. package/dist/meeting-minutes-lineage-adapters.d.ts +157 -0
  7. package/dist/meeting-minutes-lineage-adapters.js +410 -0
  8. package/dist/meeting-minutes-lineage-http.d.ts +20 -0
  9. package/dist/meeting-minutes-lineage-http.js +221 -0
  10. package/dist/meeting-minutes-lineage-lifecycle.d.ts +58 -0
  11. package/dist/meeting-minutes-lineage-lifecycle.js +146 -0
  12. package/dist/meeting-minutes-lineage.d.ts +307 -0
  13. package/dist/meeting-minutes-lineage.js +1030 -0
  14. package/dist/meeting-minutes-storage-controller.d.ts +182 -0
  15. package/dist/meeting-minutes-storage-controller.js +366 -0
  16. package/dist/meeting-minutes-storage-http.d.ts +25 -0
  17. package/dist/meeting-minutes-storage-http.js +312 -0
  18. package/dist/meeting-minutes-storage.d.ts +169 -0
  19. package/dist/meeting-minutes-storage.js +402 -0
  20. package/dist/meeting-minutes.d.ts +223 -0
  21. package/dist/meeting-minutes.js +1172 -0
  22. package/dist/world-extension.js +1 -1
  23. package/package.json +46 -3
  24. package/ui/brainbase-tokens.css +35 -35
  25. package/ui/local-web-shell.css +20 -17
  26. package/ui/local-web-shell.js +34 -1
  27. package/ui/meeting-minutes-lineage-http.js +247 -0
  28. package/ui/meeting-minutes-lineage.css +177 -0
  29. package/ui/meeting-minutes-lineage.js +485 -0
  30. package/ui/meeting-minutes-storage.css +170 -0
  31. package/ui/meeting-minutes-storage.js +388 -0
  32. package/ui/meeting-minutes.css +215 -0
  33. package/ui/meeting-minutes.js +622 -0
  34. package/ui/world/world-district.js +306 -87
  35. package/ui/world/world-scenery.js +208 -0
  36. package/ui/world/world-view.css +54 -0
  37. package/ui/world/world-view.js +65 -205
@@ -14,7 +14,8 @@
14
14
  */
15
15
 
16
16
  import { THREE, MapControls } from './world-vendor.js';
17
- import { DISTRICT_STREETS, districtStreetLots } from './world-placement.js';
17
+ import { DISTRICT_STREETS, districtStreetLots, skyAt } from './world-placement.js';
18
+ import { canvasTexture, drawFacade, FACADE_SIZES, FACADE_UNITS, gableRoof, hashUnit, paintVertices, roundedPlate, valueNoise } from './world-scenery.js';
18
19
  import { makeWorkspaceElement as el } from '../../workspace-kit.js';
19
20
 
20
21
  export const WORLD_DISTRICT_CONTRACT_VERSION = 'brainbase.world-district.v0';
@@ -48,16 +49,6 @@ export const DISTRICT_LEGEND = Object.freeze([
48
49
  ['is-ambience', '通りを歩く人と煙=街の雰囲気(記録とは関係しません)'],
49
50
  ]);
50
51
 
51
- function canvasTexture(doc, width, height, draw) {
52
- const canvas = doc.createElement('canvas');
53
- canvas.width = width;
54
- canvas.height = height;
55
- draw(canvas.getContext('2d'), width, height);
56
- const texture = new THREE.CanvasTexture(canvas);
57
- texture.colorSpace = THREE.SRGBColorSpace;
58
- return texture;
59
- }
60
-
61
52
  export function createDistrictView({ doc, stage, reducedMotion = false, onPick, onClear, onEscape }) {
62
53
  const renderer = new THREE.WebGLRenderer({ antialias: true });
63
54
  renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio ?? 1, 2));
@@ -83,24 +74,54 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
83
74
  notice.hidden = true;
84
75
  stage.append(notice);
85
76
 
77
+ // The same sky, light and exposure as the world (scenery: it carries no data), following the hour or
78
+ // the sky the viewer fixed in the world.
86
79
  const scene = new THREE.Scene();
87
- scene.background = canvasTexture(doc, 16, 256, (ctx, w, h) => {
80
+ let skyMode = 'auto';
81
+ let sky = skyAt(new Date().getHours(), skyMode);
82
+ const skyTexture = (current) => canvasTexture(doc, 256, 256, (ctx, w, h) => {
88
83
  const gradient = ctx.createLinearGradient(0, 0, 0, h);
89
- gradient.addColorStop(0, '#a9cdea');
90
- gradient.addColorStop(0.6, '#e2eef0');
91
- gradient.addColorStop(1, '#f3efe3');
84
+ gradient.addColorStop(0, current.top);
85
+ gradient.addColorStop(0.55, current.middle);
86
+ gradient.addColorStop(1, current.bottom);
92
87
  ctx.fillStyle = gradient;
93
88
  ctx.fillRect(0, 0, w, h);
89
+ if (current.stars) {
90
+ ctx.fillStyle = 'rgba(255,255,255,0.85)';
91
+ for (let i = 0; i < 90; i += 1) ctx.fillRect(hashUnit(`star${i}x`) * w, hashUnit(`star${i}y`) * h * 0.55, 1.2, 1.2);
92
+ }
94
93
  });
95
- scene.fog = new THREE.Fog(0xe7efee, 70, 170);
96
- scene.add(new THREE.HemisphereLight(0xf6f9ff, 0xd8ccb2, 1.25));
97
- const sun = new THREE.DirectionalLight(0xfff0d8, 2.4);
98
- sun.position.set(30, 55, 22);
94
+ scene.fog = new THREE.Fog(sky.fog, 70, 170);
95
+ const hemisphere = new THREE.HemisphereLight(0xf4f8ff, 0xd8ccb2, sky.hemi);
96
+ scene.add(hemisphere);
97
+ const sun = new THREE.DirectionalLight(sky.sun, sky.sunIntensity);
98
+ sun.position.set(34, 58, 24);
99
99
  sun.castShadow = true;
100
100
  sun.shadow.mapSize.set(2048, 2048);
101
- Object.assign(sun.shadow.camera, { left: -45, right: 45, top: 45, bottom: -45, near: 1, far: 160 });
102
- sun.shadow.bias = -0.0005;
101
+ sun.shadow.radius = 4;
102
+ sun.shadow.bias = -0.0004;
103
+ sun.shadow.normalBias = 0.03;
104
+ Object.assign(sun.shadow.camera, { left: -48, right: 48, top: 48, bottom: -48, near: 1, far: 170 });
103
105
  scene.add(sun);
106
+ const fill = new THREE.DirectionalLight(0xdfe9ff, 0.6);
107
+ fill.position.set(-40, 30, -30);
108
+ scene.add(fill);
109
+ // Lit windows and street lamps follow the sky (brightest at night); their meaning does not change.
110
+ const litMaterials = new Set();
111
+ const lampMaterials = new Set();
112
+ function applySky() {
113
+ sky = skyAt(new Date().getHours(), skyMode);
114
+ scene.background = skyTexture(sky);
115
+ scene.fog.color.set(sky.fog);
116
+ hemisphere.intensity = sky.hemi;
117
+ sun.color.set(sky.sun);
118
+ sun.intensity = sky.sunIntensity;
119
+ renderer.toneMappingExposure = sky.exposure;
120
+ for (const mat of litMaterials) mat.emissiveIntensity = sky.glow;
121
+ for (const mat of lampMaterials) mat.emissiveIntensity = sky.lamps ? 1.8 : 0;
122
+ }
123
+ applySky();
124
+ const maxAnisotropy = renderer.capabilities.getMaxAnisotropy?.() ?? 1;
104
125
 
105
126
  const camera = new THREE.PerspectiveCamera(40, 1, 0.5, 600);
106
127
  const controls = new MapControls(camera, renderer.domElement);
@@ -122,6 +143,150 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
122
143
  };
123
144
  const box = (w, h, d, color, at = {}, options = {}) => mesh(new THREE.BoxGeometry(w, h, d), material(color, options), { y: h / 2, ...at });
124
145
 
146
+ // Facades as in the world: windows drawn on the walls, glowing at dusk and night when the building is lit.
147
+ // Textures are shared; materials are made per lot, so fading one lot never fades another.
148
+ const facadeCache = new Map();
149
+ function facadeTiles(style, wall, lit) {
150
+ const key = `${style}|${new THREE.Color(wall).getHexString()}|${lit}`;
151
+ if (!facadeCache.has(key)) {
152
+ const make = (glowOnly) => {
153
+ const size = FACADE_SIZES[style] ?? 128;
154
+ const texture = canvasTexture(doc, size, size, (ctx, w, h) => drawFacade(ctx, w, h, style, wall, lit, glowOnly));
155
+ texture.wrapS = THREE.RepeatWrapping;
156
+ texture.wrapT = THREE.RepeatWrapping;
157
+ texture.anisotropy = maxAnisotropy;
158
+ return texture;
159
+ };
160
+ facadeCache.set(key, { map: make(false), glow: lit ? make(true) : null });
161
+ }
162
+ return facadeCache.get(key);
163
+ }
164
+ function facadeMaterial(style, wall, lit, span, h) {
165
+ const tiles = facadeTiles(style, wall, lit);
166
+ const [unitW, unitH] = FACADE_UNITS[style] ?? [1, 1];
167
+ const repeat = unitW === 0 ? [1, Math.max(1, Math.round(h / 1.1))] : [Math.max(1, Math.round(span / unitW)), Math.max(1, Math.round(h / unitH))];
168
+ const map = tiles.map.clone();
169
+ map.needsUpdate = true;
170
+ map.repeat.set(...repeat);
171
+ const mat = material(0xffffff, { map, roughness: 0.85 });
172
+ if (tiles.glow) {
173
+ const glow = tiles.glow.clone();
174
+ glow.needsUpdate = true;
175
+ glow.repeat.copy(map.repeat);
176
+ mat.emissive = new THREE.Color(0xffc867);
177
+ mat.emissiveMap = glow;
178
+ mat.emissiveIntensity = sky.glow;
179
+ litMaterials.add(mat);
180
+ }
181
+ return mat;
182
+ }
183
+ /** A block with facades on its four walls and a plain roof, standing on y = 0. */
184
+ function block(w, h, d, { style = 'office', wall = 0xe9e4da, lit = false, roof = 0xc9c5bb } = {}) {
185
+ const alongX = facadeMaterial(style, wall, lit, d, h);
186
+ const alongZ = facadeMaterial(style, wall, lit, w, h);
187
+ const top = material(roof, { roughness: 0.95 });
188
+ return mesh(new THREE.BoxGeometry(w, h, d), [alongX, alongX, top, top, alongZ, alongZ], { y: h / 2 });
189
+ }
190
+
191
+ // Trees and street lamps are many; like the world, they are instanced (scenery, no data).
192
+ function plantTrees(parent, placements) {
193
+ if (!placements.length) return;
194
+ const conifers = [];
195
+ const broadleaves = [];
196
+ for (const placement of placements) (hashUnit(`${placement[0]},${placement[1]}k`) < 0.42 ? conifers : broadleaves).push(placement);
197
+ const foliage = () => material(0xffffff, { roughness: 0.9, flatShading: true });
198
+ const trunks = new THREE.InstancedMesh(new THREE.CylinderGeometry(0.1, 0.15, 0.8, 6), material(0x7d6249, { roughness: 1 }), placements.length);
199
+ const lower = new THREE.InstancedMesh(new THREE.ConeGeometry(0.78, 1.3, 7), foliage(), Math.max(conifers.length, 1));
200
+ const upper = new THREE.InstancedMesh(new THREE.ConeGeometry(0.55, 1.05, 7), foliage(), Math.max(conifers.length, 1));
201
+ const crowns = new THREE.InstancedMesh(new THREE.IcosahedronGeometry(0.72, 0), foliage(), Math.max(broadleaves.length, 1));
202
+ const matrix = new THREE.Matrix4();
203
+ const rotation = new THREE.Quaternion();
204
+ const tint = new THREE.Color();
205
+ const up = new THREE.Vector3(0, 1, 0);
206
+ const place = (instanced, index, x, y, z, sx, sy, sz, spin) => {
207
+ rotation.setFromAxisAngle(up, spin);
208
+ matrix.compose(new THREE.Vector3(x, y, z), rotation, new THREE.Vector3(sx, sy, sz));
209
+ instanced.setMatrixAt(index, matrix);
210
+ };
211
+ placements.forEach(([x, z, scale], index) => place(trunks, index, x, 0.4 * scale, z, scale, scale, scale, 0));
212
+ conifers.forEach(([x, z, scale], index) => {
213
+ const spin = hashUnit(`${x}${z}s`) * Math.PI;
214
+ const tall = 0.9 + hashUnit(`${z}${x}t`) * 0.35;
215
+ place(lower, index, x, 1.15 * scale * tall, z, scale, scale * tall, scale, spin);
216
+ place(upper, index, x, 1.85 * scale * tall, z, scale, scale * tall, scale, spin);
217
+ const shade = tint.setHSL(0.37 + (hashUnit(`${x},${z}`) - 0.5) * 0.04, 0.34, 0.3 + hashUnit(`${z},${x}`) * 0.08);
218
+ lower.setColorAt(index, shade);
219
+ upper.setColorAt(index, shade.offsetHSL(0, 0, 0.04));
220
+ });
221
+ broadleaves.forEach(([x, z, scale], index) => {
222
+ const squash = 0.85 + hashUnit(`${x}${z}q`) * 0.25;
223
+ place(crowns, index, x, 1.25 * scale, z, scale * squash, scale, scale * (1.9 - squash), hashUnit(`${x}${z}s`) * Math.PI);
224
+ crowns.setColorAt(index, tint.setHSL(0.24 + hashUnit(`${x},${z}h`) * 0.08, 0.38, 0.4 + hashUnit(`${z},${x}l`) * 0.12));
225
+ });
226
+ lower.count = conifers.length;
227
+ upper.count = conifers.length;
228
+ crowns.count = broadleaves.length;
229
+ for (const instanced of [trunks, lower, upper, crowns]) {
230
+ instanced.castShadow = true;
231
+ instanced.receiveShadow = true;
232
+ parent.add(instanced);
233
+ }
234
+ }
235
+ function placeLamps(parent, placements) {
236
+ if (!placements.length) return;
237
+ const metal = material(0x3f4844, { roughness: 0.6, metalness: 0.4 });
238
+ const poles = new THREE.InstancedMesh(new THREE.CylinderGeometry(0.05, 0.07, 1.9, 6), metal, placements.length);
239
+ const heads = new THREE.InstancedMesh(new THREE.BoxGeometry(0.26, 0.08, 0.26), metal, placements.length);
240
+ const bulbMaterial = material(0xfff3d6, { emissive: new THREE.Color(0xffd27a), emissiveIntensity: sky.lamps ? 1.8 : 0 });
241
+ lampMaterials.add(bulbMaterial);
242
+ const bulbs = new THREE.InstancedMesh(new THREE.SphereGeometry(0.11, 10, 8), bulbMaterial, placements.length);
243
+ const matrix = new THREE.Matrix4();
244
+ placements.forEach(([x, z], index) => {
245
+ poles.setMatrixAt(index, matrix.makeTranslation(x, 0.95, z));
246
+ heads.setMatrixAt(index, matrix.makeTranslation(x, 1.92, z));
247
+ bulbs.setMatrixAt(index, matrix.makeTranslation(x, 1.82, z));
248
+ });
249
+ for (const instanced of [poles, heads, bulbs]) {
250
+ instanced.castShadow = instanced !== bulbs;
251
+ parent.add(instanced);
252
+ }
253
+ }
254
+ const dashTexture = canvasTexture(doc, 8, 64, (ctx, w, h) => {
255
+ ctx.clearRect(0, 0, w, h);
256
+ ctx.fillStyle = '#ffffff';
257
+ ctx.fillRect(1, 0, w - 2, h * 0.55);
258
+ });
259
+ dashTexture.wrapT = THREE.RepeatWrapping;
260
+ /** A street along z (or along x when `across`): sidewalks, asphalt, and a dashed centre line if wide. */
261
+ function streetStrip(parent, { x = 0, z = 0, width, length, across = false, centreLine = false }) {
262
+ const strip = (stripWidth, mat, y) => {
263
+ const surface = mesh(new THREE.PlaneGeometry(stripWidth, length), mat, { x, y, z, shadow: false });
264
+ surface.rotation.set(-Math.PI / 2, 0, across ? Math.PI / 2 : 0);
265
+ parent.add(surface);
266
+ };
267
+ strip(width + 0.7, material(0xe4dfd2, { roughness: 1 }), 0.025);
268
+ strip(width, material(0x8e938f, { roughness: 0.95 }), 0.04);
269
+ if (centreLine) {
270
+ const dashes = dashTexture.clone();
271
+ dashes.needsUpdate = true;
272
+ dashes.repeat.set(1, Math.max(1, Math.round(length / 1.6)));
273
+ strip(0.1, new THREE.MeshBasicMaterial({ map: dashes, transparent: true, opacity: 0.85, color: 0xf6f3e6 }), 0.055);
274
+ }
275
+ }
276
+ // A tarp, folded: vertical bands of light and shade on the blue sheet (the colour is the meaning).
277
+ const tarpTexture = canvasTexture(doc, 64, 64, (ctx, w, h) => {
278
+ ctx.fillStyle = '#ffffff';
279
+ ctx.fillRect(0, 0, w, h);
280
+ for (let i = 0; i < 8; i += 1) {
281
+ ctx.fillStyle = i % 2 ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.0)';
282
+ ctx.fillRect((i * w) / 8, 0, w / 8, h);
283
+ }
284
+ ctx.fillStyle = 'rgba(0,0,0,0.12)';
285
+ ctx.fillRect(0, h * 0.48, w, 2);
286
+ });
287
+ tarpTexture.wrapS = THREE.RepeatWrapping;
288
+ tarpTexture.wrapT = THREE.RepeatWrapping;
289
+
125
290
  let root = null;
126
291
  let lots = new Map();
127
292
  let labels = [];
@@ -161,7 +326,9 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
161
326
  const overdue = GAP(site, 'review_overdue');
162
327
  const fade = (color) => (overdue ? new THREE.Color(color).lerp(new THREE.Color(0xb9b29a), 0.45) : new THREE.Color(color));
163
328
  const half = DISTRICT_STREETS.lot / 2;
164
- group.add(box(DISTRICT_STREETS.lot, 0.06, DISTRICT_STREETS.lot, overdue ? 0xc9c0a2 : 0xd9d4c4));
329
+ // The lot: a rounded plate, gravel on a site and lawn round a finished house (faded when overdue).
330
+ const groundColor = site.work.status === 'completed' ? 0xb7cf9f : 0xd9d2bf;
331
+ group.add(mesh(roundedPlate(DISTRICT_STREETS.lot, DISTRICT_STREETS.lot, 0.08, 0.32), material(fade(groundColor), { roughness: 1 })));
165
332
  const status = site.work.status;
166
333
  const building = status === 'in_progress' || status === 'waiting';
167
334
  if (status !== 'completed') {
@@ -181,34 +348,57 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
181
348
  if (building) {
182
349
  const floors = 1 + Math.floor(hash(`${site.task_id}f`) * 2);
183
350
  const height = 0.75 * floors;
184
- group.add(box(1.7, height, 1.5, fade(STATE_WALL[status]), { z: -0.25 }));
185
- // Scaffolding: a frame of edges around the building.
186
- const frame = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(2.0, height + 0.6, 1.8)), new THREE.LineBasicMaterial({ color: 0x8c8f8a }));
187
- frame.position.set(0, (height + 0.6) / 2, -0.25);
188
- group.add(frame);
351
+ // The building rising: walls with their windows (unlit, no one has moved in), concrete on top.
352
+ const core = block(1.7, height, 1.5, { style: hash(`${site.task_id}s`) < 0.5 ? 'office' : 'brick', wall: fade(STATE_WALL[status]).getHex(), roof: 0xa9a59b });
353
+ core.position.z = -0.25;
354
+ group.add(core);
355
+ // Scaffolding: poles at the corners and along the faces, a plank at each floor.
356
+ const steel = material(0x9a9c97, { roughness: 0.5, metalness: 0.5 });
357
+ const plank = material(0xb88f5a, { roughness: 0.9 });
358
+ const scaffoldH = height + 0.55;
359
+ for (const x of [-1.0, -0.33, 0.33, 1.0]) {
360
+ for (const z of [-1.15, 0.65]) group.add(mesh(new THREE.CylinderGeometry(0.03, 0.03, scaffoldH, 5), steel, { x, y: scaffoldH / 2, z }));
361
+ }
362
+ // Planks only where work goes on; a held site is wrapped in its tarp.
363
+ for (let f = 1; status === 'in_progress' && f <= floors; f += 1) {
364
+ for (const z of [-1.15, 0.65]) group.add(mesh(new THREE.BoxGeometry(2.05, 0.04, 0.24), plank, { y: f * 0.75, z }));
365
+ for (const x of [-1.0, 1.0]) group.add(mesh(new THREE.BoxGeometry(0.24, 0.04, 1.8), plank, { x, y: f * 0.75, z: -0.25 }));
366
+ }
189
367
  if (status === 'in_progress') {
190
- const net = mesh(new THREE.PlaneGeometry(2.0, height + 0.5), material(0x4f9a5c, { transparent: true, opacity: 0.55, side: THREE.DoubleSide }), { y: (height + 0.5) / 2, z: 0.66, shadow: false });
368
+ const net = mesh(new THREE.PlaneGeometry(2.0, scaffoldH - 0.05), material(0x4f9a5c, { transparent: true, opacity: 0.55, side: THREE.DoubleSide }), { y: scaffoldH / 2, z: 0.7, shadow: false });
191
369
  group.add(net);
192
- // A crane over a site being worked.
193
- group.add(mesh(new THREE.BoxGeometry(0.12, 3.6, 0.12), material(0xe0a526), { x: 0.9, y: 1.8, z: -0.9 }));
194
- group.add(mesh(new THREE.BoxGeometry(2.2, 0.1, 0.1), material(0xe0a526), { x: 0.2, y: 3.5, z: -0.9 }));
370
+ // A tower crane over a site being worked: a mast, a jib and its counterweight.
371
+ const yellow = material(0xe0a526, { roughness: 0.6 });
372
+ group.add(mesh(new THREE.BoxGeometry(0.14, 3.8, 0.14), yellow, { x: 0.95, y: 1.9, z: -1.05 }));
373
+ group.add(mesh(new THREE.BoxGeometry(2.6, 0.1, 0.12), yellow, { x: 0.35, y: 3.7, z: -1.05 }));
374
+ group.add(mesh(new THREE.BoxGeometry(0.34, 0.3, 0.3), material(0x6b6b66), { x: 1.55, y: 3.55, z: -1.05 }));
375
+ group.add(mesh(new THREE.CylinderGeometry(0.008, 0.008, 1.4, 4), material(0x333333), { x: -0.7, y: 3.0, z: -1.05, shadow: false }));
195
376
  } else {
196
- // Waiting: a blue tarp over the building, as on a site where work is held.
197
- group.add(box(1.9, height + 0.4, 1.7, fade(0x3f78b5), { z: -0.25 }, { transparent: true, opacity: 0.9, roughness: 0.6 }));
377
+ // Waiting: a blue tarp over the building and its scaffold, tied down, as on a site where work is held.
378
+ const tarp = tarpTexture.clone();
379
+ tarp.needsUpdate = true;
380
+ tarp.repeat.set(2, 1);
381
+ group.add(box(2.15, scaffoldH, 1.95, fade(0x4f93db), { z: -0.25 }, { map: tarp, transparent: true, opacity: 0.95, roughness: 0.5, emissive: fade(0x1d4f8a), emissiveIntensity: 0.35 }));
382
+ const rope = material(0xf2efe6);
383
+ for (const x of [-1.08, 1.08]) group.add(mesh(new THREE.BoxGeometry(0.02, 0.02, 2.0), rope, { x, y: scaffoldH * 0.55, z: -0.25, shadow: false }));
198
384
  }
199
385
  }
200
386
  if (status === 'completed') {
201
- group.add(box(1.6, 1.0, 1.4, 0xf2ebdd, { z: -0.3 }));
202
- const roof = mesh(new THREE.ConeGeometry(1.25, 0.7, 4), material(0xb5654a), { y: 1.35, z: -0.3 });
203
- roof.rotation.y = Math.PI / 4;
387
+ // A finished house: plastered walls with lit windows, a gable roof, a door to the street.
388
+ const house = block(1.7, 1.05, 1.4, { style: 'house', wall: 0xf2ebdd, lit: true, roof: 0xd8d0c2 });
389
+ house.position.z = -0.3;
390
+ group.add(house);
391
+ const roof = mesh(gableRoof(1.7, 1.4, 0.7), material(0xb5654a, { roughness: 0.75 }), { y: 1.05, z: -0.3 });
392
+ roof.rotation.y = Math.PI / 2;
204
393
  group.add(roof);
205
- group.add(mesh(new THREE.BoxGeometry(0.5, 0.36, 0.04), material(0xffd98c, { emissive: new THREE.Color(0xffc867), emissiveIntensity: 0.9 }), { y: 0.55, z: 0.41, shadow: false }));
206
- group.add(mesh(new THREE.BoxGeometry(0.2, 0.5, 0.2), material(0x9c7a62), { x: 0.4, y: 1.5, z: -0.6 }));
207
- group.userData.chimney = new THREE.Vector3(0.4, 1.8, -0.6);
394
+ group.add(mesh(new THREE.BoxGeometry(0.34, 0.6, 0.04), material(0x8a5a3c), { y: 0.3, z: 0.41 }));
395
+ group.add(mesh(new THREE.BoxGeometry(0.2, 0.5, 0.2), material(0x9c7a62), { x: 0.45, y: 1.6, z: -0.6 }));
396
+ group.userData.chimney = new THREE.Vector3(0.45, 1.9, -0.6);
208
397
  }
209
398
  // A path to the street only when the record links its source.
210
399
  if (!GAP(site, 'source_unlinked') && !GAP(site, 'outcome_unlinked')) {
211
- group.add(mesh(new THREE.BoxGeometry(0.7, 0.04, 1.0), material(0xbdb7aa), { y: 0.05, z: half + 0.45, shadow: false }));
400
+ const stones = material(0xcfc8b8, { roughness: 1 });
401
+ for (let i = 0; i < 3; i += 1) group.add(mesh(roundedPlate(0.6, 0.26, 0.03, 0.08), stones, { x: (i % 2 ? 0.06 : -0.06), y: 0.04, z: half + 0.18 + i * 0.32, shadow: false }));
212
402
  }
213
403
  // A blueprint stands where the completion condition is, with no record of what was made.
214
404
  if (GAP(site, 'outcome_unlinked')) {
@@ -262,6 +452,8 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
262
452
  // --- the district -------------------------------------------------------------------------------
263
453
  function clear() {
264
454
  if (root) scene.remove(root);
455
+ litMaterials.clear();
456
+ lampMaterials.clear();
265
457
  for (const label of labels) label.node.remove();
266
458
  labels = [];
267
459
  lots = new Map();
@@ -284,79 +476,100 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
284
476
  const L = layout.length;
285
477
  const gateZ = L / 2 + 4;
286
478
  const hallZ = -L / 2 - 7;
287
- // Land, the district's paving, the main street and the back streets.
288
- const land = mesh(new THREE.PlaneGeometry(260, 260), material(0xa9c595, { roughness: 1 }), { shadow: false });
479
+ // Land as in the world: gentle, non-repeating shades of grass (scenery).
480
+ const grass = { low: new THREE.Color(0x9fbf8c), high: new THREE.Color(0xd3e4bd), warm: new THREE.Color(0xd9d6a6) };
481
+ const land = mesh(paintVertices(new THREE.CircleGeometry(150, 96, 0, Math.PI * 2), (color, x, y) => {
482
+ color.copy(grass.low).lerp(grass.high, valueNoise(x / 16, y / 16, 'dland') * 0.7 + valueNoise(x / 4.5, y / 4.5, 'dtuft') * 0.3);
483
+ if (valueNoise(x / 22, y / 22, 'ddry') > 0.68) color.lerp(grass.warm, 0.35);
484
+ }), material(0xffffff, { vertexColors: true, roughness: 1 }), { shadow: false });
289
485
  land.rotation.x = -Math.PI / 2;
486
+ land.receiveShadow = true;
290
487
  root.add(land);
291
- const paving = mesh(new THREE.PlaneGeometry(26, L + 22), material(0xe9e4d6, { roughness: 1 }), { y: 0.01, z: (gateZ + hallZ) / 2, shadow: false });
292
- paving.rotation.x = -Math.PI / 2;
293
- root.add(paving);
294
- const asphalt = material(0x8e938f, { roughness: 0.95 });
295
- const street = (x, width, from, to) => {
296
- const strip = mesh(new THREE.PlaneGeometry(width, Math.abs(to - from)), asphalt, { x, y: 0.02, z: (from + to) / 2, shadow: false });
297
- strip.rotation.x = -Math.PI / 2;
298
- root.add(strip);
299
- };
300
- street(0, DISTRICT_STREETS.main * 2, gateZ + 8, hallZ + 3);
301
- street(-DISTRICT_STREETS.backStreet, 2, L / 2 + 1, -L / 2 - 1);
302
- street(DISTRICT_STREETS.backStreet, 2, L / 2 + 1, -L / 2 - 1);
488
+ // The district's paving: a raised plate with a kerb, from the gate to the hall.
489
+ const pavingDepth = gateZ - hallZ + 14;
490
+ root.add(mesh(roundedPlate(28, pavingDepth, 0.04, 2.2), material(0xd4cec0, { roughness: 1 }), { z: (gateZ + hallZ) / 2, shadow: false }));
491
+ root.add(mesh(roundedPlate(27, pavingDepth - 1, 0.06, 1.8), material(0xebe6d8, { roughness: 1 }), { z: (gateZ + hallZ) / 2, shadow: false }));
492
+ const lift = new THREE.Group();
493
+ lift.position.y = 0.17;
494
+ root.add(lift);
495
+ streetStrip(lift, { x: 0, z: (gateZ + 8 + hallZ + 3) / 2, width: DISTRICT_STREETS.main * 2, length: gateZ + 8 - (hallZ + 3), centreLine: true });
496
+ streetStrip(lift, { x: -DISTRICT_STREETS.backStreet, z: 0, width: 1.6, length: L + 2 });
497
+ streetStrip(lift, { x: DISTRICT_STREETS.backStreet, z: 0, width: 1.6, length: L + 2 });
303
498
  // A cross street between the blocks, and at the hall end of each block a sign saying what its work is for.
304
499
  const crossWidth = DISTRICT_STREETS.backLot * 2 + 4;
305
500
  layout.streets.forEach((block, index) => {
306
501
  if (index < layout.streets.length - 1) {
307
- const strip = mesh(new THREE.PlaneGeometry(crossWidth, DISTRICT_STREETS.cross * 0.7), asphalt, { y: 0.02, z: block.z_to + DISTRICT_STREETS.cross / 2, shadow: false });
308
- strip.rotation.x = -Math.PI / 2;
309
- root.add(strip);
502
+ streetStrip(lift, { x: 0, z: block.z_to + DISTRICT_STREETS.cross / 2, width: DISTRICT_STREETS.cross * 0.62, length: crossWidth, across: true });
310
503
  }
311
504
  const signZ = block.z_from - (index === 0 ? 0.9 : DISTRICT_STREETS.cross / 2);
312
- root.add(mesh(new THREE.CylinderGeometry(0.07, 0.07, 2.4, 6), material(0x4a4a4a), { x: -DISTRICT_STREETS.main - 0.5, y: 1.2, z: signZ }));
313
- root.add(mesh(new THREE.BoxGeometry(1.9, 0.5, 0.08), material(block.label ? 0x2f6e4f : 0xbfb8a8), { x: -DISTRICT_STREETS.main - 0.5, y: 2.3, z: signZ }));
505
+ root.add(mesh(new THREE.CylinderGeometry(0.06, 0.08, 2.5, 8), material(0x3f4844, { roughness: 0.6, metalness: 0.4 }), { x: -DISTRICT_STREETS.main - 0.5, y: 1.25, z: signZ }));
506
+ root.add(mesh(new THREE.BoxGeometry(1.1, 0.34, 0.05), material(block.label ? 0x2f6e4f : 0xbfb8a8, { roughness: 0.6 }), { x: -DISTRICT_STREETS.main - 0.5, y: 2.2, z: signZ }));
314
507
  const text = block.label ? `${block.label}(${block.count}件)` : `未分類の空き地:何のための仕事か未記録(${block.count}件)`;
315
508
  addLabel(text, new THREE.Vector3(-DISTRICT_STREETS.main - 0.5, 2.9, signZ), `is-street${block.label ? '' : ' is-unlabelled'}`, { priority: 2 });
316
509
  });
317
- // Trees and lamps along the main street, between the lots (scenery).
318
- const trunk = material(0x7d6249);
319
- const crown = material(0x6f9a5c, { flatShading: true });
510
+ // Trees along the main street and lamps between them; a belt of trees round the district (scenery).
511
+ const trees = [];
512
+ const lamps = [];
320
513
  for (let z = -L / 2; z <= L / 2; z += DISTRICT_STREETS.row) {
321
- for (const x of [-2.4, 2.4]) {
322
- const at = z + DISTRICT_STREETS.row / 2;
323
- if (at > L / 2) continue;
324
- root.add(mesh(new THREE.CylinderGeometry(0.08, 0.1, 0.8, 6), trunk, { x, y: 0.4, z: at }));
325
- root.add(mesh(new THREE.IcosahedronGeometry(0.55, 0), crown, { x, y: 1.15, z: at }));
326
- }
514
+ const at = z + DISTRICT_STREETS.row / 2;
515
+ if (at > L / 2) continue;
516
+ for (const x of [-2.45, 2.45]) trees.push([x, at, 0.62]);
517
+ for (const x of [-2.45, 2.45]) lamps.push([x, at + DISTRICT_STREETS.row / 2]);
327
518
  }
328
- // The gate.
329
- const stone = material(0xe2dccf);
330
- root.add(box(0.7, 4.2, 0.7, 0xe2dccf, { x: -3.2, z: gateZ }));
331
- root.add(box(0.7, 4.2, 0.7, 0xe2dccf, { x: 3.2, z: gateZ }));
332
- root.add(mesh(new THREE.BoxGeometry(7.6, 0.6, 0.9), stone, { y: 4.4, z: gateZ }));
519
+ for (let i = 0; i < 90; i += 1) {
520
+ const angle = (i / 90) * Math.PI * 2 + hashUnit(`belt${i}a`) * 0.05;
521
+ const rx = 17 + hashUnit(`belt${i}r`) * 9;
522
+ const rz = pavingDepth / 2 + 3 + hashUnit(`belt${i}z`) * 8;
523
+ trees.push([Math.cos(angle) * rx, (gateZ + hallZ) / 2 + Math.sin(angle) * rz, 0.8 + hashUnit(`belt${i}s`) * 0.6]);
524
+ }
525
+ plantTrees(root, trees.filter(([x, z]) => !(Math.abs(x) < 4.2 && z > gateZ - 1)));
526
+ placeLamps(lift, lamps);
527
+ // The gate: stone pillars on plinths, a lintel with a cornice, lamps on its pillars.
528
+ const stone = material(0xe2dccf, { roughness: 0.9 });
529
+ for (const x of [-3.3, 3.3]) {
530
+ root.add(mesh(roundedPlate(1.2, 1.2, 0.3, 0.12), stone, { x, z: gateZ }));
531
+ root.add(mesh(new THREE.BoxGeometry(0.72, 4.0, 0.72), stone, { x, y: 2.3, z: gateZ }));
532
+ root.add(mesh(new THREE.BoxGeometry(0.9, 0.18, 0.9), stone, { x, y: 4.35, z: gateZ }));
533
+ }
534
+ root.add(mesh(new THREE.BoxGeometry(7.8, 0.55, 0.95), stone, { y: 4.65, z: gateZ }));
535
+ root.add(mesh(new THREE.BoxGeometry(8.2, 0.16, 1.1), material(0xcfc8ba, { roughness: 0.9 }), { y: 5.0, z: gateZ }));
536
+ placeLamps(root, [[-3.3, gateZ + 0.6], [3.3, gateZ + 0.6]]);
333
537
  addLabel(`${business.name}の区画(入口)`, new THREE.Vector3(0, 5.2, gateZ), 'is-city', { priority: 1 });
334
538
  // The hall: what this business is for, and the stones of its policies in front of it.
335
- root.add(box(8, 3.6, 4.4, 0xeee8dc, { z: hallZ }));
336
- root.add(mesh(new THREE.BoxGeometry(8.6, 0.3, 5.0), stone, { y: 3.75, z: hallZ }));
337
- for (let i = 0; i < 6; i += 1) root.add(mesh(new THREE.CylinderGeometry(0.16, 0.18, 3.0, 10), material(0xf7f4ee), { x: -3.1 + i * 1.24, y: 1.5, z: hallZ + 2.5 }));
338
- const pediment = mesh(new THREE.ConeGeometry(4.9, 1.2, 4), material(0xf7f4ee), { y: 4.5, z: hallZ });
339
- pediment.rotation.y = Math.PI / 4;
340
- pediment.scale.z = 0.6;
539
+ // A civic building as in the world: steps, a colonnade, tall lit windows, a pediment.
540
+ for (let step = 0; step < 3; step += 1) root.add(mesh(roundedPlate(9.6 - step * 0.5, 6.2 - step * 0.4, 0.1, 0.25), stone, { y: step * 0.18, z: hallZ + 0.2 }));
541
+ const hall = block(8, 3.4, 4.2, { style: 'civic', wall: 0xeee8dc, lit: true, roof: 0xd9d2c4 });
542
+ hall.position.set(0, 0.54, hallZ);
543
+ root.add(hall);
544
+ root.add(mesh(new THREE.BoxGeometry(8.6, 0.3, 4.9), stone, { y: 4.08, z: hallZ }));
545
+ for (let i = 0; i < 6; i += 1) {
546
+ const x = -3.1 + i * 1.24;
547
+ root.add(mesh(new THREE.CylinderGeometry(0.16, 0.19, 3.0, 14), material(0xf7f4ee, { roughness: 0.7 }), { x, y: 2.04, z: hallZ + 2.45 }));
548
+ root.add(mesh(new THREE.BoxGeometry(0.46, 0.12, 0.46), stone, { x, y: 3.6, z: hallZ + 2.45 }));
549
+ }
550
+ const pediment = mesh(gableRoof(8.6, 4.9, 1.15), material(0xf7f4ee, { roughness: 0.8 }), { y: 4.23, z: hallZ });
341
551
  root.add(pediment);
342
- root.add(box(0.4, 0.9, 0.4, 0x9c7a62, { x: 2.8, y: 3.9, z: hallZ - 1.2 }));
343
- const hallChimney = new THREE.Vector3(2.8, 4.8, hallZ - 1.2);
552
+ root.add(box(0.4, 0.9, 0.4, 0x9c7a62, { x: 2.8, y: 4.3, z: hallZ - 1.2 }));
553
+ const hallChimney = new THREE.Vector3(2.8, 5.2, hallZ - 1.2);
344
554
  const purposeText = work?.status === 'ok' ? (work.purpose.text ?? '目的は未登録') : '目的を読めません';
345
555
  addLabel(`庁舎:${business.name}(${purposeText})`, new THREE.Vector3(0, 5.6, hallZ), 'is-plaza', { priority: 1 });
346
- const plaza = mesh(new THREE.CircleGeometry(3.6, 40), material(0xf1ece1), { y: 0.03, z: hallZ + 5.4, shadow: false });
556
+ const plaza = mesh(new THREE.CircleGeometry(3.6, 48), material(0xf1ece1, { roughness: 1 }), { y: 0.2, z: hallZ + 5.4, shadow: false });
347
557
  plaza.rotation.x = -Math.PI / 2;
348
558
  root.add(plaza);
559
+ const ring = mesh(new THREE.RingGeometry(3.6, 3.85, 48), material(0xcfc8b8, { roughness: 1 }), { y: 0.21, z: hallZ + 5.4, shadow: false });
560
+ ring.rotation.x = -Math.PI / 2;
561
+ root.add(ring);
349
562
  const policies = work?.status === 'ok' ? work.purpose.policies.slice(0, 8) : [];
350
563
  policies.forEach((policy, index) => {
351
564
  const angle = Math.PI * (0.15 + (0.7 * index) / Math.max(policies.length - 1, 1));
352
565
  const x = Math.cos(angle) * 2.8;
353
566
  const z = hallZ + 5.4 + Math.sin(angle) * 1.6;
354
567
  if (policy.link === 'recorded') {
355
- root.add(box(0.5, 1.2, 0.22, 0xa8a49a, { x, z }));
568
+ root.add(box(0.5, 1.2, 0.22, 0xa8a49a, { x, y: 0.8, z }, { roughness: 0.9 }));
356
569
  } else {
357
570
  const outline = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(0.5, 1.2, 0.22)), new THREE.LineDashedMaterial({ color: 0x5b4a8a, dashSize: 0.12, gapSize: 0.08 }));
358
571
  outline.computeLineDistances();
359
- outline.position.set(x, 0.6, z);
572
+ outline.position.set(x, 0.8, z);
360
573
  root.add(outline);
361
574
  }
362
575
  const title = policy.title.length > 18 ? `${policy.title.slice(0, 17)}…` : policy.title;
@@ -381,7 +594,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
381
594
  for (const site of shown) {
382
595
  const lot = layout.lots[site.task_id];
383
596
  const group = lotGroup(site);
384
- group.position.set(lot.x, 0, lot.z);
597
+ group.position.set(lot.x, 0.17, lot.z);
385
598
  group.rotation.y = lot.facing > 0 ? Math.PI / 2 : -Math.PI / 2;
386
599
  group.userData = { kind: 'site', site, chimney: group.userData.chimney ?? null };
387
600
  root.add(group);
@@ -415,6 +628,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
415
628
  }
416
629
  }
417
630
  // Fog when the work could not be read: the district cannot be seen, which is not empty.
631
+ scene.fog.color.set(sky.fog);
418
632
  scene.fog.near = readable ? 70 : 6;
419
633
  scene.fog.far = readable ? 170 : 34;
420
634
  notice.hidden = readable;
@@ -489,7 +703,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
489
703
  selectionRing.visible = false;
490
704
  return;
491
705
  }
492
- selectionRing.position.set(group.position.x, 0.09, group.position.z);
706
+ selectionRing.position.set(group.position.x, 0.3, group.position.z);
493
707
  selectionRing.visible = true;
494
708
  const at = group.position.clone();
495
709
  fly(new THREE.Vector3(at.x, 0, at.z), new THREE.Vector3(at.x + (at.x > 0 ? 9 : -9), 11, at.z + 12));
@@ -574,7 +788,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
574
788
  const forward = travel < 1;
575
789
  walker.position.set(x, 0, forward ? from + travel * span : to - (travel - 1) * span);
576
790
  walker.rotation.y = forward ? 0 : Math.PI;
577
- walker.position.y = Math.abs(Math.sin(seconds * 8 * speed)) * 0.04;
791
+ walker.position.y = 0.2 + Math.abs(Math.sin(seconds * 8 * speed)) * 0.04;
578
792
  }
579
793
  for (const puff of puffs) {
580
794
  const cycle = (seconds * 0.25 + puff.userData.phase) % 1;
@@ -613,6 +827,11 @@ export function createDistrictView({ doc, stage, reducedMotion = false, onPick,
613
827
  hide() {
614
828
  setVisible(false);
615
829
  },
830
+ /** The sky: the same modes as the world ('auto' follows the hour). */
831
+ setSky(mode) {
832
+ skyMode = mode;
833
+ applySky();
834
+ },
616
835
  isOpen: () => open,
617
836
  /** Back to the view from the gate, keeping the district. */
618
837
  home() {