@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
|
@@ -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
|
+
}
|
package/ui/world/world-view.css
CHANGED
|
@@ -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;
|
package/ui/world/world-view.js
CHANGED
|
@@ -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
|
-
|
|
1676
|
-
|
|
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}
|
|
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
|
}
|