@spectratools/graphic-designer-cli 0.4.0 → 0.7.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 +728 -61
- package/dist/index.d.ts +115 -5
- package/dist/index.js +751 -61
- package/dist/qa.d.ts +14 -3
- package/dist/qa.js +263 -12
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +466 -54
- package/dist/{spec.schema-BUTof436.d.ts → spec.schema-BeFz_nk1.d.ts} +1820 -193
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +96 -9
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,153 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/compare.ts
|
|
13
|
+
var compare_exports = {};
|
|
14
|
+
__export(compare_exports, {
|
|
15
|
+
compareImages: () => compareImages
|
|
16
|
+
});
|
|
17
|
+
import sharp from "sharp";
|
|
18
|
+
function clampUnit(value) {
|
|
19
|
+
if (value < 0) {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
if (value > 1) {
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function toRegionLabel(row, column) {
|
|
28
|
+
const letter = String.fromCharCode(65 + row);
|
|
29
|
+
return `${letter}${column + 1}`;
|
|
30
|
+
}
|
|
31
|
+
function validateGrid(grid) {
|
|
32
|
+
if (!Number.isInteger(grid) || grid <= 0) {
|
|
33
|
+
throw new Error(`Invalid grid value "${grid}". Expected a positive integer.`);
|
|
34
|
+
}
|
|
35
|
+
if (grid > 26) {
|
|
36
|
+
throw new Error(`Invalid grid value "${grid}". Maximum supported grid is 26.`);
|
|
37
|
+
}
|
|
38
|
+
return grid;
|
|
39
|
+
}
|
|
40
|
+
function validateThreshold(threshold) {
|
|
41
|
+
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
|
|
42
|
+
throw new Error(`Invalid threshold value "${threshold}". Expected a number between 0 and 1.`);
|
|
43
|
+
}
|
|
44
|
+
return threshold;
|
|
45
|
+
}
|
|
46
|
+
async function readDimensions(path) {
|
|
47
|
+
const metadata = await sharp(path).metadata();
|
|
48
|
+
if (!metadata.width || !metadata.height) {
|
|
49
|
+
throw new Error(`Unable to read image dimensions for "${path}".`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
width: metadata.width,
|
|
53
|
+
height: metadata.height
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function normalizeToRaw(path, width, height) {
|
|
57
|
+
const normalized = await sharp(path).rotate().resize(width, height, {
|
|
58
|
+
fit: "contain",
|
|
59
|
+
position: "centre",
|
|
60
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
61
|
+
}).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
62
|
+
return {
|
|
63
|
+
data: normalized.data,
|
|
64
|
+
width: normalized.info.width,
|
|
65
|
+
height: normalized.info.height
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function scorePixelDifference(a, b, offset) {
|
|
69
|
+
const redDiff = Math.abs(a.data[offset] - b.data[offset]);
|
|
70
|
+
const greenDiff = Math.abs(a.data[offset + 1] - b.data[offset + 1]);
|
|
71
|
+
const blueDiff = Math.abs(a.data[offset + 2] - b.data[offset + 2]);
|
|
72
|
+
const alphaDiff = Math.abs(a.data[offset + 3] - b.data[offset + 3]);
|
|
73
|
+
const rgbDelta = (redDiff + greenDiff + blueDiff) / (3 * 255);
|
|
74
|
+
const alphaDelta = alphaDiff / 255;
|
|
75
|
+
return rgbDelta * 0.75 + alphaDelta * 0.25;
|
|
76
|
+
}
|
|
77
|
+
async function compareImages(target, rendered, options = {}) {
|
|
78
|
+
const grid = validateGrid(options.grid ?? DEFAULT_GRID);
|
|
79
|
+
const threshold = validateThreshold(options.threshold ?? DEFAULT_THRESHOLD);
|
|
80
|
+
const closeThreshold = clampUnit(threshold - (options.closeMargin ?? DEFAULT_CLOSE_MARGIN));
|
|
81
|
+
const targetDimensions = await readDimensions(target);
|
|
82
|
+
const renderedDimensions = await readDimensions(rendered);
|
|
83
|
+
const normalizedWidth = Math.max(targetDimensions.width, renderedDimensions.width);
|
|
84
|
+
const normalizedHeight = Math.max(targetDimensions.height, renderedDimensions.height);
|
|
85
|
+
const [targetImage, renderedImage] = await Promise.all([
|
|
86
|
+
normalizeToRaw(target, normalizedWidth, normalizedHeight),
|
|
87
|
+
normalizeToRaw(rendered, normalizedWidth, normalizedHeight)
|
|
88
|
+
]);
|
|
89
|
+
const regionDiffSums = new Array(grid * grid).fill(0);
|
|
90
|
+
const regionCounts = new Array(grid * grid).fill(0);
|
|
91
|
+
let totalDiff = 0;
|
|
92
|
+
for (let y = 0; y < normalizedHeight; y += 1) {
|
|
93
|
+
const row = Math.min(Math.floor(y * grid / normalizedHeight), grid - 1);
|
|
94
|
+
for (let x = 0; x < normalizedWidth; x += 1) {
|
|
95
|
+
const column = Math.min(Math.floor(x * grid / normalizedWidth), grid - 1);
|
|
96
|
+
const regionIndex = row * grid + column;
|
|
97
|
+
const offset = (y * normalizedWidth + x) * 4;
|
|
98
|
+
const diff = scorePixelDifference(targetImage, renderedImage, offset);
|
|
99
|
+
totalDiff += diff;
|
|
100
|
+
regionDiffSums[regionIndex] += diff;
|
|
101
|
+
regionCounts[regionIndex] += 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const pixelCount = normalizedWidth * normalizedHeight;
|
|
105
|
+
const similarity = clampUnit(1 - totalDiff / pixelCount);
|
|
106
|
+
const regions = [];
|
|
107
|
+
for (let row = 0; row < grid; row += 1) {
|
|
108
|
+
for (let column = 0; column < grid; column += 1) {
|
|
109
|
+
const regionIndex = row * grid + column;
|
|
110
|
+
const regionCount = regionCounts[regionIndex];
|
|
111
|
+
const regionSimilarity = regionCount > 0 ? clampUnit(1 - regionDiffSums[regionIndex] / regionCount) : 1;
|
|
112
|
+
regions.push({
|
|
113
|
+
label: toRegionLabel(row, column),
|
|
114
|
+
row,
|
|
115
|
+
column,
|
|
116
|
+
similarity: regionSimilarity
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const verdict = similarity >= threshold ? "match" : similarity >= closeThreshold ? "close" : "mismatch";
|
|
121
|
+
return {
|
|
122
|
+
targetPath: target,
|
|
123
|
+
renderedPath: rendered,
|
|
124
|
+
targetDimensions,
|
|
125
|
+
renderedDimensions,
|
|
126
|
+
normalizedDimensions: {
|
|
127
|
+
width: normalizedWidth,
|
|
128
|
+
height: normalizedHeight
|
|
129
|
+
},
|
|
130
|
+
dimensionMismatch: targetDimensions.width !== renderedDimensions.width || targetDimensions.height !== renderedDimensions.height,
|
|
131
|
+
grid,
|
|
132
|
+
threshold,
|
|
133
|
+
closeThreshold,
|
|
134
|
+
similarity,
|
|
135
|
+
verdict,
|
|
136
|
+
regions
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
var DEFAULT_GRID, DEFAULT_THRESHOLD, DEFAULT_CLOSE_MARGIN;
|
|
140
|
+
var init_compare = __esm({
|
|
141
|
+
"src/compare.ts"() {
|
|
142
|
+
"use strict";
|
|
143
|
+
DEFAULT_GRID = 3;
|
|
144
|
+
DEFAULT_THRESHOLD = 0.8;
|
|
145
|
+
DEFAULT_CLOSE_MARGIN = 0.1;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
2
148
|
|
|
3
149
|
// src/cli.ts
|
|
150
|
+
init_compare();
|
|
4
151
|
import { readFileSync, realpathSync } from "fs";
|
|
5
152
|
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
6
153
|
import { basename as basename4, dirname as dirname3, resolve as resolve4 } from "path";
|
|
@@ -198,7 +345,7 @@ async function publishToGitHub(options) {
|
|
|
198
345
|
// src/qa.ts
|
|
199
346
|
import { readFile as readFile3 } from "fs/promises";
|
|
200
347
|
import { resolve } from "path";
|
|
201
|
-
import
|
|
348
|
+
import sharp2 from "sharp";
|
|
202
349
|
|
|
203
350
|
// src/code-style.ts
|
|
204
351
|
var CARBON_SURROUND_COLOR = "rgba(171, 184, 195, 1)";
|
|
@@ -338,6 +485,10 @@ function contrastRatio(foreground, background) {
|
|
|
338
485
|
const darker = Math.min(fg, bg);
|
|
339
486
|
return (lighter + 0.05) / (darker + 0.05);
|
|
340
487
|
}
|
|
488
|
+
function withAlpha(hexColor, opacity) {
|
|
489
|
+
const rgb = parseHexColor(hexColor);
|
|
490
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`;
|
|
491
|
+
}
|
|
341
492
|
function blendColorWithOpacity(foreground, background, opacity) {
|
|
342
493
|
const fg = parseHexColor(foreground);
|
|
343
494
|
const bg = parseHexColor(background);
|
|
@@ -701,6 +852,15 @@ var drawGradientRectSchema = z2.object({
|
|
|
701
852
|
radius: z2.number().min(0).max(256).default(0),
|
|
702
853
|
opacity: z2.number().min(0).max(1).default(1)
|
|
703
854
|
}).strict();
|
|
855
|
+
var drawGridSchema = z2.object({
|
|
856
|
+
type: z2.literal("grid"),
|
|
857
|
+
spacing: z2.number().min(5).max(200).default(40),
|
|
858
|
+
color: colorHexSchema2.default("#1E2D4A"),
|
|
859
|
+
width: z2.number().min(0.1).max(4).default(0.5),
|
|
860
|
+
opacity: z2.number().min(0).max(1).default(0.2),
|
|
861
|
+
offsetX: z2.number().default(0),
|
|
862
|
+
offsetY: z2.number().default(0)
|
|
863
|
+
}).strict();
|
|
704
864
|
var drawCommandSchema = z2.discriminatedUnion("type", [
|
|
705
865
|
drawRectSchema,
|
|
706
866
|
drawCircleSchema,
|
|
@@ -709,7 +869,8 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
|
|
|
709
869
|
drawBezierSchema,
|
|
710
870
|
drawPathSchema,
|
|
711
871
|
drawBadgeSchema,
|
|
712
|
-
drawGradientRectSchema
|
|
872
|
+
drawGradientRectSchema,
|
|
873
|
+
drawGridSchema
|
|
713
874
|
]);
|
|
714
875
|
var defaultCanvas = {
|
|
715
876
|
width: 1200,
|
|
@@ -773,10 +934,26 @@ var cardElementSchema = z2.object({
|
|
|
773
934
|
tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
|
|
774
935
|
icon: z2.string().min(1).max(64).optional()
|
|
775
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();
|
|
776
944
|
var flowNodeElementSchema = z2.object({
|
|
777
945
|
type: z2.literal("flow-node"),
|
|
778
946
|
id: z2.string().min(1).max(120),
|
|
779
|
-
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"),
|
|
780
957
|
label: z2.string().min(1).max(200),
|
|
781
958
|
sublabel: z2.string().min(1).max(300).optional(),
|
|
782
959
|
sublabelColor: colorHexSchema2.optional(),
|
|
@@ -796,20 +973,35 @@ var flowNodeElementSchema = z2.object({
|
|
|
796
973
|
badgeText: z2.string().min(1).max(32).optional(),
|
|
797
974
|
badgeColor: colorHexSchema2.optional(),
|
|
798
975
|
badgeBackground: colorHexSchema2.optional(),
|
|
799
|
-
badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
|
|
976
|
+
badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
|
|
977
|
+
shadow: flowNodeShadowSchema.optional()
|
|
800
978
|
}).strict();
|
|
979
|
+
var anchorHintSchema = z2.union([
|
|
980
|
+
z2.enum(["top", "bottom", "left", "right", "center"]),
|
|
981
|
+
z2.object({
|
|
982
|
+
x: z2.number().min(-1).max(1),
|
|
983
|
+
y: z2.number().min(-1).max(1)
|
|
984
|
+
}).strict()
|
|
985
|
+
]);
|
|
801
986
|
var connectionElementSchema = z2.object({
|
|
802
987
|
type: z2.literal("connection"),
|
|
803
988
|
from: z2.string().min(1).max(120),
|
|
804
989
|
to: z2.string().min(1).max(120),
|
|
805
990
|
style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
991
|
+
strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
806
992
|
arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
|
|
807
993
|
label: z2.string().min(1).max(200).optional(),
|
|
808
994
|
labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
|
|
809
995
|
color: colorHexSchema2.optional(),
|
|
810
|
-
width: z2.number().min(0.5).max(
|
|
996
|
+
width: z2.number().min(0.5).max(10).optional(),
|
|
997
|
+
strokeWidth: z2.number().min(0.5).max(10).default(2),
|
|
811
998
|
arrowSize: z2.number().min(4).max(32).optional(),
|
|
812
|
-
|
|
999
|
+
arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
|
|
1000
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
1001
|
+
routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
|
|
1002
|
+
tension: z2.number().min(0.1).max(0.8).default(0.35),
|
|
1003
|
+
fromAnchor: anchorHintSchema.optional(),
|
|
1004
|
+
toAnchor: anchorHintSchema.optional()
|
|
813
1005
|
}).strict();
|
|
814
1006
|
var codeBlockStyleSchema = z2.object({
|
|
815
1007
|
paddingVertical: z2.number().min(0).max(128).default(56),
|
|
@@ -878,6 +1070,10 @@ var elementSchema = z2.discriminatedUnion("type", [
|
|
|
878
1070
|
shapeElementSchema,
|
|
879
1071
|
imageElementSchema
|
|
880
1072
|
]);
|
|
1073
|
+
var diagramCenterSchema = z2.object({
|
|
1074
|
+
x: z2.number(),
|
|
1075
|
+
y: z2.number()
|
|
1076
|
+
}).strict();
|
|
881
1077
|
var autoLayoutConfigSchema = z2.object({
|
|
882
1078
|
mode: z2.literal("auto"),
|
|
883
1079
|
algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
|
|
@@ -893,7 +1089,9 @@ var autoLayoutConfigSchema = z2.object({
|
|
|
893
1089
|
/** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
|
|
894
1090
|
radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
|
|
895
1091
|
/** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
|
|
896
|
-
radialSortBy: z2.enum(["id", "connections"]).optional()
|
|
1092
|
+
radialSortBy: z2.enum(["id", "connections"]).optional(),
|
|
1093
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1094
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
897
1095
|
}).strict();
|
|
898
1096
|
var gridLayoutConfigSchema = z2.object({
|
|
899
1097
|
mode: z2.literal("grid"),
|
|
@@ -901,13 +1099,17 @@ var gridLayoutConfigSchema = z2.object({
|
|
|
901
1099
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
902
1100
|
cardMinHeight: z2.number().int().min(32).max(4096).optional(),
|
|
903
1101
|
cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
|
|
904
|
-
equalHeight: z2.boolean().default(false)
|
|
1102
|
+
equalHeight: z2.boolean().default(false),
|
|
1103
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1104
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
905
1105
|
}).strict();
|
|
906
1106
|
var stackLayoutConfigSchema = z2.object({
|
|
907
1107
|
mode: z2.literal("stack"),
|
|
908
1108
|
direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
|
|
909
1109
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
910
|
-
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
|
|
1110
|
+
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
|
|
1111
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1112
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
911
1113
|
}).strict();
|
|
912
1114
|
var manualPositionSchema = z2.object({
|
|
913
1115
|
x: z2.number().int(),
|
|
@@ -917,7 +1119,9 @@ var manualPositionSchema = z2.object({
|
|
|
917
1119
|
}).strict();
|
|
918
1120
|
var manualLayoutConfigSchema = z2.object({
|
|
919
1121
|
mode: z2.literal("manual"),
|
|
920
|
-
positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
|
|
1122
|
+
positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
|
|
1123
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1124
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
921
1125
|
}).strict();
|
|
922
1126
|
var layoutConfigSchema = z2.discriminatedUnion("mode", [
|
|
923
1127
|
autoLayoutConfigSchema,
|
|
@@ -969,6 +1173,31 @@ var canvasSchema = z2.object({
|
|
|
969
1173
|
padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
|
|
970
1174
|
}).strict();
|
|
971
1175
|
var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
|
|
1176
|
+
var diagramPositionSchema = z2.object({
|
|
1177
|
+
x: z2.number(),
|
|
1178
|
+
y: z2.number(),
|
|
1179
|
+
width: z2.number().positive(),
|
|
1180
|
+
height: z2.number().positive()
|
|
1181
|
+
}).strict();
|
|
1182
|
+
var diagramElementSchema = z2.discriminatedUnion("type", [
|
|
1183
|
+
flowNodeElementSchema,
|
|
1184
|
+
connectionElementSchema
|
|
1185
|
+
]);
|
|
1186
|
+
var diagramLayoutSchema = z2.object({
|
|
1187
|
+
mode: z2.enum(["manual", "auto"]).default("manual"),
|
|
1188
|
+
positions: z2.record(z2.string(), diagramPositionSchema).optional(),
|
|
1189
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
1190
|
+
}).strict();
|
|
1191
|
+
var diagramSpecSchema = z2.object({
|
|
1192
|
+
version: z2.literal(1),
|
|
1193
|
+
canvas: z2.object({
|
|
1194
|
+
width: z2.number().int().min(320).max(4096).default(1200),
|
|
1195
|
+
height: z2.number().int().min(180).max(4096).default(675)
|
|
1196
|
+
}).default({ width: 1200, height: 675 }),
|
|
1197
|
+
theme: themeSchema.optional(),
|
|
1198
|
+
elements: z2.array(diagramElementSchema).min(1),
|
|
1199
|
+
layout: diagramLayoutSchema.default({ mode: "manual" })
|
|
1200
|
+
}).strict();
|
|
972
1201
|
var designSpecSchema = z2.object({
|
|
973
1202
|
version: z2.literal(2).default(2),
|
|
974
1203
|
canvas: canvasSchema.default(defaultCanvas),
|
|
@@ -1042,7 +1271,7 @@ async function runQa(options) {
|
|
|
1042
1271
|
const imagePath = resolve(options.imagePath);
|
|
1043
1272
|
const expectedSafeFrame = deriveSafeFrame(spec);
|
|
1044
1273
|
const expectedCanvas = canvasRect(spec);
|
|
1045
|
-
const imageMetadata = await
|
|
1274
|
+
const imageMetadata = await sharp2(imagePath).metadata();
|
|
1046
1275
|
const issues = [];
|
|
1047
1276
|
const expectedScale = options.metadata?.canvas.scale ?? resolveRenderScale(spec);
|
|
1048
1277
|
const expectedWidth = spec.canvas.width * expectedScale;
|
|
@@ -1193,6 +1422,31 @@ async function runQa(options) {
|
|
|
1193
1422
|
});
|
|
1194
1423
|
}
|
|
1195
1424
|
}
|
|
1425
|
+
let referenceResult;
|
|
1426
|
+
if (options.referencePath) {
|
|
1427
|
+
const { compareImages: compareImages2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
|
|
1428
|
+
const comparison = await compareImages2(options.referencePath, imagePath);
|
|
1429
|
+
referenceResult = {
|
|
1430
|
+
similarity: comparison.similarity,
|
|
1431
|
+
verdict: comparison.verdict,
|
|
1432
|
+
regions: comparison.regions.map((region) => ({
|
|
1433
|
+
label: region.label,
|
|
1434
|
+
similarity: region.similarity
|
|
1435
|
+
}))
|
|
1436
|
+
};
|
|
1437
|
+
if (comparison.verdict === "mismatch") {
|
|
1438
|
+
const severity = comparison.similarity < 0.5 ? "error" : "warning";
|
|
1439
|
+
issues.push({
|
|
1440
|
+
code: "REFERENCE_MISMATCH",
|
|
1441
|
+
severity,
|
|
1442
|
+
message: `Reference image comparison ${severity === "error" ? "failed" : "warned"}: similarity ${comparison.similarity.toFixed(4)} with verdict "${comparison.verdict}".`,
|
|
1443
|
+
details: {
|
|
1444
|
+
similarity: comparison.similarity,
|
|
1445
|
+
verdict: comparison.verdict
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1196
1450
|
const footerSpacingPx = options.metadata?.layout.elements ? (() => {
|
|
1197
1451
|
const footer = options.metadata.layout.elements.find((element) => element.id === "footer");
|
|
1198
1452
|
if (!footer) {
|
|
@@ -1225,7 +1479,8 @@ async function runQa(options) {
|
|
|
1225
1479
|
...imageMetadata.height !== void 0 ? { height: imageMetadata.height } : {},
|
|
1226
1480
|
...footerSpacingPx !== void 0 ? { footerSpacingPx } : {}
|
|
1227
1481
|
},
|
|
1228
|
-
issues
|
|
1482
|
+
issues,
|
|
1483
|
+
...referenceResult ? { reference: referenceResult } : {}
|
|
1229
1484
|
};
|
|
1230
1485
|
}
|
|
1231
1486
|
|
|
@@ -1292,9 +1547,9 @@ function drawRoundedRect(ctx, rect, radius, fill, stroke) {
|
|
|
1292
1547
|
roundRectPath(ctx, rect, radius);
|
|
1293
1548
|
fillAndStroke(ctx, fill, stroke);
|
|
1294
1549
|
}
|
|
1295
|
-
function drawCircle(ctx,
|
|
1550
|
+
function drawCircle(ctx, center, radius, fill, stroke) {
|
|
1296
1551
|
ctx.beginPath();
|
|
1297
|
-
ctx.arc(
|
|
1552
|
+
ctx.arc(center.x, center.y, Math.max(0, radius), 0, Math.PI * 2);
|
|
1298
1553
|
ctx.closePath();
|
|
1299
1554
|
fillAndStroke(ctx, fill, stroke);
|
|
1300
1555
|
}
|
|
@@ -1538,15 +1793,34 @@ function renderFlowNode(ctx, node, bounds, theme) {
|
|
|
1538
1793
|
const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
|
|
1539
1794
|
ctx.save();
|
|
1540
1795
|
ctx.lineWidth = borderWidth;
|
|
1796
|
+
if (node.shadow) {
|
|
1797
|
+
const shadowColor = node.shadow.color ?? borderColor ?? theme.accent;
|
|
1798
|
+
ctx.shadowColor = withAlpha(shadowColor, node.shadow.opacity);
|
|
1799
|
+
ctx.shadowBlur = node.shadow.blur;
|
|
1800
|
+
ctx.shadowOffsetX = node.shadow.offsetX;
|
|
1801
|
+
ctx.shadowOffsetY = node.shadow.offsetY;
|
|
1802
|
+
}
|
|
1541
1803
|
if (fillOpacity < 1) {
|
|
1542
1804
|
ctx.globalAlpha = node.opacity * fillOpacity;
|
|
1543
1805
|
drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
|
|
1806
|
+
if (node.shadow) {
|
|
1807
|
+
ctx.shadowColor = "transparent";
|
|
1808
|
+
ctx.shadowBlur = 0;
|
|
1809
|
+
ctx.shadowOffsetX = 0;
|
|
1810
|
+
ctx.shadowOffsetY = 0;
|
|
1811
|
+
}
|
|
1544
1812
|
ctx.globalAlpha = node.opacity;
|
|
1545
1813
|
drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
|
|
1546
1814
|
} else {
|
|
1547
1815
|
ctx.globalAlpha = node.opacity;
|
|
1548
1816
|
drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
|
|
1549
1817
|
}
|
|
1818
|
+
if (node.shadow) {
|
|
1819
|
+
ctx.shadowColor = "transparent";
|
|
1820
|
+
ctx.shadowBlur = 0;
|
|
1821
|
+
ctx.shadowOffsetX = 0;
|
|
1822
|
+
ctx.shadowOffsetY = 0;
|
|
1823
|
+
}
|
|
1550
1824
|
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
1551
1825
|
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
1552
1826
|
const monoFont = resolveFont(theme.fonts.mono, "mono");
|
|
@@ -2218,7 +2492,7 @@ function parseHexColor2(color) {
|
|
|
2218
2492
|
a: normalized.length === 8 ? parseChannel2(6) / 255 : 1
|
|
2219
2493
|
};
|
|
2220
2494
|
}
|
|
2221
|
-
function
|
|
2495
|
+
function withAlpha2(color, alpha) {
|
|
2222
2496
|
const parsed = parseHexColor2(color);
|
|
2223
2497
|
const effectiveAlpha = clamp01(parsed.a * alpha);
|
|
2224
2498
|
return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${effectiveAlpha})`;
|
|
@@ -2275,9 +2549,9 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
|
|
|
2275
2549
|
centerY,
|
|
2276
2550
|
outerRadius
|
|
2277
2551
|
);
|
|
2278
|
-
vignette.addColorStop(0,
|
|
2279
|
-
vignette.addColorStop(0.6,
|
|
2280
|
-
vignette.addColorStop(1,
|
|
2552
|
+
vignette.addColorStop(0, withAlpha2(color, 0));
|
|
2553
|
+
vignette.addColorStop(0.6, withAlpha2(color, 0));
|
|
2554
|
+
vignette.addColorStop(1, withAlpha2(color, clamp01(intensity)));
|
|
2281
2555
|
ctx.save();
|
|
2282
2556
|
ctx.fillStyle = vignette;
|
|
2283
2557
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -2408,12 +2682,12 @@ var MACOS_DOTS = [
|
|
|
2408
2682
|
{ fill: "#27C93F", stroke: "#1AAB29" }
|
|
2409
2683
|
];
|
|
2410
2684
|
function drawMacosDots(ctx, x, y) {
|
|
2411
|
-
for (const [index,
|
|
2685
|
+
for (const [index, dot2] of MACOS_DOTS.entries()) {
|
|
2412
2686
|
ctx.beginPath();
|
|
2413
2687
|
ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
|
|
2414
2688
|
ctx.closePath();
|
|
2415
|
-
ctx.fillStyle =
|
|
2416
|
-
ctx.strokeStyle =
|
|
2689
|
+
ctx.fillStyle = dot2.fill;
|
|
2690
|
+
ctx.strokeStyle = dot2.stroke;
|
|
2417
2691
|
ctx.lineWidth = DOT_STROKE_WIDTH;
|
|
2418
2692
|
ctx.fill();
|
|
2419
2693
|
ctx.stroke();
|
|
@@ -2830,25 +3104,203 @@ function drawOrthogonalPath(ctx, from, to, style) {
|
|
|
2830
3104
|
}
|
|
2831
3105
|
|
|
2832
3106
|
// src/renderers/connection.ts
|
|
2833
|
-
|
|
3107
|
+
var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
|
|
3108
|
+
function rectCenter(rect) {
|
|
2834
3109
|
return {
|
|
2835
3110
|
x: rect.x + rect.width / 2,
|
|
2836
3111
|
y: rect.y + rect.height / 2
|
|
2837
3112
|
};
|
|
2838
3113
|
}
|
|
2839
|
-
function edgeAnchor(
|
|
2840
|
-
const c =
|
|
3114
|
+
function edgeAnchor(bounds, target) {
|
|
3115
|
+
const c = rectCenter(bounds);
|
|
2841
3116
|
const dx = target.x - c.x;
|
|
2842
3117
|
const dy = target.y - c.y;
|
|
2843
|
-
if (
|
|
2844
|
-
return {
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
3118
|
+
if (dx === 0 && dy === 0) {
|
|
3119
|
+
return { x: c.x, y: c.y - bounds.height / 2 };
|
|
3120
|
+
}
|
|
3121
|
+
const hw = bounds.width / 2;
|
|
3122
|
+
const hh = bounds.height / 2;
|
|
3123
|
+
const absDx = Math.abs(dx);
|
|
3124
|
+
const absDy = Math.abs(dy);
|
|
3125
|
+
const t = absDx * hh > absDy * hw ? hw / absDx : hh / absDy;
|
|
3126
|
+
return { x: c.x + dx * t, y: c.y + dy * t };
|
|
3127
|
+
}
|
|
3128
|
+
function resolveAnchor(bounds, anchor, fallbackTarget) {
|
|
3129
|
+
if (!anchor) return edgeAnchor(bounds, fallbackTarget);
|
|
3130
|
+
if (typeof anchor === "string") {
|
|
3131
|
+
const c2 = rectCenter(bounds);
|
|
3132
|
+
switch (anchor) {
|
|
3133
|
+
case "top":
|
|
3134
|
+
return { x: c2.x, y: bounds.y };
|
|
3135
|
+
case "bottom":
|
|
3136
|
+
return { x: c2.x, y: bounds.y + bounds.height };
|
|
3137
|
+
case "left":
|
|
3138
|
+
return { x: bounds.x, y: c2.y };
|
|
3139
|
+
case "right":
|
|
3140
|
+
return { x: bounds.x + bounds.width, y: c2.y };
|
|
3141
|
+
case "center":
|
|
3142
|
+
return c2;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
const c = rectCenter(bounds);
|
|
3146
|
+
return {
|
|
3147
|
+
x: c.x + anchor.x * (bounds.width / 2),
|
|
3148
|
+
y: c.y + anchor.y * (bounds.height / 2)
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
function anchorNormal(anchor, point, diagramCenter) {
|
|
3152
|
+
if (typeof anchor === "string") {
|
|
3153
|
+
switch (anchor) {
|
|
3154
|
+
case "top":
|
|
3155
|
+
return { x: 0, y: -1 };
|
|
3156
|
+
case "bottom":
|
|
3157
|
+
return { x: 0, y: 1 };
|
|
3158
|
+
case "left":
|
|
3159
|
+
return { x: -1, y: 0 };
|
|
3160
|
+
case "right":
|
|
3161
|
+
return { x: 1, y: 0 };
|
|
3162
|
+
case "center":
|
|
3163
|
+
return outwardNormal(point, diagramCenter);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return outwardNormal(point, diagramCenter);
|
|
3167
|
+
}
|
|
3168
|
+
function outwardNormal(point, diagramCenter) {
|
|
3169
|
+
const dx = point.x - diagramCenter.x;
|
|
3170
|
+
const dy = point.y - diagramCenter.y;
|
|
3171
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
3172
|
+
return { x: dx / len, y: dy / len };
|
|
3173
|
+
}
|
|
3174
|
+
function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
|
|
3175
|
+
const fromCenter = rectCenter(fromBounds);
|
|
3176
|
+
const toCenter = rectCenter(toBounds);
|
|
3177
|
+
const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
|
|
3178
|
+
const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
|
|
3179
|
+
const dist = Math.hypot(p3.x - p0.x, p3.y - p0.y);
|
|
3180
|
+
const offset = dist * tension;
|
|
3181
|
+
const n0 = anchorNormal(fromAnchor, p0, diagramCenter);
|
|
3182
|
+
const n3 = anchorNormal(toAnchor, p3, diagramCenter);
|
|
3183
|
+
const cp1 = { x: p0.x + n0.x * offset, y: p0.y + n0.y * offset };
|
|
3184
|
+
const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
|
|
3185
|
+
return [p0, cp1, cp2, p3];
|
|
3186
|
+
}
|
|
3187
|
+
function dot(a, b) {
|
|
3188
|
+
return a.x * b.x + a.y * b.y;
|
|
3189
|
+
}
|
|
3190
|
+
function localToWorld(origin, axisX, axisY, local) {
|
|
3191
|
+
return {
|
|
3192
|
+
x: origin.x + axisX.x * local.x + axisY.x * local.y,
|
|
3193
|
+
y: origin.y + axisX.y * local.x + axisY.y * local.y
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
|
|
3197
|
+
const fromCenter = rectCenter(fromBounds);
|
|
3198
|
+
const toCenter = rectCenter(toBounds);
|
|
3199
|
+
const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
|
|
3200
|
+
const end = resolveAnchor(toBounds, toAnchor, fromCenter);
|
|
3201
|
+
const chord = { x: end.x - start.x, y: end.y - start.y };
|
|
3202
|
+
const chordLength = Math.hypot(chord.x, chord.y);
|
|
3203
|
+
if (chordLength < 1e-6) {
|
|
3204
|
+
const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
3205
|
+
return [
|
|
3206
|
+
[start, start, mid, mid],
|
|
3207
|
+
[mid, mid, end, end]
|
|
3208
|
+
];
|
|
3209
|
+
}
|
|
3210
|
+
const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
|
|
3211
|
+
let axisY = { x: -axisX.y, y: axisX.x };
|
|
3212
|
+
const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
3213
|
+
const outwardHint = outwardNormal(midpoint, diagramCenter);
|
|
3214
|
+
if (dot(axisY, outwardHint) < 0) {
|
|
3215
|
+
axisY = { x: -axisY.x, y: -axisY.y };
|
|
3216
|
+
}
|
|
3217
|
+
const semiMajor = chordLength / 2;
|
|
3218
|
+
const semiMinor = Math.max(12, chordLength * tension * 0.75);
|
|
3219
|
+
const p0Local = { x: -semiMajor, y: 0 };
|
|
3220
|
+
const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
|
|
3221
|
+
const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
|
|
3222
|
+
const pMidLocal = { x: 0, y: semiMinor };
|
|
3223
|
+
const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
|
|
3224
|
+
const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
|
|
3225
|
+
const p3Local = { x: semiMajor, y: 0 };
|
|
3226
|
+
const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
|
|
3227
|
+
const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
|
|
3228
|
+
const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
|
|
3229
|
+
const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
|
|
3230
|
+
const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
|
|
3231
|
+
const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
|
|
3232
|
+
const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
|
|
3233
|
+
return [
|
|
3234
|
+
[p0, cp1, cp2, pMid],
|
|
3235
|
+
[pMid, cp3, cp4, p3]
|
|
3236
|
+
];
|
|
3237
|
+
}
|
|
3238
|
+
function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
|
|
3239
|
+
const fromC = rectCenter(fromBounds);
|
|
3240
|
+
const toC = rectCenter(toBounds);
|
|
3241
|
+
const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
|
|
3242
|
+
const p3 = resolveAnchor(toBounds, toAnchor, fromC);
|
|
3243
|
+
const midX = (p0.x + p3.x) / 2;
|
|
3244
|
+
return [p0, { x: midX, y: p0.y }, { x: midX, y: p3.y }, p3];
|
|
3245
|
+
}
|
|
3246
|
+
function bezierPointAt(p0, cp1, cp2, p3, t) {
|
|
3247
|
+
const mt = 1 - t;
|
|
3248
|
+
return {
|
|
3249
|
+
x: mt * mt * mt * p0.x + 3 * mt * mt * t * cp1.x + 3 * mt * t * t * cp2.x + t * t * t * p3.x,
|
|
3250
|
+
y: mt * mt * mt * p0.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * p3.y
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
function bezierTangentAt(p0, cp1, cp2, p3, t) {
|
|
3254
|
+
const mt = 1 - t;
|
|
3255
|
+
return {
|
|
3256
|
+
x: 3 * mt * mt * (cp1.x - p0.x) + 6 * mt * t * (cp2.x - cp1.x) + 3 * t * t * (p3.x - cp2.x),
|
|
3257
|
+
y: 3 * mt * mt * (cp1.y - p0.y) + 6 * mt * t * (cp2.y - cp1.y) + 3 * t * t * (p3.y - cp2.y)
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
function isInsideRect(point, rect) {
|
|
3261
|
+
return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
|
|
3262
|
+
}
|
|
3263
|
+
function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
|
|
3264
|
+
const step = 5e-3;
|
|
3265
|
+
if (searchFromEnd) {
|
|
3266
|
+
for (let t = 0.95; t >= 0.5; t -= step) {
|
|
3267
|
+
const pt = bezierPointAt(p0, cp1, cp2, p3, t);
|
|
3268
|
+
if (!isInsideRect(pt, targetRect)) {
|
|
3269
|
+
return t;
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
} else {
|
|
3273
|
+
for (let t = 0.05; t <= 0.5; t += step) {
|
|
3274
|
+
const pt = bezierPointAt(p0, cp1, cp2, p3, t);
|
|
3275
|
+
if (!isInsideRect(pt, targetRect)) {
|
|
3276
|
+
return t;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
return void 0;
|
|
3281
|
+
}
|
|
3282
|
+
function pointAlongArc(route, t) {
|
|
3283
|
+
const [first, second] = route;
|
|
3284
|
+
if (t <= 0.5) {
|
|
3285
|
+
const localT2 = Math.max(0, Math.min(1, t * 2));
|
|
3286
|
+
return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
|
|
3287
|
+
}
|
|
3288
|
+
const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
|
|
3289
|
+
return bezierPointAt(second[0], second[1], second[2], second[3], localT);
|
|
3290
|
+
}
|
|
3291
|
+
function computeDiagramCenter(nodeBounds, canvasCenter) {
|
|
3292
|
+
if (nodeBounds.length === 0) {
|
|
3293
|
+
return canvasCenter ?? { x: 0, y: 0 };
|
|
3294
|
+
}
|
|
3295
|
+
let totalX = 0;
|
|
3296
|
+
let totalY = 0;
|
|
3297
|
+
for (const bounds of nodeBounds) {
|
|
3298
|
+
totalX += bounds.x + bounds.width / 2;
|
|
3299
|
+
totalY += bounds.y + bounds.height / 2;
|
|
2848
3300
|
}
|
|
2849
3301
|
return {
|
|
2850
|
-
x:
|
|
2851
|
-
y:
|
|
3302
|
+
x: totalX / nodeBounds.length,
|
|
3303
|
+
y: totalY / nodeBounds.length
|
|
2852
3304
|
};
|
|
2853
3305
|
}
|
|
2854
3306
|
function dashFromStyle(style) {
|
|
@@ -2932,51 +3384,148 @@ function polylineBounds(points) {
|
|
|
2932
3384
|
height: Math.max(1, maxY - minY)
|
|
2933
3385
|
};
|
|
2934
3386
|
}
|
|
2935
|
-
function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute) {
|
|
2936
|
-
const
|
|
2937
|
-
const
|
|
2938
|
-
const
|
|
2939
|
-
const
|
|
2940
|
-
const dash = dashFromStyle(
|
|
3387
|
+
function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
|
|
3388
|
+
const routing = conn.routing ?? "auto";
|
|
3389
|
+
const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
|
|
3390
|
+
const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
|
|
3391
|
+
const tension = conn.tension ?? 0.35;
|
|
3392
|
+
const dash = dashFromStyle(strokeStyle);
|
|
2941
3393
|
const style = {
|
|
2942
3394
|
color: conn.color ?? theme.borderMuted,
|
|
2943
|
-
width:
|
|
3395
|
+
width: strokeWidth,
|
|
2944
3396
|
headSize: conn.arrowSize ?? 10,
|
|
2945
3397
|
...dash ? { dash } : {}
|
|
2946
3398
|
};
|
|
2947
|
-
const
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
let
|
|
2952
|
-
let
|
|
3399
|
+
const labelT = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
|
|
3400
|
+
const diagramCenter = options?.diagramCenter ?? computeDiagramCenter([fromBounds, toBounds]);
|
|
3401
|
+
let linePoints;
|
|
3402
|
+
let startPoint;
|
|
3403
|
+
let endPoint;
|
|
3404
|
+
let startAngle;
|
|
3405
|
+
let endAngle;
|
|
3406
|
+
let labelPoint;
|
|
3407
|
+
ctx.save();
|
|
3408
|
+
ctx.globalAlpha = conn.opacity;
|
|
3409
|
+
const arrowPlacement = conn.arrowPlacement ?? "endpoint";
|
|
3410
|
+
if (routing === "curve") {
|
|
3411
|
+
const [p0, cp1, cp2, p3] = curveRoute(
|
|
3412
|
+
fromBounds,
|
|
3413
|
+
toBounds,
|
|
3414
|
+
diagramCenter,
|
|
3415
|
+
tension,
|
|
3416
|
+
conn.fromAnchor,
|
|
3417
|
+
conn.toAnchor
|
|
3418
|
+
);
|
|
3419
|
+
ctx.strokeStyle = style.color;
|
|
3420
|
+
ctx.lineWidth = style.width;
|
|
3421
|
+
ctx.setLineDash(style.dash ?? []);
|
|
3422
|
+
ctx.beginPath();
|
|
3423
|
+
ctx.moveTo(p0.x, p0.y);
|
|
3424
|
+
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, p3.x, p3.y);
|
|
3425
|
+
ctx.stroke();
|
|
3426
|
+
linePoints = [p0, cp1, cp2, p3];
|
|
3427
|
+
startPoint = p0;
|
|
3428
|
+
endPoint = p3;
|
|
3429
|
+
startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
|
|
3430
|
+
endAngle = Math.atan2(p3.y - cp2.y, p3.x - cp2.x);
|
|
3431
|
+
labelPoint = bezierPointAt(p0, cp1, cp2, p3, labelT);
|
|
3432
|
+
if (arrowPlacement === "boundary") {
|
|
3433
|
+
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
3434
|
+
const tEnd = findBoundaryIntersection(p0, cp1, cp2, p3, toBounds, true);
|
|
3435
|
+
if (tEnd !== void 0) {
|
|
3436
|
+
endPoint = bezierPointAt(p0, cp1, cp2, p3, tEnd);
|
|
3437
|
+
const tangent = bezierTangentAt(p0, cp1, cp2, p3, tEnd);
|
|
3438
|
+
endAngle = Math.atan2(tangent.y, tangent.x);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
3442
|
+
const tStart = findBoundaryIntersection(p0, cp1, cp2, p3, fromBounds, false);
|
|
3443
|
+
if (tStart !== void 0) {
|
|
3444
|
+
startPoint = bezierPointAt(p0, cp1, cp2, p3, tStart);
|
|
3445
|
+
const tangent = bezierTangentAt(p0, cp1, cp2, p3, tStart);
|
|
3446
|
+
startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
} else if (routing === "arc") {
|
|
3451
|
+
const [first, second] = arcRoute(
|
|
3452
|
+
fromBounds,
|
|
3453
|
+
toBounds,
|
|
3454
|
+
diagramCenter,
|
|
3455
|
+
tension,
|
|
3456
|
+
conn.fromAnchor,
|
|
3457
|
+
conn.toAnchor
|
|
3458
|
+
);
|
|
3459
|
+
const [p0, cp1, cp2, pMid] = first;
|
|
3460
|
+
const [, cp3, cp4, p3] = second;
|
|
3461
|
+
ctx.strokeStyle = style.color;
|
|
3462
|
+
ctx.lineWidth = style.width;
|
|
3463
|
+
ctx.setLineDash(style.dash ?? []);
|
|
3464
|
+
ctx.beginPath();
|
|
3465
|
+
ctx.moveTo(p0.x, p0.y);
|
|
3466
|
+
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
|
|
3467
|
+
ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
|
|
3468
|
+
ctx.stroke();
|
|
3469
|
+
linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
|
|
3470
|
+
startPoint = p0;
|
|
3471
|
+
endPoint = p3;
|
|
3472
|
+
startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
|
|
3473
|
+
endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
|
|
3474
|
+
labelPoint = pointAlongArc([first, second], labelT);
|
|
3475
|
+
if (arrowPlacement === "boundary") {
|
|
3476
|
+
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
3477
|
+
const [, s_cp3, s_cp4, s_p3] = second;
|
|
3478
|
+
const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
|
|
3479
|
+
if (tEnd !== void 0) {
|
|
3480
|
+
endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
|
|
3481
|
+
const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
|
|
3482
|
+
endAngle = Math.atan2(tangent.y, tangent.x);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
3486
|
+
const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
|
|
3487
|
+
if (tStart !== void 0) {
|
|
3488
|
+
startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
|
|
3489
|
+
const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
|
|
3490
|
+
startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
} else {
|
|
3495
|
+
const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
|
|
3496
|
+
const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
|
|
3497
|
+
linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor) : orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
|
|
3498
|
+
startPoint = linePoints[0];
|
|
3499
|
+
const startSegment = linePoints[1] ?? linePoints[0];
|
|
3500
|
+
const endStart = linePoints[linePoints.length - 2] ?? linePoints[0];
|
|
3501
|
+
endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
|
|
3502
|
+
startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
|
|
3503
|
+
endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
|
|
3504
|
+
if (useElkRoute) {
|
|
3505
|
+
drawCubicInterpolatedPath(ctx, linePoints, style);
|
|
3506
|
+
} else {
|
|
3507
|
+
drawOrthogonalPath(ctx, startPoint, endPoint, style);
|
|
3508
|
+
}
|
|
3509
|
+
labelPoint = pointAlongPolyline(linePoints, labelT);
|
|
3510
|
+
}
|
|
2953
3511
|
if (!Number.isFinite(startAngle)) {
|
|
2954
3512
|
startAngle = 0;
|
|
2955
3513
|
}
|
|
2956
3514
|
if (!Number.isFinite(endAngle)) {
|
|
2957
3515
|
endAngle = 0;
|
|
2958
3516
|
}
|
|
2959
|
-
const t = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
|
|
2960
|
-
const labelPoint = pointAlongPolyline(points, t);
|
|
2961
|
-
ctx.save();
|
|
2962
|
-
ctx.globalAlpha = conn.opacity;
|
|
2963
|
-
if (edgeRoute && edgeRoute.points.length >= 2) {
|
|
2964
|
-
drawCubicInterpolatedPath(ctx, points, style);
|
|
2965
|
-
} else {
|
|
2966
|
-
drawOrthogonalPath(ctx, points[0], points[points.length - 1], style);
|
|
2967
|
-
}
|
|
2968
3517
|
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
2969
|
-
drawArrowhead(ctx,
|
|
3518
|
+
drawArrowhead(ctx, startPoint, startAngle, style.headSize, style.color);
|
|
2970
3519
|
}
|
|
2971
3520
|
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
2972
|
-
drawArrowhead(ctx,
|
|
3521
|
+
drawArrowhead(ctx, endPoint, endAngle, style.headSize, style.color);
|
|
2973
3522
|
}
|
|
2974
3523
|
ctx.restore();
|
|
2975
3524
|
const elements = [
|
|
2976
3525
|
{
|
|
2977
3526
|
id: `connection-${conn.from}-${conn.to}`,
|
|
2978
3527
|
kind: "connection",
|
|
2979
|
-
bounds: polylineBounds(
|
|
3528
|
+
bounds: polylineBounds(linePoints),
|
|
2980
3529
|
foregroundColor: style.color
|
|
2981
3530
|
}
|
|
2982
3531
|
];
|
|
@@ -3602,6 +4151,36 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
3602
4151
|
});
|
|
3603
4152
|
break;
|
|
3604
4153
|
}
|
|
4154
|
+
case "grid": {
|
|
4155
|
+
const canvasWidth = ctx.canvas.width;
|
|
4156
|
+
const canvasHeight = ctx.canvas.height;
|
|
4157
|
+
withOpacity(ctx, command.opacity, () => {
|
|
4158
|
+
ctx.strokeStyle = command.color;
|
|
4159
|
+
ctx.lineWidth = command.width;
|
|
4160
|
+
const startX = command.offsetX % command.spacing;
|
|
4161
|
+
for (let x = startX; x <= canvasWidth; x += command.spacing) {
|
|
4162
|
+
ctx.beginPath();
|
|
4163
|
+
ctx.moveTo(x, 0);
|
|
4164
|
+
ctx.lineTo(x, canvasHeight);
|
|
4165
|
+
ctx.stroke();
|
|
4166
|
+
}
|
|
4167
|
+
const startY = command.offsetY % command.spacing;
|
|
4168
|
+
for (let y = startY; y <= canvasHeight; y += command.spacing) {
|
|
4169
|
+
ctx.beginPath();
|
|
4170
|
+
ctx.moveTo(0, y);
|
|
4171
|
+
ctx.lineTo(canvasWidth, y);
|
|
4172
|
+
ctx.stroke();
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
rendered.push({
|
|
4176
|
+
id,
|
|
4177
|
+
kind: "draw",
|
|
4178
|
+
bounds: { x: 0, y: 0, width: canvasWidth, height: canvasHeight },
|
|
4179
|
+
foregroundColor: command.color,
|
|
4180
|
+
allowOverlap: true
|
|
4181
|
+
});
|
|
4182
|
+
break;
|
|
4183
|
+
}
|
|
3605
4184
|
}
|
|
3606
4185
|
}
|
|
3607
4186
|
return rendered;
|
|
@@ -4190,6 +4769,10 @@ async function renderDesign(input, options = {}) {
|
|
|
4190
4769
|
break;
|
|
4191
4770
|
}
|
|
4192
4771
|
}
|
|
4772
|
+
const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
|
|
4773
|
+
spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
|
|
4774
|
+
{ x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
|
|
4775
|
+
);
|
|
4193
4776
|
for (const element of spec.elements) {
|
|
4194
4777
|
if (element.type !== "connection") {
|
|
4195
4778
|
continue;
|
|
@@ -4202,7 +4785,9 @@ async function renderDesign(input, options = {}) {
|
|
|
4202
4785
|
);
|
|
4203
4786
|
}
|
|
4204
4787
|
const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
|
|
4205
|
-
elements.push(
|
|
4788
|
+
elements.push(
|
|
4789
|
+
...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
|
|
4790
|
+
);
|
|
4206
4791
|
}
|
|
4207
4792
|
if (footerRect && spec.footer) {
|
|
4208
4793
|
const footerText = spec.footer.tagline ? `${spec.footer.text} \u2022 ${spec.footer.tagline}` : spec.footer.text;
|
|
@@ -4565,6 +5150,36 @@ var renderOutputSchema = z3.object({
|
|
|
4565
5150
|
)
|
|
4566
5151
|
})
|
|
4567
5152
|
});
|
|
5153
|
+
var compareOutputSchema = z3.object({
|
|
5154
|
+
targetPath: z3.string(),
|
|
5155
|
+
renderedPath: z3.string(),
|
|
5156
|
+
targetDimensions: z3.object({
|
|
5157
|
+
width: z3.number().int().positive(),
|
|
5158
|
+
height: z3.number().int().positive()
|
|
5159
|
+
}),
|
|
5160
|
+
renderedDimensions: z3.object({
|
|
5161
|
+
width: z3.number().int().positive(),
|
|
5162
|
+
height: z3.number().int().positive()
|
|
5163
|
+
}),
|
|
5164
|
+
normalizedDimensions: z3.object({
|
|
5165
|
+
width: z3.number().int().positive(),
|
|
5166
|
+
height: z3.number().int().positive()
|
|
5167
|
+
}),
|
|
5168
|
+
dimensionMismatch: z3.boolean(),
|
|
5169
|
+
grid: z3.number().int().positive(),
|
|
5170
|
+
threshold: z3.number(),
|
|
5171
|
+
closeThreshold: z3.number(),
|
|
5172
|
+
similarity: z3.number(),
|
|
5173
|
+
verdict: z3.enum(["match", "close", "mismatch"]),
|
|
5174
|
+
regions: z3.array(
|
|
5175
|
+
z3.object({
|
|
5176
|
+
label: z3.string(),
|
|
5177
|
+
row: z3.number().int().nonnegative(),
|
|
5178
|
+
column: z3.number().int().nonnegative(),
|
|
5179
|
+
similarity: z3.number()
|
|
5180
|
+
})
|
|
5181
|
+
)
|
|
5182
|
+
});
|
|
4568
5183
|
async function readJson(path) {
|
|
4569
5184
|
if (path === "-") {
|
|
4570
5185
|
const chunks = [];
|
|
@@ -4667,6 +5282,44 @@ cli.command("render", {
|
|
|
4667
5282
|
return c.ok(runReport);
|
|
4668
5283
|
}
|
|
4669
5284
|
});
|
|
5285
|
+
cli.command("compare", {
|
|
5286
|
+
description: "Compare a rendered design against a target image using structural similarity scoring.",
|
|
5287
|
+
options: z3.object({
|
|
5288
|
+
target: z3.string().describe("Path to target image (baseline)"),
|
|
5289
|
+
rendered: z3.string().describe("Path to rendered image to evaluate"),
|
|
5290
|
+
grid: z3.number().int().positive().default(3).describe("Grid size for per-region scoring"),
|
|
5291
|
+
threshold: z3.number().min(0).max(1).default(0.8).describe("Minimum similarity score required for a match verdict")
|
|
5292
|
+
}),
|
|
5293
|
+
output: compareOutputSchema,
|
|
5294
|
+
examples: [
|
|
5295
|
+
{
|
|
5296
|
+
options: {
|
|
5297
|
+
target: "./designs/target.png",
|
|
5298
|
+
rendered: "./output/design-v2-g0.4.0-sabc123.png",
|
|
5299
|
+
grid: 3,
|
|
5300
|
+
threshold: 0.8
|
|
5301
|
+
},
|
|
5302
|
+
description: "Compare two images and report overall + per-region similarity scores"
|
|
5303
|
+
}
|
|
5304
|
+
],
|
|
5305
|
+
async run(c) {
|
|
5306
|
+
try {
|
|
5307
|
+
return c.ok(
|
|
5308
|
+
await compareImages(c.options.target, c.options.rendered, {
|
|
5309
|
+
grid: c.options.grid,
|
|
5310
|
+
threshold: c.options.threshold
|
|
5311
|
+
})
|
|
5312
|
+
);
|
|
5313
|
+
} catch (error) {
|
|
5314
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5315
|
+
return c.error({
|
|
5316
|
+
code: "COMPARE_FAILED",
|
|
5317
|
+
message: `Unable to compare images: ${message}`,
|
|
5318
|
+
retryable: false
|
|
5319
|
+
});
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
});
|
|
4670
5323
|
var template = Cli.create("template", {
|
|
4671
5324
|
description: "Generate common design templates and run the full render \u2192 QA pipeline."
|
|
4672
5325
|
});
|
|
@@ -4908,7 +5561,8 @@ cli.command("qa", {
|
|
|
4908
5561
|
options: z3.object({
|
|
4909
5562
|
in: z3.string().describe("Path to rendered PNG"),
|
|
4910
5563
|
spec: z3.string().describe("Path to normalized DesignSpec JSON"),
|
|
4911
|
-
meta: z3.string().optional().describe("Optional sidecar metadata path (.meta.json)")
|
|
5564
|
+
meta: z3.string().optional().describe("Optional sidecar metadata path (.meta.json)"),
|
|
5565
|
+
reference: z3.string().optional().describe("Optional reference image path for visual comparison")
|
|
4912
5566
|
}),
|
|
4913
5567
|
output: z3.object({
|
|
4914
5568
|
pass: z3.boolean(),
|
|
@@ -4922,7 +5576,18 @@ cli.command("qa", {
|
|
|
4922
5576
|
message: z3.string(),
|
|
4923
5577
|
elementId: z3.string().optional()
|
|
4924
5578
|
})
|
|
4925
|
-
)
|
|
5579
|
+
),
|
|
5580
|
+
reference: z3.object({
|
|
5581
|
+
similarity: z3.number(),
|
|
5582
|
+
verdict: z3.enum(["match", "close", "mismatch"]),
|
|
5583
|
+
regions: z3.array(
|
|
5584
|
+
z3.object({
|
|
5585
|
+
label: z3.string(),
|
|
5586
|
+
similarity: z3.number(),
|
|
5587
|
+
description: z3.string().optional()
|
|
5588
|
+
})
|
|
5589
|
+
)
|
|
5590
|
+
}).optional()
|
|
4926
5591
|
}),
|
|
4927
5592
|
examples: [
|
|
4928
5593
|
{
|
|
@@ -4945,14 +5610,16 @@ cli.command("qa", {
|
|
|
4945
5610
|
const report = await runQa({
|
|
4946
5611
|
imagePath: c.options.in,
|
|
4947
5612
|
spec,
|
|
4948
|
-
...metadata ? { metadata } : {}
|
|
5613
|
+
...metadata ? { metadata } : {},
|
|
5614
|
+
...c.options.reference ? { referencePath: c.options.reference } : {}
|
|
4949
5615
|
});
|
|
4950
5616
|
const response = {
|
|
4951
5617
|
pass: report.pass,
|
|
4952
5618
|
checkedAt: report.checkedAt,
|
|
4953
5619
|
imagePath: report.imagePath,
|
|
4954
5620
|
issueCount: report.issues.length,
|
|
4955
|
-
issues: report.issues
|
|
5621
|
+
issues: report.issues,
|
|
5622
|
+
...report.reference ? { reference: report.reference } : {}
|
|
4956
5623
|
};
|
|
4957
5624
|
if (!report.pass) {
|
|
4958
5625
|
return c.error({
|