@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
@@ -0,0 +1,208 @@
1
+ /*
2
+ * Scenery shared by the world and the inside of a district: textures, value noise, facades, and the
3
+ * plate and roof shapes. None of it carries data; it only gives both scales the same look.
4
+ */
5
+
6
+ import { THREE } from './world-vendor.js';
7
+
8
+ export function hashUnit(text) {
9
+ let h = 2166136261;
10
+ for (let i = 0; i < text.length; i += 1) {
11
+ h ^= text.charCodeAt(i);
12
+ h = Math.imul(h, 16777619);
13
+ }
14
+ return ((h >>> 0) % 10000) / 10000;
15
+ }
16
+
17
+ export function canvasTexture(doc, width, height, draw) {
18
+ const canvas = doc.createElement('canvas');
19
+ canvas.width = width;
20
+ canvas.height = height;
21
+ draw(canvas.getContext('2d'), width, height);
22
+ const texture = new THREE.CanvasTexture(canvas);
23
+ texture.colorSpace = THREE.SRGBColorSpace;
24
+ return texture;
25
+ }
26
+
27
+ /** Smooth value noise in [0, 1]; used only to shade scenery (ground, sea, mountains) without repeating textures. */
28
+ export function valueNoise(x, z, seed = 'n') {
29
+ const xi = Math.floor(x);
30
+ const zi = Math.floor(z);
31
+ const fx = x - xi;
32
+ const fz = z - zi;
33
+ const sx = fx * fx * (3 - 2 * fx);
34
+ const sz = fz * fz * (3 - 2 * fz);
35
+ const at = (i, j) => hashUnit(`${seed}${i},${j}`);
36
+ const near = at(xi, zi) + (at(xi + 1, zi) - at(xi, zi)) * sx;
37
+ const far = at(xi, zi + 1) + (at(xi + 1, zi + 1) - at(xi, zi + 1)) * sx;
38
+ return near + (far - near) * sz;
39
+ }
40
+
41
+ /** Colours a geometry per vertex from its position (x, y, z before any rotation). */
42
+ export function paintVertices(geometry, colorAt) {
43
+ const position = geometry.attributes.position;
44
+ const colors = new Float32Array(position.count * 3);
45
+ const color = new THREE.Color();
46
+ for (let i = 0; i < position.count; i += 1) {
47
+ colorAt(color, position.getX(i), position.getY(i), position.getZ(i));
48
+ colors[i * 3] = color.r;
49
+ colors[i * 3 + 1] = color.g;
50
+ colors[i * 3 + 2] = color.b;
51
+ }
52
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
53
+ return geometry;
54
+ }
55
+
56
+ /*
57
+ * Facades: one tile of a wall, drawn for a style and a wall colour. Windows are on the walls only (roofs
58
+ * get a plain material). Which panes are lit is fixed per pane, so the glow map matches the colour map;
59
+ * whether a building has lit windows at all still means "moved in the last 30 days".
60
+ */
61
+ export const FACADE_UNITS = Object.freeze({ glass: [2.0, 2.3], office: [3.0, 2.0], brick: [1.3, 1.35], civic: [0.85, 1.5], research: [0.9, 1.2], house: [0, 0] });
62
+
63
+ /** Tile size in pixels: glass and office tiles hold several floors so lit panes do not repeat as a pattern. */
64
+ export const FACADE_SIZES = Object.freeze({ glass: 256, office: 256 });
65
+
66
+ export function drawFacade(ctx, w, h, style, wall, lit, glowOnly) {
67
+ const wallColor = `#${new THREE.Color(wall).getHexString()}`;
68
+ const dark = `#${new THREE.Color(wall).multiplyScalar(0.72).getHexString()}`;
69
+ const light = `#${new THREE.Color(wall).lerp(new THREE.Color(0xffffff), 0.35).getHexString()}`;
70
+ ctx.fillStyle = glowOnly ? '#000000' : wallColor;
71
+ ctx.fillRect(0, 0, w, h);
72
+ const pane = (x, y, pw, ph, index) => {
73
+ const on = lit && hashUnit(`${style}pane${index}x`) < 0.55;
74
+ if (glowOnly) {
75
+ ctx.fillStyle = on ? '#ffffff' : '#000000';
76
+ ctx.fillRect(x, y, pw, ph);
77
+ return;
78
+ }
79
+ if (on) {
80
+ ctx.fillStyle = '#ffd98c';
81
+ ctx.fillRect(x, y, pw, ph);
82
+ ctx.fillStyle = 'rgba(255,244,214,0.55)';
83
+ ctx.fillRect(x, y, pw, ph * 0.3);
84
+ } else {
85
+ const glass = ctx.createLinearGradient(x, y, x + pw, y + ph);
86
+ glass.addColorStop(0, '#9fb3c4');
87
+ glass.addColorStop(0.5, '#56687a');
88
+ glass.addColorStop(1, '#3f4f60');
89
+ ctx.fillStyle = glass;
90
+ ctx.fillRect(x, y, pw, ph);
91
+ }
92
+ };
93
+ if (style === 'glass') {
94
+ // A curtain wall: four floors of four panes, thin mullions, a floor band under each floor.
95
+ const cw = w / 4;
96
+ const rh = h / 4;
97
+ for (let r = 0; r < 4; r += 1) {
98
+ if (!glowOnly) {
99
+ ctx.fillStyle = dark;
100
+ ctx.fillRect(0, r * rh + rh - 7, w, 7);
101
+ }
102
+ for (let c = 0; c < 4; c += 1) pane(c * cw + 4, r * rh + 5, cw - 8, rh - 15, r * 4 + c);
103
+ }
104
+ if (!glowOnly) {
105
+ ctx.fillStyle = light;
106
+ for (let c = 0; c <= 4; c += 1) ctx.fillRect(c * cw - 2, 0, 4, h);
107
+ }
108
+ } else if (style === 'office') {
109
+ // Ribbon windows: two floors, each a long band split by mullions over a concrete spandrel.
110
+ const rh = h / 2;
111
+ for (let r = 0; r < 2; r += 1) {
112
+ if (!glowOnly) {
113
+ ctx.fillStyle = light;
114
+ ctx.fillRect(0, r * rh, w, 22);
115
+ ctx.fillStyle = dark;
116
+ ctx.fillRect(0, r * rh + rh - 10, w, 6);
117
+ }
118
+ for (let c = 0; c < 8; c += 1) pane(4 + c * (w / 8), r * rh + 34, w / 8 - 6, rh - 54, r * 8 + c);
119
+ }
120
+ } else if (style === 'brick') {
121
+ if (!glowOnly) {
122
+ ctx.strokeStyle = 'rgba(0,0,0,0.07)';
123
+ ctx.lineWidth = 2;
124
+ for (let y = 16; y < h; y += 16) {
125
+ ctx.beginPath();
126
+ ctx.moveTo(0, y);
127
+ ctx.lineTo(w, y);
128
+ ctx.stroke();
129
+ }
130
+ }
131
+ for (let c = 0; c < 2; c += 1) {
132
+ const x = 18 + c * 58;
133
+ if (!glowOnly) {
134
+ ctx.fillStyle = '#f5f1e8';
135
+ ctx.fillRect(x - 5, 22, 44, 82);
136
+ }
137
+ pane(x, 26, 34, 74, c);
138
+ if (!glowOnly) {
139
+ ctx.fillStyle = '#f5f1e8';
140
+ ctx.fillRect(x + 15, 26, 4, 74);
141
+ ctx.fillRect(x, 58, 34, 4);
142
+ }
143
+ }
144
+ } else if (style === 'civic') {
145
+ // Pilasters and tall windows.
146
+ if (!glowOnly) {
147
+ ctx.fillStyle = light;
148
+ ctx.fillRect(0, 0, 18, h);
149
+ ctx.fillRect(w - 18, 0, 18, h);
150
+ }
151
+ pane(30, 14, 68, 96, 0);
152
+ if (!glowOnly) {
153
+ ctx.fillStyle = light;
154
+ ctx.fillRect(62, 14, 4, 96);
155
+ }
156
+ } else if (style === 'research') {
157
+ for (let c = 0; c < 2; c += 1) pane(20 + c * 60, 18, 28, 90, c);
158
+ } else if (style === 'house') {
159
+ // A plastered wall with two framed windows (doors are separate meshes).
160
+ for (let c = 0; c < 2; c += 1) {
161
+ const x = 22 + c * 52;
162
+ if (!glowOnly) {
163
+ ctx.fillStyle = '#ffffff';
164
+ ctx.fillRect(x - 4, 36, 40, 50);
165
+ }
166
+ pane(x, 40, 32, 42, c);
167
+ if (!glowOnly) {
168
+ ctx.fillStyle = '#ffffff';
169
+ ctx.fillRect(x + 14, 40, 4, 42);
170
+ ctx.fillStyle = dark;
171
+ ctx.fillRect(x - 6, 86, 44, 5);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ export function roundedPlate(width, depth, height, radius) {
178
+ const shape = new THREE.Shape();
179
+ const w = width / 2;
180
+ const d = depth / 2;
181
+ shape.moveTo(-w + radius, -d);
182
+ shape.lineTo(w - radius, -d);
183
+ shape.quadraticCurveTo(w, -d, w, -d + radius);
184
+ shape.lineTo(w, d - radius);
185
+ shape.quadraticCurveTo(w, d, w - radius, d);
186
+ shape.lineTo(-w + radius, d);
187
+ shape.quadraticCurveTo(-w, d, -w, d - radius);
188
+ shape.lineTo(-w, -d + radius);
189
+ shape.quadraticCurveTo(-w, -d, -w + radius, -d);
190
+ const geometry = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: true, bevelThickness: 0.12, bevelSize: 0.12, bevelSegments: 2 });
191
+ geometry.rotateX(-Math.PI / 2);
192
+ return geometry;
193
+ }
194
+
195
+ export function gableRoof(width, depth, rise) {
196
+ const shape = new THREE.Shape();
197
+ shape.moveTo(-width / 2 - 0.1, 0);
198
+ shape.lineTo(width / 2 + 0.1, 0);
199
+ shape.lineTo(0, rise);
200
+ shape.lineTo(-width / 2 - 0.1, 0);
201
+ const geometry = new THREE.ExtrudeGeometry(shape, { depth: depth + 0.2, bevelEnabled: false });
202
+ geometry.translate(0, 0, -(depth + 0.2) / 2);
203
+ return geometry;
204
+ }
205
+
206
+ export function muted(color, amount) {
207
+ return new THREE.Color(color).lerp(new THREE.Color(0xb9c2bb), amount);
208
+ }
@@ -132,6 +132,60 @@
132
132
  font-size: 13px;
133
133
  }
134
134
 
135
+ /* A city's name is a way into its district (AC-20). */
136
+ .bb-world-label.is-enterable {
137
+ pointer-events: auto;
138
+ cursor: pointer;
139
+ }
140
+
141
+ .bb-world-label.is-enterable::after {
142
+ content: ' ›';
143
+ color: var(--bb-color-muted);
144
+ }
145
+
146
+ .bb-world-label.is-enterable:hover,
147
+ .bb-world-label.is-enterable.is-hover {
148
+ background: #1f5f47;
149
+ border-color: #1f5f47;
150
+ color: #ffffff;
151
+ }
152
+
153
+ .bb-world-label.is-enterable:hover::after,
154
+ .bb-world-label.is-enterable.is-hover::after {
155
+ content: ' 区画に入る ›';
156
+ color: #ffffff;
157
+ }
158
+
159
+ /* With nothing chosen, the rail lists the ways in (AC-19). */
160
+ .bb-world-entrances {
161
+ list-style: none;
162
+ margin: 0;
163
+ padding: 0;
164
+ display: grid;
165
+ gap: 8px;
166
+ }
167
+
168
+ .bb-world-entrance {
169
+ display: flex;
170
+ flex-wrap: wrap;
171
+ align-items: center;
172
+ gap: 4px 10px;
173
+ }
174
+
175
+ .bb-world-entrance-note {
176
+ font-size: 12px;
177
+ color: var(--bb-color-muted);
178
+ }
179
+
180
+ .bb-world-entrance-note.is-check {
181
+ color: #8a5a00;
182
+ font-weight: 600;
183
+ }
184
+
185
+ .bb-world-entrance-note.is-muted {
186
+ color: #6b6b6b;
187
+ }
188
+
135
189
 
136
190
  .bb-world-label.is-sector {
137
191
  background: transparent;
@@ -25,6 +25,7 @@
25
25
  import { THREE, MapControls } from './world-vendor.js';
26
26
  import { changesSince, cityMeasures, districtLots, groupJudgmentPlaces, skyAt, UNPLACED_REASON_TEXT } from './world-placement.js';
27
27
  import { createDistrictView } from './world-district.js';
28
+ import { canvasTexture, drawFacade, FACADE_SIZES, FACADE_UNITS, gableRoof, hashUnit, muted, paintVertices, roundedPlate, valueNoise } from './world-scenery.js';
28
29
  import { WORK_STATES, workCityBlocks, workPickBlock, workSign, workSiteBlocks } from './world-work-rail.js';
29
30
  import {
30
31
  makeWorkspaceElement as el,
@@ -74,15 +75,6 @@ const statusText = (status) => (status ? statusEntry(status)?.label ?? status :
74
75
  const PLAZA_RADIUS = 7;
75
76
  const CITY_RING = 30;
76
77
 
77
- function hashUnit(text) {
78
- let h = 2166136261;
79
- for (let i = 0; i < text.length; i += 1) {
80
- h ^= text.charCodeAt(i);
81
- h = Math.imul(h, 16777619);
82
- }
83
- return ((h >>> 0) % 10000) / 10000;
84
- }
85
-
86
78
  async function readJson(fetcher, path) {
87
79
  try {
88
80
  const response = await fetcher(path);
@@ -189,199 +181,6 @@ const GAP_SHORT_TEXT = Object.freeze({
189
181
  review_overdue: '見直し超過',
190
182
  });
191
183
 
192
- function canvasTexture(doc, width, height, draw) {
193
- const canvas = doc.createElement('canvas');
194
- canvas.width = width;
195
- canvas.height = height;
196
- draw(canvas.getContext('2d'), width, height);
197
- const texture = new THREE.CanvasTexture(canvas);
198
- texture.colorSpace = THREE.SRGBColorSpace;
199
- return texture;
200
- }
201
-
202
- /** Smooth value noise in [0, 1]; used only to shade scenery (ground, sea, mountains) without repeating textures. */
203
- function valueNoise(x, z, seed = 'n') {
204
- const xi = Math.floor(x);
205
- const zi = Math.floor(z);
206
- const fx = x - xi;
207
- const fz = z - zi;
208
- const sx = fx * fx * (3 - 2 * fx);
209
- const sz = fz * fz * (3 - 2 * fz);
210
- const at = (i, j) => hashUnit(`${seed}${i},${j}`);
211
- const near = at(xi, zi) + (at(xi + 1, zi) - at(xi, zi)) * sx;
212
- const far = at(xi, zi + 1) + (at(xi + 1, zi + 1) - at(xi, zi + 1)) * sx;
213
- return near + (far - near) * sz;
214
- }
215
-
216
- /** Colours a geometry per vertex from its position (x, y, z before any rotation). */
217
- function paintVertices(geometry, colorAt) {
218
- const position = geometry.attributes.position;
219
- const colors = new Float32Array(position.count * 3);
220
- const color = new THREE.Color();
221
- for (let i = 0; i < position.count; i += 1) {
222
- colorAt(color, position.getX(i), position.getY(i), position.getZ(i));
223
- colors[i * 3] = color.r;
224
- colors[i * 3 + 1] = color.g;
225
- colors[i * 3 + 2] = color.b;
226
- }
227
- geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
228
- return geometry;
229
- }
230
-
231
- /*
232
- * Facades: one tile of a wall, drawn for a style and a wall colour. Windows are on the walls only (roofs
233
- * get a plain material). Which panes are lit is fixed per pane, so the glow map matches the colour map;
234
- * whether a building has lit windows at all still means "moved in the last 30 days".
235
- */
236
- const FACADE_UNITS = Object.freeze({ glass: [2.0, 2.3], office: [3.0, 2.0], brick: [1.3, 1.35], civic: [0.85, 1.5], research: [0.9, 1.2], house: [0, 0] });
237
-
238
- /** Tile size in pixels: glass and office tiles hold several floors so lit panes do not repeat as a pattern. */
239
- const FACADE_SIZES = Object.freeze({ glass: 256, office: 256 });
240
-
241
- function drawFacade(ctx, w, h, style, wall, lit, glowOnly) {
242
- const wallColor = `#${new THREE.Color(wall).getHexString()}`;
243
- const dark = `#${new THREE.Color(wall).multiplyScalar(0.72).getHexString()}`;
244
- const light = `#${new THREE.Color(wall).lerp(new THREE.Color(0xffffff), 0.35).getHexString()}`;
245
- ctx.fillStyle = glowOnly ? '#000000' : wallColor;
246
- ctx.fillRect(0, 0, w, h);
247
- const pane = (x, y, pw, ph, index) => {
248
- const on = lit && hashUnit(`${style}pane${index}x`) < 0.55;
249
- if (glowOnly) {
250
- ctx.fillStyle = on ? '#ffffff' : '#000000';
251
- ctx.fillRect(x, y, pw, ph);
252
- return;
253
- }
254
- if (on) {
255
- ctx.fillStyle = '#ffd98c';
256
- ctx.fillRect(x, y, pw, ph);
257
- ctx.fillStyle = 'rgba(255,244,214,0.55)';
258
- ctx.fillRect(x, y, pw, ph * 0.3);
259
- } else {
260
- const glass = ctx.createLinearGradient(x, y, x + pw, y + ph);
261
- glass.addColorStop(0, '#9fb3c4');
262
- glass.addColorStop(0.5, '#56687a');
263
- glass.addColorStop(1, '#3f4f60');
264
- ctx.fillStyle = glass;
265
- ctx.fillRect(x, y, pw, ph);
266
- }
267
- };
268
- if (style === 'glass') {
269
- // A curtain wall: four floors of four panes, thin mullions, a floor band under each floor.
270
- const cw = w / 4;
271
- const rh = h / 4;
272
- for (let r = 0; r < 4; r += 1) {
273
- if (!glowOnly) {
274
- ctx.fillStyle = dark;
275
- ctx.fillRect(0, r * rh + rh - 7, w, 7);
276
- }
277
- for (let c = 0; c < 4; c += 1) pane(c * cw + 4, r * rh + 5, cw - 8, rh - 15, r * 4 + c);
278
- }
279
- if (!glowOnly) {
280
- ctx.fillStyle = light;
281
- for (let c = 0; c <= 4; c += 1) ctx.fillRect(c * cw - 2, 0, 4, h);
282
- }
283
- } else if (style === 'office') {
284
- // Ribbon windows: two floors, each a long band split by mullions over a concrete spandrel.
285
- const rh = h / 2;
286
- for (let r = 0; r < 2; r += 1) {
287
- if (!glowOnly) {
288
- ctx.fillStyle = light;
289
- ctx.fillRect(0, r * rh, w, 22);
290
- ctx.fillStyle = dark;
291
- ctx.fillRect(0, r * rh + rh - 10, w, 6);
292
- }
293
- for (let c = 0; c < 8; c += 1) pane(4 + c * (w / 8), r * rh + 34, w / 8 - 6, rh - 54, r * 8 + c);
294
- }
295
- } else if (style === 'brick') {
296
- if (!glowOnly) {
297
- ctx.strokeStyle = 'rgba(0,0,0,0.07)';
298
- ctx.lineWidth = 2;
299
- for (let y = 16; y < h; y += 16) {
300
- ctx.beginPath();
301
- ctx.moveTo(0, y);
302
- ctx.lineTo(w, y);
303
- ctx.stroke();
304
- }
305
- }
306
- for (let c = 0; c < 2; c += 1) {
307
- const x = 18 + c * 58;
308
- if (!glowOnly) {
309
- ctx.fillStyle = '#f5f1e8';
310
- ctx.fillRect(x - 5, 22, 44, 82);
311
- }
312
- pane(x, 26, 34, 74, c);
313
- if (!glowOnly) {
314
- ctx.fillStyle = '#f5f1e8';
315
- ctx.fillRect(x + 15, 26, 4, 74);
316
- ctx.fillRect(x, 58, 34, 4);
317
- }
318
- }
319
- } else if (style === 'civic') {
320
- // Pilasters and tall windows.
321
- if (!glowOnly) {
322
- ctx.fillStyle = light;
323
- ctx.fillRect(0, 0, 18, h);
324
- ctx.fillRect(w - 18, 0, 18, h);
325
- }
326
- pane(30, 14, 68, 96, 0);
327
- if (!glowOnly) {
328
- ctx.fillStyle = light;
329
- ctx.fillRect(62, 14, 4, 96);
330
- }
331
- } else if (style === 'research') {
332
- for (let c = 0; c < 2; c += 1) pane(20 + c * 60, 18, 28, 90, c);
333
- } else if (style === 'house') {
334
- // A plastered wall with two framed windows (doors are separate meshes).
335
- for (let c = 0; c < 2; c += 1) {
336
- const x = 22 + c * 52;
337
- if (!glowOnly) {
338
- ctx.fillStyle = '#ffffff';
339
- ctx.fillRect(x - 4, 36, 40, 50);
340
- }
341
- pane(x, 40, 32, 42, c);
342
- if (!glowOnly) {
343
- ctx.fillStyle = '#ffffff';
344
- ctx.fillRect(x + 14, 40, 4, 42);
345
- ctx.fillStyle = dark;
346
- ctx.fillRect(x - 6, 86, 44, 5);
347
- }
348
- }
349
- }
350
- }
351
-
352
- function roundedPlate(width, depth, height, radius) {
353
- const shape = new THREE.Shape();
354
- const w = width / 2;
355
- const d = depth / 2;
356
- shape.moveTo(-w + radius, -d);
357
- shape.lineTo(w - radius, -d);
358
- shape.quadraticCurveTo(w, -d, w, -d + radius);
359
- shape.lineTo(w, d - radius);
360
- shape.quadraticCurveTo(w, d, w - radius, d);
361
- shape.lineTo(-w + radius, d);
362
- shape.quadraticCurveTo(-w, d, -w, d - radius);
363
- shape.lineTo(-w, -d + radius);
364
- shape.quadraticCurveTo(-w, -d, -w + radius, -d);
365
- const geometry = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: true, bevelThickness: 0.12, bevelSize: 0.12, bevelSegments: 2 });
366
- geometry.rotateX(-Math.PI / 2);
367
- return geometry;
368
- }
369
-
370
- function gableRoof(width, depth, rise) {
371
- const shape = new THREE.Shape();
372
- shape.moveTo(-width / 2 - 0.1, 0);
373
- shape.lineTo(width / 2 + 0.1, 0);
374
- shape.lineTo(0, rise);
375
- shape.lineTo(-width / 2 - 0.1, 0);
376
- const geometry = new THREE.ExtrudeGeometry(shape, { depth: depth + 0.2, bevelEnabled: false });
377
- geometry.translate(0, 0, -(depth + 0.2) / 2);
378
- return geometry;
379
- }
380
-
381
- function muted(color, amount) {
382
- return new THREE.Color(color).lerp(new THREE.Color(0xb9c2bb), amount);
383
- }
384
-
385
184
  function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear, onEscape }) {
386
185
  const renderer = new THREE.WebGLRenderer({ antialias: true });
387
186
  renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio ?? 1, 2));
@@ -1097,6 +896,13 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear,
1097
896
  cityIndex.set(business.code, { city, group, label: null });
1098
897
  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 });
