@unson/brainbase-mcp 0.20.0 → 0.21.1
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/canonical-task-service.d.ts +4 -0
- package/dist/canonical-task-service.js +14 -0
- package/dist/graph-foundation-reader.d.ts +1 -1
- package/dist/graph-foundation-reader.js +1 -0
- package/dist/world-extension.d.ts +1 -1
- package/dist/world-extension.js +10 -1
- package/dist/world-work.d.ts +248 -0
- package/dist/world-work.js +558 -0
- package/package.json +2 -2
- package/ui/world/world-district.js +642 -0
- package/ui/world/world-placement.js +60 -0
- package/ui/world/world-view.css +237 -0
- package/ui/world/world-view.js +546 -47
- package/ui/world/world-work-rail.js +337 -0
package/ui/world/world-view.js
CHANGED
|
@@ -12,10 +12,20 @@
|
|
|
12
12
|
* in the plaza; nothing is assigned to a city by guesswork (P9). The world
|
|
13
13
|
* never writes. Selecting something opens its facts in the rail and hands off
|
|
14
14
|
* to the existing screens.
|
|
15
|
+
*
|
|
16
|
+
* Inside a city (story-world-work-sites-and-gaps-v1) the host's Canonical Tasks
|
|
17
|
+
* stand as small work sites around the plate, read only when the city is
|
|
18
|
+
* opened. A site's wall is its recorded state; a sign says something about it
|
|
19
|
+
* cannot be seen from the records (an empty assignee field, no source link, a
|
|
20
|
+
* review date passed); a dashed ring says the text names a person the assignee
|
|
21
|
+
* field does not. Above the cities, a sign counts the open work with such a
|
|
22
|
+
* gap, and a grey sign says the work could not be read (never zero).
|
|
15
23
|
*/
|
|
16
24
|
|
|
17
25
|
import { THREE, MapControls } from './world-vendor.js';
|
|
18
26
|
import { changesSince, cityMeasures, districtLots, groupJudgmentPlaces, skyAt, UNPLACED_REASON_TEXT } from './world-placement.js';
|
|
27
|
+
import { createDistrictView } from './world-district.js';
|
|
28
|
+
import { WORK_STATES, workCityBlocks, workPickBlock, workSign, workSiteBlocks } from './world-work-rail.js';
|
|
19
29
|
import {
|
|
20
30
|
makeWorkspaceElement as el,
|
|
21
31
|
workspacePageHeader,
|
|
@@ -169,7 +179,15 @@ function webglAvailable(doc) {
|
|
|
169
179
|
// ---------------------------------------------------------------------------
|
|
170
180
|
|
|
171
181
|
const RECENT_MS = 24 * 60 * 60 * 1000;
|
|
172
|
-
const LABEL_PRIORITY = Object.freeze({ selected: 0, plaza: 1, city: 2, sector: 3, judgment: 4, engagement: 5 });
|
|
182
|
+
const LABEL_PRIORITY = Object.freeze({ selected: 0, plaza: 1, city: 2, sector: 3, judgment: 4, engagement: 5, site: 6 });
|
|
183
|
+
const WORK_COLORS = Object.freeze(Object.fromEntries(WORK_STATES.map((state) => [state.key, state.color])));
|
|
184
|
+
const GAP_SHORT_TEXT = Object.freeze({
|
|
185
|
+
assignee_unlinked_mentioned: '担当欄に未接続(本文に人物)',
|
|
186
|
+
assignee_unrecorded: '担当の記録なし',
|
|
187
|
+
outcome_unlinked: '成果物の記録が未接続',
|
|
188
|
+
source_unlinked: '出典リンクなし',
|
|
189
|
+
review_overdue: '見直し超過',
|
|
190
|
+
});
|
|
173
191
|
|
|
174
192
|
function canvasTexture(doc, width, height, draw) {
|
|
175
193
|
const canvas = doc.createElement('canvas');
|
|
@@ -364,7 +382,7 @@ function muted(color, amount) {
|
|
|
364
382
|
return new THREE.Color(color).lerp(new THREE.Color(0xb9c2bb), amount);
|
|
365
383
|
}
|
|
366
384
|
|
|
367
|
-
function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }) {
|
|
385
|
+
function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear, onEscape }) {
|
|
368
386
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
369
387
|
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio ?? 1, 2));
|
|
370
388
|
renderer.shadowMap.enabled = true;
|
|
@@ -1042,6 +1060,8 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
1042
1060
|
}
|
|
1043
1061
|
|
|
1044
1062
|
const markers = { cities: new Map(), plaza: new Map() };
|
|
1063
|
+
/** code → { city (layout), group, label } */
|
|
1064
|
+
const cityIndex = new Map();
|
|
1045
1065
|
function buildCities({ cities, sectors, extent }, judgmentsByBusiness = new Map()) {
|
|
1046
1066
|
fitExtent(extent);
|
|
1047
1067
|
const landRadius = buildTerrain(extent);
|
|
@@ -1074,8 +1094,10 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
1074
1094
|
register(group, { kind: 'city', business });
|
|
1075
1095
|
const top = entry.form === 'tower' ? towerHeight * 1.45 + 1.8 : towerHeight + 1;
|
|
1076
1096
|
markers.cities.set(business.code, new THREE.Vector3(x, top, z));
|
|
1097
|
+
cityIndex.set(business.code, { city, group, label: null });
|
|
1077
1098
|
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 });
|
|
1078
1099
|
cityLabel.node.style.borderColor = `${entry.color}80`;
|
|
1100
|
+
cityIndex.get(business.code).label = cityLabel;
|
|
1079
1101
|
cityLabel.business = business;
|
|
1080
1102
|
const n = business.engagements.length;
|
|
1081
1103
|
const step = 2.5;
|
|
@@ -1152,7 +1174,8 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
1152
1174
|
const sizeVector = box3.getSize(new THREE.Vector3());
|
|
1153
1175
|
const radius = Math.max(sizeVector.x, sizeVector.z) * 0.62 + 0.4;
|
|
1154
1176
|
selectionRing.scale.set(radius, radius, 1);
|
|
1155
|
-
|
|
1177
|
+
const ringY = group.userData.kind === 'judgment' ? 0.76 : group.userData.kind === 'site' ? 0.1 : 0.6;
|
|
1178
|
+
selectionRing.position.set((box3.min.x + box3.max.x) / 2, ringY, (box3.min.z + box3.max.z) / 2);
|
|
1156
1179
|
selectionRing.visible = true;
|
|
1157
1180
|
}
|
|
1158
1181
|
|
|
@@ -1197,11 +1220,8 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
1197
1220
|
}
|
|
1198
1221
|
});
|
|
1199
1222
|
renderer.domElement.addEventListener('keydown', (event) => {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
onClear();
|
|
1203
|
-
resetView();
|
|
1204
|
-
}
|
|
1223
|
+
// Escape goes one level up; the view decides what that level is.
|
|
1224
|
+
if (event.key === 'Escape') onEscape();
|
|
1205
1225
|
});
|
|
1206
1226
|
|
|
1207
1227
|
let flight = null;
|
|
@@ -1223,6 +1243,130 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
1223
1243
|
flyTo(home.target, home.zoom);
|
|
1224
1244
|
}
|
|
1225
1245
|
|
|
1246
|
+
// --- the work inside a city (sites, signs, roads between businesses) -------
|
|
1247
|
+
const signTextures = new Map();
|
|
1248
|
+
function signTexture(kind) {
|
|
1249
|
+
if (!signTextures.has(kind)) {
|
|
1250
|
+
signTextures.set(kind, canvasTexture(doc, 64, 64, (ctx, w, h) => {
|
|
1251
|
+
ctx.fillStyle = kind === 'unreadable' ? '#8a9096' : '#e3a21a';
|
|
1252
|
+
ctx.fillRect(0, 0, w, h);
|
|
1253
|
+
ctx.fillStyle = '#ffffff';
|
|
1254
|
+
ctx.font = 'bold 44px sans-serif';
|
|
1255
|
+
ctx.textAlign = 'center';
|
|
1256
|
+
ctx.textBaseline = 'middle';
|
|
1257
|
+
ctx.fillText(kind === 'unreadable' ? '…' : '?', w / 2, h / 2 + 2);
|
|
1258
|
+
}));
|
|
1259
|
+
}
|
|
1260
|
+
return signTextures.get(kind);
|
|
1261
|
+
}
|
|
1262
|
+
/** A small sign on a pole: amber '?' (something cannot be seen) or grey '…' (could not be read). */
|
|
1263
|
+
function sign(kind, height = 1.2, boardSize = 0.5) {
|
|
1264
|
+
const group = new THREE.Group();
|
|
1265
|
+
group.add(mesh(new THREE.CylinderGeometry(0.035, 0.045, height, 6), standard(0x4b524e), { y: height / 2 }));
|
|
1266
|
+
const face = new THREE.MeshBasicMaterial({ map: signTexture(kind) });
|
|
1267
|
+
const edge = standard(0xf3efe6);
|
|
1268
|
+
group.add(mesh(new THREE.BoxGeometry(boardSize, boardSize, 0.06), [edge, edge, edge, edge, face, face], { y: height + boardSize / 2 - 0.05 }));
|
|
1269
|
+
return group;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const citySigns = [];
|
|
1273
|
+
/** Signs over the cities from the work summary; a city without one has no gap, or its work is not connected. */
|
|
1274
|
+
function setWorkSigns(summaries) {
|
|
1275
|
+
for (const post of citySigns.splice(0)) scene.remove(post);
|
|
1276
|
+
for (const [code, entry] of cityIndex) {
|
|
1277
|
+
const summary = summaries?.[code];
|
|
1278
|
+
entry.label?.node.querySelector('.bb-world-badge')?.remove();
|
|
1279
|
+
if (entry.label) entry.label.size = null;
|
|
1280
|
+
const signal = workSign(summary);
|
|
1281
|
+
if (!signal) continue;
|
|
1282
|
+
const { text } = signal;
|
|
1283
|
+
const unreadable = signal.kind === 'unreadable';
|
|
1284
|
+
const { city } = entry;
|
|
1285
|
+
const post = sign(unreadable ? 'unreadable' : 'check', 2.2, 0.9);
|
|
1286
|
+
post.position.set(city.x - city.size / 2 + 0.6, 0.45, city.z + city.size / 2 - 0.6);
|
|
1287
|
+
scene.add(post);
|
|
1288
|
+
citySigns.push(post);
|
|
1289
|
+
if (entry.label) entry.label.node.append(el(doc, 'span', { className: `bb-world-badge ${unreadable ? 'is-unreadable' : 'is-check'}`, text }));
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const workLayer = { group: null, code: null, labels: [] };
|
|
1294
|
+
function clearWork() {
|
|
1295
|
+
if (workLayer.group) scene.remove(workLayer.group);
|
|
1296
|
+
for (const label of workLayer.labels) {
|
|
1297
|
+
label.node.remove();
|
|
1298
|
+
const index = labels.indexOf(label);
|
|
1299
|
+
if (index !== -1) labels.splice(index, 1);
|
|
1300
|
+
}
|
|
1301
|
+
workLayer.group = null;
|
|
1302
|
+
workLayer.code = null;
|
|
1303
|
+
workLayer.labels = [];
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** An arc between two cities: solid for a recorded relation, dashed for an inferred one. */
|
|
1307
|
+
function cityLink(from, to, link) {
|
|
1308
|
+
const a = new THREE.Vector3(from.x, 1.2, from.z);
|
|
1309
|
+
const b = new THREE.Vector3(to.x, 1.2, to.z);
|
|
1310
|
+
const mid = a.clone().add(b).multiplyScalar(0.5);
|
|
1311
|
+
mid.y = 4 + a.distanceTo(b) * 0.12;
|
|
1312
|
+
const curve = new THREE.QuadraticBezierCurve3(a, mid, b);
|
|
1313
|
+
const material = new THREE.MeshBasicMaterial({ color: link === 'recorded' ? 0x1261ad : 0x7a6a9e, transparent: true, opacity: 0.85, fog: false });
|
|
1314
|
+
const group = new THREE.Group();
|
|
1315
|
+
if (link === 'recorded') {
|
|
1316
|
+
group.add(mesh(new THREE.TubeGeometry(curve, 48, 0.12, 6), material, { shadow: false }));
|
|
1317
|
+
} else {
|
|
1318
|
+
const segments = 28;
|
|
1319
|
+
for (let i = 0; i < segments; i += 2) {
|
|
1320
|
+
const piece = new THREE.CatmullRomCurve3([curve.getPoint(i / segments), curve.getPoint((i + 0.5) / segments), curve.getPoint((i + 1) / segments)]);
|
|
1321
|
+
group.add(mesh(new THREE.TubeGeometry(piece, 4, 0.1, 6), material, { shadow: false }));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return { group, mid: curve.getPoint(0.5) };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/** Draws the roads from one city to the businesses its work relates to (its work itself is inside its district). */
|
|
1328
|
+
function showWork(code, work) {
|
|
1329
|
+
clearWork();
|
|
1330
|
+
const entry = cityIndex.get(code);
|
|
1331
|
+
if (!entry || work?.status !== 'ok') return;
|
|
1332
|
+
const { city } = entry;
|
|
1333
|
+
const layer = new THREE.Group();
|
|
1334
|
+
// Roads to other businesses: recorded relations solid; decisions found only by their titles dashed.
|
|
1335
|
+
const links = new Map();
|
|
1336
|
+
for (const relation of work.relations) {
|
|
1337
|
+
if (!relation.business_code || relation.link === 'unreadable') continue;
|
|
1338
|
+
const key = `${relation.business_code}|${relation.link}`;
|
|
1339
|
+
links.set(key, [...(links.get(key) ?? []), relation.label]);
|
|
1340
|
+
}
|
|
1341
|
+
for (const policy of work.purpose.policies) {
|
|
1342
|
+
if (policy.link !== 'inferred' || !policy.scope_code || policy.scope_code === code || !cityIndex.has(policy.scope_code)) continue;
|
|
1343
|
+
const key = `${policy.scope_code}|inferred`;
|
|
1344
|
+
links.set(key, links.get(key) ?? []);
|
|
1345
|
+
}
|
|
1346
|
+
for (const [key, texts] of links) {
|
|
1347
|
+
const [otherCode, link] = key.split('|');
|
|
1348
|
+
const other = cityIndex.get(otherCode);
|
|
1349
|
+
if (!other) continue;
|
|
1350
|
+
const inferredDecisions = link === 'inferred' ? work.purpose.policies.filter((policy) => policy.link === 'inferred' && policy.scope_code === otherCode).length : 0;
|
|
1351
|
+
const words = [...texts, ...(inferredDecisions ? [`${other.city.business.name}の範囲の決定${inferredDecisions}件が題名で${city.business.name}を指す(推定)`] : [])];
|
|
1352
|
+
const { group, mid } = cityLink(other.city, city, link);
|
|
1353
|
+
layer.add(group);
|
|
1354
|
+
const label = addLabel(`${link === 'recorded' ? '━' : '┅'} ${words.join('・')}`, mid, `is-link is-${link}`, { minZoom: 0.8, priority: LABEL_PRIORITY.judgment });
|
|
1355
|
+
workLayer.labels.push(label);
|
|
1356
|
+
}
|
|
1357
|
+
scene.add(layer);
|
|
1358
|
+
workLayer.group = layer;
|
|
1359
|
+
workLayer.code = code;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** Selects a city by its code, as a click would (without the host's callbacks). */
|
|
1363
|
+
function selectKey({ code }) {
|
|
1364
|
+
const group = cityIndex.get(code)?.group;
|
|
1365
|
+
if (!group) return null;
|
|
1366
|
+
select(group);
|
|
1367
|
+
return { data: group.userData, center: new THREE.Box3().setFromObject(group).getCenter(new THREE.Vector3()) };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1226
1370
|
// --- frame loop -----------------------------------------------------------
|
|
1227
1371
|
const projected = new THREE.Vector3();
|
|
1228
1372
|
let worldExtent = 60;
|
|
@@ -1255,7 +1399,7 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
1255
1399
|
const onScreen = projected.z < 1 && projected.x > -1.05 && projected.x < 1.05 && projected.y > -1.05 && projected.y < 1.05;
|
|
1256
1400
|
const focused = selectedBusiness && label.business === selectedBusiness;
|
|
1257
1401
|
const zoomOk = camera.zoom >= label.minZoom || (focused && label.priority === LABEL_PRIORITY.engagement && camera.zoom >= 1.8);
|
|
1258
|
-
if (!onScreen || !zoomOk) {
|
|
1402
|
+
if (!onScreen || !zoomOk || label.hiddenByFocus) {
|
|
1259
1403
|
label.node.hidden = true;
|
|
1260
1404
|
continue;
|
|
1261
1405
|
}
|
|
@@ -1308,9 +1452,11 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
1308
1452
|
|
|
1309
1453
|
let lastSkyMinute = -1;
|
|
1310
1454
|
let running = true;
|
|
1455
|
+
let paused = false;
|
|
1311
1456
|
function frame(time) {
|
|
1312
1457
|
if (!running) return;
|
|
1313
1458
|
requestAnimationFrame(frame);
|
|
1459
|
+
if (paused) return;
|
|
1314
1460
|
if (!stage.isConnected || stage.offsetParent === null) return;
|
|
1315
1461
|
if (width === 0) resize();
|
|
1316
1462
|
if (flight) {
|
|
@@ -1396,6 +1542,16 @@ function createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear }
|
|
|
1396
1542
|
clearSelection() {
|
|
1397
1543
|
select(null);
|
|
1398
1544
|
},
|
|
1545
|
+
setWorkSigns,
|
|
1546
|
+
showWork,
|
|
1547
|
+
selectKey,
|
|
1548
|
+
/** While a district is open the world is not drawn (its canvas and labels are hidden). */
|
|
1549
|
+
setPaused(value) {
|
|
1550
|
+
paused = value;
|
|
1551
|
+
renderer.domElement.hidden = value;
|
|
1552
|
+
labelsLayer.hidden = value;
|
|
1553
|
+
if (!value) resize();
|
|
1554
|
+
},
|
|
1399
1555
|
dispose() {
|
|
1400
1556
|
running = false;
|
|
1401
1557
|
observer?.disconnect();
|
|
@@ -1424,13 +1580,45 @@ const JUDGMENT_UNAVAILABLE_TEXT = Object.freeze({
|
|
|
1424
1580
|
judgment_journal_not_connected: '判断の記録は各メンバーのMacにあり、組織版には送らないため未接続です',
|
|
1425
1581
|
});
|
|
1426
1582
|
|
|
1583
|
+
/**
|
|
1584
|
+
* Where the selected city and work site are kept: the page address (`world_business`, `world_site`),
|
|
1585
|
+
* so leaving for another screen and coming back opens the same place. A host may pass its own.
|
|
1586
|
+
*/
|
|
1587
|
+
function addressSelection() {
|
|
1588
|
+
return {
|
|
1589
|
+
read() {
|
|
1590
|
+
try {
|
|
1591
|
+
const params = new URLSearchParams(globalThis.location?.search ?? '');
|
|
1592
|
+
return { business: params.get('world_business'), site: params.get('world_site') };
|
|
1593
|
+
} catch {
|
|
1594
|
+
return { business: null, site: null };
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
write({ business, site }) {
|
|
1598
|
+
try {
|
|
1599
|
+
const url = new URL(globalThis.location.href);
|
|
1600
|
+
if (business) url.searchParams.set('world_business', business);
|
|
1601
|
+
else url.searchParams.delete('world_business');
|
|
1602
|
+
if (site) url.searchParams.set('world_site', site);
|
|
1603
|
+
else url.searchParams.delete('world_site');
|
|
1604
|
+
globalThis.history?.replaceState(globalThis.history.state, '', `${url.pathname}${url.search}${url.hash}`);
|
|
1605
|
+
} catch {
|
|
1606
|
+
// Without an address the selection simply is not kept.
|
|
1607
|
+
}
|
|
1608
|
+
},
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1427
1612
|
/**
|
|
1428
1613
|
* @param {object} options
|
|
1429
1614
|
* @param {(project: { id: string, code?: string | null }) => string} [options.projectHref] Where a business or
|
|
1430
1615
|
* engagement opens in the host's プロジェクトと関係者. The local web opens `#projects?project=<id>`; an
|
|
1431
1616
|
* organization web passes its own route.
|
|
1617
|
+
* @param {(site: { task_id: string }) => string | null} [options.taskHref] Where a work site's task opens in the
|
|
1618
|
+
* host's task screen (to check or correct it). Without one the rail says it cannot be corrected here.
|
|
1619
|
+
* @param {{ read(): { business: string | null, site: string | null }, write(selection): void }} [options.selection]
|
|
1432
1620
|
*/
|
|
1433
|
-
export function createWorldView({ root, rail, page, document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}
|
|
1621
|
+
export function createWorldView({ root, rail, page, document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, selection = addressSelection() }) {
|
|
1434
1622
|
const doc = explicitDocument ?? globalThis.document;
|
|
1435
1623
|
const reducedMotion = Boolean(globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
|
|
1436
1624
|
const wrap = el(doc, 'div', { className: 'bb-world' });
|
|
@@ -1451,11 +1639,12 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1451
1639
|
const header = workspacePageHeader(doc, {
|
|
1452
1640
|
crumbs: page?.crumbs ?? ['あなたのBrainbase', '世界'],
|
|
1453
1641
|
title: '世界',
|
|
1454
|
-
lead: '
|
|
1642
|
+
lead: '事業を都市、案件を区画、判断の種類を広場の建物として描いています。都市を選ぶとその事業の区画に入り、仕事(タスク)が通りに面した現場として建ち、記録から把握できていないことが街の様子で見えます。見るための画面で、ここからは何も書き換えません。',
|
|
1455
1643
|
source: page?.source ?? null,
|
|
1456
1644
|
actions: [
|
|
1457
1645
|
skyButton,
|
|
1458
|
-
|
|
1646
|
+
// Back to the whole world, keeping the city in hand (its ring and its rail stay).
|
|
1647
|
+
workspaceButton(doc, { text: '全体に戻る', onClick: () => goToWorld() }),
|
|
1459
1648
|
],
|
|
1460
1649
|
});
|
|
1461
1650
|
const notices = el(doc, 'div', { className: 'bb-world-hud', attrs: { 'aria-live': 'polite' } });
|
|
@@ -1465,9 +1654,18 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1465
1654
|
legend.append(chip);
|
|
1466
1655
|
}
|
|
1467
1656
|
legend.append(el(doc, 'span', { className: 'bb-world-chip is-flag', text: '赤い旗=任せたのに訂正・取り消し' }));
|
|
1657
|
+
// Shown once a city's work is open.
|
|
1658
|
+
const workLegend = el(doc, 'div', { className: 'bb-world-legend is-work', attrs: { 'aria-label': '仕事の凡例' } });
|
|
1659
|
+
workLegend.hidden = true;
|
|
1660
|
+
for (const state of WORK_STATES.slice(0, 3)) workLegend.append(el(doc, 'span', { className: `bb-world-chip is-work-${state.key}`, text: `${state.label}(記録上)` }));
|
|
1661
|
+
workLegend.append(
|
|
1662
|
+
el(doc, 'span', { className: 'bb-world-chip is-sign', text: '?の札=把握できていないことがある' }),
|
|
1663
|
+
el(doc, 'span', { className: 'bb-world-chip is-ring', text: '破線の輪=本文に人物(担当欄は未接続)' }),
|
|
1664
|
+
el(doc, 'span', { className: 'bb-world-chip is-road', text: '実線=記録された関係・破線=推定' }),
|
|
1665
|
+
);
|
|
1468
1666
|
const stage = el(doc, 'div', { className: 'bb-world-stage' });
|
|
1469
1667
|
const labelsLayer = el(doc, 'div', { className: 'bb-world-labels' });
|
|
1470
|
-
stage.append(labelsLayer, notices, legend);
|
|
1668
|
+
stage.append(labelsLayer, notices, legend, workLegend);
|
|
1471
1669
|
wrap.append(header, stage);
|
|
1472
1670
|
|
|
1473
1671
|
function showRail(nodes) {
|
|
@@ -1534,45 +1732,341 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1534
1732
|
});
|
|
1535
1733
|
}
|
|
1536
1734
|
|
|
1735
|
+
// --- which city and work site are open (story-world-work-sites-and-gaps-v1) ---
|
|
1736
|
+
let knownBusinesses = [];
|
|
1737
|
+
/** Whether the host has a task store: true, false (not connected), or null (not known yet / unreadable). */
|
|
1738
|
+
let workConnected = null;
|
|
1739
|
+
const workCache = new Map();
|
|
1740
|
+
const current = { code: null, siteId: null, level: 'world', focus: null, pendingSite: null };
|
|
1741
|
+
|
|
1742
|
+
/** Inside a city the notices fold away (they would cover it); its rail carries the sources and times. */
|
|
1743
|
+
function setLevel(level) {
|
|
1744
|
+
current.level = level;
|
|
1745
|
+
notices.hidden = level !== 'world';
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function setCurrent(code, siteId) {
|
|
1749
|
+
current.code = code;
|
|
1750
|
+
current.siteId = siteId;
|
|
1751
|
+
selection?.write?.({ business: code, site: siteId });
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function businessOf(code) {
|
|
1755
|
+
return knownBusinesses.find((business) => business.code === code) ?? null;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/** The registration, engagements, link out and judgments of a city (as before the work layer). */
|
|
1759
|
+
function cityRegistrationBlocks(business) {
|
|
1760
|
+
const blocks = [];
|
|
1761
|
+
const measures = cityMeasures(business, { isFinished, judgments: placement.byBusiness.get(business.code) ?? [] });
|
|
1762
|
+
const kind = kindEntry(business.kind);
|
|
1763
|
+
const kindText = kind.definition ? `${kind.label}(${kind.definition})` : kind.label;
|
|
1764
|
+
const judgmentText = judgmentsConnected ? `・判断${measures.judgments}件` : '(判断は未接続)';
|
|
1765
|
+
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [
|
|
1766
|
+
['分類', kindText],
|
|
1767
|
+
['状態', statusText(business.status)],
|
|
1768
|
+
['コード', business.code],
|
|
1769
|
+
['進行中の案件(都市の広さ)', `${measures.open}件${measures.finished ? `(完了・終了${measures.finished}件は記念公園)` : ''}`],
|
|
1770
|
+
['最近30日の動き(高さ・明かり)', `決定${measures.decisions}件${judgmentText}`],
|
|
1771
|
+
]) }));
|
|
1772
|
+
const ul = el(doc, 'ul', { className: 'bb-world-rail-list' });
|
|
1773
|
+
for (const engagement of business.engagements) {
|
|
1774
|
+
const li = el(doc, 'li', { text: `${engagement.name}(${statusText(engagement.status)})` });
|
|
1775
|
+
li.append(projectLink(engagement, '開く'));
|
|
1776
|
+
ul.append(li);
|
|
1777
|
+
}
|
|
1778
|
+
blocks.push(workspaceRailBlock(doc, { title: '案件(区画)', content: business.engagements.length ? ul : { text: '登録された案件はありません' } }));
|
|
1779
|
+
blocks.push(detailBlock(business, '「プロジェクトと関係者」でこの事業を開く'));
|
|
1780
|
+
blocks.push(cityJudgmentBlock(business));
|
|
1781
|
+
return blocks;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function workBlocksFor(business) {
|
|
1785
|
+
if (workConnected === false) {
|
|
1786
|
+
return [workspaceRailBlock(doc, { title: '仕事', content: { text: 'この画面はタスクの正本に接続していないため、都市の中の仕事は描いていません。仕事が0件という意味ではありません。' } })];
|
|
1787
|
+
}
|
|
1788
|
+
const work = workCache.get(business.code) ?? null;
|
|
1789
|
+
return workCityBlocks(doc, {
|
|
1790
|
+
business,
|
|
1791
|
+
work,
|
|
1792
|
+
onSelectGap: (kind) => showFocus(business, { kind }),
|
|
1793
|
+
onSelectStatus: (status) => showFocus(business, { status }),
|
|
1794
|
+
onReload: () => {
|
|
1795
|
+
workCache.delete(business.code);
|
|
1796
|
+
renderCityRail(business);
|
|
1797
|
+
void loadWork(business);
|
|
1798
|
+
},
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/** Brings a rail block the viewer just asked for into view (the rail may be scrolled down, or below the city). */
|
|
1803
|
+
function reveal(node) {
|
|
1804
|
+
try {
|
|
1805
|
+
node?.scrollIntoView?.({ block: 'start', behavior: reducedMotion ? 'auto' : 'smooth' });
|
|
1806
|
+
} catch {
|
|
1807
|
+
// Without scrolling the block is still in the rail.
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function renderCityRail(business, { revealFocus = false } = {}) {
|
|
1812
|
+
const blocks = [workspaceRailHead(doc, { kicker: `事業・${kindEntry(business.kind).label}`, title: business.name, lead: business.purpose ?? undefined })];
|
|
1813
|
+
let focusBlock = null;
|
|
1814
|
+
const work = workCache.get(business.code);
|
|
1815
|
+
if (current.focus && work?.status === 'ok') {
|
|
1816
|
+
const ids = new Set(current.focus.kind
|
|
1817
|
+
? work.summary.gaps?.[current.focus.kind] ?? []
|
|
1818
|
+
: work.sites.filter((site) => site.work.open && site.work.status === current.focus.status).map((site) => site.task_id));
|
|
1819
|
+
const sites = work.sites.filter((site) => ids.has(site.task_id));
|
|
1820
|
+
const label = current.focus.kind
|
|
1821
|
+
? sites[0]?.gaps.find((gap) => gap.kind === current.focus.kind)?.label ?? current.focus.kind
|
|
1822
|
+
: `記録上の状態「${WORK_STATES.find((state) => state.key === current.focus.status)?.label ?? current.focus.status}」`;
|
|
1823
|
+
blocks.push(focusBlock = workPickBlock(doc, {
|
|
1824
|
+
title: `${label} ${sites.length}件`,
|
|
1825
|
+
sites,
|
|
1826
|
+
note: '該当する仕事を街の中で濃く、ほかを薄く出しています。選ぶと、その仕事の根拠が開きます。',
|
|
1827
|
+
onSelectSite: (taskId) => openSiteById(business, taskId),
|
|
1828
|
+
}));
|
|
1829
|
+
blocks.push(workspaceRailBlock(doc, { title: '', content: workspaceButton(doc, { text: 'すべての仕事を表示', onClick: () => showFocus(business, null) }) }));
|
|
1830
|
+
}
|
|
1831
|
+
blocks.push(...workBlocksFor(business), ...cityRegistrationBlocks(business));
|
|
1832
|
+
showRail(blocks);
|
|
1833
|
+
if (revealFocus) reveal(focusBlock ?? blocks[0]);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/** The open tasks a focus picks: a kind of gap, or a recorded state; null for all. */
|
|
1837
|
+
function focusIdsFor(work, focus) {
|
|
1838
|
+
if (!focus || work?.status !== 'ok') return null;
|
|
1839
|
+
return focus.kind
|
|
1840
|
+
? work.summary.gaps?.[focus.kind] ?? []
|
|
1841
|
+
: work.sites.filter((site) => site.work.open && site.work.status === focus.status).map((site) => site.task_id);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function showFocus(business, focus) {
|
|
1845
|
+
current.focus = focus;
|
|
1846
|
+
district?.focus(focusIdsFor(workCache.get(business.code), focus));
|
|
1847
|
+
renderCityRail(business, { revealFocus: true });
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// --- inside a district (AC-11): the world rests behind it while it is open ---------------------
|
|
1851
|
+
let district = null;
|
|
1852
|
+
let districtCode = null;
|
|
1853
|
+
function enterDistrict(business, work) {
|
|
1854
|
+
if (!scene || !work || work.status === 'not_connected') return false;
|
|
1855
|
+
district ??= createDistrictView({
|
|
1856
|
+
doc,
|
|
1857
|
+
stage,
|
|
1858
|
+
reducedMotion,
|
|
1859
|
+
onPick: (site) => {
|
|
1860
|
+
const owner = businessOf(current.code);
|
|
1861
|
+
if (owner) openSite(owner, site);
|
|
1862
|
+
},
|
|
1863
|
+
onClear: () => {
|
|
1864
|
+
const owner = businessOf(current.code);
|
|
1865
|
+
if (owner && current.level === 'site') openCity(owner, null);
|
|
1866
|
+
},
|
|
1867
|
+
onEscape: goUp,
|
|
1868
|
+
});
|
|
1869
|
+
scene.setPaused(true);
|
|
1870
|
+
legend.hidden = true;
|
|
1871
|
+
workLegend.hidden = true;
|
|
1872
|
+
if (districtCode !== business.code || !district.isOpen()) {
|
|
1873
|
+
district.show(business, work, { neighbours: new Map(knownBusinesses.map((entry) => [entry.code, entry.name])) });
|
|
1874
|
+
districtCode = business.code;
|
|
1875
|
+
}
|
|
1876
|
+
district.focus(focusIdsFor(work, current.focus));
|
|
1877
|
+
return true;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function leaveDistrict() {
|
|
1881
|
+
if (!district?.isOpen()) return;
|
|
1882
|
+
district.hide();
|
|
1883
|
+
districtCode = null;
|
|
1884
|
+
scene?.setPaused(false);
|
|
1885
|
+
legend.hidden = false;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
async function loadWork(business) {
|
|
1889
|
+
if (workConnected === false || workCache.has(business.code)) return workCache.get(business.code) ?? null;
|
|
1890
|
+
const result = await readJson(fetcher, `/api/extensions/world/businesses/${encodeURIComponent(business.code)}/work`);
|
|
1891
|
+
const work = result.ok ? result.data : { status: 'unavailable', reason: result.error };
|
|
1892
|
+
if (work?.status === 'not_connected') workConnected = false;
|
|
1893
|
+
workCache.set(business.code, work);
|
|
1894
|
+
if (current.code !== business.code) return work;
|
|
1895
|
+
scene?.showWork(business.code, work);
|
|
1896
|
+
if (current.level !== 'world') enterDistrict(business, work);
|
|
1897
|
+
if (current.pendingSite) {
|
|
1898
|
+
const siteId = current.pendingSite;
|
|
1899
|
+
current.pendingSite = null;
|
|
1900
|
+
if (openSiteById(business, siteId)) return work;
|
|
1901
|
+
}
|
|
1902
|
+
if (current.level !== 'site') renderCityRail(business);
|
|
1903
|
+
return work;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
/** Opens a city: its rail, and its district once its work is read (the district is where its work is). */
|
|
1907
|
+
function openCity(business, center, { zoom = 2.8 } = {}) {
|
|
1908
|
+
if (current.code !== business.code) current.focus = null;
|
|
1909
|
+
setCurrent(business.code, null);
|
|
1910
|
+
setLevel('city');
|
|
1911
|
+
const work = workCache.get(business.code);
|
|
1912
|
+
if (districtCode === business.code && district?.isOpen()) {
|
|
1913
|
+
district.home();
|
|
1914
|
+
district.focus(focusIdsFor(work, current.focus));
|
|
1915
|
+
} else {
|
|
1916
|
+
if (center) scene?.flyTo(new THREE.Vector3(center.x, 0, center.z), zoom);
|
|
1917
|
+
if (work) {
|
|
1918
|
+
scene?.showWork(business.code, work);
|
|
1919
|
+
enterDistrict(business, work);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
renderCityRail(business);
|
|
1923
|
+
if (!work) void loadWork(business);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function openSite(business, site) {
|
|
1927
|
+
setCurrent(business.code, site.task_id);
|
|
1928
|
+
setLevel('site');
|
|
1929
|
+
if (districtCode === business.code) district?.select(site.task_id);
|
|
1930
|
+
const work = workCache.get(business.code);
|
|
1931
|
+
const back = workspaceButton(doc, { text: `← ${business.name}の区画に戻る`, onClick: () => goUp() });
|
|
1932
|
+
const head = workspaceRailHead(doc, { kicker: `${business.name} の仕事(タスク)`, title: site.title, sub: `記録上:${site.work.label}${site.gaps.length ? `・把握できていないこと${site.gaps.length}種` : ''}` });
|
|
1933
|
+
showRail([
|
|
1934
|
+
head,
|
|
1935
|
+
workspaceRailBlock(doc, { title: '', content: back }),
|
|
1936
|
+
...workSiteBlocks(doc, { business, site, readAt: work?.reads?.tasks?.read_at ?? null, taskHref }),
|
|
1937
|
+
]);
|
|
1938
|
+
reveal(head);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
function openSiteById(business, taskId) {
|
|
1942
|
+
const work = workCache.get(business.code);
|
|
1943
|
+
const site = work?.status === 'ok' ? work.sites.find((entry) => entry.task_id === taskId) : null;
|
|
1944
|
+
if (!site) return false;
|
|
1945
|
+
openSite(business, site);
|
|
1946
|
+
return true;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/** The whole world again, with the open city kept in hand (ring and rail stay). */
|
|
1950
|
+
function goToWorld() {
|
|
1951
|
+
leaveDistrict();
|
|
1952
|
+
setLevel('world');
|
|
1953
|
+
if (current.code) {
|
|
1954
|
+
const business = businessOf(current.code);
|
|
1955
|
+
scene?.selectKey({ code: current.code });
|
|
1956
|
+
if (business) renderCityRail(business);
|
|
1957
|
+
setCurrent(current.code, null);
|
|
1958
|
+
} else {
|
|
1959
|
+
scene?.clearSelection();
|
|
1960
|
+
showEmptyRail();
|
|
1961
|
+
}
|
|
1962
|
+
scene?.resetView();
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function clearAll() {
|
|
1966
|
+
setCurrent(null, null);
|
|
1967
|
+
setLevel('world');
|
|
1968
|
+
current.focus = null;
|
|
1969
|
+
showEmptyRail();
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/** One level up: a work site → its district, a district → the whole world (city kept), the world → nothing selected. */
|
|
1973
|
+
function goUp() {
|
|
1974
|
+
const business = current.code ? businessOf(current.code) : null;
|
|
1975
|
+
if (business && current.level === 'site') {
|
|
1976
|
+
district?.clearSelection();
|
|
1977
|
+
openCity(business, null);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
if (business && current.level === 'city') {
|
|
1981
|
+
goToWorld();
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
scene?.clearSelection();
|
|
1985
|
+
clearAll();
|
|
1986
|
+
scene?.resetView();
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function restoreSelection() {
|
|
1990
|
+
const saved = selection?.read?.() ?? {};
|
|
1991
|
+
const business = saved.business ? businessOf(saved.business) : null;
|
|
1992
|
+
if (!business) return;
|
|
1993
|
+
const picked = scene?.selectKey({ code: business.code });
|
|
1994
|
+
current.pendingSite = saved.site ?? null;
|
|
1995
|
+
openCity(business, picked?.center ?? null);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
async function loadWorkSummary(note) {
|
|
1999
|
+
const result = await readJson(fetcher, '/api/extensions/world/work-summary');
|
|
2000
|
+
if (!result.ok) {
|
|
2001
|
+
note('仕事', `仕事の要約を読めません(${result.error})。都市を選ぶと、その事業の仕事を読みにいきます。0件ではありません。`, 'warning');
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (result.data?.status === 'not_connected') {
|
|
2005
|
+
workConnected = false;
|
|
2006
|
+
note('仕事', 'この画面はタスクの正本に接続していないため、都市の中の仕事は描いていません(0件ではありません)。');
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (result.data?.status !== 'ok') {
|
|
2010
|
+
note('仕事', `仕事の要約を読めません(${result.data?.reason ?? result.data?.status ?? '理由不明'})。0件ではありません。`, 'warning');
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
workConnected = true;
|
|
2014
|
+
const summaries = result.data.businesses ?? {};
|
|
2015
|
+
scene?.setWorkSigns(summaries);
|
|
2016
|
+
const entries = Object.values(summaries);
|
|
2017
|
+
const readable = entries.filter((entry) => entry.state === 'complete' || entry.state === 'partial');
|
|
2018
|
+
const unreadable = entries.length - readable.length;
|
|
2019
|
+
const open = readable.reduce((sum, entry) => sum + (entry.open ?? 0), 0);
|
|
2020
|
+
const needs = readable.reduce((sum, entry) => sum + (entry.needs_check?.length ?? 0), 0);
|
|
2021
|
+
const cities = readable.filter((entry) => entry.needs_check?.length).length;
|
|
2022
|
+
const partial = readable.filter((entry) => entry.state === 'partial').length;
|
|
2023
|
+
// Which kinds of gap, across the businesses read: what is not visible, not how bad it is.
|
|
2024
|
+
const kinds = new Map();
|
|
2025
|
+
for (const entry of readable) for (const [kind, ids] of Object.entries(entry.gaps ?? {})) kinds.set(kind, (kinds.get(kind) ?? 0) + ids.length);
|
|
2026
|
+
const kindText = [...kinds].sort((a, b) => b[1] - a[1]).map(([kind, count]) => `${GAP_SHORT_TEXT[kind] ?? kind}${count}`).join('・');
|
|
2027
|
+
const parts = readable.length
|
|
2028
|
+
? [`未完了の仕事${open}件のうち${needs}件(${cities}事業)に、記録から把握できていないことがあります(?の札${kindText ? `。内訳:${kindText}` : ''})`]
|
|
2029
|
+
: [];
|
|
2030
|
+
if (unreadable) parts.push(`${readable.length ? '' : 'どの事業の仕事も読めませんでした。'}仕事を読めなかった事業${unreadable}件(灰色の札。0件ではありません)`);
|
|
2031
|
+
if (partial) parts.push(`一部だけ読めた事業${partial}件`);
|
|
2032
|
+
note('仕事', `${parts.join('。')}。「止まっている」という意味ではありません(${shortTimeText(result.data.as_of)}時点)。`);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
function shortTimeText(value) {
|
|
2036
|
+
const at = Date.parse(value ?? '');
|
|
2037
|
+
if (!Number.isFinite(at)) return '時点不明';
|
|
2038
|
+
const date = new Date(at);
|
|
2039
|
+
const pad = (number) => String(number).padStart(2, '0');
|
|
2040
|
+
return `${date.getMonth() + 1}/${date.getDate()} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1537
2043
|
function onPick(data, position) {
|
|
1538
|
-
if (data.kind === '
|
|
2044
|
+
if (data.kind === 'site') {
|
|
2045
|
+
openSite(data.business, data.site);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
if (data.kind === 'city') {
|
|
2049
|
+
openCity(data.business, position);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
if (data.kind === 'engagement') {
|
|
1539
2053
|
const business = data.business;
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
const kind = kindEntry(business.kind);
|
|
1552
|
-
const kindText = kind.definition ? `${kind.label}(${kind.definition})` : kind.label;
|
|
1553
|
-
const judgmentText = judgmentsConnected ? `・判断${measures.judgments}件` : '(判断は未接続)';
|
|
1554
|
-
blocks.push(workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [
|
|
1555
|
-
['分類', kindText],
|
|
1556
|
-
['状態', statusText(business.status)],
|
|
1557
|
-
['コード', business.code],
|
|
1558
|
-
['進行中の案件(都市の広さ)', `${measures.open}件${measures.finished ? `(完了・終了${measures.finished}件は記念公園)` : ''}`],
|
|
1559
|
-
['最近30日の動き(高さ・明かり)', `決定${measures.decisions}件${judgmentText}`],
|
|
1560
|
-
]) }));
|
|
1561
|
-
const ul = el(doc, 'ul', { className: 'bb-world-rail-list' });
|
|
1562
|
-
for (const engagement of business.engagements) {
|
|
1563
|
-
const li = el(doc, 'li', { text: `${engagement.name}(${statusText(engagement.status)})` });
|
|
1564
|
-
li.append(projectLink(engagement, '開く'));
|
|
1565
|
-
ul.append(li);
|
|
1566
|
-
}
|
|
1567
|
-
blocks.push(workspaceRailBlock(doc, { title: '案件(区画)', content: business.engagements.length ? ul : { text: '登録された案件はありません' } }));
|
|
1568
|
-
blocks.push(detailBlock(business, '「プロジェクトと関係者」でこの事業を開く'));
|
|
1569
|
-
}
|
|
1570
|
-
blocks.push(cityJudgmentBlock(business));
|
|
1571
|
-
showRail(blocks);
|
|
2054
|
+
setCurrent(business.code, null);
|
|
2055
|
+
setLevel('city');
|
|
2056
|
+
scene?.flyTo(new THREE.Vector3(position.x, 0, position.z), 3.6);
|
|
2057
|
+
if (!workCache.has(business.code)) void loadWork(business);
|
|
2058
|
+
showRail([
|
|
2059
|
+
workspaceRailHead(doc, { kicker: `${business.name} の案件`, title: data.engagement.name, lead: data.engagement.summary ?? undefined }),
|
|
2060
|
+
workspaceRailBlock(doc, { title: '登録', content: workspaceDefinition(doc, [['状態', statusText(data.engagement.status)], ['Graph ID', data.engagement.id]]) }),
|
|
2061
|
+
detailBlock(data.engagement, '「プロジェクトと関係者」でこの案件を開く'),
|
|
2062
|
+
workspaceRailBlock(doc, { title: '', content: workspaceButton(doc, { text: `${business.name}の仕事と関係を見る`, onClick: () => openCity(business, position) }) }),
|
|
2063
|
+
cityJudgmentBlock(business),
|
|
2064
|
+
]);
|
|
1572
2065
|
return;
|
|
1573
2066
|
}
|
|
1574
2067
|
if (data.kind === 'cityJudgments') {
|
|
1575
2068
|
const business = data.business;
|
|
2069
|
+
setCurrent(business.code, null);
|
|
1576
2070
|
scene?.flyTo(new THREE.Vector3(position.x, 0, position.z), 3.2);
|
|
1577
2071
|
showRail([
|
|
1578
2072
|
workspaceRailHead(doc, { kicker: `${business.name} の判断所`, title: `判断 ${data.judgments.length}件`, lead: business.purpose ?? undefined }),
|
|
@@ -1583,6 +2077,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1583
2077
|
}
|
|
1584
2078
|
if (data.kind === 'judgment') {
|
|
1585
2079
|
const { row, items } = data;
|
|
2080
|
+
setCurrent(null, null);
|
|
1586
2081
|
scene?.flyTo(new THREE.Vector3(0, 0, 0), 2.2);
|
|
1587
2082
|
const blocks = [workspaceRailHead(doc, {
|
|
1588
2083
|
kicker: `判断の種類・${STATE_LABELS[row.state] ?? row.state}`,
|
|
@@ -1613,6 +2108,7 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1613
2108
|
return;
|
|
1614
2109
|
}
|
|
1615
2110
|
if (data.kind === 'plaza') {
|
|
2111
|
+
setCurrent(null, null);
|
|
1616
2112
|
scene?.flyTo(new THREE.Vector3(0, 0, 0), 2.2);
|
|
1617
2113
|
const blocks = [workspaceRailHead(doc, {
|
|
1618
2114
|
kicker: '広場',
|
|
@@ -1708,10 +2204,13 @@ export function createWorldView({ root, rail, page, document: explicitDocument,
|
|
|
1708
2204
|
renderFallbackList(doc, wrap, businesses, rows);
|
|
1709
2205
|
return;
|
|
1710
2206
|
}
|
|
1711
|
-
scene = createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear:
|
|
2207
|
+
scene = createScene({ doc, stage, labelsLayer, reducedMotion, onPick, onClear: clearAll, onEscape: goUp });
|
|
1712
2208
|
scene.buildPlaza(rows, rowItems, (row) => (rowItems.get(row.key) ?? []).filter((ref) => isWaiting(proofs.get(ref.decision_attempt_id))).length, Date.now());
|
|
1713
2209
|
scene.buildCities(layoutWorld(businesses), placement.byBusiness);
|
|
1714
2210
|
showChangesSinceLastVisit(note, businesses, rows);
|
|
2211
|
+
knownBusinesses = businesses;
|
|
2212
|
+
restoreSelection();
|
|
2213
|
+
void loadWorkSummary(note);
|
|
1715
2214
|
}
|
|
1716
2215
|
|
|
1717
2216
|
/**
|