@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.
- package/dist/local-web-fetch-bridge.js +1 -1
- package/dist/local-web-host.d.ts +59 -0
- package/dist/local-web-host.js +171 -4
- package/dist/meeting-minutes-http.d.ts +22 -0
- package/dist/meeting-minutes-http.js +252 -0
- package/dist/meeting-minutes-lineage-adapters.d.ts +157 -0
- package/dist/meeting-minutes-lineage-adapters.js +410 -0
- package/dist/meeting-minutes-lineage-http.d.ts +20 -0
- package/dist/meeting-minutes-lineage-http.js +221 -0
- package/dist/meeting-minutes-lineage-lifecycle.d.ts +58 -0
- package/dist/meeting-minutes-lineage-lifecycle.js +146 -0
- package/dist/meeting-minutes-lineage.d.ts +307 -0
- package/dist/meeting-minutes-lineage.js +1030 -0
- package/dist/meeting-minutes-storage-controller.d.ts +182 -0
- package/dist/meeting-minutes-storage-controller.js +366 -0
- package/dist/meeting-minutes-storage-http.d.ts +25 -0
- package/dist/meeting-minutes-storage-http.js +312 -0
- package/dist/meeting-minutes-storage.d.ts +169 -0
- package/dist/meeting-minutes-storage.js +402 -0
- package/dist/meeting-minutes.d.ts +223 -0
- package/dist/meeting-minutes.js +1172 -0
- package/dist/world-extension.js +1 -1
- package/package.json +46 -3
- package/ui/brainbase-tokens.css +35 -35
- package/ui/local-web-shell.css +20 -17
- package/ui/local-web-shell.js +34 -1
- package/ui/meeting-minutes-lineage-http.js +247 -0
- package/ui/meeting-minutes-lineage.css +177 -0
- package/ui/meeting-minutes-lineage.js +485 -0
- package/ui/meeting-minutes-storage.css +170 -0
- package/ui/meeting-minutes-storage.js +388 -0
- package/ui/meeting-minutes.css +215 -0
- package/ui/meeting-minutes.js +622 -0
- package/ui/world/world-district.js +306 -87
- package/ui/world/world-scenery.js +208 -0
- package/ui/world/world-view.css +54 -0
- 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
|
-
|
|
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,
|
|
90
|
-
gradient.addColorStop(0.
|
|
91
|
-
gradient.addColorStop(1,
|
|
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(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
sun.
|
|
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
|
-
|
|
102
|
-
sun.shadow.bias = -0.
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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,
|
|
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
|
-
|
|
194
|
-
group.add(mesh(new THREE.BoxGeometry(
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
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.
|
|
206
|
-
group.add(mesh(new THREE.BoxGeometry(0.2, 0.5, 0.2), material(0x9c7a62), { x: 0.
|
|
207
|
-
group.userData.chimney = new THREE.Vector3(0.
|
|
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
|
-
|
|
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
|
|
288
|
-
const
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
root.add(
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
};
|
|
300
|
-
|
|
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
|
-
|
|
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.
|
|
313
|
-
root.add(mesh(new THREE.BoxGeometry(1.
|
|
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
|
|
318
|
-
const
|
|
319
|
-
const
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
336
|
-
root.add(mesh(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
343
|
-
const hallChimney = new THREE.Vector3(2.8,
|
|
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,
|
|
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.
|
|
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.
|
|
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() {
|