hoi4-agent-tools 3.3.0 → 3.4.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/CHANGELOG.md +12 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +13 -8
- package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
- 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 +16 -0
- 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 +38 -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/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/render.js +286 -31
- package/dist/hoi4_agent_tools/technology/service.js +55 -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 +3 -1
- package/docs/focus.md +10 -0
- package/docs/gui.md +49 -3
- package/docs/map.md +8 -0
- package/docs/probability.md +3 -1
- package/docs/technology.md +7 -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,28 @@ 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
|
+
return {
|
|
76
|
+
id,
|
|
77
|
+
...(definition === undefined ? {} : { iconSprite: definition.icon.sprite }),
|
|
78
|
+
placementStatus: slot === undefined ? 'missing_source_slot' : 'source_slot',
|
|
79
|
+
...(slot === undefined ? {} : { slot }),
|
|
80
|
+
};
|
|
81
|
+
}),
|
|
82
|
+
}),
|
|
51
83
|
...(placement === undefined ? {} : { placement }),
|
|
52
84
|
};
|
|
53
85
|
}
|
|
@@ -58,10 +90,52 @@ function sortedUniqueNodes(values) {
|
|
|
58
90
|
result.set(value.id, value);
|
|
59
91
|
return [...result.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
60
92
|
}
|
|
93
|
+
function presentScenario(graph, nodes, scenario) {
|
|
94
|
+
const known = new Map(graph.technologies.map((technology) => [technology.id, technology]));
|
|
95
|
+
const researched = new Set(scenario.researchedTechnologyIds);
|
|
96
|
+
for (const id of researched)
|
|
97
|
+
if (!known.has(id))
|
|
98
|
+
throw new ServiceError('TECH_RENDER_SCENARIO_TECH_UNKNOWN', `Scenario references unknown researched technology ${id}`, { technologyId: id });
|
|
99
|
+
return nodes.map((node) => {
|
|
100
|
+
const technology = known.get(node.id);
|
|
101
|
+
if (technology === undefined)
|
|
102
|
+
return node;
|
|
103
|
+
const missingPrerequisiteIds = graph.edges
|
|
104
|
+
.filter(({ kind, to, from }) => kind === 'prerequisite' && to === node.id && !researched.has(from))
|
|
105
|
+
.map(({ from }) => from)
|
|
106
|
+
.sort(compareCodeUnits);
|
|
107
|
+
const exclusiveConflictIds = graph.edges
|
|
108
|
+
.filter(({ kind, to, from }) => kind === 'exclusive' &&
|
|
109
|
+
((to === node.id && researched.has(from)) || (from === node.id && researched.has(to))))
|
|
110
|
+
.map(({ to, from }) => (to === node.id ? from : to))
|
|
111
|
+
.sort(compareCodeUnits);
|
|
112
|
+
const parsedYear = Number(technology.startYear);
|
|
113
|
+
const aheadOfTimeYears = technology.startYear !== undefined &&
|
|
114
|
+
/^\d+$/u.test(technology.startYear) &&
|
|
115
|
+
Number.isSafeInteger(parsedYear)
|
|
116
|
+
? Math.max(0, parsedYear - scenario.year)
|
|
117
|
+
: undefined;
|
|
118
|
+
const scenarioStatus = researched.has(node.id)
|
|
119
|
+
? 'researched'
|
|
120
|
+
: exclusiveConflictIds.length > 0
|
|
121
|
+
? 'exclusive_conflict'
|
|
122
|
+
: missingPrerequisiteIds.length > 0
|
|
123
|
+
? 'missing_prerequisite'
|
|
124
|
+
: 'structural_candidate';
|
|
125
|
+
return {
|
|
126
|
+
...node,
|
|
127
|
+
scenarioStatus,
|
|
128
|
+
missingPrerequisiteIds,
|
|
129
|
+
exclusiveConflictIds,
|
|
130
|
+
...(aheadOfTimeYears === undefined ? {} : { aheadOfTimeYears }),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
61
134
|
function viewSelection(graph, options) {
|
|
62
135
|
const nodes = [];
|
|
63
136
|
const edges = [];
|
|
64
137
|
let yearMarkers = [];
|
|
138
|
+
let backgrounds = [];
|
|
65
139
|
let title;
|
|
66
140
|
let sourceAccurate = false;
|
|
67
141
|
if (options.view === 'summary') {
|
|
@@ -117,8 +191,9 @@ function viewSelection(graph, options) {
|
|
|
117
191
|
edges.push(...graph.edges
|
|
118
192
|
.filter(({ from, to }) => selected.has(from) && selected.has(to))
|
|
119
193
|
.map(({ id, from, to, kind }) => ({ id, from, to, kind })));
|
|
120
|
-
sourceAccurate =
|
|
194
|
+
sourceAccurate = nodes.every(({ placement }) => placement?.geometryStatus === 'source_pixel');
|
|
121
195
|
yearMarkers = graph.yearMarkers.filter(({ folderId }) => folderId === options.folderId);
|
|
196
|
+
backgrounds = (graph.backgrounds ?? []).filter(({ folderId }) => folderId === options.folderId);
|
|
122
197
|
}
|
|
123
198
|
else if (options.view === 'dependencies' || options.view === 'technology') {
|
|
124
199
|
const ids = options.technologyId === undefined
|
|
@@ -410,6 +485,7 @@ function viewSelection(graph, options) {
|
|
|
410
485
|
nodes: selectedNodes,
|
|
411
486
|
edges: selectedEdges,
|
|
412
487
|
yearMarkers,
|
|
488
|
+
backgrounds,
|
|
413
489
|
sourceAccurate,
|
|
414
490
|
title,
|
|
415
491
|
payload: {
|
|
@@ -428,7 +504,7 @@ function viewSelection(graph, options) {
|
|
|
428
504
|
function issueKey(issue) {
|
|
429
505
|
return sha256Bytes(canonicalJson({ code: issue.code, details: issue.details })).slice(0, 20);
|
|
430
506
|
}
|
|
431
|
-
function layoutFolder(nodes, yearMarkers) {
|
|
507
|
+
function layoutFolder(nodes, yearMarkers, backgrounds) {
|
|
432
508
|
const dimensions = (node) => node.placement?.layoutWidth !== undefined && node.placement.layoutHeight !== undefined
|
|
433
509
|
? { width: node.placement.layoutWidth, height: node.placement.layoutHeight }
|
|
434
510
|
: node.layoutSize === 'small'
|
|
@@ -436,23 +512,24 @@ function layoutFolder(nodes, yearMarkers) {
|
|
|
436
512
|
: { width: LARGE_NODE_WIDTH, height: LARGE_NODE_HEIGHT };
|
|
437
513
|
const sourcePixel = nodes.filter(({ placement }) => placement?.pixelX !== undefined && placement.pixelY !== undefined);
|
|
438
514
|
if (sourcePixel.length === nodes.length && sourcePixel.length > 0) {
|
|
515
|
+
const placedBackgrounds = backgrounds.filter(({ name }) => name !== 'folder_panel');
|
|
439
516
|
const sourceNodeX = (node) => node.placement.pixelX + (node.placement?.layoutOffsetX ?? 0);
|
|
440
517
|
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));
|
|
518
|
+
const minimumX = Math.min(...sourcePixel.map(sourceNodeX), ...yearMarkers.map(({ position }) => position.x), ...placedBackgrounds.map(({ position }) => position.x));
|
|
519
|
+
const minimumY = Math.min(...sourcePixel.map(sourceNodeY), ...yearMarkers.map(({ position }) => position.y), ...placedBackgrounds.map(({ position }) => position.y));
|
|
443
520
|
const positionedNodes = sourcePixel.map((node) => {
|
|
444
521
|
const size = dimensions(node);
|
|
445
522
|
return {
|
|
446
523
|
...node,
|
|
447
524
|
x: PADDING + sourceNodeX(node) - minimumX,
|
|
448
|
-
y:
|
|
525
|
+
y: PADDING + sourceNodeY(node) - minimumY,
|
|
449
526
|
...size,
|
|
450
527
|
};
|
|
451
528
|
});
|
|
452
529
|
const positionedYearMarkers = yearMarkers.map((marker) => ({
|
|
453
530
|
...marker,
|
|
454
531
|
x: PADDING + marker.position.x - minimumX,
|
|
455
|
-
y:
|
|
532
|
+
y: PADDING + marker.position.y - minimumY,
|
|
456
533
|
}));
|
|
457
534
|
const xValues = yearMarkers.map(({ position }) => position.x);
|
|
458
535
|
const yValues = yearMarkers.map(({ position }) => position.y);
|
|
@@ -571,9 +648,13 @@ function colours(kind) {
|
|
|
571
648
|
removed: { fill: '#4a2828', stroke: '#ef7d7d' },
|
|
572
649
|
}[kind];
|
|
573
650
|
}
|
|
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
|
-
|
|
651
|
+
function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, spriteDimensions, yearMarkers, backgrounds, yearAxis, folderView = false) {
|
|
652
|
+
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
|
|
653
|
+
.filter(({ name }) => name !== 'folder_panel')
|
|
654
|
+
.map(({ x, size, sprite }) => x + (size.width ?? spriteDimensions[sprite]?.width ?? 0)));
|
|
655
|
+
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
|
|
656
|
+
.filter(({ name }) => name !== 'folder_panel')
|
|
657
|
+
.map(({ y, size, sprite }) => y + (size.height ?? spriteDimensions[sprite]?.height ?? 0)));
|
|
577
658
|
const width = Math.ceil(maximumX + PADDING);
|
|
578
659
|
const height = Math.ceil(maximumY + PADDING);
|
|
579
660
|
if (width > 16_384 || height > 16_384 || width * height > RENDER_MAX_PIXELS)
|
|
@@ -583,35 +664,134 @@ function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, year
|
|
|
583
664
|
});
|
|
584
665
|
const byId = new Map(positioned.map((node) => [node.id, node]));
|
|
585
666
|
const text = new DeterministicSvgTextRenderer();
|
|
667
|
+
const panel = folderView ? backgrounds.find(({ name }) => name === 'folder_panel') : undefined;
|
|
668
|
+
const panelDimensions = panel === undefined ? undefined : spriteDimensions[panel.sprite];
|
|
669
|
+
const panelUri = panel === undefined ? undefined : iconDataUris[panel.sprite];
|
|
670
|
+
const borderWidth = panelDimensions?.borderWidth ?? Math.floor((panelDimensions?.width ?? 0) / 3);
|
|
671
|
+
const borderHeight = panelDimensions?.borderHeight ?? Math.floor((panelDimensions?.height ?? 0) / 3);
|
|
672
|
+
const tileWidth = (panelDimensions?.width ?? 0) - 2 * borderWidth;
|
|
673
|
+
const tileHeight = (panelDimensions?.height ?? 0) - 2 * borderHeight;
|
|
674
|
+
const panelPattern = panelUri !== undefined && panelDimensions !== undefined && tileWidth > 0 && tileHeight > 0
|
|
675
|
+
? `<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>`
|
|
676
|
+
: '';
|
|
677
|
+
const backgroundSvg = backgrounds.flatMap((background) => {
|
|
678
|
+
if (background.name === 'folder_panel')
|
|
679
|
+
return [];
|
|
680
|
+
const uri = iconDataUris[background.sprite];
|
|
681
|
+
const width = background.size.width ?? spriteDimensions[background.sprite]?.width;
|
|
682
|
+
const height = background.size.height ?? spriteDimensions[background.sprite]?.height;
|
|
683
|
+
if (uri === undefined || width === undefined || height === undefined)
|
|
684
|
+
return [];
|
|
685
|
+
return [
|
|
686
|
+
`<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"/>`,
|
|
687
|
+
];
|
|
688
|
+
});
|
|
586
689
|
const yearGuideSvg = yearMarkers.map((marker) => {
|
|
587
|
-
const guide =
|
|
588
|
-
?
|
|
589
|
-
:
|
|
690
|
+
const guide = folderView
|
|
691
|
+
? ''
|
|
692
|
+
: yearAxis === 'vertical'
|
|
693
|
+
? `<line x1="${PADDING}" y1="${marker.y + 15}" x2="${width - PADDING}" y2="${marker.y + 15}" stroke="#34495c" stroke-width="1" stroke-dasharray="5 7"/>`
|
|
694
|
+
: `<line x1="${marker.x + 24}" y1="${HEADER_HEIGHT}" x2="${marker.x + 24}" y2="${height - PADDING}" stroke="#34495c" stroke-width="1" stroke-dasharray="5 7"/>`;
|
|
695
|
+
const fontSize = Math.max(10, Math.min(32, (marker.style?.maxHeight ?? 24) * 0.75));
|
|
696
|
+
const measuredWidth = text.measure(String(marker.year), fontSize);
|
|
697
|
+
const alignment = marker.style?.format?.toLowerCase();
|
|
698
|
+
const labelX = alignment === 'center'
|
|
699
|
+
? marker.x + ((marker.style?.maxWidth ?? measuredWidth) - measuredWidth) / 2
|
|
700
|
+
: alignment === 'right'
|
|
701
|
+
? marker.x + (marker.style?.maxWidth ?? measuredWidth) - measuredWidth
|
|
702
|
+
: marker.x;
|
|
590
703
|
const label = text.render(String(marker.year), {
|
|
591
|
-
x:
|
|
592
|
-
y: marker.y +
|
|
593
|
-
fontSize
|
|
594
|
-
fill: '#
|
|
704
|
+
x: labelX,
|
|
705
|
+
y: marker.y + fontSize,
|
|
706
|
+
fontSize,
|
|
707
|
+
fill: marker.style?.colour?.match(/^#[0-9a-fA-F]{6}$/u) ? marker.style.colour : '#dadada',
|
|
595
708
|
weight: 700,
|
|
596
709
|
});
|
|
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>`;
|
|
710
|
+
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
711
|
});
|
|
599
712
|
const edgeSvg = edges.flatMap((edge) => {
|
|
600
713
|
const from = byId.get(edge.from);
|
|
601
714
|
const to = byId.get(edge.to);
|
|
602
715
|
if (from === undefined || to === undefined)
|
|
603
716
|
return [];
|
|
604
|
-
const x1 = from.x + from.width;
|
|
717
|
+
const x1 = folderView ? from.x + from.width / 2 : from.x + from.width;
|
|
605
718
|
const y1 = from.y + from.height / 2;
|
|
606
|
-
const x2 = to.x;
|
|
719
|
+
const x2 = folderView ? to.x + to.width / 2 : to.x;
|
|
607
720
|
const y2 = to.y + to.height / 2;
|
|
608
721
|
const middle = (x1 + x2) / 2;
|
|
609
|
-
const colour = edge.kind.includes('exclusive') ? '#
|
|
722
|
+
const colour = edge.kind.includes('exclusive') ? '#a47942' : folderView ? '#70716e' : '#8298ad';
|
|
610
723
|
const dash = edge.kind.includes('exclusive') ? ' stroke-dasharray="7 5"' : '';
|
|
724
|
+
if (folderView) {
|
|
725
|
+
const horizontal = Math.abs(x2 - x1) >= Math.abs(y2 - y1);
|
|
726
|
+
const route = horizontal
|
|
727
|
+
? `M ${x1} ${y1} H ${middle} V ${y2} H ${x2}`
|
|
728
|
+
: `M ${x1} ${y1} V ${(y1 + y2) / 2} H ${x2} V ${y2}`;
|
|
729
|
+
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>`;
|
|
730
|
+
}
|
|
611
731
|
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
732
|
});
|
|
613
733
|
const nodeSvg = positioned.map((node) => {
|
|
614
|
-
|
|
734
|
+
if (folderView && node.placement !== undefined) {
|
|
735
|
+
const preferredSprite = node.itemBackgroundSprite === undefined
|
|
736
|
+
? undefined
|
|
737
|
+
: node.scenarioStatus === 'researched'
|
|
738
|
+
? node.itemBackgroundSprite.replace('_unavailable_item_bg', '_researched_item_bg')
|
|
739
|
+
: node.scenarioStatus === 'structural_candidate'
|
|
740
|
+
? node.itemBackgroundSprite.replace('_unavailable_item_bg', '_available_item_bg')
|
|
741
|
+
: node.itemBackgroundSprite;
|
|
742
|
+
const itemSkin = preferredSprite === undefined
|
|
743
|
+
? undefined
|
|
744
|
+
: (iconDataUris[preferredSprite] ?? iconDataUris[node.itemBackgroundSprite]);
|
|
745
|
+
const icon = node.iconSprite === undefined ? undefined : iconDataUris[node.iconSprite];
|
|
746
|
+
const iconSize = node.layoutSize === 'small' ? 62 : 70;
|
|
747
|
+
const center = node.iconPosition ?? { x: node.width / 2, y: node.height / 2, centered: true };
|
|
748
|
+
const iconX = node.x + center.x - (center.centered ? iconSize / 2 : 0);
|
|
749
|
+
const iconY = node.y + center.y - (center.centered ? iconSize / 2 : 0);
|
|
750
|
+
const name = node.namePosition ?? { x: 3, y: -3, maxWidth: node.width - 6 };
|
|
751
|
+
const labelWidth = Math.min(name.maxWidth ?? node.width - 6, node.width - name.x - 3);
|
|
752
|
+
let fittedLabel = node.label;
|
|
753
|
+
while (fittedLabel.length > 1 && text.measure(fittedLabel, 12) > labelWidth)
|
|
754
|
+
fittedLabel = `${fittedLabel.slice(0, -2)}…`;
|
|
755
|
+
const label = node.layoutSize === 'small'
|
|
756
|
+
? ''
|
|
757
|
+
: text.render(fittedLabel, {
|
|
758
|
+
x: node.x + name.x,
|
|
759
|
+
y: node.y + name.y + 15,
|
|
760
|
+
fontSize: 12,
|
|
761
|
+
fill: '#e8e7e1',
|
|
762
|
+
weight: 600,
|
|
763
|
+
});
|
|
764
|
+
const skin = itemSkin === undefined
|
|
765
|
+
? `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="3" fill="#3a3936" stroke="#8b8b86" stroke-width="1"/>`
|
|
766
|
+
: `<image data-item-background-sprite="${escapeXml(preferredSprite)}" href="${escapeXml(itemSkin)}" x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" preserveAspectRatio="none"/>`;
|
|
767
|
+
const iconSvg = icon === undefined
|
|
768
|
+
? ''
|
|
769
|
+
: `<image href="${escapeXml(icon)}" x="${iconX}" y="${iconY}" width="${iconSize}" height="${iconSize}" preserveAspectRatio="xMidYMid meet"/>`;
|
|
770
|
+
const designTeamSprite = node.designTeamIcon?.sprite;
|
|
771
|
+
const designTeamImage = designTeamSprite === undefined ? undefined : iconDataUris[designTeamSprite];
|
|
772
|
+
const designTeamSize = designTeamSprite === undefined ? undefined : spriteDimensions[designTeamSprite];
|
|
773
|
+
const designTeamSvg = designTeamImage === undefined || node.designTeamIcon === undefined
|
|
774
|
+
? ''
|
|
775
|
+
: `<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"/>`;
|
|
776
|
+
const subTechnologySvg = (node.subTechnologies ?? []).flatMap(({ id, iconSprite, slot }) => {
|
|
777
|
+
if (slot === undefined)
|
|
778
|
+
return [];
|
|
779
|
+
const x = node.x + slot.position.x;
|
|
780
|
+
const y = node.y + slot.position.y;
|
|
781
|
+
const subIcon = iconSprite === undefined ? undefined : iconDataUris[iconSprite];
|
|
782
|
+
return [
|
|
783
|
+
`<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>`,
|
|
784
|
+
];
|
|
785
|
+
});
|
|
786
|
+
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>`;
|
|
787
|
+
}
|
|
788
|
+
const colour = node.scenarioStatus === 'researched'
|
|
789
|
+
? { fill: '#214c38', stroke: '#79d29b' }
|
|
790
|
+
: node.scenarioStatus === 'exclusive_conflict'
|
|
791
|
+
? { fill: '#503039', stroke: '#e27878' }
|
|
792
|
+
: node.scenarioStatus === 'missing_prerequisite'
|
|
793
|
+
? { fill: '#423c29', stroke: '#e0b66b' }
|
|
794
|
+
: colours(node.kind);
|
|
615
795
|
const small = node.layoutSize === 'small';
|
|
616
796
|
const iconDataUri = node.iconSprite === undefined ? undefined : iconDataUris[node.iconSprite];
|
|
617
797
|
const compactIcon = iconDataUri !== undefined && small && node.width <= SMALL_NODE_WIDTH;
|
|
@@ -651,7 +831,17 @@ function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, year
|
|
|
651
831
|
: `<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
832
|
const layoutAttribute = node.layoutSize === undefined ? '' : ` data-layout-size="${node.layoutSize}"`;
|
|
653
833
|
const iconAttribute = node.iconSprite === undefined ? '' : ` data-icon-sprite="${escapeXml(node.iconSprite)}"`;
|
|
654
|
-
|
|
834
|
+
const subTechnologySvg = (node.subTechnologies ?? []).flatMap(({ id, iconSprite, slot }) => {
|
|
835
|
+
if (slot === undefined)
|
|
836
|
+
return [];
|
|
837
|
+
const x = node.x + slot.position.x;
|
|
838
|
+
const y = node.y + slot.position.y;
|
|
839
|
+
const image = iconSprite === undefined ? undefined : iconDataUris[iconSprite];
|
|
840
|
+
return [
|
|
841
|
+
`<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>`,
|
|
842
|
+
];
|
|
843
|
+
});
|
|
844
|
+
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
845
|
});
|
|
656
846
|
const title = text.render(titleText, {
|
|
657
847
|
x: PADDING,
|
|
@@ -664,7 +854,7 @@ function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris, year
|
|
|
664
854
|
return {
|
|
665
855
|
width,
|
|
666
856
|
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"
|
|
857
|
+
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
858
|
};
|
|
669
859
|
}
|
|
670
860
|
function htmlFor(title, svg, json) {
|
|
@@ -674,20 +864,50 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
674
864
|
options.signal?.throwIfAborted();
|
|
675
865
|
const maximum = Math.max(1, Math.min(options.maxNodes ?? 600, MAX_RENDER_NODES));
|
|
676
866
|
const selection = viewSelection(graph, options);
|
|
677
|
-
const retained =
|
|
867
|
+
const retained = options.scenario === undefined
|
|
868
|
+
? selection.nodes.slice(0, maximum)
|
|
869
|
+
: presentScenario(graph, selection.nodes.slice(0, maximum), options.scenario);
|
|
678
870
|
const retainedIds = new Set(retained.map(({ id }) => id));
|
|
679
871
|
const edges = selection.edges.filter(({ from, to }) => retainedIds.has(from) && retainedIds.has(to));
|
|
680
|
-
const folderLayout =
|
|
681
|
-
? layoutFolder(retained, selection.yearMarkers)
|
|
872
|
+
const folderLayout = options.view === 'folder'
|
|
873
|
+
? layoutFolder(retained, selection.yearMarkers, selection.backgrounds)
|
|
682
874
|
: { nodes: layoutGenerated(retained, edges), yearMarkers: [] };
|
|
683
875
|
const positioned = folderLayout.nodes;
|
|
684
876
|
const iconDataUris = options.iconDataUris ?? {};
|
|
877
|
+
const spriteDimensions = options.spriteDimensions ?? {};
|
|
878
|
+
const firstSourceNode = positioned.length > 0 &&
|
|
879
|
+
positioned.every(({ placement }) => placement?.pixelX !== undefined && placement.pixelY !== undefined)
|
|
880
|
+
? positioned[0]
|
|
881
|
+
: undefined;
|
|
882
|
+
const positionedBackgrounds = firstSourceNode === undefined
|
|
883
|
+
? []
|
|
884
|
+
: selection.backgrounds.map((background) => ({
|
|
885
|
+
...background,
|
|
886
|
+
x: background.position.x +
|
|
887
|
+
firstSourceNode.x -
|
|
888
|
+
firstSourceNode.placement.pixelX -
|
|
889
|
+
(firstSourceNode.placement.layoutOffsetX ?? 0),
|
|
890
|
+
y: background.position.y +
|
|
891
|
+
firstSourceNode.y -
|
|
892
|
+
firstSourceNode.placement.pixelY -
|
|
893
|
+
(firstSourceNode.placement.layoutOffsetY ?? 0),
|
|
894
|
+
}));
|
|
685
895
|
const requestedIconSprites = [
|
|
686
|
-
...new Set(
|
|
896
|
+
...new Set([
|
|
897
|
+
...retained.flatMap(({ iconSprite, itemBackgroundSprite, designTeamIcon, subTechnologies }) => [
|
|
898
|
+
...(iconSprite === undefined ? [] : [iconSprite]),
|
|
899
|
+
...(designTeamIcon === undefined ? [] : [designTeamIcon.sprite]),
|
|
900
|
+
...(itemBackgroundSprite === undefined
|
|
901
|
+
? []
|
|
902
|
+
: itemBackgroundSprites(itemBackgroundSprite)),
|
|
903
|
+
...(subTechnologies ?? []).flatMap((sub) => sub.iconSprite === undefined ? [] : [sub.iconSprite]),
|
|
904
|
+
]),
|
|
905
|
+
...selection.backgrounds.map(({ sprite }) => sprite),
|
|
906
|
+
]),
|
|
687
907
|
].sort(compareCodeUnits);
|
|
688
908
|
const renderedIconSprites = requestedIconSprites.filter((sprite) => iconDataUris[sprite] !== undefined);
|
|
689
909
|
const unresolvedIconSprites = requestedIconSprites.filter((sprite) => iconDataUris[sprite] === undefined);
|
|
690
|
-
const rendered = svgFor(selection.title, positioned, edges, selection.sourceAccurate, iconDataUris, folderLayout.yearMarkers, folderLayout.yearAxis);
|
|
910
|
+
const rendered = svgFor(selection.title, positioned, edges, selection.sourceAccurate, iconDataUris, spriteDimensions, folderLayout.yearMarkers, positionedBackgrounds, folderLayout.yearAxis, options.view === 'folder' && selection.sourceAccurate);
|
|
691
911
|
const budget = options.budget ?? new RenderBudget();
|
|
692
912
|
budget.reserve(rendered.width, rendered.height, `technology ${options.view} render`);
|
|
693
913
|
budget.reserveRasterOperation(`technology-render:${graph.revision}:${options.view}:${sha256Bytes(rendered.svg)}`, `technology ${options.view} rasterization`);
|
|
@@ -699,6 +919,15 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
699
919
|
options.signal?.throwIfAborted();
|
|
700
920
|
const json = `${canonicalJson({
|
|
701
921
|
...selection.payload,
|
|
922
|
+
...(options.scenario === undefined
|
|
923
|
+
? {}
|
|
924
|
+
: {
|
|
925
|
+
scenarioPresentation: {
|
|
926
|
+
year: options.scenario.year,
|
|
927
|
+
researchedTechnologyIds: [...new Set(options.scenario.researchedTechnologyIds)].sort(compareCodeUnits),
|
|
928
|
+
interpretation: 'Structural prerequisites and exclusivity only; dynamic country conditions remain unresolved',
|
|
929
|
+
},
|
|
930
|
+
}),
|
|
702
931
|
nodes: retained,
|
|
703
932
|
edges,
|
|
704
933
|
omittedNodeCount: Math.max(0, selection.nodes.length - retained.length),
|
|
@@ -712,6 +941,21 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
712
941
|
axis: folderLayout.yearAxis ?? null,
|
|
713
942
|
markerCount: folderLayout.yearMarkers.length,
|
|
714
943
|
},
|
|
944
|
+
backgrounds: selection.backgrounds.map((background) => ({
|
|
945
|
+
id: background.id,
|
|
946
|
+
folderId: background.folderId,
|
|
947
|
+
name: background.name,
|
|
948
|
+
sprite: background.sprite,
|
|
949
|
+
sourcePath: background.sourcePath,
|
|
950
|
+
sourceLine: background.location.start.line,
|
|
951
|
+
placement: positionedBackgrounds.find(({ id }) => id === background.id) ?? null,
|
|
952
|
+
status: positionedBackgrounds.some(({ id }) => id === background.id) &&
|
|
953
|
+
iconDataUris[background.sprite] !== undefined &&
|
|
954
|
+
(background.size.width ?? spriteDimensions[background.sprite]?.width) !== undefined &&
|
|
955
|
+
(background.size.height ?? spriteDimensions[background.sprite]?.height) !== undefined
|
|
956
|
+
? 'rendered'
|
|
957
|
+
: 'unresolved',
|
|
958
|
+
})),
|
|
715
959
|
})}\n`;
|
|
716
960
|
const html = options.includeHtml === true ? htmlFor(selection.title, rendered.svg, json) : undefined;
|
|
717
961
|
return {
|
|
@@ -740,9 +984,20 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
740
984
|
/** Return only the sprite names that the bounded render selection will display. */
|
|
741
985
|
export function technologyRenderIconSprites(graph, options) {
|
|
742
986
|
const maximum = Math.max(1, Math.min(options.maxNodes ?? 600, MAX_RENDER_NODES));
|
|
987
|
+
const selection = viewSelection(graph, options);
|
|
743
988
|
return [
|
|
744
|
-
...new Set(
|
|
745
|
-
.nodes
|
|
746
|
-
|
|
989
|
+
...new Set([
|
|
990
|
+
...selection.nodes
|
|
991
|
+
.slice(0, maximum)
|
|
992
|
+
.flatMap(({ iconSprite, itemBackgroundSprite, designTeamIcon, subTechnologies }) => [
|
|
993
|
+
...(iconSprite === undefined ? [] : [iconSprite]),
|
|
994
|
+
...(designTeamIcon === undefined ? [] : [designTeamIcon.sprite]),
|
|
995
|
+
...(itemBackgroundSprite === undefined
|
|
996
|
+
? []
|
|
997
|
+
: itemBackgroundSprites(itemBackgroundSprite)),
|
|
998
|
+
...(subTechnologies ?? []).flatMap((sub) => sub.iconSprite === undefined ? [] : [sub.iconSprite]),
|
|
999
|
+
]),
|
|
1000
|
+
...selection.backgrounds.map(({ sprite }) => sprite),
|
|
1001
|
+
]),
|
|
747
1002
|
].sort(compareCodeUnits);
|
|
748
1003
|
}
|
|
@@ -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,37 @@ 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(({ backgroundSprite }) => backgroundSprite === undefined
|
|
491
|
+
? []
|
|
492
|
+
: [
|
|
493
|
+
backgroundSprite,
|
|
494
|
+
backgroundSprite.replace('_unavailable_item_bg', '_available_item_bg'),
|
|
495
|
+
backgroundSprite.replace('_unavailable_item_bg', '_researched_item_bg'),
|
|
496
|
+
]),
|
|
497
|
+
...(preliminary.backgrounds ?? []).map(({ sprite }) => sprite),
|
|
498
|
+
];
|
|
499
|
+
const guiTextures = guiSprites.flatMap((sprite) => {
|
|
500
|
+
const texture = snapshot.index.find('sprite', sprite)?.metadata.texture;
|
|
501
|
+
if (typeof texture !== 'string')
|
|
502
|
+
return [];
|
|
503
|
+
const normalized = texture.replaceAll('\\', '/').replace(/\/{2,}/gu, '/');
|
|
504
|
+
return normalized.includes('..') || /[[\]{}*?!]/u.test(normalized) ? [] : [normalized];
|
|
505
|
+
});
|
|
506
|
+
const patterns = [...new Set([...technologyAssetPatterns(preliminary), ...guiTextures])].sort(compareCodeUnits);
|
|
464
507
|
const assetFiles = patterns.length === 0
|
|
465
508
|
? []
|
|
466
509
|
: await this.engine.scanner.scan(workspace, {
|
|
@@ -479,7 +522,7 @@ export class TechnologyTreeViewer {
|
|
|
479
522
|
workspaceIdentity: workspace.workspaceIdentity,
|
|
480
523
|
cache: this.#state.fragments,
|
|
481
524
|
assetFiles,
|
|
482
|
-
analysisMode,
|
|
525
|
+
...(analysisMode === undefined ? {} : { analysisMode }),
|
|
483
526
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
484
527
|
});
|
|
485
528
|
this.#state.current.set(workspaceId, { generation, snapshot, assetFiles, graph });
|
|
@@ -620,6 +663,7 @@ export class TechnologyTreeViewer {
|
|
|
620
663
|
maxNodes: input.maxNodes ?? 1_000,
|
|
621
664
|
includeHtml: input.includeHtml ?? false,
|
|
622
665
|
budget,
|
|
666
|
+
...(input.scenario === undefined ? {} : { scenario: input.scenario }),
|
|
623
667
|
...(input.folderId === undefined ? {} : { folderId: input.folderId }),
|
|
624
668
|
...(input.technologyId === undefined ? {} : { technologyId: input.technologyId }),
|
|
625
669
|
...(input.categoryId === undefined ? {} : { categoryId: input.categoryId }),
|
|
@@ -639,8 +683,9 @@ export class TechnologyTreeViewer {
|
|
|
639
683
|
maxNodes: 1_000,
|
|
640
684
|
})),
|
|
641
685
|
]);
|
|
642
|
-
const iconDataUris = await resolveTechnologyIconDataUris(cached, requestedIconSprites, budget, input.signal);
|
|
686
|
+
const { dataUris: iconDataUris, dimensions: spriteDimensions } = await resolveTechnologyIconDataUris(cached, requestedIconSprites, budget, input.signal);
|
|
643
687
|
renderOptions.iconDataUris = iconDataUris;
|
|
688
|
+
renderOptions.spriteDimensions = spriteDimensions;
|
|
644
689
|
const render = await renderTechnologyGraph(graph, renderOptions);
|
|
645
690
|
const focused = [];
|
|
646
691
|
for (const folder of focusedFolders) {
|
|
@@ -651,7 +696,9 @@ export class TechnologyTreeViewer {
|
|
|
651
696
|
maxNodes: 1_000,
|
|
652
697
|
includeHtml: false,
|
|
653
698
|
iconDataUris,
|
|
699
|
+
spriteDimensions,
|
|
654
700
|
budget,
|
|
701
|
+
...(input.scenario === undefined ? {} : { scenario: input.scenario }),
|
|
655
702
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
656
703
|
}));
|
|
657
704
|
}
|