1099
898
  cityLabel.node.style.borderColor = `${entry.color}80`;
899
+ // The name is a way in too (AC-20): pressing it enters the district, as pressing the city does.
900
+ cityLabel.node.classList.add('is-enterable');
901
+ cityLabel.node.title = `${business.name}の区画に入る`;
902
+ cityLabel.node.addEventListener('click', () => {
903
+ select(group);
904
+ onPick(group.userData, new THREE.Box3().setFromObject(group).getCenter(new THREE.Vector3()));
905
+ });
1100
906
  cityIndex.get(business.code).label = cityLabel;
1101
907
  cityLabel.business = business;
1102
908
  const n = business.engagements.length;
@@ -1150,6 +956,12 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear,
1150
956
  let selected = null;
1151
957
  let hovered = null;
1152
958
 
959
+ /** The name label of a city group, to say "enter" next to it while the pointer is on the city. */
960
+ function labelOfCity(group) {
961
+ if (group?.userData?.kind !== 'city') return null;
962
+ return cityIndex.get(group.userData.business.code)?.label ?? null;
963
+ }
964
+
1153
965
  function setHighlight(group, on) {
1154
966
  for (const entry of highlightable.get(group) ?? []) {
1155
967
  if (on) {
@@ -1204,6 +1016,7 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear,
1204
1016
  });
1205
1017
  renderer.domElement.addEventListener('pointerleave', () => {
1206
1018
  pendingHover = null;
1019
+ labelOfCity(hovered)?.node.classList.remove('is-hover');
1207
1020
  if (hovered) setHighlight(hovered, false);
1208
1021
  hovered = null;
1209
1022
  });
@@ -1478,8 +1291,10 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear,
1478
1291
  pendingHover = null;
1479
1292
  if (group !== hovered) {
1480
1293
  if (hovered) setHighlight(hovered, false);
1294
+ labelOfCity(hovered)?.node.classList.remove('is-hover');
1481
1295
  hovered = group;
1482
1296
  if (hovered) setHighlight(hovered, true);
1297
+ labelOfCity(hovered)?.node.classList.add('is-hover');
1483
1298
  renderer.domElement.style.cursor = hovered ? 'pointer' : '';
1484
1299
  }
1485
1300
  }
