@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/README.md +32 -2
- package/dist/cli.js +555 -60
- package/dist/index.d.ts +105 -5
- package/dist/index.js +578 -60
- package/dist/qa.d.ts +14 -3
- package/dist/qa.js +242 -11
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +293 -53
- package/dist/{spec.schema-BUTof436.d.ts → spec.schema-Dm_wOLTd.d.ts} +1375 -114
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +75 -8
- package/package.json +1 -1
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
|
|
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([
|
|
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(
|
|
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
|
|
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,
|
|
1543
|
+
function drawCircle(ctx, center, radius, fill, stroke) {
|
|
1305
1544
|
ctx.beginPath();
|
|
1306
|
-
ctx.arc(
|
|
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
|
|
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,
|
|
2288
|
-
vignette.addColorStop(0.6,
|
|
2289
|
-
vignette.addColorStop(1,
|
|
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,
|
|
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 =
|
|
2425
|
-
ctx.strokeStyle =
|
|
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
|
-
|
|
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(
|
|
2853
|
-
const c =
|
|
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 (
|
|
2857
|
-
return {
|
|
2858
|
-
|
|
2859
|
-
|
|
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:
|
|
2864
|
-
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
|
|
2950
|
-
const
|
|
2951
|
-
const
|
|
2952
|
-
const
|
|
2953
|
-
const dash = dashFromStyle(
|
|
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:
|
|
3323
|
+
width: strokeWidth,
|
|
2957
3324
|
headSize: conn.arrowSize ?? 10,
|
|
2958
3325
|
...dash ? { dash } : {}
|
|
2959
3326
|
};
|
|
2960
|
-
const
|
|
2961
|
-
const
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
let
|
|
2965
|
-
let
|
|
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,
|
|
3393
|
+
drawArrowhead(ctx, startPoint, startAngle, style.headSize, style.color);
|
|
2983
3394
|
}
|
|
2984
3395
|
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
2985
|
-
drawArrowhead(ctx,
|
|
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(
|
|
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(
|
|
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,
|