@spectratools/graphic-designer-cli 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -1,4 +1,152 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/compare.ts
12
+ var compare_exports = {};
13
+ __export(compare_exports, {
14
+ compareImages: () => compareImages
15
+ });
16
+ import sharp from "sharp";
17
+ function clampUnit(value) {
18
+ if (value < 0) {
19
+ return 0;
20
+ }
21
+ if (value > 1) {
22
+ return 1;
23
+ }
24
+ return value;
25
+ }
26
+ function toRegionLabel(row, column) {
27
+ const letter = String.fromCharCode(65 + row);
28
+ return `${letter}${column + 1}`;
29
+ }
30
+ function validateGrid(grid) {
31
+ if (!Number.isInteger(grid) || grid <= 0) {
32
+ throw new Error(`Invalid grid value "${grid}". Expected a positive integer.`);
33
+ }
34
+ if (grid > 26) {
35
+ throw new Error(`Invalid grid value "${grid}". Maximum supported grid is 26.`);
36
+ }
37
+ return grid;
38
+ }
39
+ function validateThreshold(threshold) {
40
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
41
+ throw new Error(`Invalid threshold value "${threshold}". Expected a number between 0 and 1.`);
42
+ }
43
+ return threshold;
44
+ }
45
+ async function readDimensions(path) {
46
+ const metadata = await sharp(path).metadata();
47
+ if (!metadata.width || !metadata.height) {
48
+ throw new Error(`Unable to read image dimensions for "${path}".`);
49
+ }
50
+ return {
51
+ width: metadata.width,
52
+ height: metadata.height
53
+ };
54
+ }
55
+ async function normalizeToRaw(path, width, height) {
56
+ const normalized = await sharp(path).rotate().resize(width, height, {
57
+ fit: "contain",
58
+ position: "centre",
59
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
60
+ }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
61
+ return {
62
+ data: normalized.data,
63
+ width: normalized.info.width,
64
+ height: normalized.info.height
65
+ };
66
+ }
67
+ function scorePixelDifference(a, b, offset) {
68
+ const redDiff = Math.abs(a.data[offset] - b.data[offset]);
69
+ const greenDiff = Math.abs(a.data[offset + 1] - b.data[offset + 1]);
70
+ const blueDiff = Math.abs(a.data[offset + 2] - b.data[offset + 2]);
71
+ const alphaDiff = Math.abs(a.data[offset + 3] - b.data[offset + 3]);
72
+ const rgbDelta = (redDiff + greenDiff + blueDiff) / (3 * 255);
73
+ const alphaDelta = alphaDiff / 255;
74
+ return rgbDelta * 0.75 + alphaDelta * 0.25;
75
+ }
76
+ async function compareImages(target, rendered, options = {}) {
77
+ const grid = validateGrid(options.grid ?? DEFAULT_GRID);
78
+ const threshold = validateThreshold(options.threshold ?? DEFAULT_THRESHOLD);
79
+ const closeThreshold = clampUnit(threshold - (options.closeMargin ?? DEFAULT_CLOSE_MARGIN));
80
+ const targetDimensions = await readDimensions(target);
81
+ const renderedDimensions = await readDimensions(rendered);
82
+ const normalizedWidth = Math.max(targetDimensions.width, renderedDimensions.width);
83
+ const normalizedHeight = Math.max(targetDimensions.height, renderedDimensions.height);
84
+ const [targetImage, renderedImage] = await Promise.all([
85
+ normalizeToRaw(target, normalizedWidth, normalizedHeight),
86
+ normalizeToRaw(rendered, normalizedWidth, normalizedHeight)
87
+ ]);
88
+ const regionDiffSums = new Array(grid * grid).fill(0);
89
+ const regionCounts = new Array(grid * grid).fill(0);
90
+ let totalDiff = 0;
91
+ for (let y = 0; y < normalizedHeight; y += 1) {
92
+ const row = Math.min(Math.floor(y * grid / normalizedHeight), grid - 1);
93
+ for (let x = 0; x < normalizedWidth; x += 1) {
94
+ const column = Math.min(Math.floor(x * grid / normalizedWidth), grid - 1);
95
+ const regionIndex = row * grid + column;
96
+ const offset = (y * normalizedWidth + x) * 4;
97
+ const diff = scorePixelDifference(targetImage, renderedImage, offset);
98
+ totalDiff += diff;
99
+ regionDiffSums[regionIndex] += diff;
100
+ regionCounts[regionIndex] += 1;
101
+ }
102
+ }
103
+ const pixelCount = normalizedWidth * normalizedHeight;
104
+ const similarity = clampUnit(1 - totalDiff / pixelCount);
105
+ const regions = [];
106
+ for (let row = 0; row < grid; row += 1) {
107
+ for (let column = 0; column < grid; column += 1) {
108
+ const regionIndex = row * grid + column;
109
+ const regionCount = regionCounts[regionIndex];
110
+ const regionSimilarity = regionCount > 0 ? clampUnit(1 - regionDiffSums[regionIndex] / regionCount) : 1;
111
+ regions.push({
112
+ label: toRegionLabel(row, column),
113
+ row,
114
+ column,
115
+ similarity: regionSimilarity
116
+ });
117
+ }
118
+ }
119
+ const verdict = similarity >= threshold ? "match" : similarity >= closeThreshold ? "close" : "mismatch";
120
+ return {
121
+ targetPath: target,
122
+ renderedPath: rendered,
123
+ targetDimensions,
124
+ renderedDimensions,
125
+ normalizedDimensions: {
126
+ width: normalizedWidth,
127
+ height: normalizedHeight
128
+ },
129
+ dimensionMismatch: targetDimensions.width !== renderedDimensions.width || targetDimensions.height !== renderedDimensions.height,
130
+ grid,
131
+ threshold,
132
+ closeThreshold,
133
+ similarity,
134
+ verdict,
135
+ regions
136
+ };
137
+ }
138
+ var DEFAULT_GRID, DEFAULT_THRESHOLD, DEFAULT_CLOSE_MARGIN;
139
+ var init_compare = __esm({
140
+ "src/compare.ts"() {
141
+ "use strict";
142
+ DEFAULT_GRID = 3;
143
+ DEFAULT_THRESHOLD = 0.8;
144
+ DEFAULT_CLOSE_MARGIN = 0.1;
145
+ }
146
+ });
147
+
1
148
  // src/cli.ts
149
+ init_compare();
2
150
  import { readFileSync, realpathSync } from "fs";
3
151
  import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
4
152
  import { basename as basename4, dirname as dirname3, resolve as resolve4 } from "path";
@@ -196,7 +344,7 @@ async function publishToGitHub(options) {
196
344
  // src/qa.ts
197
345
  import { readFile as readFile3 } from "fs/promises";
198
346
  import { resolve } from "path";
199
- import sharp from "sharp";
347
+ import sharp2 from "sharp";
200
348
 
201
349
  // src/code-style.ts
202
350
  var CARBON_SURROUND_COLOR = "rgba(171, 184, 195, 1)";
@@ -336,6 +484,10 @@ function contrastRatio(foreground, background) {
336
484
  const darker = Math.min(fg, bg);
337
485
  return (lighter + 0.05) / (darker + 0.05);
338
486
  }
487
+ function withAlpha(hexColor, opacity) {
488
+ const rgb = parseHexColor(hexColor);
489
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`;
490
+ }
339
491
  function blendColorWithOpacity(foreground, background, opacity) {
340
492
  const fg = parseHexColor(foreground);
341
493
  const bg = parseHexColor(background);
@@ -782,10 +934,26 @@ var cardElementSchema = z2.object({
782
934
  tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
783
935
  icon: z2.string().min(1).max(64).optional()
784
936
  }).strict();
937
+ var flowNodeShadowSchema = z2.object({
938
+ color: colorHexSchema2.optional(),
939
+ blur: z2.number().min(0).max(64).default(8),
940
+ offsetX: z2.number().min(-32).max(32).default(0),
941
+ offsetY: z2.number().min(-32).max(32).default(0),
942
+ opacity: z2.number().min(0).max(1).default(0.3)
943
+ }).strict();
785
944
  var flowNodeElementSchema = z2.object({
786
945
  type: z2.literal("flow-node"),
787
946
  id: z2.string().min(1).max(120),
788
- shape: z2.enum(["box", "rounded-box", "diamond", "circle", "pill", "cylinder", "parallelogram"]),
947
+ shape: z2.enum([
948
+ "box",
949
+ "rounded-box",
950
+ "diamond",
951
+ "circle",
952
+ "pill",
953
+ "cylinder",
954
+ "parallelogram",
955
+ "hexagon"
956
+ ]).default("rounded-box"),
789
957
  label: z2.string().min(1).max(200),
790
958
  sublabel: z2.string().min(1).max(300).optional(),
791
959
  sublabelColor: colorHexSchema2.optional(),
@@ -805,20 +973,25 @@ var flowNodeElementSchema = z2.object({
805
973
  badgeText: z2.string().min(1).max(32).optional(),
806
974
  badgeColor: colorHexSchema2.optional(),
807
975
  badgeBackground: colorHexSchema2.optional(),
808
- badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
976
+ badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
977
+ shadow: flowNodeShadowSchema.optional()
809
978
  }).strict();
810
979
  var connectionElementSchema = z2.object({
811
980
  type: z2.literal("connection"),
812
981
  from: z2.string().min(1).max(120),
813
982
  to: z2.string().min(1).max(120),
814
983
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
984
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
815
985
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
816
986
  label: z2.string().min(1).max(200).optional(),
817
987
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
818
988
  color: colorHexSchema2.optional(),
819
- width: z2.number().min(0.5).max(8).optional(),
989
+ width: z2.number().min(0.5).max(10).optional(),
990
+ strokeWidth: z2.number().min(0.5).max(10).default(2),
820
991
  arrowSize: z2.number().min(4).max(32).optional(),
821
- opacity: z2.number().min(0).max(1).default(1)
992
+ opacity: z2.number().min(0).max(1).default(1),
993
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
994
+ tension: z2.number().min(0.1).max(0.8).default(0.35)
822
995
  }).strict();
823
996
  var codeBlockStyleSchema = z2.object({
824
997
  paddingVertical: z2.number().min(0).max(128).default(56),
@@ -887,6 +1060,10 @@ var elementSchema = z2.discriminatedUnion("type", [
887
1060
  shapeElementSchema,
888
1061
  imageElementSchema
889
1062
  ]);
1063
+ var diagramCenterSchema = z2.object({
1064
+ x: z2.number(),
1065
+ y: z2.number()
1066
+ }).strict();
890
1067
  var autoLayoutConfigSchema = z2.object({
891
1068
  mode: z2.literal("auto"),
892
1069
  algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
@@ -902,7 +1079,9 @@ var autoLayoutConfigSchema = z2.object({
902
1079
  /** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
903
1080
  radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
904
1081
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
905
- radialSortBy: z2.enum(["id", "connections"]).optional()
1082
+ radialSortBy: z2.enum(["id", "connections"]).optional(),
1083
+ /** Explicit center used by curve/arc connection routing. */
1084
+ diagramCenter: diagramCenterSchema.optional()
906
1085
  }).strict();
907
1086
  var gridLayoutConfigSchema = z2.object({
908
1087
  mode: z2.literal("grid"),
@@ -910,13 +1089,17 @@ var gridLayoutConfigSchema = z2.object({
910
1089
  gap: z2.number().int().min(0).max(256).default(24),
911
1090
  cardMinHeight: z2.number().int().min(32).max(4096).optional(),
912
1091
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
913
- equalHeight: z2.boolean().default(false)
1092
+ equalHeight: z2.boolean().default(false),
1093
+ /** Explicit center used by curve/arc connection routing. */
1094
+ diagramCenter: diagramCenterSchema.optional()
914
1095
  }).strict();
915
1096
  var stackLayoutConfigSchema = z2.object({
916
1097
  mode: z2.literal("stack"),
917
1098
  direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
918
1099
  gap: z2.number().int().min(0).max(256).default(24),
919
- alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
1100
+ alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
1101
+ /** Explicit center used by curve/arc connection routing. */
1102
+ diagramCenter: diagramCenterSchema.optional()
920
1103
  }).strict();
921
1104
  var manualPositionSchema = z2.object({
922
1105
  x: z2.number().int(),
@@ -926,7 +1109,9 @@ var manualPositionSchema = z2.object({
926
1109
  }).strict();
927
1110
  var manualLayoutConfigSchema = z2.object({
928
1111
  mode: z2.literal("manual"),
929
- positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
1112
+ positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
1113
+ /** Explicit center used by curve/arc connection routing. */
1114
+ diagramCenter: diagramCenterSchema.optional()
930
1115
  }).strict();
931
1116
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
932
1117
  autoLayoutConfigSchema,
@@ -978,6 +1163,31 @@ var canvasSchema = z2.object({
978
1163
  padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
979
1164
  }).strict();
980
1165
  var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
1166
+ var diagramPositionSchema = z2.object({
1167
+ x: z2.number(),
1168
+ y: z2.number(),
1169
+ width: z2.number().positive(),
1170
+ height: z2.number().positive()
1171
+ }).strict();
1172
+ var diagramElementSchema = z2.discriminatedUnion("type", [
1173
+ flowNodeElementSchema,
1174
+ connectionElementSchema
1175
+ ]);
1176
+ var diagramLayoutSchema = z2.object({
1177
+ mode: z2.enum(["manual", "auto"]).default("manual"),
1178
+ positions: z2.record(z2.string(), diagramPositionSchema).optional(),
1179
+ diagramCenter: diagramCenterSchema.optional()
1180
+ }).strict();
1181
+ var diagramSpecSchema = z2.object({
1182
+ version: z2.literal(1),
1183
+ canvas: z2.object({
1184
+ width: z2.number().int().min(320).max(4096).default(1200),
1185
+ height: z2.number().int().min(180).max(4096).default(675)
1186
+ }).default({ width: 1200, height: 675 }),
1187
+ theme: themeSchema.optional(),
1188
+ elements: z2.array(diagramElementSchema).min(1),
1189
+ layout: diagramLayoutSchema.default({ mode: "manual" })
1190
+ }).strict();
981
1191
  var designSpecSchema = z2.object({
982
1192
  version: z2.literal(2).default(2),
983
1193
  canvas: canvasSchema.default(defaultCanvas),
@@ -1002,6 +1212,9 @@ function deriveSafeFrame(spec) {
1002
1212
  height: spec.canvas.height - spec.canvas.padding * 2
1003
1213
  };
1004
1214
  }
1215
+ function parseDiagramSpec(input) {
1216
+ return diagramSpecSchema.parse(input);
1217
+ }
1005
1218
  function parseDesignSpec(input) {
1006
1219
  return designSpecSchema.parse(input);
1007
1220
  }
@@ -1051,7 +1264,7 @@ async function runQa(options) {
1051
1264
  const imagePath = resolve(options.imagePath);
1052
1265
  const expectedSafeFrame = deriveSafeFrame(spec);
1053
1266
  const expectedCanvas = canvasRect(spec);
1054
- const imageMetadata = await sharp(imagePath).metadata();
1267
+ const imageMetadata = await sharp2(imagePath).metadata();
1055
1268
  const issues = [];
1056
1269
  const expectedScale = options.metadata?.canvas.scale ?? resolveRenderScale(spec);
1057
1270
  const expectedWidth = spec.canvas.width * expectedScale;
@@ -1202,6 +1415,31 @@ async function runQa(options) {
1202
1415
  });
1203
1416
  }
1204
1417
  }
1418
+ let referenceResult;
1419
+ if (options.referencePath) {
1420
+ const { compareImages: compareImages2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
1421
+ const comparison = await compareImages2(options.referencePath, imagePath);
1422
+ referenceResult = {
1423
+ similarity: comparison.similarity,
1424
+ verdict: comparison.verdict,
1425
+ regions: comparison.regions.map((region) => ({
1426
+ label: region.label,
1427
+ similarity: region.similarity
1428
+ }))
1429
+ };
1430
+ if (comparison.verdict === "mismatch") {
1431
+ const severity = comparison.similarity < 0.5 ? "error" : "warning";
1432
+ issues.push({
1433
+ code: "REFERENCE_MISMATCH",
1434
+ severity,
1435
+ message: `Reference image comparison ${severity === "error" ? "failed" : "warned"}: similarity ${comparison.similarity.toFixed(4)} with verdict "${comparison.verdict}".`,
1436
+ details: {
1437
+ similarity: comparison.similarity,
1438
+ verdict: comparison.verdict
1439
+ }
1440
+ });
1441
+ }
1442
+ }
1205
1443
  const footerSpacingPx = options.metadata?.layout.elements ? (() => {
1206
1444
  const footer = options.metadata.layout.elements.find((element) => element.id === "footer");
1207
1445
  if (!footer) {
@@ -1234,7 +1472,8 @@ async function runQa(options) {
1234
1472
  ...imageMetadata.height !== void 0 ? { height: imageMetadata.height } : {},
1235
1473
  ...footerSpacingPx !== void 0 ? { footerSpacingPx } : {}
1236
1474
  },
1237
- issues
1475
+ issues,
1476
+ ...referenceResult ? { reference: referenceResult } : {}
1238
1477
  };
1239
1478
  }
1240
1479
 
@@ -1301,9 +1540,9 @@ function drawRoundedRect(ctx, rect, radius, fill, stroke) {
1301
1540
  roundRectPath(ctx, rect, radius);
1302
1541
  fillAndStroke(ctx, fill, stroke);
1303
1542
  }
1304
- function drawCircle(ctx, center2, radius, fill, stroke) {
1543
+ function drawCircle(ctx, center, radius, fill, stroke) {
1305
1544
  ctx.beginPath();
1306
- ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
1545
+ ctx.arc(center.x, center.y, Math.max(0, radius), 0, Math.PI * 2);
1307
1546
  ctx.closePath();
1308
1547
  fillAndStroke(ctx, fill, stroke);
1309
1548
  }
@@ -1547,15 +1786,34 @@ function renderFlowNode(ctx, node, bounds, theme) {
1547
1786
  const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
1548
1787
  ctx.save();
1549
1788
  ctx.lineWidth = borderWidth;
1789
+ if (node.shadow) {
1790
+ const shadowColor = node.shadow.color ?? borderColor ?? theme.accent;
1791
+ ctx.shadowColor = withAlpha(shadowColor, node.shadow.opacity);
1792
+ ctx.shadowBlur = node.shadow.blur;
1793
+ ctx.shadowOffsetX = node.shadow.offsetX;
1794
+ ctx.shadowOffsetY = node.shadow.offsetY;
1795
+ }
1550
1796
  if (fillOpacity < 1) {
1551
1797
  ctx.globalAlpha = node.opacity * fillOpacity;
1552
1798
  drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
1799
+ if (node.shadow) {
1800
+ ctx.shadowColor = "transparent";
1801
+ ctx.shadowBlur = 0;
1802
+ ctx.shadowOffsetX = 0;
1803
+ ctx.shadowOffsetY = 0;
1804
+ }
1553
1805
  ctx.globalAlpha = node.opacity;
1554
1806
  drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
1555
1807
  } else {
1556
1808
  ctx.globalAlpha = node.opacity;
1557
1809
  drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
1558
1810
  }
1811
+ if (node.shadow) {
1812
+ ctx.shadowColor = "transparent";
1813
+ ctx.shadowBlur = 0;
1814
+ ctx.shadowOffsetX = 0;
1815
+ ctx.shadowOffsetY = 0;
1816
+ }
1559
1817
  const headingFont = resolveFont(theme.fonts.heading, "heading");
1560
1818
  const bodyFont = resolveFont(theme.fonts.body, "body");
1561
1819
  const monoFont = resolveFont(theme.fonts.mono, "mono");
@@ -2227,7 +2485,7 @@ function parseHexColor2(color) {
2227
2485
  a: normalized.length === 8 ? parseChannel2(6) / 255 : 1
2228
2486
  };
2229
2487
  }
2230
- function withAlpha(color, alpha) {
2488
+ function withAlpha2(color, alpha) {
2231
2489
  const parsed = parseHexColor2(color);
2232
2490
  const effectiveAlpha = clamp01(parsed.a * alpha);
2233
2491
  return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${effectiveAlpha})`;
@@ -2284,9 +2542,9 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
2284
2542
  centerY,
2285
2543
  outerRadius
2286
2544
  );
2287
- vignette.addColorStop(0, withAlpha(color, 0));
2288
- vignette.addColorStop(0.6, withAlpha(color, 0));
2289
- vignette.addColorStop(1, withAlpha(color, clamp01(intensity)));
2545
+ vignette.addColorStop(0, withAlpha2(color, 0));
2546
+ vignette.addColorStop(0.6, withAlpha2(color, 0));
2547
+ vignette.addColorStop(1, withAlpha2(color, clamp01(intensity)));
2290
2548
  ctx.save();
2291
2549
  ctx.fillStyle = vignette;
2292
2550
  ctx.fillRect(0, 0, width, height);
@@ -2417,12 +2675,12 @@ var MACOS_DOTS = [
2417
2675
  { fill: "#27C93F", stroke: "#1AAB29" }
2418
2676
  ];
2419
2677
  function drawMacosDots(ctx, x, y) {
2420
- for (const [index, dot] of MACOS_DOTS.entries()) {
2678
+ for (const [index, dot2] of MACOS_DOTS.entries()) {
2421
2679
  ctx.beginPath();
2422
2680
  ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
2423
2681
  ctx.closePath();
2424
- ctx.fillStyle = dot.fill;
2425
- ctx.strokeStyle = dot.stroke;
2682
+ ctx.fillStyle = dot2.fill;
2683
+ ctx.strokeStyle = dot2.stroke;
2426
2684
  ctx.lineWidth = DOT_STROKE_WIDTH;
2427
2685
  ctx.fill();
2428
2686
  ctx.stroke();
@@ -2843,25 +3101,134 @@ function drawOrthogonalPath(ctx, from, to, style) {
2843
3101
  }
2844
3102
 
2845
3103
  // src/renderers/connection.ts
2846
- function center(rect) {
3104
+ var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
3105
+ function rectCenter(rect) {
2847
3106
  return {
2848
3107
  x: rect.x + rect.width / 2,
2849
3108
  y: rect.y + rect.height / 2
2850
3109
  };
2851
3110
  }
2852
- function edgeAnchor(rect, target) {
2853
- const c = center(rect);
3111
+ function edgeAnchor(bounds, target) {
3112
+ const c = rectCenter(bounds);
2854
3113
  const dx = target.x - c.x;
2855
3114
  const dy = target.y - c.y;
2856
- if (Math.abs(dx) >= Math.abs(dy)) {
2857
- return {
2858
- x: dx >= 0 ? rect.x + rect.width : rect.x,
2859
- y: c.y
2860
- };
3115
+ if (dx === 0 && dy === 0) {
3116
+ return { x: c.x, y: c.y - bounds.height / 2 };
3117
+ }
3118
+ const hw = bounds.width / 2;
3119
+ const hh = bounds.height / 2;
3120
+ const absDx = Math.abs(dx);
3121
+ const absDy = Math.abs(dy);
3122
+ const t = absDx * hh > absDy * hw ? hw / absDx : hh / absDy;
3123
+ return { x: c.x + dx * t, y: c.y + dy * t };
3124
+ }
3125
+ function outwardNormal(point, diagramCenter) {
3126
+ const dx = point.x - diagramCenter.x;
3127
+ const dy = point.y - diagramCenter.y;
3128
+ const len = Math.hypot(dx, dy) || 1;
3129
+ return { x: dx / len, y: dy / len };
3130
+ }
3131
+ function curveRoute(fromBounds, toBounds, diagramCenter, tension) {
3132
+ const fromCenter = rectCenter(fromBounds);
3133
+ const toCenter = rectCenter(toBounds);
3134
+ const p0 = edgeAnchor(fromBounds, toCenter);
3135
+ const p3 = edgeAnchor(toBounds, fromCenter);
3136
+ const dist = Math.hypot(p3.x - p0.x, p3.y - p0.y);
3137
+ const offset = dist * tension;
3138
+ const n0 = outwardNormal(p0, diagramCenter);
3139
+ const n3 = outwardNormal(p3, diagramCenter);
3140
+ const cp1 = { x: p0.x + n0.x * offset, y: p0.y + n0.y * offset };
3141
+ const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
3142
+ return [p0, cp1, cp2, p3];
3143
+ }
3144
+ function dot(a, b) {
3145
+ return a.x * b.x + a.y * b.y;
3146
+ }
3147
+ function localToWorld(origin, axisX, axisY, local) {
3148
+ return {
3149
+ x: origin.x + axisX.x * local.x + axisY.x * local.y,
3150
+ y: origin.y + axisX.y * local.x + axisY.y * local.y
3151
+ };
3152
+ }
3153
+ function arcRoute(fromBounds, toBounds, diagramCenter, tension) {
3154
+ const fromCenter = rectCenter(fromBounds);
3155
+ const toCenter = rectCenter(toBounds);
3156
+ const start = edgeAnchor(fromBounds, toCenter);
3157
+ const end = edgeAnchor(toBounds, fromCenter);
3158
+ const chord = { x: end.x - start.x, y: end.y - start.y };
3159
+ const chordLength = Math.hypot(chord.x, chord.y);
3160
+ if (chordLength < 1e-6) {
3161
+ const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
3162
+ return [
3163
+ [start, start, mid, mid],
3164
+ [mid, mid, end, end]
3165
+ ];
3166
+ }
3167
+ const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
3168
+ let axisY = { x: -axisX.y, y: axisX.x };
3169
+ const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
3170
+ const outwardHint = outwardNormal(midpoint, diagramCenter);
3171
+ if (dot(axisY, outwardHint) < 0) {
3172
+ axisY = { x: -axisY.x, y: -axisY.y };
3173
+ }
3174
+ const semiMajor = chordLength / 2;
3175
+ const semiMinor = Math.max(12, chordLength * tension * 0.75);
3176
+ const p0Local = { x: -semiMajor, y: 0 };
3177
+ const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
3178
+ const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
3179
+ const pMidLocal = { x: 0, y: semiMinor };
3180
+ const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
3181
+ const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
3182
+ const p3Local = { x: semiMajor, y: 0 };
3183
+ const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
3184
+ const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
3185
+ const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
3186
+ const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
3187
+ const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
3188
+ const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
3189
+ const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
3190
+ return [
3191
+ [p0, cp1, cp2, pMid],
3192
+ [pMid, cp3, cp4, p3]
3193
+ ];
3194
+ }
3195
+ function orthogonalRoute(fromBounds, toBounds) {
3196
+ const fromC = rectCenter(fromBounds);
3197
+ const toC = rectCenter(toBounds);
3198
+ const p0 = edgeAnchor(fromBounds, toC);
3199
+ const p3 = edgeAnchor(toBounds, fromC);
3200
+ const midX = (p0.x + p3.x) / 2;
3201
+ return [p0, { x: midX, y: p0.y }, { x: midX, y: p3.y }, p3];
3202
+ }
3203
+ function bezierPointAt(p0, cp1, cp2, p3, t) {
3204
+ const mt = 1 - t;
3205
+ return {
3206
+ x: mt * mt * mt * p0.x + 3 * mt * mt * t * cp1.x + 3 * mt * t * t * cp2.x + t * t * t * p3.x,
3207
+ y: mt * mt * mt * p0.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * p3.y
3208
+ };
3209
+ }
3210
+ function pointAlongArc(route, t) {
3211
+ const [first, second] = route;
3212
+ if (t <= 0.5) {
3213
+ const localT2 = Math.max(0, Math.min(1, t * 2));
3214
+ return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
3215
+ }
3216
+ const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
3217
+ return bezierPointAt(second[0], second[1], second[2], second[3], localT);
3218
+ }
3219
+ function computeDiagramCenter(nodeBounds, canvasCenter) {
3220
+ if (nodeBounds.length === 0) {
3221
+ return canvasCenter ?? { x: 0, y: 0 };
3222
+ }
3223
+ let totalX = 0;
3224
+ let totalY = 0;
3225
+ for (const bounds of nodeBounds) {
3226
+ totalX += bounds.x + bounds.width / 2;
3227
+ totalY += bounds.y + bounds.height / 2;
2861
3228
  }
2862
3229
  return {
2863
- x: c.x,
2864
- y: dy >= 0 ? rect.y + rect.height : rect.y
3230
+ x: totalX / nodeBounds.length,
3231
+ y: totalY / nodeBounds.length
2865
3232
  };
2866
3233
  }
2867
3234
  function dashFromStyle(style) {
@@ -2945,51 +3312,95 @@ function polylineBounds(points) {
2945
3312
  height: Math.max(1, maxY - minY)
2946
3313
  };
2947
3314
  }
2948
- function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute) {
2949
- const fromCenter = center(fromBounds);
2950
- const toCenter = center(toBounds);
2951
- const from = edgeAnchor(fromBounds, toCenter);
2952
- const to = edgeAnchor(toBounds, fromCenter);
2953
- const dash = dashFromStyle(conn.style);
3315
+ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
3316
+ const routing = conn.routing ?? "auto";
3317
+ const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
3318
+ const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
3319
+ const tension = conn.tension ?? 0.35;
3320
+ const dash = dashFromStyle(strokeStyle);
2954
3321
  const style = {
2955
3322
  color: conn.color ?? theme.borderMuted,
2956
- width: conn.width ?? 2,
3323
+ width: strokeWidth,
2957
3324
  headSize: conn.arrowSize ?? 10,
2958
3325
  ...dash ? { dash } : {}
2959
3326
  };
2960
- const points = edgeRoute && edgeRoute.points.length >= 2 ? edgeRoute.points : [from, { x: (from.x + to.x) / 2, y: from.y }, { x: (from.x + to.x) / 2, y: to.y }, to];
2961
- const startSegment = points[1] ?? points[0];
2962
- const endStart = points[points.length - 2] ?? points[0];
2963
- const end = points[points.length - 1] ?? points[0];
2964
- let startAngle = Math.atan2(startSegment.y - points[0].y, startSegment.x - points[0].x) + Math.PI;
2965
- let endAngle = Math.atan2(end.y - endStart.y, end.x - endStart.x);
3327
+ const labelT = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
3328
+ const diagramCenter = options?.diagramCenter ?? computeDiagramCenter([fromBounds, toBounds]);
3329
+ let linePoints;
3330
+ let startPoint;
3331
+ let endPoint;
3332
+ let startAngle;
3333
+ let endAngle;
3334
+ let labelPoint;
3335
+ ctx.save();
3336
+ ctx.globalAlpha = conn.opacity;
3337
+ if (routing === "curve") {
3338
+ const [p0, cp1, cp2, p3] = curveRoute(fromBounds, toBounds, diagramCenter, tension);
3339
+ ctx.strokeStyle = style.color;
3340
+ ctx.lineWidth = style.width;
3341
+ ctx.setLineDash(style.dash ?? []);
3342
+ ctx.beginPath();
3343
+ ctx.moveTo(p0.x, p0.y);
3344
+ ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, p3.x, p3.y);
3345
+ ctx.stroke();
3346
+ linePoints = [p0, cp1, cp2, p3];
3347
+ startPoint = p0;
3348
+ endPoint = p3;
3349
+ startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3350
+ endAngle = Math.atan2(p3.y - cp2.y, p3.x - cp2.x);
3351
+ labelPoint = bezierPointAt(p0, cp1, cp2, p3, labelT);
3352
+ } else if (routing === "arc") {
3353
+ const [first, second] = arcRoute(fromBounds, toBounds, diagramCenter, tension);
3354
+ const [p0, cp1, cp2, pMid] = first;
3355
+ const [, cp3, cp4, p3] = second;
3356
+ ctx.strokeStyle = style.color;
3357
+ ctx.lineWidth = style.width;
3358
+ ctx.setLineDash(style.dash ?? []);
3359
+ ctx.beginPath();
3360
+ ctx.moveTo(p0.x, p0.y);
3361
+ ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
3362
+ ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
3363
+ ctx.stroke();
3364
+ linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
3365
+ startPoint = p0;
3366
+ endPoint = p3;
3367
+ startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3368
+ endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
3369
+ labelPoint = pointAlongArc([first, second], labelT);
3370
+ } else {
3371
+ const useElkRoute = routing === "auto" && (edgeRoute?.points.length ?? 0) >= 2;
3372
+ linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds) : orthogonalRoute(fromBounds, toBounds);
3373
+ startPoint = linePoints[0];
3374
+ const startSegment = linePoints[1] ?? linePoints[0];
3375
+ const endStart = linePoints[linePoints.length - 2] ?? linePoints[0];
3376
+ endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
3377
+ startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
3378
+ endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
3379
+ if (useElkRoute) {
3380
+ drawCubicInterpolatedPath(ctx, linePoints, style);
3381
+ } else {
3382
+ drawOrthogonalPath(ctx, startPoint, endPoint, style);
3383
+ }
3384
+ labelPoint = pointAlongPolyline(linePoints, labelT);
3385
+ }
2966
3386
  if (!Number.isFinite(startAngle)) {
2967
3387
  startAngle = 0;
2968
3388
  }
2969
3389
  if (!Number.isFinite(endAngle)) {
2970
3390
  endAngle = 0;
2971
3391
  }
2972
- const t = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
2973
- const labelPoint = pointAlongPolyline(points, t);
2974
- ctx.save();
2975
- ctx.globalAlpha = conn.opacity;
2976
- if (edgeRoute && edgeRoute.points.length >= 2) {
2977
- drawCubicInterpolatedPath(ctx, points, style);
2978
- } else {
2979
- drawOrthogonalPath(ctx, points[0], points[points.length - 1], style);
2980
- }
2981
3392
  if (conn.arrow === "start" || conn.arrow === "both") {
2982
- drawArrowhead(ctx, points[0], startAngle, style.headSize, style.color);
3393
+ drawArrowhead(ctx, startPoint, startAngle, style.headSize, style.color);
2983
3394
  }
2984
3395
  if (conn.arrow === "end" || conn.arrow === "both") {
2985
- drawArrowhead(ctx, end, endAngle, style.headSize, style.color);
3396
+ drawArrowhead(ctx, endPoint, endAngle, style.headSize, style.color);
2986
3397
  }
2987
3398
  ctx.restore();
2988
3399
  const elements = [
2989
3400
  {
2990
3401
  id: `connection-${conn.from}-${conn.to}`,
2991
3402
  kind: "connection",
2992
- bounds: polylineBounds(points),
3403
+ bounds: polylineBounds(linePoints),
2993
3404
  foregroundColor: style.color
2994
3405
  }
2995
3406
  ];
@@ -4203,6 +4614,10 @@ async function renderDesign(input, options = {}) {
4203
4614
  break;
4204
4615
  }
4205
4616
  }
4617
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
4618
+ spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
4619
+ { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
4620
+ );
4206
4621
  for (const element of spec.elements) {
4207
4622
  if (element.type !== "connection") {
4208
4623
  continue;
@@ -4215,7 +4630,9 @@ async function renderDesign(input, options = {}) {
4215
4630
  );
4216
4631
  }
4217
4632
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
4218
- elements.push(...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute));
4633
+ elements.push(
4634
+ ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
4635
+ );
4219
4636
  }
4220
4637
  if (footerRect && spec.footer) {
4221
4638
  const footerText = spec.footer.tagline ? `${spec.footer.text} \u2022 ${spec.footer.tagline}` : spec.footer.text;
@@ -4578,6 +4995,36 @@ var renderOutputSchema = z3.object({
4578
4995
  )
4579
4996
  })
4580
4997
  });
4998
+ var compareOutputSchema = z3.object({
4999
+ targetPath: z3.string(),
5000
+ renderedPath: z3.string(),
5001
+ targetDimensions: z3.object({
5002
+ width: z3.number().int().positive(),
5003
+ height: z3.number().int().positive()
5004
+ }),
5005
+ renderedDimensions: z3.object({
5006
+ width: z3.number().int().positive(),
5007
+ height: z3.number().int().positive()
5008
+ }),
5009
+ normalizedDimensions: z3.object({
5010
+ width: z3.number().int().positive(),
5011
+ height: z3.number().int().positive()
5012
+ }),
5013
+ dimensionMismatch: z3.boolean(),
5014
+ grid: z3.number().int().positive(),
5015
+ threshold: z3.number(),
5016
+ closeThreshold: z3.number(),
5017
+ similarity: z3.number(),
5018
+ verdict: z3.enum(["match", "close", "mismatch"]),
5019
+ regions: z3.array(
5020
+ z3.object({
5021
+ label: z3.string(),
5022
+ row: z3.number().int().nonnegative(),
5023
+ column: z3.number().int().nonnegative(),
5024
+ similarity: z3.number()
5025
+ })
5026
+ )
5027
+ });
4581
5028
  async function readJson(path) {
4582
5029
  if (path === "-") {
4583
5030
  const chunks = [];
@@ -4680,6 +5127,44 @@ cli.command("render", {
4680
5127
  return c.ok(runReport);
4681
5128
  }
4682
5129
  });
5130
+ cli.command("compare", {
5131
+ description: "Compare a rendered design against a target image using structural similarity scoring.",
5132
+ options: z3.object({
5133
+ target: z3.string().describe("Path to target image (baseline)"),
5134
+ rendered: z3.string().describe("Path to rendered image to evaluate"),
5135
+ grid: z3.number().int().positive().default(3).describe("Grid size for per-region scoring"),
5136
+ threshold: z3.number().min(0).max(1).default(0.8).describe("Minimum similarity score required for a match verdict")
5137
+ }),
5138
+ output: compareOutputSchema,
5139
+ examples: [
5140
+ {
5141
+ options: {
5142
+ target: "./designs/target.png",
5143
+ rendered: "./output/design-v2-g0.4.0-sabc123.png",
5144
+ grid: 3,
5145
+ threshold: 0.8
5146
+ },
5147
+ description: "Compare two images and report overall + per-region similarity scores"
5148
+ }
5149
+ ],
5150
+ async run(c) {
5151
+ try {
5152
+ return c.ok(
5153
+ await compareImages(c.options.target, c.options.rendered, {
5154
+ grid: c.options.grid,
5155
+ threshold: c.options.threshold
5156
+ })
5157
+ );
5158
+ } catch (error) {
5159
+ const message = error instanceof Error ? error.message : String(error);
5160
+ return c.error({
5161
+ code: "COMPARE_FAILED",
5162
+ message: `Unable to compare images: ${message}`,
5163
+ retryable: false
5164
+ });
5165
+ }
5166
+ }
5167
+ });
4683
5168
  var template = Cli.create("template", {
4684
5169
  description: "Generate common design templates and run the full render \u2192 QA pipeline."
4685
5170
  });
@@ -4921,7 +5406,8 @@ cli.command("qa", {
4921
5406
  options: z3.object({
4922
5407
  in: z3.string().describe("Path to rendered PNG"),
4923
5408
  spec: z3.string().describe("Path to normalized DesignSpec JSON"),
4924
- meta: z3.string().optional().describe("Optional sidecar metadata path (.meta.json)")
5409
+ meta: z3.string().optional().describe("Optional sidecar metadata path (.meta.json)"),
5410
+ reference: z3.string().optional().describe("Optional reference image path for visual comparison")
4925
5411
  }),
4926
5412
  output: z3.object({
4927
5413
  pass: z3.boolean(),
@@ -4935,7 +5421,18 @@ cli.command("qa", {
4935
5421
  message: z3.string(),
4936
5422
  elementId: z3.string().optional()
4937
5423
  })
4938
- )
5424
+ ),
5425
+ reference: z3.object({
5426
+ similarity: z3.number(),
5427
+ verdict: z3.enum(["match", "close", "mismatch"]),
5428
+ regions: z3.array(
5429
+ z3.object({
5430
+ label: z3.string(),
5431
+ similarity: z3.number(),
5432
+ description: z3.string().optional()
5433
+ })
5434
+ )
5435
+ }).optional()
4939
5436
  }),
4940
5437
  examples: [
4941
5438
  {
@@ -4958,14 +5455,16 @@ cli.command("qa", {
4958
5455
  const report = await runQa({
4959
5456
  imagePath: c.options.in,
4960
5457
  spec,
4961
- ...metadata ? { metadata } : {}
5458
+ ...metadata ? { metadata } : {},
5459
+ ...c.options.reference ? { referencePath: c.options.reference } : {}
4962
5460
  });
4963
5461
  const response = {
4964
5462
  pass: report.pass,
4965
5463
  checkedAt: report.checkedAt,
4966
5464
  imagePath: report.imagePath,
4967
5465
  issueCount: report.issues.length,
4968
- issues: report.issues
5466
+ issues: report.issues,
5467
+ ...report.reference ? { reference: report.reference } : {}
4969
5468
  };
4970
5469
  if (!report.pass) {
4971
5470
  return c.error({
@@ -5099,9 +5598,14 @@ var isMain = (() => {
5099
5598
  if (isMain) {
5100
5599
  cli.serve();
5101
5600
  }
5601
+
5602
+ // src/index.ts
5603
+ init_compare();
5102
5604
  export {
5103
5605
  DEFAULT_GENERATOR_VERSION,
5104
5606
  DEFAULT_RAINBOW_COLORS,
5607
+ arcRoute,
5608
+ bezierPointAt,
5105
5609
  buildCardsSpec,
5106
5610
  buildCodeSpec,
5107
5611
  buildFlowchartSpec,
@@ -5109,7 +5613,11 @@ export {
5109
5613
  builtInThemeBackgrounds,
5110
5614
  builtInThemes,
5111
5615
  cli,
5616
+ compareImages,
5617
+ computeDiagramCenter,
5112
5618
  computeSpecHash,
5619
+ connectionElementSchema,
5620
+ curveRoute,
5113
5621
  defaultAutoLayout,
5114
5622
  defaultCanvas,
5115
5623
  defaultConstraints,
@@ -5119,19 +5627,29 @@ export {
5119
5627
  defaultTheme,
5120
5628
  deriveSafeFrame,
5121
5629
  designSpecSchema,
5630
+ diagramElementSchema,
5631
+ diagramLayoutSchema,
5632
+ diagramSpecSchema,
5122
5633
  disposeHighlighter,
5123
5634
  drawGradientRect,
5124
5635
  drawRainbowRule,
5125
5636
  drawVignette,
5637
+ edgeAnchor,
5638
+ flowNodeElementSchema,
5126
5639
  highlightCode,
5127
5640
  inferLayout,
5128
5641
  inferSidecarPath,
5129
5642
  initHighlighter,
5130
5643
  loadFonts,
5644
+ orthogonalRoute,
5645
+ outwardNormal,
5131
5646
  parseDesignSpec,
5647
+ parseDiagramSpec,
5132
5648
  publishToGist,
5133
5649
  publishToGitHub,
5134
5650
  readMetadata,
5651
+ rectCenter,
5652
+ renderConnection,
5135
5653
  renderDesign,
5136
5654
  renderDrawCommands,
5137
5655
  resolveShikiTheme,