@@ -1634,6 +1449,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
1634
1449
  skyIndex = (skyIndex + 1) % SKY_MODES.length;
1635
1450
  skyButton.textContent = SKY_MODES[skyIndex][1];
1636
1451
  scene?.setSky(SKY_MODES[skyIndex][0]);
1452
+ district?.setSky(SKY_MODES[skyIndex][0]);
1637
1453
  },
1638
1454
  });
1639
1455
  const header = workspacePageHeader(doc, {
@@ -1672,8 +1488,48 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
1672
1488
  if (!rail) return;
1673
1489
  rail.replaceChildren(...nodes);
1674
1490
  }
1675
- const showEmptyRail = () => showRail([workspaceDetailEmpty(doc, { mark: '◇', title: '都市や建物を選ぶと、ここに出ます', text: 'ドラッグで移動、ホイールで寄る・離れる、右ドラッグで回転します。Escで全体に戻ります。' })]);
1676
- showEmptyRail();
1491
+ /**
1492
+ * With nothing chosen, the rail lists the ways in (AC-19): one button per business, the ones with the
1493
+ * most work to check first. The count orders the list; it is not a score.
1494
+ */
1495
+ let workSummaries = {};
1496
+ const showEmptyRail = () => {
1497
+ if (!knownBusinesses.length) {
1498
+ showRail([workspaceDetailEmpty(doc, { mark: '◇', title: '都市や建物を選ぶと、ここに出ます', text: 'ドラッグで移動、ホイールで寄る・離れる、右ドラッグで回転します。Escで全体に戻ります。' })]);
1499
+ return;
1500
+ }
1501
+ const noteOf = (business) => {
1502
+ const summary = workSummaries[business.code];
1503
+ if (!summary) return { text: workConnected === false ? '' : '仕事は区画で読みます', count: -1 };
1504
+ if (summary.state !== 'complete' && summary.state !== 'partial') return { text: '仕事を読めない(0件ではありません)', count: -1, muted: true };
1505
+ const needs = summary.needs_check?.length ?? 0;
1506
+ const parts = [needs ? `要確認 ${needs}` : `未完了 ${summary.open ?? 0}`];
1507
+ if (summary.state === 'partial') parts.push('一部だけ読めた');
1508
+ return { text: parts.join('・'), count: needs };
1509
+ };
1510
+ const rows = knownBusinesses
1511
+ .map((business) => ({ business, note: noteOf(business) }))
1512
+ .sort((a, b) => b.note.count - a.note.count || a.business.name.localeCompare(b.business.name, 'ja'));
1513
+ const list = el(doc, 'ul', { className: 'bb-world-entrances' });
1514
+ for (const { business, note } of rows) {
1515
+ const item = el(doc, 'li', { className: 'bb-world-entrance' });
1516
+ const button = workspaceButton(doc, { text: `${business.name}の区画に入る ›`, onClick: () => enterFromRail(business) });
1517
+ item.append(button);
1518
+ if (note.text) item.append(el(doc, 'span', { className: `bb-world-entrance-note${note.count > 0 ? ' is-check' : ''}${note.muted ? ' is-muted' : ''}`, text: note.text }));
1519
+ list.append(item);
1520
+ }
1521
+ showRail([
1522
+ workspaceRailHead(doc, { kicker: '区画の入口', title: 'どの区画に入りますか', lead: '下のボタン、または地図の都市(建物や名前)を押すと、その事業の区画に入ります。Escで一つ上に戻ります。' }),
1523
+ workspaceRailBlock(doc, { title: '事業', content: list }),
1524
+ ]);
1525
+ };
1526
+ // The businesses are not known yet here (the list is drawn again once the world is built).
1527
+ showRail([workspaceDetailEmpty(doc, { mark: '◇', title: '都市や建物を選ぶと、ここに出ます', text: 'ドラッグで移動、ホイールで寄る・離れる、右ドラッグで回転します。Escで全体に戻ります。' })]);
1528
+
1529
+ function enterFromRail(business) {
1530
+ const picked = scene?.selectKey({ code: business.code });
1531
+ openCity(business, picked?.center ?? null);
1532
+ }
1677
1533
 
1678
1534
  let proofs = new Map();
1679
1535
  let placement = { byBusiness: new Map(), unplaced: [] };
@@ -1866,6 +1722,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
1866
1722
  },
