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.
Files changed (89) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/hoi4_agent_tools/core/condition-evaluator.js +20 -8
  3. package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
  4. package/dist/hoi4_agent_tools/core/index.js +1 -0
  5. package/dist/hoi4_agent_tools/core/job-worker-host.js +19 -1
  6. package/dist/hoi4_agent_tools/core/operation-result.js +4 -1
  7. package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
  8. package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
  9. package/dist/hoi4_agent_tools/event/operations.js +32 -10
  10. package/dist/hoi4_agent_tools/event/service.js +21 -2
  11. package/dist/hoi4_agent_tools/focus/compact.js +241 -27
  12. package/dist/hoi4_agent_tools/focus/layout.js +119 -8
  13. package/dist/hoi4_agent_tools/focus/render.js +62 -0
  14. package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
  15. package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
  16. package/dist/hoi4_agent_tools/gui/assets.js +54 -4
  17. package/dist/hoi4_agent_tools/gui/layout.js +935 -104
  18. package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
  19. package/dist/hoi4_agent_tools/gui/shader.js +515 -0
  20. package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
  21. package/dist/hoi4_agent_tools/gui/studio.js +245 -19
  22. package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
  23. package/dist/hoi4_agent_tools/gui/validators.js +41 -12
  24. package/dist/hoi4_agent_tools/map/operations.js +70 -1
  25. package/dist/hoi4_agent_tools/map/render.js +70 -1
  26. package/dist/hoi4_agent_tools/map/service.js +30 -6
  27. package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
  28. package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
  29. package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
  30. package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
  31. package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
  32. package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
  33. package/dist/hoi4_agent_tools/mechanic/interpreter.js +91 -30
  34. package/dist/hoi4_agent_tools/mechanic/source.js +19 -4
  35. package/dist/hoi4_agent_tools/probability/evaluation.js +3 -0
  36. package/dist/hoi4_agent_tools/probability/operations.js +8 -0
  37. package/dist/hoi4_agent_tools/probability/service.js +33 -12
  38. package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
  39. package/dist/hoi4_agent_tools/schemas/event.js +8 -0
  40. package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
  41. package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
  42. package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
  43. package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
  44. package/dist/hoi4_agent_tools/technology/graph.js +25 -10
  45. package/dist/hoi4_agent_tools/technology/operations.js +29 -1
  46. package/dist/hoi4_agent_tools/technology/render.js +287 -31
  47. package/dist/hoi4_agent_tools/technology/service.js +56 -8
  48. package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
  49. package/dist/hoi4_agent_tools/version.js +1 -1
  50. package/docs/events.md +4 -2
  51. package/docs/focus.md +10 -0
  52. package/docs/gui.md +50 -3
  53. package/docs/jobs.md +1 -0
  54. package/docs/map.md +8 -0
  55. package/docs/mechanics.md +2 -1
  56. package/docs/probability.md +3 -1
  57. package/docs/technology.md +11 -2
  58. package/package.json +1 -1
  59. package/schemas/configuration.schema.json +1 -1
  60. package/schemas/continuous-focus-palette.schema.json +1 -1
  61. package/schemas/custom-weighted-pool.schema.json +1 -1
  62. package/schemas/decision-inspect-input.schema.json +1 -1
  63. package/schemas/event-feature-manifest.schema.json +1 -1
  64. package/schemas/focus-plan.schema.json +1 -1
  65. package/schemas/focus-planning-sidecar.schema.json +1 -1
  66. package/schemas/gui-animation-source.schema.json +1 -1
  67. package/schemas/gui-helper.schema.json +1 -1
  68. package/schemas/gui-scenario.schema.json +1 -1
  69. package/schemas/helper-expansion-request.schema.json +1 -1
  70. package/schemas/helper-expansion-summary.schema.json +1 -1
  71. package/schemas/impact-inspect-input.schema.json +1 -1
  72. package/schemas/map-operation.schema.json +1 -1
  73. package/schemas/mechanic-test-input.schema.json +1 -1
  74. package/schemas/operation-result.schema.json +1 -1
  75. package/schemas/package-check-input.schema.json +1 -1
  76. package/schemas/package-manifest.schema.json +1 -1
  77. package/schemas/probability-analysis-result.schema.json +1 -1
  78. package/schemas/probability-compare-input.schema.json +1 -1
  79. package/schemas/probability-evaluate-input.schema.json +1 -1
  80. package/schemas/probability-inspect-input.schema.json +1 -1
  81. package/schemas/probability-render-input.schema.json +1 -1
  82. package/schemas/probability-scenario-set.schema.json +1 -1
  83. package/schemas/probability-sequence-input.schema.json +1 -1
  84. package/schemas/probability-simulate-input.schema.json +1 -1
  85. package/schemas/probability-sweep-input.schema.json +1 -1
  86. package/schemas/scenario-suite.schema.json +1 -1
  87. package/schemas/scenario-test-input.schema.json +1 -1
  88. package/schemas/shared-scenario.schema.json +1 -1
  89. 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 = true;
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: HEADER_HEIGHT + PADDING + sourceNodeY(node) - minimumY,
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: HEADER_HEIGHT + PADDING + marker.position.y - minimumY,
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
- const maximumY = Math.max(HEADER_HEIGHT + LARGE_NODE_HEIGHT, ...positioned.map(({ y, height }) => y + height), ...yearMarkers.map(({ y }) => y + 30));
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 = yearAxis === 'vertical'
588
- ? `<line x1="${PADDING}" y1="${marker.y + 15}" x2="${width - PADDING}" y2="${marker.y + 15}" stroke="#34495c" stroke-width="1" stroke-dasharray="5 7"/>`
589
- : `<line x1="${marker.x + 24}" y1="${HEADER_HEIGHT}" x2="${marker.x + 24}" y2="${height - PADDING}" stroke="#34495c" stroke-width="1" stroke-dasharray="5 7"/>`;
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: marker.x,
592
- y: marker.y + 22,
593
- fontSize: 18,
594
- fill: '#c7d4df',
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') ? '#e27878' : '#8298ad';
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
- const colour = colours(node.kind);
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
- return `<g data-node-id="${escapeXml(node.id)}"${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}</g>`;
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"/>${title}${mode}<g>${yearGuideSvg.join('')}</g><g>${edgeSvg.join('')}</g><g>${nodeSvg.join('')}</g></svg>`,
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 = selection.nodes.slice(0, maximum);
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 = selection.sourceAccurate
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(retained.flatMap(({ iconSprite }) => (iconSprite === undefined ? [] : [iconSprite]))),
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(viewSelection(graph, options)
745
- .nodes.slice(0, maximum)
746
- .flatMap(({ iconSprite }) => (iconSprite === undefined ? [] : [iconSprite]))),
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 Object.fromEntries(Object.entries(dataUris).sort(([left], [right]) => compareCodeUnits(left, right)));
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 = 'full';
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 patterns = technologyAssetPatterns(preliminary);
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
  }