hoi4-agent-tools 2.5.2 → 2.5.3
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 +7 -0
- package/dist/hoi4_agent_tools/core/index.js +22 -6
- package/dist/hoi4_agent_tools/focus/compact.js +103 -12
- package/dist/hoi4_agent_tools/focus/continuous-render.js +4 -10
- package/dist/hoi4_agent_tools/focus/layout.js +15 -7
- package/dist/hoi4_agent_tools/focus/render.js +2 -8
- package/dist/hoi4_agent_tools/gui/dds.js +5 -2
- package/dist/hoi4_agent_tools/gui/layout.js +1 -1
- package/dist/hoi4_agent_tools/gui/renderer.js +4 -16
- package/dist/hoi4_agent_tools/gui/source-graph.js +9 -8
- package/dist/hoi4_agent_tools/gui/studio.js +58 -4
- package/dist/hoi4_agent_tools/map/render.js +1 -1
- package/dist/hoi4_agent_tools/mcp/transports/http.js +4 -5
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +7 -7
- package/dist/hoi4_agent_tools/technology/render.js +65 -19
- package/dist/hoi4_agent_tools/technology/service.js +67 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/docs/gui.md +2 -0
- package/docs/technology.md +1 -1
- package/package.json +3 -2
- 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/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/map-operation.schema.json +1 -1
- package/schemas/operation-result.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/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
No unreleased changes.
|
|
6
6
|
|
|
7
|
+
## 2.5.3 - 2026-08-22
|
|
8
|
+
|
|
9
|
+
- Resolved and embedded technology icons through the complete mod, dependency, and installed-game load order, including vanilla sprites used by mod content.
|
|
10
|
+
- Recognized vanilla `SpriteType` capitalization consistently in the shared index and GUI source graph.
|
|
11
|
+
- Decoded uncompressed vanilla DDS textures whose header stores total linear size instead of row pitch.
|
|
12
|
+
- Added deterministic cross-root focus, scripted-GUI, and technology regressions plus an opt-in installed-game DDS render test.
|
|
13
|
+
|
|
7
14
|
## 2.5.2 - 2026-08-15
|
|
8
15
|
|
|
9
16
|
- Distinguished HOI4 small and large technology layouts in graph data and source-positioned folder renders, including exact 72×72 small items selected by `force_use_small_tech_layout`.
|
|
@@ -2,6 +2,22 @@ import { compareCodeUnits } from './canonical.js';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { sortDiagnostics } from './diagnostics.js';
|
|
4
4
|
import { parseClausewitz, parseLocalisation, assignments, childBlocks, firstScalar, locationFor, sourcePartialLimitDiagnostics, SOURCE_MAX_BYTES, SOURCE_MAX_NESTING, nodeLocation, } from './source/index.js';
|
|
5
|
+
const spriteDefinitionKeys = new Set([
|
|
6
|
+
'spritetype',
|
|
7
|
+
'textspritetype',
|
|
8
|
+
'frameanimatedspritetype',
|
|
9
|
+
'corneredtilespritetype',
|
|
10
|
+
'maskedshieldtype',
|
|
11
|
+
]);
|
|
12
|
+
function firstScalarInsensitiveNode(block, ...keys) {
|
|
13
|
+
const normalizedKeys = new Set(keys.map((key) => key.toLowerCase()));
|
|
14
|
+
for (const assignment of assignments(block)) {
|
|
15
|
+
if (assignment.value.type === 'scalar' &&
|
|
16
|
+
normalizedKeys.has(assignment.key.value.toLowerCase()))
|
|
17
|
+
return assignment.value;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
5
21
|
const guiElementKeys = new Set([
|
|
6
22
|
'containerWindowType',
|
|
7
23
|
'windowType',
|
|
@@ -992,10 +1008,11 @@ export class SymbolIndex {
|
|
|
992
1008
|
metadata: {},
|
|
993
1009
|
});
|
|
994
1010
|
}
|
|
995
|
-
else if ((
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
1011
|
+
else if (sourcePath.endsWith('.gfx') &&
|
|
1012
|
+
spriteDefinitionKeys.has(key.toLowerCase()) &&
|
|
1013
|
+
firstScalarInsensitiveNode(child, 'name') !== undefined) {
|
|
1014
|
+
const name = firstScalarInsensitiveNode(child, 'name');
|
|
1015
|
+
const texture = firstScalarInsensitiveNode(child, 'texturefile', 'textureFile');
|
|
999
1016
|
this.addSymbol({
|
|
1000
1017
|
kind: 'sprite',
|
|
1001
1018
|
id: name.value,
|
|
@@ -1006,8 +1023,7 @@ export class SymbolIndex {
|
|
|
1006
1023
|
metadata: {
|
|
1007
1024
|
spriteType: key,
|
|
1008
1025
|
texture: texture?.value,
|
|
1009
|
-
frames: numeric(
|
|
1010
|
-
numeric(firstScalar(child, 'noofframes')?.value),
|
|
1026
|
+
frames: numeric(firstScalarInsensitiveNode(child, 'noOfFrames', 'noofframes')?.value),
|
|
1011
1027
|
},
|
|
1012
1028
|
});
|
|
1013
1029
|
if (texture !== undefined)
|
|
@@ -90,6 +90,51 @@ function compactStructuralAnchorX(plan, focus, parents, preferredX, laneBases) {
|
|
|
90
90
|
}
|
|
91
91
|
return laneBases.get(focusLaneId(plan, focus)) ?? 0;
|
|
92
92
|
}
|
|
93
|
+
function structuralRows(plan) {
|
|
94
|
+
const focusIds = new Set(plan.focuses.map(({ id }) => id));
|
|
95
|
+
const parentsByFocus = new Map(plan.focuses.map((focus) => [
|
|
96
|
+
focus.id,
|
|
97
|
+
[...new Set(prerequisiteIds(plan, focus.id).filter((parentId) => focusIds.has(parentId)))],
|
|
98
|
+
]));
|
|
99
|
+
const childrenByFocus = new Map();
|
|
100
|
+
const unresolvedParents = new Map();
|
|
101
|
+
for (const focus of plan.focuses) {
|
|
102
|
+
const parents = parentsByFocus.get(focus.id) ?? [];
|
|
103
|
+
unresolvedParents.set(focus.id, parents.length);
|
|
104
|
+
for (const parentId of parents) {
|
|
105
|
+
const children = childrenByFocus.get(parentId) ?? [];
|
|
106
|
+
children.push(focus.id);
|
|
107
|
+
childrenByFocus.set(parentId, children);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const children of childrenByFocus.values())
|
|
111
|
+
children.sort((left, right) => compareCodeUnits(left, right));
|
|
112
|
+
const rows = new Map();
|
|
113
|
+
const pending = [...plan.focuses]
|
|
114
|
+
.map(({ id }) => id)
|
|
115
|
+
.filter((focusId) => (unresolvedParents.get(focusId) ?? 0) === 0)
|
|
116
|
+
.sort((left, right) => compareCodeUnits(right, left));
|
|
117
|
+
for (const focusId of pending)
|
|
118
|
+
rows.set(focusId, 0);
|
|
119
|
+
let visited = 0;
|
|
120
|
+
while (pending.length > 0) {
|
|
121
|
+
const focusId = pending.pop();
|
|
122
|
+
if (focusId === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
visited += 1;
|
|
125
|
+
const nextRow = (rows.get(focusId) ?? 0) + 1;
|
|
126
|
+
for (const childId of childrenByFocus.get(focusId) ?? []) {
|
|
127
|
+
rows.set(childId, Math.max(rows.get(childId) ?? 0, nextRow));
|
|
128
|
+
const remaining = (unresolvedParents.get(childId) ?? 0) - 1;
|
|
129
|
+
unresolvedParents.set(childId, remaining);
|
|
130
|
+
if (remaining === 0)
|
|
131
|
+
pending.push(childId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// The layout engine reports dependency cycles. Keep authored row preferences
|
|
135
|
+
// for a cyclic graph instead of inventing a partial topological reflow.
|
|
136
|
+
return visited === plan.focuses.length ? rows : undefined;
|
|
137
|
+
}
|
|
93
138
|
function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
94
139
|
const cohorts = new Map();
|
|
95
140
|
const laneBases = compactLaneBases(plan);
|
|
@@ -182,7 +227,7 @@ function straightenLinearChains(plan, preferredX, preferredY) {
|
|
|
182
227
|
}
|
|
183
228
|
}
|
|
184
229
|
function compactCandidate(plan, layout, strategy) {
|
|
185
|
-
const { scale, compressRows, mirrorSiblings, straightenChains } = strategy;
|
|
230
|
+
const { scale, compressRows, structuralRows: useStructuralRows, mirrorSiblings, straightenChains, } = strategy;
|
|
186
231
|
const minimumX = Math.min(...layout.nodes.map(({ x }) => x));
|
|
187
232
|
const maximumX = Math.max(...layout.nodes.map(({ x }) => x));
|
|
188
233
|
const minimumY = Math.min(...layout.nodes.map(({ y }) => y));
|
|
@@ -194,11 +239,19 @@ function compactCandidate(plan, layout, strategy) {
|
|
|
194
239
|
node.id,
|
|
195
240
|
compressRows ? (compactRows.get(node.y) ?? node.y - minimumY) : node.y - minimumY,
|
|
196
241
|
]));
|
|
242
|
+
if (useStructuralRows) {
|
|
243
|
+
const normalizedRows = structuralRows(plan);
|
|
244
|
+
if (normalizedRows !== undefined) {
|
|
245
|
+
for (const [focusId, row] of normalizedRows)
|
|
246
|
+
preferredY.set(focusId, row);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
197
249
|
const rows = new Map();
|
|
198
250
|
for (const node of layout.nodes) {
|
|
199
|
-
const
|
|
251
|
+
const y = preferredY.get(node.id) ?? node.y;
|
|
252
|
+
const row = rows.get(y) ?? [];
|
|
200
253
|
row.push(node);
|
|
201
|
-
rows.set(
|
|
254
|
+
rows.set(y, row);
|
|
202
255
|
}
|
|
203
256
|
for (const row of rows.values()) {
|
|
204
257
|
row.sort((left, right) => left.x - right.x || compareCodeUnits(left.id, right.id));
|
|
@@ -249,12 +302,12 @@ function compactScore(layout) {
|
|
|
249
302
|
metrics.symmetry.offAnchorSiblingCohortCount,
|
|
250
303
|
metrics.symmetry.totalSiblingDeviation,
|
|
251
304
|
metrics.symmetry.totalSiblingAnchorDeviation,
|
|
252
|
-
metrics.bounds.rowCount,
|
|
253
|
-
metrics.bounds.columnCount,
|
|
254
305
|
metrics.connectors.maximumHorizontalSpan,
|
|
255
|
-
metrics.connectors.totalHorizontalSpan,
|
|
256
306
|
metrics.connectors.maximumManhattanSpan,
|
|
307
|
+
metrics.connectors.totalHorizontalSpan,
|
|
257
308
|
metrics.connectors.totalVerticalSpan,
|
|
309
|
+
metrics.bounds.rowCount,
|
|
310
|
+
metrics.bounds.columnCount,
|
|
258
311
|
layout.layoutHash,
|
|
259
312
|
];
|
|
260
313
|
}
|
|
@@ -281,14 +334,50 @@ function compactScales(focusCount) {
|
|
|
281
334
|
}
|
|
282
335
|
function compactStrategies(focusCount) {
|
|
283
336
|
const preservationStrategies = [
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
337
|
+
{
|
|
338
|
+
scale: 1,
|
|
339
|
+
compressRows: false,
|
|
340
|
+
structuralRows: false,
|
|
341
|
+
mirrorSiblings: false,
|
|
342
|
+
straightenChains: false,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
scale: 1,
|
|
346
|
+
compressRows: true,
|
|
347
|
+
structuralRows: false,
|
|
348
|
+
mirrorSiblings: false,
|
|
349
|
+
straightenChains: false,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
scale: 1,
|
|
353
|
+
compressRows: false,
|
|
354
|
+
structuralRows: false,
|
|
355
|
+
mirrorSiblings: false,
|
|
356
|
+
straightenChains: true,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
scale: 1,
|
|
360
|
+
compressRows: false,
|
|
361
|
+
structuralRows: false,
|
|
362
|
+
mirrorSiblings: true,
|
|
363
|
+
straightenChains: false,
|
|
364
|
+
},
|
|
288
365
|
];
|
|
289
366
|
const compactStrategies = compactScales(focusCount).flatMap((scale) => [
|
|
290
|
-
{
|
|
291
|
-
|
|
367
|
+
{
|
|
368
|
+
scale,
|
|
369
|
+
compressRows: false,
|
|
370
|
+
structuralRows: false,
|
|
371
|
+
mirrorSiblings: true,
|
|
372
|
+
straightenChains: true,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
scale,
|
|
376
|
+
compressRows: false,
|
|
377
|
+
structuralRows: true,
|
|
378
|
+
mirrorSiblings: true,
|
|
379
|
+
straightenChains: true,
|
|
380
|
+
},
|
|
292
381
|
]);
|
|
293
382
|
return [...preservationStrategies, ...compactStrategies];
|
|
294
383
|
}
|
|
@@ -307,6 +396,7 @@ export function compactFocusTreePlan(plan) {
|
|
|
307
396
|
? compactCandidate(plan, currentLayout, {
|
|
308
397
|
scale: 1,
|
|
309
398
|
compressRows: false,
|
|
399
|
+
structuralRows: false,
|
|
310
400
|
mirrorSiblings: false,
|
|
311
401
|
straightenChains: false,
|
|
312
402
|
})
|
|
@@ -358,6 +448,7 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
|
|
|
358
448
|
? compactCandidate(plan, currentLayout, {
|
|
359
449
|
scale: 1,
|
|
360
450
|
compressRows: false,
|
|
451
|
+
structuralRows: false,
|
|
361
452
|
mirrorSiblings: false,
|
|
362
453
|
straightenChains: false,
|
|
363
454
|
})
|
|
@@ -42,7 +42,7 @@ export async function renderContinuousFocusPalette(plan, diagnostics, options =
|
|
|
42
42
|
const rows = Math.max(1, Math.ceil(plan.focuses.length / columns));
|
|
43
43
|
const usedColumns = Math.max(1, Math.min(columns, plan.focuses.length));
|
|
44
44
|
const width = padding * 2 + usedColumns * cardWidth + Math.max(0, usedColumns - 1) * gap;
|
|
45
|
-
const height = padding * 2 +
|
|
45
|
+
const height = padding * 2 + 52 + rows * cardHeight + Math.max(0, rows - 1) * gap;
|
|
46
46
|
if (options.rasterize !== false) {
|
|
47
47
|
budget.reserve(width, height, 'continuous focus palette PNG');
|
|
48
48
|
}
|
|
@@ -61,7 +61,7 @@ export async function renderContinuousFocusPalette(plan, diagnostics, options =
|
|
|
61
61
|
const column = index % columns;
|
|
62
62
|
const row = Math.floor(index / columns);
|
|
63
63
|
const x = padding + column * (cardWidth + gap);
|
|
64
|
-
const y = padding +
|
|
64
|
+
const y = padding + 52 + row * (cardHeight + gap);
|
|
65
65
|
const entry = options.presentation?.entries[focus.id];
|
|
66
66
|
const iconSprite = focus.icons[0]?.sprite;
|
|
67
67
|
const icon = iconSprite === undefined ? undefined : options.presentation?.icons[iconSprite];
|
|
@@ -80,13 +80,7 @@ export async function renderContinuousFocusPalette(plan, diagnostics, options =
|
|
|
80
80
|
weight: 700,
|
|
81
81
|
fill: '#f3f6f8',
|
|
82
82
|
});
|
|
83
|
-
const
|
|
84
|
-
x: padding,
|
|
85
|
-
y: padding + 27,
|
|
86
|
-
fontSize: 12,
|
|
87
|
-
fill: '#d7b65b',
|
|
88
|
-
});
|
|
89
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><title>${escapeXml(plan.id)} continuous focus palette</title><desc>Offline source-derived continuous focus palette; not an in-game screenshot.</desc><defs>${toolText.definitions()}</defs><rect width="${width}" height="${height}" fill="#101820"/>${heading}${notice}${cards.join('')}</svg>`;
|
|
83
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><title>${escapeXml(plan.id)} continuous focus palette</title><desc>${plan.focuses.length} continuous focuses.</desc><defs>${toolText.definitions()}</defs><rect width="${width}" height="${height}" fill="#101820"/>${heading}${cards.join('')}</svg>`;
|
|
90
84
|
const compiled = compileContinuousFocusPaletteWithSourceMap(plan);
|
|
91
85
|
const json = `${canonicalJson({
|
|
92
86
|
schemaVersion: 1,
|
|
@@ -122,7 +116,7 @@ export async function renderContinuousFocusPalette(plan, diagnostics, options =
|
|
|
122
116
|
},
|
|
123
117
|
renderProfile: options.renderProfile ?? {},
|
|
124
118
|
})}\n`;
|
|
125
|
-
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeXml(plan.id)} continuous focus palette</title><style>html{background:#101820;color:#f3f6f8;font:14px system-ui}body{margin:0;padding:24px}
|
|
119
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeXml(plan.id)} continuous focus palette</title><style>html{background:#101820;color:#f3f6f8;font:14px system-ui}body{margin:0;padding:24px}svg{max-width:100%;height:auto;border:1px solid #385064}pre{white-space:pre-wrap}</style></head><body><h1>${escapeXml(plan.id)}</h1>${svg}<details><summary>Structured source model</summary><pre>${escapeXml(json)}</pre></details></body></html>`;
|
|
126
120
|
options.signal?.throwIfAborted();
|
|
127
121
|
let png = Buffer.alloc(0);
|
|
128
122
|
if (options.rasterize !== false) {
|
|
@@ -1529,6 +1529,7 @@ function siblingBatchConflicts(context, cohort, override) {
|
|
|
1529
1529
|
}
|
|
1530
1530
|
function* refineSiblingCohorts(context, edges, initial) {
|
|
1531
1531
|
let current = initial;
|
|
1532
|
+
let evaluatedEdges = edges;
|
|
1532
1533
|
for (let pass = 0; pass < 2; pass += 1) {
|
|
1533
1534
|
let accepted = false;
|
|
1534
1535
|
for (const [cohortIndex, cohort] of context.siblingCohorts.entries()) {
|
|
@@ -1558,7 +1559,7 @@ function* refineSiblingCohorts(context, edges, initial) {
|
|
|
1558
1559
|
continue;
|
|
1559
1560
|
if (siblingBatchConflicts(context, cohortIds, override))
|
|
1560
1561
|
continue;
|
|
1561
|
-
const candidateEdges = edgesWithXOverride(
|
|
1562
|
+
const candidateEdges = edgesWithXOverride(evaluatedEdges, override);
|
|
1562
1563
|
const crossingSummary = yield* connectorCrossings(candidateEdges, context);
|
|
1563
1564
|
if (crossingSummary.count > current.crossingCount)
|
|
1564
1565
|
continue;
|
|
@@ -1587,6 +1588,7 @@ function* refineSiblingCohorts(context, edges, initial) {
|
|
|
1587
1588
|
movePlacedFocusX(context, node, x);
|
|
1588
1589
|
}
|
|
1589
1590
|
current = best.score;
|
|
1591
|
+
evaluatedEdges = edgesWithXOverride(evaluatedEdges, best.override);
|
|
1590
1592
|
accepted = true;
|
|
1591
1593
|
}
|
|
1592
1594
|
if (!accepted)
|
|
@@ -1644,13 +1646,19 @@ const CONNECTOR_REFINEMENT_WORK_MAX = 7_000_000;
|
|
|
1644
1646
|
function* refineConnectorQualityWithinBudget(context) {
|
|
1645
1647
|
const maximumAcceptedMoves = context.aggressiveAestheticRepair ? 32 : 24;
|
|
1646
1648
|
const maximumCandidateNodes = context.aggressiveAestheticRepair ? 48 : 32;
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1649
|
+
let edges = yield* connectorEdges(context);
|
|
1650
|
+
let crossingSummary = yield* connectorCrossings(edges, context);
|
|
1651
|
+
let intersectionSummary = yield* connectorNodeIntersectionSummary(context, edges);
|
|
1652
|
+
let symmetry = siblingQuality(context);
|
|
1651
1653
|
let current = connectorQualityScore(context, edges, crossingSummary.count, intersectionSummary.count, symmetry);
|
|
1652
|
-
if (context.aggressiveAestheticRepair)
|
|
1653
|
-
|
|
1654
|
+
if (context.aggressiveAestheticRepair) {
|
|
1655
|
+
yield* refineSiblingCohorts(context, edges, current);
|
|
1656
|
+
edges = yield* connectorEdges(context);
|
|
1657
|
+
crossingSummary = yield* connectorCrossings(edges, context);
|
|
1658
|
+
intersectionSummary = yield* connectorNodeIntersectionSummary(context, edges);
|
|
1659
|
+
symmetry = siblingQuality(context);
|
|
1660
|
+
current = connectorQualityScore(context, edges, crossingSummary.count, intersectionSummary.count, symmetry);
|
|
1661
|
+
}
|
|
1654
1662
|
const candidateIds = new Set(intersectionSummary.involvedFocusIds);
|
|
1655
1663
|
const longFocusIds = new Set();
|
|
1656
1664
|
for (const edge of edges.filter(longConnector)) {
|
|
@@ -216,7 +216,7 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
216
216
|
const parts = [
|
|
217
217
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${logicalWidth} ${logicalHeight}" role="img" aria-labelledby="title description">`,
|
|
218
218
|
`<title id="title">${escapeXml(plan.id)} focus tree</title>`,
|
|
219
|
-
`<desc id="description"
|
|
219
|
+
`<desc id="description">${plan.focuses.length} focuses.</desc>`,
|
|
220
220
|
'<defs>',
|
|
221
221
|
'__DETERMINISTIC_FONT_DEFINITIONS__',
|
|
222
222
|
'<marker id="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="#8da2bc"/></marker>',
|
|
@@ -224,12 +224,6 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
224
224
|
'</defs>',
|
|
225
225
|
'<rect width="100%" height="100%" fill="#111723"/>',
|
|
226
226
|
toolText.render(plan.id, { x: padding, y: 36, fontSize: 22, weight: 700, fill: '#e8edf5' }),
|
|
227
|
-
toolText.render(`Offline agent render · layout ${layout.layoutHash.slice(0, 12)}`, {
|
|
228
|
-
x: padding,
|
|
229
|
-
y: 58,
|
|
230
|
-
fontSize: 12,
|
|
231
|
-
fill: '#95a4b8',
|
|
232
|
-
}),
|
|
233
227
|
'<g id="prerequisites">',
|
|
234
228
|
];
|
|
235
229
|
for (const edge of prerequisiteEdges) {
|
|
@@ -403,7 +397,7 @@ function htmlDocument(plan, svg, graphJson, diagnostics) {
|
|
|
403
397
|
<body>
|
|
404
398
|
<main class="canvas" aria-label="Focus tree artifact">${svg}</main>
|
|
405
399
|
<aside class="panel">
|
|
406
|
-
<div class="controls"><h1>${escapeXml(plan.id)}</h1><
|
|
400
|
+
<div class="controls"><h1>${escapeXml(plan.id)}</h1><label>Find focus<input id="search" type="search" placeholder="ID, title, branch, or lane"></label></div>
|
|
407
401
|
<h2>Diagnostics</h2><ol>${diagnosticsHtml}</ol>
|
|
408
402
|
<p class="muted">Source revision: <code>${escapeXml(plan.provenance.sourceHash)}</code></p>
|
|
409
403
|
</aside>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RenderBudget, RENDER_MAX_DECODED_PIXELS } from '../core/render-budget.js';
|
|
2
2
|
import { sha256Bytes } from '../core/canonical.js';
|
|
3
3
|
const DDS_MAGIC = 0x2053_4444;
|
|
4
|
+
const DDSD_PITCH = 0x8;
|
|
4
5
|
const DDPF_FOURCC = 0x4;
|
|
5
6
|
const DDPF_RGB = 0x40;
|
|
6
7
|
function fourCc(bytes, offset) {
|
|
@@ -180,7 +181,9 @@ export function decodeDds(bytes, budget = new RenderBudget(), operationKey = `dd
|
|
|
180
181
|
budget.reserve(width, height, 'DDS texture decode', {
|
|
181
182
|
maximumPixels: RENDER_MAX_DECODED_PIXELS,
|
|
182
183
|
});
|
|
183
|
-
const
|
|
184
|
+
const headerFlags = bytes.readUInt32LE(8);
|
|
185
|
+
const pitchOrLinearSize = bytes.readUInt32LE(20);
|
|
186
|
+
const rowPitch = (headerFlags & DDSD_PITCH) === 0 ? 0 : pitchOrLinearSize;
|
|
184
187
|
const flags = bytes.readUInt32LE(80);
|
|
185
188
|
const code = fourCc(bytes, 84).toUpperCase();
|
|
186
189
|
if ((flags & DDPF_FOURCC) !== 0) {
|
|
@@ -236,7 +239,7 @@ export function decodeDds(bytes, budget = new RenderBudget(), operationKey = `dd
|
|
|
236
239
|
bytes.readUInt32LE(100),
|
|
237
240
|
bytes.readUInt32LE(104),
|
|
238
241
|
];
|
|
239
|
-
const output = decodeRgb(bytes, 128, width, height, bitCount,
|
|
242
|
+
const output = decodeRgb(bytes, 128, width, height, bitCount, rowPitch, masks);
|
|
240
243
|
const format = bitCount === 24 ? 'rgb24' : 'rgba32';
|
|
241
244
|
return output === undefined
|
|
242
245
|
? unsupported(format, 'The DDS RGB pixel data is truncated.')
|
|
@@ -750,7 +750,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
750
750
|
}
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
|
-
/** Build a deterministic
|
|
753
|
+
/** Build a deterministic GUI scene from the connected source graph and scenario. */
|
|
754
754
|
export async function buildGuiScene(graph, scannedFiles, windowName, scenario, catalog = new GuiAssetCatalog(graph, scannedFiles)) {
|
|
755
755
|
if (graph.nodes.length > GUI_GRAPH_MAX_NODES ||
|
|
756
756
|
graph.edges.length > GUI_GRAPH_MAX_EDGES ||
|
|
@@ -214,13 +214,7 @@ export function sceneToSvg(scene, variant) {
|
|
|
214
214
|
.map((element) => renderOverlay(element, variant, toolText))
|
|
215
215
|
.join('');
|
|
216
216
|
const fidelity = escapeXml(canonicalJson(scene.fidelity));
|
|
217
|
-
|
|
218
|
-
x: view.viewBox.x + 15,
|
|
219
|
-
y: view.viewBox.y + 22,
|
|
220
|
-
fontSize: 11,
|
|
221
|
-
fill: '#f1c75b',
|
|
222
|
-
});
|
|
223
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions.join('')}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}<g><rect x="${finite(view.viewBox.x + 8)}" y="${finite(view.viewBox.y + 8)}" width="215" height="20" rx="3" fill="#05080c" fill-opacity="0.82"/>${banner}</g></svg>`;
|
|
217
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions.join('')}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}</svg>`;
|
|
224
218
|
}
|
|
225
219
|
async function cooperativeParts(values, render, signal) {
|
|
226
220
|
const output = [];
|
|
@@ -244,13 +238,7 @@ async function sceneToSvgCooperative(scene, variant, signal) {
|
|
|
244
238
|
const body = await cooperativeParts(scene.elements, (element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText), signal);
|
|
245
239
|
const overlays = await cooperativeParts(scene.elements, (element) => renderOverlay(element, variant, toolText), signal);
|
|
246
240
|
const fidelity = escapeXml(canonicalJson(scene.fidelity));
|
|
247
|
-
|
|
248
|
-
x: view.viewBox.x + 15,
|
|
249
|
-
y: view.viewBox.y + 22,
|
|
250
|
-
fontSize: 11,
|
|
251
|
-
fill: '#f1c75b',
|
|
252
|
-
});
|
|
253
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}<g><rect x="${finite(view.viewBox.x + 8)}" y="${finite(view.viewBox.y + 8)}" width="215" height="20" rx="3" fill="#05080c" fill-opacity="0.82"/>${banner}</g></svg>`;
|
|
241
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}</svg>`;
|
|
254
242
|
}
|
|
255
243
|
export function hierarchyToSvg(scene) {
|
|
256
244
|
const rows = scene.elements.toSorted((left, right) => left.depth - right.depth || left.zIndex - right.zIndex || compareCodeUnits(left.id, right.id));
|
|
@@ -283,7 +271,7 @@ export function hierarchyToSvg(scene) {
|
|
|
283
271
|
return `${parentLine}<circle cx="${x}" cy="${y - 8}" r="4" fill="${colour}"/>${nameText}${detailText}`;
|
|
284
272
|
})
|
|
285
273
|
.join('');
|
|
286
|
-
const heading = toolText.render(
|
|
274
|
+
const heading = toolText.render(`${scene.windowName} \u00b7 HIERARCHY`, {
|
|
287
275
|
x: 16,
|
|
288
276
|
y: 20,
|
|
289
277
|
fontSize: 12,
|
|
@@ -393,7 +381,7 @@ export function renderGallerySvg(title, items) {
|
|
|
393
381
|
return `<g>${toolText.render(item.label, { x, y: y + 14, fontSize: 12, fill: '#f1c75b' })}<image x="${x}" y="${y + 22}" width="${finite(item.width * scale)}" height="${finite(item.height * scale)}" href="data:image/png;base64,${item.png.toString('base64')}"/></g>`;
|
|
394
382
|
})
|
|
395
383
|
.join('');
|
|
396
|
-
const heading = toolText.render(
|
|
384
|
+
const heading = toolText.render(title, {
|
|
397
385
|
x: 12,
|
|
398
386
|
y: 24,
|
|
399
387
|
fontSize: 15,
|
|
@@ -31,12 +31,12 @@ const explicitGuiElementTypes = new Set([
|
|
|
31
31
|
'mapIconType',
|
|
32
32
|
]);
|
|
33
33
|
const spriteTypes = new Set([
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
34
|
+
'spritetype',
|
|
35
|
+
'textspritetype',
|
|
36
|
+
'frameanimatedspritetype',
|
|
37
|
+
'corneredtilespritetype',
|
|
38
|
+
'progressbartype',
|
|
39
|
+
'maskedshieldtype',
|
|
40
40
|
]);
|
|
41
41
|
const modelledElementAttributes = new Set([
|
|
42
42
|
'name',
|
|
@@ -330,7 +330,8 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
330
330
|
if (assignment.value.type !== 'block')
|
|
331
331
|
continue;
|
|
332
332
|
const child = assignment.value;
|
|
333
|
-
|
|
333
|
+
const spriteType = assignment.key.value.toLowerCase();
|
|
334
|
+
if (spriteTypes.has(spriteType)) {
|
|
334
335
|
const name = firstScalarInsensitive(child, 'name');
|
|
335
336
|
if (name !== undefined) {
|
|
336
337
|
const id = deterministicId('gui_sprite', { path: file.displayPath, name });
|
|
@@ -352,7 +353,7 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
352
353
|
...(texturePath === undefined ? {} : { texturePath }),
|
|
353
354
|
...(texturePath2 === undefined ? {} : { texturePath2 }),
|
|
354
355
|
frameCount: Math.trunc(numberScalar(child, 'noOfFrames', 'noofframes') ?? 1),
|
|
355
|
-
frameAnimated:
|
|
356
|
+
frameAnimated: spriteType === 'frameanimatedspritetype',
|
|
356
357
|
...(animationRateFps === undefined ? {} : { animationRateFps }),
|
|
357
358
|
...(looping === undefined ? {} : { looping }),
|
|
358
359
|
...(playOnShow === undefined ? {} : { playOnShow }),
|
|
@@ -11,6 +11,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
11
11
|
import { WorkspaceScanner } from '../core/scanner.js';
|
|
12
12
|
import { applyReplacements, decodeSource, encodeSource, parseClausewitz, parseLocalisation, SOURCE_MAX_BYTES, sourcePartialLimitDiagnostics, } from '../core/source/index.js';
|
|
13
13
|
import { readDependenciesFromScannedFiles, } from '../core/transactions.js';
|
|
14
|
+
import { TRANSACTION_MAX_DIAGNOSTICS } from '../core/transaction-limits.js';
|
|
14
15
|
import { PACKAGE_VERSION } from '../version.js';
|
|
15
16
|
import { isSafeAnimationSourcePath } from './animation-manifest.js';
|
|
16
17
|
import { GuiAssetCatalog, parseBmFont } from './assets.js';
|
|
@@ -30,6 +31,43 @@ const staticGuiDefinitionPatterns = [
|
|
|
30
31
|
];
|
|
31
32
|
export const GUI_TEXT_PACKAGE_MAX_FILES = 32;
|
|
32
33
|
export const GUI_TEXT_PACKAGE_MAX_BYTES = 16_777_216;
|
|
34
|
+
const GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET = Math.floor(TRANSACTION_MAX_DIAGNOSTICS / 3);
|
|
35
|
+
function hardDiagnostic(diagnostic) {
|
|
36
|
+
return diagnostic.severity === 'error' || diagnostic.severity === 'blocker';
|
|
37
|
+
}
|
|
38
|
+
export function boundedGuiTransactionDiagnostics(diagnostics, maximumDiagnostics, evidenceArtifactName) {
|
|
39
|
+
if (diagnostics.length <= maximumDiagnostics)
|
|
40
|
+
return [...diagnostics];
|
|
41
|
+
if (maximumDiagnostics <= 0)
|
|
42
|
+
return [];
|
|
43
|
+
const retainedLimit = maximumDiagnostics - 1;
|
|
44
|
+
const hard = diagnostics.filter(hardDiagnostic);
|
|
45
|
+
const retainedHard = hard.slice(0, retainedLimit);
|
|
46
|
+
const retainedHardSet = new Set(retainedHard);
|
|
47
|
+
const retainedSoft = diagnostics
|
|
48
|
+
.filter((diagnostic) => !hardDiagnostic(diagnostic) && !retainedHardSet.has(diagnostic))
|
|
49
|
+
.slice(0, retainedLimit - retainedHard.length);
|
|
50
|
+
const omittedHard = hard.length - retainedHard.length;
|
|
51
|
+
return [
|
|
52
|
+
...retainedHard,
|
|
53
|
+
...retainedSoft,
|
|
54
|
+
{
|
|
55
|
+
code: 'GUI_REWRITE_DIAGNOSTICS_TRUNCATED',
|
|
56
|
+
severity: omittedHard > 0 ? 'blocker' : 'warning',
|
|
57
|
+
category: 'validation',
|
|
58
|
+
message: evidenceArtifactName === undefined
|
|
59
|
+
? `GUI rewrite validation produced ${diagnostics.length} diagnostics; transaction evidence retains the highest-priority ${maximumDiagnostics}`
|
|
60
|
+
: `GUI rewrite validation produced ${diagnostics.length} diagnostics; complete evidence is stored in ${evidenceArtifactName}`,
|
|
61
|
+
details: {
|
|
62
|
+
total: diagnostics.length,
|
|
63
|
+
returned: maximumDiagnostics,
|
|
64
|
+
omitted: diagnostics.length - retainedLimit,
|
|
65
|
+
omittedHard,
|
|
66
|
+
...(evidenceArtifactName === undefined ? {} : { evidenceArtifactName }),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
33
71
|
function normalizeConfiguredRoot(value) {
|
|
34
72
|
const normalized = value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/$/u, '');
|
|
35
73
|
if (!isSafeAnimationSourcePath(normalized)) {
|
|
@@ -439,7 +477,7 @@ export function guiArtifactProvenance(graph, kind, primaryScene, scenarioScenes
|
|
|
439
477
|
})),
|
|
440
478
|
},
|
|
441
479
|
metadata: {
|
|
442
|
-
rendererLabel: '
|
|
480
|
+
rendererLabel: 'HOI4 Agent Tools deterministic GUI render',
|
|
443
481
|
...metadata,
|
|
444
482
|
sourceHashInventory: sourceEvidence.inventory,
|
|
445
483
|
},
|
|
@@ -859,6 +897,7 @@ export class ScriptedGuiStudio {
|
|
|
859
897
|
const preflightDiagnostics = [];
|
|
860
898
|
const preflightChecks = [];
|
|
861
899
|
const preflightArtifacts = [];
|
|
900
|
+
let rewriteEvidenceArtifactName;
|
|
862
901
|
let dependencyFiles = baseline.files;
|
|
863
902
|
if (input.windowName !== undefined && input.scenario !== undefined) {
|
|
864
903
|
let proposedFiles = overlayGuiTextPackageFiles(baseline.files, prepared);
|
|
@@ -942,6 +981,7 @@ export class ScriptedGuiStudio {
|
|
|
942
981
|
}
|
|
943
982
|
const comparison = await compareGuiImages(baselinePng, proposedImage.png, renderBudget, input.signal);
|
|
944
983
|
const slug = safeSlug(input.windowName);
|
|
984
|
+
rewriteEvidenceArtifactName = `${slug}-rewrite-validation.json`;
|
|
945
985
|
const metadata = {
|
|
946
986
|
relativePath: input.relativePath,
|
|
947
987
|
packageFiles: prepared.map(({ relativePath }) => relativePath),
|
|
@@ -982,10 +1022,24 @@ export class ScriptedGuiStudio {
|
|
|
982
1022
|
})}\n`,
|
|
983
1023
|
provenance: guiArtifactProvenance(proposedGraph, 'gui-proposed-fidelity', proposedScene, [proposedScene], metadata),
|
|
984
1024
|
},
|
|
1025
|
+
{
|
|
1026
|
+
name: rewriteEvidenceArtifactName,
|
|
1027
|
+
mimeType: 'application/json',
|
|
1028
|
+
content: `${canonicalJson({
|
|
1029
|
+
sourceDiagnostics: proposedSourceDiagnostics,
|
|
1030
|
+
graphDiagnostics: proposedGraph.diagnostics,
|
|
1031
|
+
sceneDiagnostics: proposedScene.diagnostics,
|
|
1032
|
+
validation: proposedValidation,
|
|
1033
|
+
transactionDiagnostics: preflightDiagnostics,
|
|
1034
|
+
transactionChecks: preflightChecks,
|
|
1035
|
+
})}\n`,
|
|
1036
|
+
provenance: guiArtifactProvenance(proposedGraph, 'gui-rewrite-validation', proposedScene, [proposedScene], metadata),
|
|
1037
|
+
},
|
|
985
1038
|
];
|
|
986
1039
|
const stored = await this.artifacts.withAtomicWrites(workspace, writes, (artifacts) => Promise.resolve([...artifacts]), input.signal);
|
|
987
1040
|
preflightArtifacts.push(...stored.map(publicArtifactLink));
|
|
988
1041
|
}
|
|
1042
|
+
const transactionPreflightDiagnostics = boundedGuiTransactionDiagnostics(preflightDiagnostics, GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET, rewriteEvidenceArtifactName);
|
|
989
1043
|
const operationId = deterministicId('gui_source_change', {
|
|
990
1044
|
relativePath: input.relativePath,
|
|
991
1045
|
files: prepared.map(({ relativePath, bytes }) => ({
|
|
@@ -1020,7 +1074,7 @@ export class ScriptedGuiStudio {
|
|
|
1020
1074
|
})),
|
|
1021
1075
|
readDependencies: readDependenciesFromScannedFiles(dependencyFiles),
|
|
1022
1076
|
artifacts: preflightArtifacts,
|
|
1023
|
-
diagnostics:
|
|
1077
|
+
diagnostics: transactionPreflightDiagnostics,
|
|
1024
1078
|
validate: (proposed, signal) => {
|
|
1025
1079
|
const diagnostics = [];
|
|
1026
1080
|
let present = true;
|
|
@@ -1049,7 +1103,7 @@ export class ScriptedGuiStudio {
|
|
|
1049
1103
|
diagnostics.push(...blocking.map((diagnostic) => ({ ...diagnostic, operationId })));
|
|
1050
1104
|
}
|
|
1051
1105
|
return Promise.resolve({
|
|
1052
|
-
diagnostics,
|
|
1106
|
+
diagnostics: boundedGuiTransactionDiagnostics(diagnostics, GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET, rewriteEvidenceArtifactName),
|
|
1053
1107
|
checks: [
|
|
1054
1108
|
...preflightChecks,
|
|
1055
1109
|
{
|
|
@@ -1077,7 +1131,7 @@ export class ScriptedGuiStudio {
|
|
|
1077
1131
|
const scanned = await this.scan(workspaceId, principal, validationSignal);
|
|
1078
1132
|
const passed = !scanned.graph.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker');
|
|
1079
1133
|
return {
|
|
1080
|
-
diagnostics: scanned.graph.diagnostics,
|
|
1134
|
+
diagnostics: boundedGuiTransactionDiagnostics(scanned.graph.diagnostics, Math.max(0, TRANSACTION_MAX_DIAGNOSTICS - _manifest.diagnostics.length), _manifest.artifacts.find(({ name }) => name.endsWith('-rewrite-validation.json'))?.name),
|
|
1081
1135
|
checks: [
|
|
1082
1136
|
{
|
|
1083
1137
|
id: 'post-write-gui-graph',
|
|
@@ -433,7 +433,7 @@ function htmlDocument(title, png, json) {
|
|
|
433
433
|
.replaceAll('<', '<')
|
|
434
434
|
.replaceAll('>', '>');
|
|
435
435
|
const escapedJson = json.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
436
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapedTitle}</title><style>html{background:#10151b;color:#edf2f7;font:14px system-ui}body{margin:20px}.controls{display:flex;gap:8px;align-items:center;margin:12px 0}button{background:#263241;color:#edf2f7;border:1px solid #526173;border-radius:4px;padding:6px 10px}.viewport{height:70vh;overflow:hidden;border:1px solid #48515c;background:#080b0f;position:relative;touch-action:none}.viewport img{image-rendering:pixelated;transform-origin:0 0;position:absolute;max-width:none;cursor:grab;user-select:none}.viewport img.dragging{cursor:grabbing}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><h1>${escapedTitle}</h1><
|
|
436
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapedTitle}</title><style>html{background:#10151b;color:#edf2f7;font:14px system-ui}body{margin:20px}.controls{display:flex;gap:8px;align-items:center;margin:12px 0}button{background:#263241;color:#edf2f7;border:1px solid #526173;border-radius:4px;padding:6px 10px}.viewport{height:70vh;overflow:hidden;border:1px solid #48515c;background:#080b0f;position:relative;touch-action:none}.viewport img{image-rendering:pixelated;transform-origin:0 0;position:absolute;max-width:none;cursor:grab;user-select:none}.viewport img.dragging{cursor:grabbing}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><h1>${escapedTitle}</h1><div class="controls"><button id="out" type="button">-</button><button id="reset" type="button">Reset</button><button id="in" type="button">+</button><output id="zoom">100%</output></div><div class="viewport" id="viewport"><img id="map" draggable="false" alt="${escapedTitle}" src="data:image/png;base64,${png.toString('base64')}"></div><details><summary>Render metadata</summary><pre>${escapedJson}</pre></details><script>(()=>{const image=document.getElementById('map'),viewport=document.getElementById('viewport'),label=document.getElementById('zoom');let scale=1,x=0,y=0,drag=false,lastX=0,lastY=0;const draw=()=>{image.style.transform='translate('+x+'px,'+y+'px) scale('+scale+')';label.value=Math.round(scale*100)+'%'};const zoom=(factor,cx=viewport.clientWidth/2,cy=viewport.clientHeight/2)=>{const next=Math.min(16,Math.max(.1,scale*factor));x=cx-(cx-x)*(next/scale);y=cy-(cy-y)*(next/scale);scale=next;draw()};document.getElementById('in').onclick=()=>zoom(1.25);document.getElementById('out').onclick=()=>zoom(.8);document.getElementById('reset').onclick=()=>{scale=1;x=0;y=0;draw()};viewport.addEventListener('wheel',event=>{event.preventDefault();const bounds=viewport.getBoundingClientRect();zoom(event.deltaY<0?1.15:.87,event.clientX-bounds.left,event.clientY-bounds.top)},{passive:false});image.addEventListener('pointerdown',event=>{drag=true;lastX=event.clientX;lastY=event.clientY;image.setPointerCapture(event.pointerId);image.classList.add('dragging')});image.addEventListener('pointermove',event=>{if(!drag)return;x+=event.clientX-lastX;y+=event.clientY-lastY;lastX=event.clientX;lastY=event.clientY;draw()});image.addEventListener('pointerup',()=>{drag=false;image.classList.remove('dragging')});draw()})();</script></body></html>`;
|
|
437
437
|
}
|
|
438
438
|
async function encodePng(raw, width, height, scale) {
|
|
439
439
|
assertRenderDimensions(width, height, 'map Sharp source raster');
|