1867
1723
  onEscape: goUp,
1868
1724
  });
1725
+ district.setSky(SKY_MODES[skyIndex][0]);
1869
1726
  scene.setPaused(true);
1870
1727
  legend.hidden = true;
1871
1728
  workLegend.hidden = true;
@@ -2012,6 +1869,8 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
2012
1869
  }
2013
1870
  workConnected = true;
2014
1871
  const summaries = result.data.businesses ?? {};
1872
+ workSummaries = summaries;
1873
+ if (current.level === 'world' && !current.code) showEmptyRail();
2015
1874
  scene?.setWorkSigns(summaries);
2016
1875
  const entries = Object.values(summaries);
2017
1876
  const readable = entries.filter((entry) => entry.state === 'complete' || entry.state === 'partial');
@@ -2059,7 +1918,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
2059
1918
  workspaceRailHead(doc, { kicker: `${business.name} の案件`, title: data.engagement.name, lead: data.engagement.summary ?? undefined }),
2060
1919
  workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['状態', statusText(data.engagement.status)], ['Graph ID', data.engagement.id]]) }),
2061
1920
  detailBlock(data.engagement, '「プロジェクトと関係者」でこの案件を開く'),
2062
- workspaceRailBlock(doc, { title: '', content: workspaceButton(doc, { text: `${business.name}の仕事と関係を見る`, onClick: () => openCity(business, position) }) }),
1921
+ workspaceRailBlock(doc, { title: '', content: workspaceButton(doc, { text: `${business.name}の区画に入る`, variant: 'primary', onClick: () => openCity(business, position) }) }),
2063
1922
  cityJudgmentBlock(business),
2064
1923
  ]);
2065
1924
  return;
@@ -2209,6 +2068,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
2209
2068
  scene.buildCities(layoutWorld(businesses), placement.byBusiness);
2210
2069
  showChangesSinceLastVisit(note, businesses, rows);
2211
2070
  knownBusinesses = businesses;
2071
+ if (!current.code) showEmptyRail();
2212
2072
  restoreSelection();
2213
2073
  void loadWorkSummary(note);
2214
2074
  }