hoi4-agent-tools 3.3.0 → 3.4.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/CHANGELOG.md +22 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +20 -8
- package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +19 -1
- package/dist/hoi4_agent_tools/core/operation-result.js +4 -1
- package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
- package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
- package/dist/hoi4_agent_tools/event/operations.js +32 -10
- package/dist/hoi4_agent_tools/event/service.js +21 -2
- package/dist/hoi4_agent_tools/focus/compact.js +241 -27
- package/dist/hoi4_agent_tools/focus/layout.js +119 -8
- package/dist/hoi4_agent_tools/focus/render.js +62 -0
- package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
- package/dist/hoi4_agent_tools/gui/assets.js +54 -4
- package/dist/hoi4_agent_tools/gui/layout.js +935 -104
- package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
- package/dist/hoi4_agent_tools/gui/shader.js +515 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
- package/dist/hoi4_agent_tools/gui/studio.js +245 -19
- package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
- package/dist/hoi4_agent_tools/gui/validators.js +41 -12
- package/dist/hoi4_agent_tools/map/operations.js +70 -1
- package/dist/hoi4_agent_tools/map/render.js +70 -1
- package/dist/hoi4_agent_tools/map/service.js +30 -6
- package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
- package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +91 -30
- package/dist/hoi4_agent_tools/mechanic/source.js +19 -4
- package/dist/hoi4_agent_tools/probability/evaluation.js +3 -0
- package/dist/hoi4_agent_tools/probability/operations.js +8 -0
- package/dist/hoi4_agent_tools/probability/service.js +33 -12
- package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
- package/dist/hoi4_agent_tools/schemas/event.js +8 -0
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
- package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
- package/dist/hoi4_agent_tools/technology/graph.js +25 -10
- package/dist/hoi4_agent_tools/technology/operations.js +29 -1
- package/dist/hoi4_agent_tools/technology/render.js +287 -31
- package/dist/hoi4_agent_tools/technology/service.js +56 -8
- package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/events.md +4 -2
- package/docs/focus.md +10 -0
- package/docs/gui.md +50 -3
- package/docs/jobs.md +1 -0
- package/docs/map.md +8 -0
- package/docs/mechanics.md +2 -1
- package/docs/probability.md +3 -1
- package/docs/technology.md +11 -2
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/server.json +2 -2
|
@@ -19,6 +19,15 @@ const GENERATED_LAYER_ROWS = 32;
|
|
|
19
19
|
const GENERATED_BAND_GAP = 32;
|
|
20
20
|
const PADDING = 42;
|
|
21
21
|
const HEADER_HEIGHT = 86;
|
|
22
|
+
function itemBackgroundSprites(sprite) {
|
|
23
|
+
if (!sprite.includes('_unavailable_item_bg'))
|
|
24
|
+
return [sprite];
|
|
25
|
+
return [
|
|
26
|
+
sprite,
|
|
27
|
+
sprite.replace('_unavailable_item_bg', '_available_item_bg'),
|
|
28
|
+
sprite.replace('_unavailable_item_bg', '_researched_item_bg'),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
22
31
|
function escapeXml(value) {
|
|
23
32
|
return value
|
|
24
33
|
.replaceAll('&', '&')
|
|
@@ -31,6 +40,7 @@ function technologyNode(graph, technologyId, placement) {
|
|
|
31
40
|
const technology = graph.technologies.find(({ id }) => id === technologyId);
|
|
32
41
|
if (technology === undefined)
|
|
33
42
|
return undefined;
|
|
43
|
+
const itemLayout = graph.itemLayouts.find(({ id }) => id === placement?.itemLayoutId);
|
|
34
44
|
return {
|
|
35
45
|
id: technology.id,
|
|
36
46
|
label: technology.localisation.name ?? technology.id,
|
|
@@ -48,6 +58,29 @@ function technologyNode(graph, technologyId, placement) {
|
|
|
48
58
|
sourceLine: technology.source.location.start.line,
|
|
49
59
|
layoutSize: placement?.layoutSize ?? technology.layoutSize,
|
|
50
60
|
iconSprite: technology.icon.sprite,
|
|
61
|
+
...(itemLayout?.backgroundSprite === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: { itemBackgroundSprite: itemLayout.backgroundSprite }),
|
|
64
|
+
...(itemLayout?.iconPosition === undefined ? {} : { iconPosition: itemLayout.iconPosition }),
|
|
65
|
+
...(itemLayout?.designTeamIcon === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: { designTeamIcon: itemLayout.designTeamIcon }),
|
|
68
|
+
...(itemLayout?.namePosition === undefined ? {} : { namePosition: itemLayout.namePosition }),
|
|
69
|
+
...(placement === undefined || technology.subTechnologies.length === 0
|
|
70
|
+
? {}
|
|
71
|
+
: {
|
|
72
|
+
subTechnologies: technology.subTechnologies.map((id, index) => {
|
|
73
|
+
const definition = graph.technologies.find((candidate) => candidate.id === id);
|
|
74
|
+
const slot = itemLayout?.subTechnologySlots?.find((candidate) => candidate.index === index);
|
|
75
|
+
const iconSprite = slot?.sprite ?? definition?.icon.sprite;
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
...(iconSprite === undefined ? {} : { iconSprite }),
|
|
79
|
+
placementStatus: slot === undefined ? 'missing_source_slot' : 'source_slot',
|
|
80
|
+
...(slot === undefined ? {} : { slot }),
|
|
81
|
+
};
|
|
82
|
+
}),
|
|
83
|
+
}),
|
|
51
84
|
...(placement === undefined ? {} : { placement }),
|
|
52
85
|
};
|
|
53
86
|
}
|
|
@@ -58,10 +91,52 @@ function sortedUniqueNodes(values) {
|
|
|
58
91
|
result.set(value.id, value);
|
|
59
92
|
return [...result.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
60
93
|
}
|
|
94
|
+
function presentScenario(graph, nodes, scenario) {
|
|
95
|
+
const known = new Map(graph.technologies.map((technology) => [technology.id, technology]));
|
|
96
|
+
const researched = new Set(scenario.researchedTechnologyIds);
|
|
97
|
+
for (const id of researched)
|
|
98
|
+
if (!known.has(id))
|
|
99
|
+
throw new ServiceError('TECH_RENDER_SCENARIO_TECH_UNKNOWN', `Scenario references unknown researched technology ${id}`, { technologyId: id });
|
|
100
|
+
return nodes.map((node) => {
|
|
101
|
+
const technology = known.get(node.id);
|
|
102
|
+
if (technology === undefined)
|
|
103
|
+
return node;
|
|
104
|
+
const missingPrerequisiteIds = graph.edges
|
|
105
|
+
.filter(({ kind, to, from }) => kind === 'prerequisite' && to === node.id && !researched.has(from))
|
|
106
|
+
.map(({ from }) => from)
|
|
107
|
+
.sort(compareCodeUnits);
|
|
108
|
+
const exclusiveConflictIds = graph.edges
|
|
109
|
+
.filter(({ kind, to, from }) => kind === 'exclusive' &&
|
|
110
|
+
((to === node.id && researched.has(from)) || (from === node.id && researched.has(to))))
|
|
111
|
+
.map(({ to, from }) => (to === node.id ? from : to))
|
|
112
|
+
.sort(compareCodeUnits);
|
|
113
|
+
const parsedYear = Number(technology.startYear);
|
|
114
|
+
const aheadOfTimeYears = technology.startYear !== undefined &&
|
|
115
|
+
/^\d+$/u.test(technology.startYear) &&
|
|
116
|
+
Number.isSafeInteger(parsedYear)
|
|
117
|
+
? Math.max(0, parsedYear - scenario.year)
|
|
118
|
+
: undefined;
|
|
119
|
+
const scenarioStatus = researched.has(node.id)
|
|
120
|
+
? 'researched'
|
|
121
|
+
: exclusiveConflictIds.length > 0
|
|
122
|
+
? 'exclusive_conflict'
|
|
123
|
+
: missingPrerequisiteIds.length > 0
|
|
124
|
+
? 'missing_prerequisite'
|
|
125
|
+
: 'structural_candidate';
|
|
126
|
+
return {
|
|
127
|
+
...node,
|
|
128
|
+
scenarioStatus,
|
|
129
|
+
missingPrerequisiteIds,
|
|
130
|
+
exclusiveConflictIds,
|
|
131
|
+
...(aheadOfTimeYears === undefined ? {} : { aheadOfTimeYears }),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
61
135
|
function viewSelection(graph, options) {
|
|
62
136
|
const nodes = [];
|
|
63
137
|
const edges = [];
|
|
64
138
|
let yearMarkers = [];
|
|
139
|
+
let backgrounds = [];
|
|
65
140
|
let title;
|
|
66
141
|
let sourceAccurate = false;
|
|
67
142
|
if (options.view === 'summary') {
|
|
@@ -117,8 +192,9 @@ function viewSelection(graph, options) {
|
|
|
117
192
|
edges.push(...graph.edges
|
|
118
193
|
.filter(({ from, to }) => selected.has(from) && selected.has(to))
|
|
119
194
|
.map(({ id, from, to, kind }) => ({ id, from, to, kind })));
|
|
120
|
-
sourceAccurate =
|
|
195
|
+
sourceAccurate = nodes.every(({ placement }) => placement?.geometryStatus === 'source_pixel');
|
|
121
196
|
yearMarkers = graph.yearMarkers.filter(({ folderId }) => folderId === options.folderId);
|
|
197
|
+
backgrounds = (graph.backgrounds ?? []).filter(({ folderId }) => folderId === options.folderId);
|
|
122
198
|
}
|
|
123
199
|
else if (options.view === 'dependencies' || options.view === 'technology') {
|
|
124
200
|
const ids = options.technologyId === undefined
|
|
@@ -410,6 +486,7 @@ function viewSelection(graph, options) {
|
|
|
410
486
|
nodes: selectedNodes,
|
|
411
487
|
edges: selectedEdges,
|
|
412
488
|
yearMarkers,
|
|
489
|
+
backgrounds,
|
|
413
490
|
sourceAccurate,
|
|
414
491
|
title,
|
|
415
492
|
payload: {
|
|
@@ -428,7 +505,7 @@ function viewSelection(graph, options) {
|
|
|
428
505
|
function issueKey(issue) {
|
|
429
506
|
return sha256Bytes(canonicalJson({ code: issue.code, details: issue.details })).slice(0, 20);
|
|
430
507
|
}
|
|
431
|
-
function layoutFolder(nodes, yearMarkers) {
|
|
508
|
+
function layoutFolder(nodes, yearMarkers, backgrounds) {
|
|
432
509
|
const dimensions = (node) => node.placement?.layoutWidth !== undefined && node.placement.layoutHeight !== undefined
|
|
433
510
|
? { width: node.placement.layoutWidth, height: node.placement.layoutHeight }
|
|
434
511
|
: node.layoutSize === 'small'
|
|
@@ -436,23 +513,24 @@ function layoutFolder(nodes, yearMarkers) {
|
|
|
436
513
|
: { width: LARGE_NODE_WIDTH, height: LARGE_NODE_HEIGHT };
|
|
437
514
|
const sourcePixel = nodes.filter(({ placement }) => placement?.pixelX !== undefined && placement.pixelY !== undefined);
|
|
438
515
|
if (sourcePixel.length === nodes.length && sourcePixel.length > 0) {
|
|
516
|
+
const placedBackgrounds = backgrounds.filter(({ name }) => name !== 'folder_panel');
|
|
439
517
|
const sourceNodeX = (node) => node.placement.pixelX + (node.placement?.layoutOffsetX ?? 0);
|
|
440
518
|
const sourceNodeY = (node) => node.placement.pixelY + (node.placement?.layoutOffsetY ?? 0);
|
|
441
|
-
const minimumX = Math.min(...sourcePixel.map(sourceNodeX), ...yearMarkers.map(({ position }) => position.x));
|
|
442
|
-
const minimumY = Math.min(...sourcePixel.map(sourceNodeY), ...yearMarkers.map(({ position }) => position.y));
|
|
519
|
+
const minimumX = Math.min(...sourcePixel.map(sourceNodeX), ...yearMarkers.map(({ position }) => position.x), ...placedBackgrounds.map(({ position }) => position.x));
|
|
520
|
+
const minimumY = Math.min(...sourcePixel.map(sourceNodeY), ...yearMarkers.map(({ position }) => position.y), ...placedBackgrounds.map(({ position }) => position.y));
|
|
443
521
|
const positionedNodes = sourcePixel.map((node) => {
|
|
444
522
|
const size = dimensions(node);
|
|
445
523
|
return {
|
|
446
524
|
...node,
|
|
447
525
|
x: PADDING + sourceNodeX(node) - minimumX,
|
|
448
|
-
y:
|
|
526
|
+
y: PADDING + sourceNodeY(node) - minimumY,
|
|
449
527
|
...size,
|
|
450
528
|
};
|
|
451
529
|
});
|
|
452
530
|
const positionedYearMarkers = yearMarkers.map((marker) => ({
|
|
453
531
|
...marker,
|
|
454
532
|
x: PADDING + marker.position.x - minimumX,
|
|
455
|
-
y:
|
|
533
|
+
y: PADDING + marker.position.y - minimumY,
|
|
456
534
|
}));
|
|
457
535
|
const xValues = yearMarkers.map(({ position }) => position.x);
|
|
458
536
|
const yValues = yearMarkers.map(({ position }) => position.y);
|
|
@@ -571,9 +649,13 @@ function colours(kind) {
|
|
|
571
649
|
removed: { fill: '#4a2828', stroke: '#ef7d7d' },
|
|
572
650
|
}[kind];
|
|
573
651
|
}
|
|
574
|
-
function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, yearMarkers, yearAxis) {
|
|
575
|
-
const maximumX = Math.max(PADDING + LARGE_NODE_WIDTH, ...positioned.map(({ x, width }) => x + width), ...yearMarkers.map(({ x }) => x + 70)
|
|
576
|
-
|
|
652
|
+
function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, spriteDimensions, yearMarkers, backgrounds, yearAxis, folderView = false) {
|
|
653
|
+
const maximumX = Math.max(PADDING + LARGE_NODE_WIDTH, ...positioned.map(({ x, width }) => x + width), ...positioned.flatMap(({ x, subTechnologies }) => (subTechnologies ?? []).flatMap(({ slot }) => slot === undefined ? [] : [x + slot.position.x + slot.size.width])), ...yearMarkers.map(({ x }) => x + 70), ...backgrounds
|
|
654
|
+
.filter(({ name }) => name !== 'folder_panel')
|
|
655
|
+
.map(({ x, size, sprite }) => x + (size.width ?? spriteDimensions[sprite]?.width ?? 0)));
|
|
656
|
+
const maximumY = Math.max((folderView ? PADDING : HEADER_HEIGHT) + LARGE_NODE_HEIGHT, ...positioned.map(({ y, height }) => y + height), ...positioned.flatMap(({ y, subTechnologies }) => (subTechnologies ?? []).flatMap(({ slot }) => slot === undefined ? [] : [y + slot.position.y + slot.size.height])), ...yearMarkers.map(({ y }) => y + 30), ...backgrounds
|
|
657
|
+
.filter(({ name }) => name !== 'folder_panel')
|
|
658
|
+
.map(({ y, size, sprite }) => y + (size.height ?? spriteDimensions[sprite]?.height ?? 0)));
|
|
577
659
|
const width = Math.ceil(maximumX + PADDING);
|
|
578
660
|
const height = Math.ceil(maximumY + PADDING);
|
|
579
661
|
if (width > 16_384 || height > 16_384 || width * height > RENDER_MAX_PIXELS)
|
|
@@ -583,35 +665,134 @@ function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, year
|
|
|
583
665
|
});
|
|
584
666
|
const byId = new Map(positioned.map((node) => [node.id, node]));
|
|
585
667
|
const text = new DeterministicSvgTextRenderer();
|
|
668
|
+
const panel = folderView ? backgrounds.find(({ name }) => name === 'folder_panel') : undefined;
|
|
669
|
+
const panelDimensions = panel === undefined ? undefined : spriteDimensions[panel.sprite];
|
|
670
|
+
const panelUri = panel === undefined ? undefined : iconDataUris[panel.sprite];
|
|
671
|
+
const borderWidth = panelDimensions?.borderWidth ?? Math.floor((panelDimensions?.width ?? 0) / 3);
|
|
672
|
+
const borderHeight = panelDimensions?.borderHeight ?? Math.floor((panelDimensions?.height ?? 0) / 3);
|
|
673
|
+
const tileWidth = (panelDimensions?.width ?? 0) - 2 * borderWidth;
|
|
674
|
+
const tileHeight = (panelDimensions?.height ?? 0) - 2 * borderHeight;
|
|
675
|
+
const panelPattern = panelUri !== undefined && panelDimensions !== undefined && tileWidth > 0 && tileHeight > 0
|
|
676
|
+
? `<pattern id="tech-folder-panel" patternUnits="userSpaceOnUse" width="${tileWidth}" height="${tileHeight}"><image href="${escapeXml(panelUri)}" x="${-borderWidth}" y="${-borderHeight}" width="${panelDimensions.width}" height="${panelDimensions.height}"/></pattern>`
|
|
677
|
+
: '';
|
|
678
|
+
const backgroundSvg = backgrounds.flatMap((background) => {
|
|
679
|
+
if (background.name === 'folder_panel')
|
|
680
|
+
return [];
|
|
681
|
+
const uri = iconDataUris[background.sprite];
|
|
682
|
+
const width = background.size.width ?? spriteDimensions[background.sprite]?.width;
|
|
683
|
+
const height = background.size.height ?? spriteDimensions[background.sprite]?.height;
|
|
684
|
+
if (uri === undefined || width === undefined || height === undefined)
|
|
685
|
+
return [];
|
|
686
|
+
return [
|
|
687
|
+
`<image data-tech-background="${escapeXml(background.name)}" data-sprite="${escapeXml(background.sprite)}" data-source-path="${escapeXml(background.sourcePath)}" data-source-line="${background.location.start.line}" href="${escapeXml(uri)}" x="${background.x}" y="${background.y}" width="${width}" height="${height}" preserveAspectRatio="xMinYMin meet"/>`,
|
|
688
|
+
];
|
|
689
|
+
});
|
|
586
690
|
const yearGuideSvg = yearMarkers.map((marker) => {
|
|
587
|
-
const guide =
|
|
588
|
-
?
|
|
589
|
-
:
|
|
691
|
+
const guide = folderView
|
|
692
|
+
? ''
|
|
693
|
+
: yearAxis === 'vertical'
|
|
694
|
+
? `<line x1="${PADDING}" y1="${marker.y + 15}" x2="${width - PADDING}" y2="${marker.y + 15}" stroke="#34495c" stroke-width="1" stroke-dasharray="5 7"/>`
|
|
695
|
+
: `<line x1="${marker.x + 24}" y1="${HEADER_HEIGHT}" x2="${marker.x + 24}" y2="${height - PADDING}" stroke="#34495c" stroke-width="1" stroke-dasharray="5 7"/>`;
|
|
696
|
+
const fontSize = Math.max(10, Math.min(32, (marker.style?.maxHeight ?? 24) * 0.75));
|
|
697
|
+
const measuredWidth = text.measure(String(marker.year), fontSize);
|
|
698
|
+
const alignment = marker.style?.format?.toLowerCase();
|
|
699
|
+
const labelX = alignment === 'center'
|
|
700
|
+
? marker.x + ((marker.style?.maxWidth ?? measuredWidth) - measuredWidth) / 2
|
|
701
|
+
: alignment === 'right'
|
|
702
|
+
? marker.x + (marker.style?.maxWidth ?? measuredWidth) - measuredWidth
|
|
703
|
+
: marker.x;
|
|
590
704
|
const label = text.render(String(marker.year), {
|
|
591
|
-
x:
|
|
592
|
-
y: marker.y +
|
|
593
|
-
fontSize
|
|
594
|
-
fill: '#
|
|
705
|
+
x: labelX,
|
|
706
|
+
y: marker.y + fontSize,
|
|
707
|
+
fontSize,
|
|
708
|
+
fill: marker.style?.colour?.match(/^#[0-9a-fA-F]{6}$/u) ? marker.style.colour : '#dadada',
|
|
595
709
|
weight: 700,
|
|
596
710
|
});
|
|
597
|
-
return `<g data-tech-year="${marker.year}" data-year-axis="${yearAxis ?? 'horizontal'}" data-source-path="${escapeXml(marker.sourcePath)}" data-source-line="${marker.location.start.line}">${guide}${label}</g>`;
|
|
711
|
+
return `<g data-tech-year="${marker.year}" data-year-axis="${yearAxis ?? 'horizontal'}" data-source-path="${escapeXml(marker.sourcePath)}" data-source-line="${marker.location.start.line}" data-font="${escapeXml(marker.style?.fontName ?? '')}" data-format="${escapeXml(marker.style?.format ?? '')}">${guide}${label}</g>`;
|
|
598
712
|
});
|
|
599
713
|
const edgeSvg = edges.flatMap((edge) => {
|
|
600
714
|
const from = byId.get(edge.from);
|
|
601
715
|
const to = byId.get(edge.to);
|
|
602
716
|
if (from === undefined || to === undefined)
|
|
603
717
|
return [];
|
|
604
|
-
const x1 = from.x + from.width;
|
|
718
|
+
const x1 = folderView ? from.x + from.width / 2 : from.x + from.width;
|
|
605
719
|
const y1 = from.y + from.height / 2;
|
|
606
|
-
const x2 = to.x;
|
|
720
|
+
const x2 = folderView ? to.x + to.width / 2 : to.x;
|
|
607
721
|
const y2 = to.y + to.height / 2;
|
|
608
722
|
const middle = (x1 + x2) / 2;
|
|
609
|
-
const colour = edge.kind.includes('exclusive') ? '#
|
|
723
|
+
const colour = edge.kind.includes('exclusive') ? '#a47942' : folderView ? '#70716e' : '#8298ad';
|
|
610
724
|
const dash = edge.kind.includes('exclusive') ? ' stroke-dasharray="7 5"' : '';
|
|
725
|
+
if (folderView) {
|
|
726
|
+
const horizontal = Math.abs(x2 - x1) >= Math.abs(y2 - y1);
|
|
727
|
+
const route = horizontal
|
|
728
|
+
? `M ${x1} ${y1} H ${middle} V ${y2} H ${x2}`
|
|
729
|
+
: `M ${x1} ${y1} V ${(y1 + y2) / 2} H ${x2} V ${y2}`;
|
|
730
|
+
return `<path d="${route}" fill="none" stroke="#242523" stroke-width="6"/><path d="${route}" fill="none" stroke="${colour}" stroke-width="3"${dash}><title>${escapeXml(edge.kind)}</title></path>`;
|
|
731
|
+
}
|
|
611
732
|
return `<path d="M ${x1} ${y1} C ${middle} ${y1}, ${middle} ${y2}, ${x2} ${y2}" fill="none" stroke="${colour}" stroke-width="2" marker-end="url(#tech-arrow)"${dash}><title>${escapeXml(edge.kind)}</title></path>`;
|
|
612
733
|
});
|
|
613
734
|
const nodeSvg = positioned.map((node) => {
|
|
614
|
-
|
|
735
|
+
if (folderView && node.placement !== undefined) {
|
|
736
|
+
const preferredSprite = node.itemBackgroundSprite === undefined
|
|
737
|
+
? undefined
|
|
738
|
+
: node.scenarioStatus === 'researched'
|
|
739
|
+
? node.itemBackgroundSprite.replace('_unavailable_item_bg', '_researched_item_bg')
|
|
740
|
+
: node.scenarioStatus === 'structural_candidate'
|
|
741
|
+
? node.itemBackgroundSprite.replace('_unavailable_item_bg', '_available_item_bg')
|
|
742
|
+
: node.itemBackgroundSprite;
|
|
743
|
+
const itemSkin = preferredSprite === undefined
|
|
744
|
+
? undefined
|
|
745
|
+
: (iconDataUris[preferredSprite] ?? iconDataUris[node.itemBackgroundSprite]);
|
|
746
|
+
const icon = node.iconSprite === undefined ? undefined : iconDataUris[node.iconSprite];
|
|
747
|
+
const iconSize = node.layoutSize === 'small' ? 62 : 70;
|
|
748
|
+
const center = node.iconPosition ?? { x: node.width / 2, y: node.height / 2, centered: true };
|
|
749
|
+
const iconX = node.x + center.x - (center.centered ? iconSize / 2 : 0);
|
|
750
|
+
const iconY = node.y + center.y - (center.centered ? iconSize / 2 : 0);
|
|
751
|
+
const name = node.namePosition ?? { x: 3, y: -3, maxWidth: node.width - 6 };
|
|
752
|
+
const labelWidth = Math.min(name.maxWidth ?? node.width - 6, node.width - name.x - 3);
|
|
753
|
+
let fittedLabel = node.label;
|
|
754
|
+
while (fittedLabel.length > 1 && text.measure(fittedLabel, 12) > labelWidth)
|
|
755
|
+
fittedLabel = `${fittedLabel.slice(0, -2)}…`;
|
|
756
|
+
const label = node.layoutSize === 'small'
|
|
757
|
+
? ''
|
|
758
|
+
: text.render(fittedLabel, {
|
|
759
|
+
x: node.x + name.x,
|
|
760
|
+
y: node.y + name.y + 15,
|
|
761
|
+
fontSize: 12,
|
|
762
|
+
fill: '#e8e7e1',
|
|
763
|
+
weight: 600,
|
|
764
|
+
});
|
|
765
|
+
const skin = itemSkin === undefined
|
|
766
|
+
? `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="3" fill="#3a3936" stroke="#8b8b86" stroke-width="1"/>`
|
|
767
|
+
: `<image data-item-background-sprite="${escapeXml(preferredSprite)}" href="${escapeXml(itemSkin)}" x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" preserveAspectRatio="none"/>`;
|
|
768
|
+
const iconSvg = icon === undefined
|
|
769
|
+
? ''
|
|
770
|
+
: `<image href="${escapeXml(icon)}" x="${iconX}" y="${iconY}" width="${iconSize}" height="${iconSize}" preserveAspectRatio="xMidYMid meet"/>`;
|
|
771
|
+
const designTeamSprite = node.designTeamIcon?.sprite;
|
|
772
|
+
const designTeamImage = designTeamSprite === undefined ? undefined : iconDataUris[designTeamSprite];
|
|
773
|
+
const designTeamSize = designTeamSprite === undefined ? undefined : spriteDimensions[designTeamSprite];
|
|
774
|
+
const designTeamSvg = designTeamImage === undefined || node.designTeamIcon === undefined
|
|
775
|
+
? ''
|
|
776
|
+
: `<image data-design-team-icon="${escapeXml(designTeamSprite)}" href="${escapeXml(designTeamImage)}" x="${node.x + node.designTeamIcon.x}" y="${node.y + node.designTeamIcon.y}" width="${designTeamSize?.width ?? 20}" height="${designTeamSize?.height ?? 20}" preserveAspectRatio="xMinYMin meet"/>`;
|
|
777
|
+
const subTechnologySvg = (node.subTechnologies ?? []).flatMap(({ id, iconSprite, slot }) => {
|
|
778
|
+
if (slot === undefined)
|
|
779
|
+
return [];
|
|
780
|
+
const x = node.x + slot.position.x;
|
|
781
|
+
const y = node.y + slot.position.y;
|
|
782
|
+
const subIcon = iconSprite === undefined ? undefined : iconDataUris[iconSprite];
|
|
783
|
+
return [
|
|
784
|
+
`<g data-subtechnology-id="${escapeXml(id)}" data-slot-index="${slot.index}" data-icon-sprite="${escapeXml(iconSprite ?? '')}"><rect x="${x}" y="${y}" width="${slot.size.width}" height="${slot.size.height}" rx="3" fill="#3a3936" stroke="#8b8b86" stroke-width="1"/>${subIcon === undefined ? '' : `<image href="${escapeXml(subIcon)}" x="${x + 2}" y="${y + 2}" width="${Math.max(1, slot.size.width - 4)}" height="${Math.max(1, slot.size.height - 4)}" preserveAspectRatio="xMidYMid meet"/>`}<title>${escapeXml(id)}</title></g>`,
|
|
785
|
+
];
|
|
786
|
+
});
|
|
787
|
+
return `<g data-node-id="${escapeXml(node.id)}"${node.scenarioStatus === undefined ? '' : ` data-scenario-status="${node.scenarioStatus}"`} data-layout-size="${node.layoutSize ?? 'unknown'}" data-icon-sprite="${escapeXml(node.iconSprite ?? '')}"${node.sourcePath === undefined ? '' : ` data-source-path="${escapeXml(node.sourcePath)}" data-source-line="${node.sourceLine ?? 1}"`}>${skin}${iconSvg}${label}${subTechnologySvg.join('')}${designTeamSvg}<title>${escapeXml(node.id)}</title></g>`;
|
|
788
|
+
}
|
|
789
|
+
const colour = node.scenarioStatus === 'researched'
|
|
790
|
+
? { fill: '#214c38', stroke: '#79d29b' }
|
|
791
|
+
: node.scenarioStatus === 'exclusive_conflict'
|
|
792
|
+
? { fill: '#503039', stroke: '#e27878' }
|
|
793
|
+
: node.scenarioStatus === 'missing_prerequisite'
|
|
794
|
+
? { fill: '#423c29', stroke: '#e0b66b' }
|
|
795
|
+
: colours(node.kind);
|
|
615
796
|
const small = node.layoutSize === 'small';
|
|
616
797
|
const iconDataUri = node.iconSprite === undefined ? undefined : iconDataUris[node.iconSprite];
|
|
617
798
|
const compactIcon = iconDataUri !== undefined && small && node.width <= SMALL_NODE_WIDTH;
|
|
@@ -651,7 +832,17 @@ function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, year
|
|
|
651
832
|
: `<image href="${escapeXml(iconDataUri)}" x="${node.x + (compactIcon ? 6 : 8)}" y="${node.y + (node.height - iconSize) / 2}" width="${iconSize}" height="${iconSize}" preserveAspectRatio="xMidYMid meet"/>`;
|
|
652
833
|
const layoutAttribute = node.layoutSize === undefined ? '' : ` data-layout-size="${node.layoutSize}"`;
|
|
653
834
|
const iconAttribute = node.iconSprite === undefined ? '' : ` data-icon-sprite="${escapeXml(node.iconSprite)}"`;
|
|
654
|
-
|
|
835
|
+
const subTechnologySvg = (node.subTechnologies ?? []).flatMap(({ id, iconSprite, slot }) => {
|
|
836
|
+
if (slot === undefined)
|
|
837
|
+
return [];
|
|
838
|
+
const x = node.x + slot.position.x;
|
|
839
|
+
const y = node.y + slot.position.y;
|
|
840
|
+
const image = iconSprite === undefined ? undefined : iconDataUris[iconSprite];
|
|
841
|
+
return [
|
|
842
|
+
`<g data-subtechnology-id="${escapeXml(id)}" data-slot-index="${slot.index}" data-icon-sprite="${escapeXml(iconSprite ?? '')}"><rect x="${x}" y="${y}" width="${slot.size.width}" height="${slot.size.height}" rx="3" fill="#23364b" stroke="#91a9bd" stroke-width="1"/>${image === undefined ? '' : `<image href="${escapeXml(image)}" x="${x + 2}" y="${y + 2}" width="${Math.max(1, slot.size.width - 4)}" height="${Math.max(1, slot.size.height - 4)}" preserveAspectRatio="xMidYMid meet"/>`}<title>${escapeXml(id)}</title></g>`,
|
|
843
|
+
];
|
|
844
|
+
});
|
|
845
|
+
return `<g data-node-id="${escapeXml(node.id)}"${node.scenarioStatus === undefined ? '' : ` data-scenario-status="${node.scenarioStatus}"`}${layoutAttribute}${iconAttribute}${node.sourcePath === undefined ? '' : ` data-source-path="${escapeXml(node.sourcePath)}" data-source-line="${node.sourceLine ?? 1}"`}><rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="8" fill="${colour.fill}" stroke="${colour.stroke}" stroke-width="2"/><title>${escapeXml(`${node.id}${node.layoutSize === undefined ? '' : ` · ${node.layoutSize} layout`}${node.sourcePath === undefined ? '' : ` · ${node.sourcePath}:${node.sourceLine ?? 1}`}`)}</title>${iconSvg}${labelSvg}${subtitleSvg}${subTechnologySvg.join('')}</g>`;
|
|
655
846
|
});
|
|
656
847
|
const title = text.render(titleText, {
|
|
657
848
|
x: PADDING,
|
|
@@ -664,7 +855,7 @@ function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, year
|
|
|
664
855
|
return {
|
|
665
856
|
width,
|
|
666
857
|
height,
|
|
667
|
-
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(titleText)}"><defs><marker id="tech-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#8298ad"/></marker>${text.definitions()}</defs><rect width="100%" height="100%" fill="#0d1721"
|
|
858
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(titleText)}"><defs><marker id="tech-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#8298ad"/></marker>${panelPattern}${text.definitions()}</defs><rect width="100%" height="100%" fill="${panelPattern === '' ? (folderView ? '#171715' : '#0d1721') : 'url(#tech-folder-panel)'}"/><g>${backgroundSvg.join('')}</g>${folderView ? '' : title + mode}<g>${yearGuideSvg.join('')}</g><g>${edgeSvg.join('')}</g><g>${nodeSvg.join('')}</g></svg>`,
|
|
668
859
|
};
|
|
669
860
|
}
|
|
670
861
|
function htmlFor(title, svg, json) {
|
|
@@ -674,20 +865,50 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
674
865
|
options.signal?.throwIfAborted();
|
|
675
866
|
const maximum = Math.max(1, Math.min(options.maxNodes ?? 600, MAX_RENDER_NODES));
|
|
676
867
|
const selection = viewSelection(graph, options);
|
|
677
|
-
const retained =
|
|
868
|
+
const retained = options.scenario === undefined
|
|
869
|
+
? selection.nodes.slice(0, maximum)
|
|
870
|
+
: presentScenario(graph, selection.nodes.slice(0, maximum), options.scenario);
|
|
678
871
|
const retainedIds = new Set(retained.map(({ id }) => id));
|
|
679
872
|
const edges = selection.edges.filter(({ from, to }) => retainedIds.has(from) && retainedIds.has(to));
|
|
680
|
-
const folderLayout =
|
|
681
|
-
? layoutFolder(retained, selection.yearMarkers)
|
|
873
|
+
const folderLayout = options.view === 'folder'
|
|
874
|
+
? layoutFolder(retained, selection.yearMarkers, selection.backgrounds)
|
|
682
875
|
: { nodes: layoutGenerated(retained, edges), yearMarkers: [] };
|
|
683
876
|
const positioned = folderLayout.nodes;
|
|
684
877
|
const iconDataUris = options.iconDataUris ?? {};
|
|
878
|
+
const spriteDimensions = options.spriteDimensions ?? {};
|
|
879
|
+
const firstSourceNode = positioned.length > 0 &&
|
|
880
|
+
positioned.every(({ placement }) => placement?.pixelX !== undefined && placement.pixelY !== undefined)
|
|
881
|
+
? positioned[0]
|
|
882
|
+
: undefined;
|
|
883
|
+
const positionedBackgrounds = firstSourceNode === undefined
|
|
884
|
+
? []
|
|
885
|
+
: selection.backgrounds.map((background) => ({
|
|
886
|
+
...background,
|
|
887
|
+
x: background.position.x +
|
|
888
|
+
firstSourceNode.x -
|
|
889
|
+
firstSourceNode.placement.pixelX -
|
|
890
|
+
(firstSourceNode.placement.layoutOffsetX ?? 0),
|
|
891
|
+
y: background.position.y +
|
|
892
|
+
firstSourceNode.y -
|
|
893
|
+
firstSourceNode.placement.pixelY -
|
|
894
|
+
(firstSourceNode.placement.layoutOffsetY ?? 0),
|
|
895
|
+
}));
|
|
685
896
|
const requestedIconSprites = [
|
|
686
|
-
...new Set(
|
|
897
|
+
...new Set([
|
|
898
|
+
...retained.flatMap(({ iconSprite, itemBackgroundSprite, designTeamIcon, subTechnologies }) => [
|
|
899
|
+
...(iconSprite === undefined ? [] : [iconSprite]),
|
|
900
|
+
...(designTeamIcon === undefined ? [] : [designTeamIcon.sprite]),
|
|
901
|
+
...(itemBackgroundSprite === undefined
|
|
902
|
+
? []
|
|
903
|
+
: itemBackgroundSprites(itemBackgroundSprite)),
|
|
904
|
+
...(subTechnologies ?? []).flatMap((sub) => sub.iconSprite === undefined ? [] : [sub.iconSprite]),
|
|
905
|
+
]),
|
|
906
|
+
...selection.backgrounds.map(({ sprite }) => sprite),
|
|
907
|
+
]),
|
|
687
908
|
].sort(compareCodeUnits);
|
|
688
909
|
const renderedIconSprites = requestedIconSprites.filter((sprite) => iconDataUris[sprite] !== undefined);
|
|
689
910
|
const unresolvedIconSprites = requestedIconSprites.filter((sprite) => iconDataUris[sprite] === undefined);
|
|
690
|
-
const rendered = svgFor(selection.title, positioned, edges, selection.sourceAccurate, iconDataUris, folderLayout.yearMarkers, folderLayout.yearAxis);
|
|
911
|
+
const rendered = svgFor(selection.title, positioned, edges, selection.sourceAccurate, iconDataUris, spriteDimensions, folderLayout.yearMarkers, positionedBackgrounds, folderLayout.yearAxis, options.view === 'folder' && selection.sourceAccurate);
|
|
691
912
|
const budget = options.budget ?? new RenderBudget();
|
|
692
913
|
budget.reserve(rendered.width, rendered.height, `technology ${options.view} render`);
|
|
693
914
|
budget.reserveRasterOperation(`technology-render:${graph.revision}:${options.view}:${sha256Bytes(rendered.svg)}`, `technology ${options.view} rasterization`);
|
|
@@ -699,6 +920,15 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
699
920
|
options.signal?.throwIfAborted();
|
|
700
921
|
const json = `${canonicalJson({
|
|
701
922
|
...selection.payload,
|
|
923
|
+
...(options.scenario === undefined
|
|
924
|
+
? {}
|
|
925
|
+
: {
|
|
926
|
+
scenarioPresentation: {
|
|
927
|
+
year: options.scenario.year,
|
|
928
|
+
researchedTechnologyIds: [...new Set(options.scenario.researchedTechnologyIds)].sort(compareCodeUnits),
|
|
929
|
+
interpretation: 'Structural prerequisites and exclusivity only; dynamic country conditions remain unresolved',
|
|
930
|
+
},
|
|
931
|
+
}),
|
|
702
932
|
nodes: retained,
|
|
703
933
|
edges,
|
|
704
934
|
omittedNodeCount: Math.max(0, selection.nodes.length - retained.length),
|
|
@@ -712,6 +942,21 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
712
942
|
axis: folderLayout.yearAxis ?? null,
|
|
713
943
|
markerCount: folderLayout.yearMarkers.length,
|
|
714
944
|
},
|
|
945
|
+
backgrounds: selection.backgrounds.map((background) => ({
|
|
946
|
+
id: background.id,
|
|
947
|
+
folderId: background.folderId,
|
|
948
|
+
name: background.name,
|
|
949
|
+
sprite: background.sprite,
|
|
950
|
+
sourcePath: background.sourcePath,
|
|
951
|
+
sourceLine: background.location.start.line,
|
|
952
|
+
placement: positionedBackgrounds.find(({ id }) => id === background.id) ?? null,
|
|
953
|
+
status: positionedBackgrounds.some(({ id }) => id === background.id) &&
|
|
954
|
+
iconDataUris[background.sprite] !== undefined &&
|
|
955
|
+
(background.size.width ?? spriteDimensions[background.sprite]?.width) !== undefined &&
|
|
956
|
+
(background.size.height ?? spriteDimensions[background.sprite]?.height) !== undefined
|
|
957
|
+
? 'rendered'
|
|
958
|
+
: 'unresolved',
|
|
959
|
+
})),
|
|
715
960
|
})}\n`;
|
|
716
961
|
const html = options.includeHtml === true ? htmlFor(selection.title, rendered.svg, json) : undefined;
|
|
717
962
|
return {
|
|
@@ -740,9 +985,20 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
740
985
|
/** Return only the sprite names that the bounded render selection will display. */
|
|
741
986
|
export function technologyRenderIconSprites(graph, options) {
|
|
742
987
|
const maximum = Math.max(1, Math.min(options.maxNodes ?? 600, MAX_RENDER_NODES));
|
|
988
|
+
const selection = viewSelection(graph, options);
|
|
743
989
|
return [
|
|
744
|
-
...new Set(
|
|
745
|
-
.nodes
|
|
746
|
-
|
|
990
|
+
...new Set([
|
|
991
|
+
...selection.nodes
|
|
992
|
+
.slice(0, maximum)
|
|
993
|
+
.flatMap(({ iconSprite, itemBackgroundSprite, designTeamIcon, subTechnologies }) => [
|
|
994
|
+
...(iconSprite === undefined ? [] : [iconSprite]),
|
|
995
|
+
...(designTeamIcon === undefined ? [] : [designTeamIcon.sprite]),
|
|
996
|
+
...(itemBackgroundSprite === undefined
|
|
997
|
+
? []
|
|
998
|
+
: itemBackgroundSprites(itemBackgroundSprite)),
|
|
999
|
+
...(subTechnologies ?? []).flatMap((sub) => sub.iconSprite === undefined ? [] : [sub.iconSprite]),
|
|
1000
|
+
]),
|
|
1001
|
+
...selection.backgrounds.map(({ sprite }) => sprite),
|
|
1002
|
+
]),
|
|
747
1003
|
].sort(compareCodeUnits);
|
|
748
1004
|
}
|
|
@@ -142,11 +142,19 @@ async function resolveTechnologyIconDataUris(cached, requestedSprites, budget, s
|
|
|
142
142
|
const icons = [
|
|
143
143
|
...cached.graph.technologies.map(({ icon }) => icon),
|
|
144
144
|
...cached.graph.doctrineDefinitions.flatMap(({ icon }) => icon ?? []),
|
|
145
|
+
...(cached.graph.backgrounds ?? []).map(({ sprite }) => ({
|
|
146
|
+
sprite,
|
|
147
|
+
spritePath: cached.snapshot.index.find('sprite', sprite)?.path,
|
|
148
|
+
})),
|
|
149
|
+
...[...requestedSprites].map((sprite) => ({
|
|
150
|
+
sprite,
|
|
151
|
+
spritePath: cached.snapshot.index.find('sprite', sprite)?.path,
|
|
152
|
+
})),
|
|
145
153
|
].filter(({ sprite }) => requestedSprites.has(sprite));
|
|
146
154
|
const spriteSourcePaths = new Set(icons.flatMap(({ spritePath }) => (spritePath === undefined ? [] : [spritePath])));
|
|
147
155
|
const spriteFiles = cached.snapshot.files.filter(({ displayPath }) => spriteSourcePaths.has(displayPath));
|
|
148
156
|
if (spriteFiles.length === 0)
|
|
149
|
-
return {};
|
|
157
|
+
return { dataUris: {}, dimensions: {} };
|
|
150
158
|
const graph = buildGuiSourceGraph(spriteFiles, cached.snapshot.index);
|
|
151
159
|
const files = new Map();
|
|
152
160
|
for (const file of [...spriteFiles, ...cached.assetFiles])
|
|
@@ -158,6 +166,7 @@ async function resolveTechnologyIconDataUris(cached, requestedSprites, budget, s
|
|
|
158
166
|
unique.set(icon.sprite, icon);
|
|
159
167
|
const entries = [...unique.entries()].sort(([left], [right]) => compareCodeUnits(left, right));
|
|
160
168
|
const dataUris = {};
|
|
169
|
+
const dimensions = {};
|
|
161
170
|
let cursor = 0;
|
|
162
171
|
await Promise.all(Array.from({ length: Math.min(8, entries.length) }, async () => {
|
|
163
172
|
while (cursor < entries.length) {
|
|
@@ -171,11 +180,25 @@ async function resolveTechnologyIconDataUris(cached, requestedSprites, budget, s
|
|
|
171
180
|
if (sprite === undefined)
|
|
172
181
|
continue;
|
|
173
182
|
const frame = await catalog.loadSpriteFrame(sprite, 0);
|
|
174
|
-
if (frame?.supported === true && frame.dataUri !== undefined)
|
|
183
|
+
if (frame?.supported === true && frame.dataUri !== undefined) {
|
|
175
184
|
dataUris[spriteName] = frame.dataUri;
|
|
185
|
+
dimensions[spriteName] = {
|
|
186
|
+
width: frame.width,
|
|
187
|
+
height: frame.height,
|
|
188
|
+
...(sprite.borderSize?.width === undefined
|
|
189
|
+
? {}
|
|
190
|
+
: { borderWidth: sprite.borderSize.width }),
|
|
191
|
+
...(sprite.borderSize?.height === undefined
|
|
192
|
+
? {}
|
|
193
|
+
: { borderHeight: sprite.borderSize.height }),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
176
196
|
}
|
|
177
197
|
}));
|
|
178
|
-
return
|
|
198
|
+
return {
|
|
199
|
+
dataUris: Object.fromEntries(Object.entries(dataUris).sort(([left], [right]) => compareCodeUnits(left, right))),
|
|
200
|
+
dimensions: Object.fromEntries(Object.entries(dimensions).sort(([left], [right]) => compareCodeUnits(left, right))),
|
|
201
|
+
};
|
|
179
202
|
}
|
|
180
203
|
function rememberGraph(state, graph) {
|
|
181
204
|
const history = state.history.get(graph.workspaceId) ?? new Map();
|
|
@@ -450,17 +473,38 @@ export class TechnologyTreeViewer {
|
|
|
450
473
|
const generation = this.engine.generation(workspaceId);
|
|
451
474
|
const cached = this.#state.current.get(workspaceId);
|
|
452
475
|
const snapshot = await scanTechnologySources(this.engine, workspaceId, workspace, options.principal, options.signal);
|
|
453
|
-
const analysisMode = '
|
|
476
|
+
const analysisMode = workspace.roots.some(({ kind }) => kind === 'game') || snapshot.files.length > 1_000
|
|
477
|
+
? 'focused'
|
|
478
|
+
: undefined;
|
|
454
479
|
const unchangedSource = cached?.generation === generation && cached.snapshot.revision === snapshot.revision;
|
|
455
480
|
const preliminary = unchangedSource
|
|
456
481
|
? cached.graph
|
|
457
482
|
: await buildTechnologyGraphAsync(snapshot, {
|
|
458
483
|
workspaceIdentity: workspace.workspaceIdentity,
|
|
459
484
|
cache: this.#state.fragments,
|
|
460
|
-
analysisMode,
|
|
485
|
+
...(analysisMode === undefined ? {} : { analysisMode }),
|
|
461
486
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
462
487
|
});
|
|
463
|
-
const
|
|
488
|
+
const guiSprites = [
|
|
489
|
+
...preliminary.itemLayouts.flatMap(({ designTeamIcon }) => designTeamIcon === undefined ? [] : [designTeamIcon.sprite]),
|
|
490
|
+
...preliminary.itemLayouts.flatMap(({ subTechnologySlots }) => (subTechnologySlots ?? []).flatMap(({ sprite }) => (sprite === undefined ? [] : [sprite]))),
|
|
491
|
+
...preliminary.itemLayouts.flatMap(({ backgroundSprite }) => backgroundSprite === undefined
|
|
492
|
+
? []
|
|
493
|
+
: [
|
|
494
|
+
backgroundSprite,
|
|
495
|
+
backgroundSprite.replace('_unavailable_item_bg', '_available_item_bg'),
|
|
496
|
+
backgroundSprite.replace('_unavailable_item_bg', '_researched_item_bg'),
|
|
497
|
+
]),
|
|
498
|
+
...(preliminary.backgrounds ?? []).map(({ sprite }) => sprite),
|
|
499
|
+
];
|
|
500
|
+
const guiTextures = guiSprites.flatMap((sprite) => {
|
|
501
|
+
const texture = snapshot.index.find('sprite', sprite)?.metadata.texture;
|
|
502
|
+
if (typeof texture !== 'string')
|
|
503
|
+
return [];
|
|
504
|
+
const normalized = texture.replaceAll('\\', '/').replace(/\/{2,}/gu, '/');
|
|
505
|
+
return normalized.includes('..') || /[[\]{}*?!]/u.test(normalized) ? [] : [normalized];
|
|
506
|
+
});
|
|
507
|
+
const patterns = [...new Set([...technologyAssetPatterns(preliminary), ...guiTextures])].sort(compareCodeUnits);
|
|
464
508
|
const assetFiles = patterns.length === 0
|
|
465
509
|
? []
|
|
466
510
|
: await this.engine.scanner.scan(workspace, {
|
|
@@ -479,7 +523,7 @@ export class TechnologyTreeViewer {
|
|
|
479
523
|
workspaceIdentity: workspace.workspaceIdentity,
|
|
480
524
|
cache: this.#state.fragments,
|
|
481
525
|
assetFiles,
|
|
482
|
-
analysisMode,
|
|
526
|
+
...(analysisMode === undefined ? {} : { analysisMode }),
|
|
483
527
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
484
528
|
});
|
|
485
529
|
this.#state.current.set(workspaceId, { generation, snapshot, assetFiles, graph });
|
|
@@ -620,6 +664,7 @@ export class TechnologyTreeViewer {
|
|
|
620
664
|
maxNodes: input.maxNodes ?? 1_000,
|
|
621
665
|
includeHtml: input.includeHtml ?? false,
|
|
622
666
|
budget,
|
|
667
|
+
...(input.scenario === undefined ? {} : { scenario: input.scenario }),
|
|
623
668
|
...(input.folderId === undefined ? {} : { folderId: input.folderId }),
|
|
624
669
|
...(input.technologyId === undefined ? {} : { technologyId: input.technologyId }),
|
|
625
670
|
...(input.categoryId === undefined ? {} : { categoryId: input.categoryId }),
|
|
@@ -639,8 +684,9 @@ export class TechnologyTreeViewer {
|
|
|
639
684
|
maxNodes: 1_000,
|
|
640
685
|
})),
|
|
641
686
|
]);
|
|
642
|
-
const iconDataUris = await resolveTechnologyIconDataUris(cached, requestedIconSprites, budget, input.signal);
|
|
687
|
+
const { dataUris: iconDataUris, dimensions: spriteDimensions } = await resolveTechnologyIconDataUris(cached, requestedIconSprites, budget, input.signal);
|
|
643
688
|
renderOptions.iconDataUris = iconDataUris;
|
|
689
|
+
renderOptions.spriteDimensions = spriteDimensions;
|
|
644
690
|
const render = await renderTechnologyGraph(graph, renderOptions);
|
|
645
691
|
const focused = [];
|
|
646
692
|
for (const folder of focusedFolders) {
|
|
@@ -651,7 +697,9 @@ export class TechnologyTreeViewer {
|
|
|
651
697
|
maxNodes: 1_000,
|
|
652
698
|
includeHtml: false,
|
|
653
699
|
iconDataUris,
|
|
700
|
+
spriteDimensions,
|
|
654
701
|
budget,
|
|
702
|
+
...(input.scenario === undefined ? {} : { scenario: input.scenario }),
|
|
655
703
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
656
704
|
}));
|
|
657
705
|
}
|