@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/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);
|
|
@@ -709,6 +861,15 @@ var drawGradientRectSchema = z2.object({
|
|
|
709
861
|
radius: z2.number().min(0).max(256).default(0),
|
|
710
862
|
opacity: z2.number().min(0).max(1).default(1)
|
|
711
863
|
}).strict();
|
|
864
|
+
var drawGridSchema = z2.object({
|
|
865
|
+
type: z2.literal("grid"),
|
|
866
|
+
spacing: z2.number().min(5).max(200).default(40),
|
|
867
|
+
color: colorHexSchema2.default("#1E2D4A"),
|
|
868
|
+
width: z2.number().min(0.1).max(4).default(0.5),
|
|
869
|
+
opacity: z2.number().min(0).max(1).default(0.2),
|
|
870
|
+
offsetX: z2.number().default(0),
|
|
871
|
+
offsetY: z2.number().default(0)
|
|
872
|
+
}).strict();
|
|
712
873
|
var drawCommandSchema = z2.discriminatedUnion("type", [
|
|
713
874
|
drawRectSchema,
|
|
714
875
|
drawCircleSchema,
|
|
@@ -717,7 +878,8 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
|
|
|
717
878
|
drawBezierSchema,
|
|
718
879
|
drawPathSchema,
|
|
719
880
|
drawBadgeSchema,
|
|
720
|
-
drawGradientRectSchema
|
|
881
|
+
drawGradientRectSchema,
|
|
882
|
+
drawGridSchema
|
|
721
883
|
]);
|
|
722
884
|
var defaultCanvas = {
|
|
723
885
|
width: 1200,
|
|
@@ -782,10 +944,26 @@ var cardElementSchema = z2.object({
|
|
|
782
944
|
tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
|
|
783
945
|
icon: z2.string().min(1).max(64).optional()
|
|
784
946
|
}).strict();
|
|
947
|
+
var flowNodeShadowSchema = z2.object({
|
|
948
|
+
color: colorHexSchema2.optional(),
|
|
949
|
+
blur: z2.number().min(0).max(64).default(8),
|
|
950
|
+
offsetX: z2.number().min(-32).max(32).default(0),
|
|
951
|
+
offsetY: z2.number().min(-32).max(32).default(0),
|
|
952
|
+
opacity: z2.number().min(0).max(1).default(0.3)
|
|
953
|
+
}).strict();
|
|
785
954
|
var flowNodeElementSchema = z2.object({
|
|
786
955
|
type: z2.literal("flow-node"),
|
|
787
956
|
id: z2.string().min(1).max(120),
|
|
788
|
-
shape: z2.enum([
|
|
957
|
+
shape: z2.enum([
|
|
958
|
+
"box",
|
|
959
|
+
"rounded-box",
|
|
960
|
+
"diamond",
|
|
961
|
+
"circle",
|
|
962
|
+
"pill",
|
|
963
|
+
"cylinder",
|
|
964
|
+
"parallelogram",
|
|
965
|
+
"hexagon"
|
|
966
|
+
]).default("rounded-box"),
|
|
789
967
|
label: z2.string().min(1).max(200),
|
|
790
968
|
sublabel: z2.string().min(1).max(300).optional(),
|
|
791
969
|
sublabelColor: colorHexSchema2.optional(),
|
|
@@ -805,20 +983,35 @@ var flowNodeElementSchema = z2.object({
|
|
|
805
983
|
badgeText: z2.string().min(1).max(32).optional(),
|
|
806
984
|
badgeColor: colorHexSchema2.optional(),
|
|
807
985
|
badgeBackground: colorHexSchema2.optional(),
|
|
808
|
-
badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
|
|
986
|
+
badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
|
|
987
|
+
shadow: flowNodeShadowSchema.optional()
|
|
809
988
|
}).strict();
|
|
989
|
+
var anchorHintSchema = z2.union([
|
|
990
|
+
z2.enum(["top", "bottom", "left", "right", "center"]),
|
|
991
|
+
z2.object({
|
|
992
|
+
x: z2.number().min(-1).max(1),
|
|
993
|
+
y: z2.number().min(-1).max(1)
|
|
994
|
+
}).strict()
|
|
995
|
+
]);
|
|
810
996
|
var connectionElementSchema = z2.object({
|
|
811
997
|
type: z2.literal("connection"),
|
|
812
998
|
from: z2.string().min(1).max(120),
|
|
813
999
|
to: z2.string().min(1).max(120),
|
|
814
1000
|
style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
1001
|
+
strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
815
1002
|
arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
|
|
816
1003
|
label: z2.string().min(1).max(200).optional(),
|
|
817
1004
|
labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
|
|
818
1005
|
color: colorHexSchema2.optional(),
|
|
819
|
-
width: z2.number().min(0.5).max(
|
|
1006
|
+
width: z2.number().min(0.5).max(10).optional(),
|
|
1007
|
+
strokeWidth: z2.number().min(0.5).max(10).default(2),
|
|
820
1008
|
arrowSize: z2.number().min(4).max(32).optional(),
|
|
821
|
-
|
|
1009
|
+
arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
|
|
1010
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
1011
|
+
routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
|
|
1012
|
+
tension: z2.number().min(0.1).max(0.8).default(0.35),
|
|
1013
|
+
fromAnchor: anchorHintSchema.optional(),
|
|
1014
|
+
toAnchor: anchorHintSchema.optional()
|
|
822
1015
|
}).strict();
|
|
823
1016
|
var codeBlockStyleSchema = z2.object({
|
|
824
1017
|
paddingVertical: z2.number().min(0).max(128).default(56),
|
|
@@ -887,6 +1080,10 @@ var elementSchema = z2.discriminatedUnion("type", [
|
|
|
887
1080
|
shapeElementSchema,
|
|
888
1081
|
imageElementSchema
|
|
889
1082
|
]);
|
|
1083
|
+
var diagramCenterSchema = z2.object({
|
|
1084
|
+
x: z2.number(),
|
|
1085
|
+
y: z2.number()
|
|
1086
|
+
}).strict();
|
|
890
1087
|
var autoLayoutConfigSchema = z2.object({
|
|
891
1088
|
mode: z2.literal("auto"),
|
|
892
1089
|
algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
|
|
@@ -902,7 +1099,9 @@ var autoLayoutConfigSchema = z2.object({
|
|
|
902
1099
|
/** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
|
|
903
1100
|
radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
|
|
904
1101
|
/** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
|
|
905
|
-
radialSortBy: z2.enum(["id", "connections"]).optional()
|
|
1102
|
+
radialSortBy: z2.enum(["id", "connections"]).optional(),
|
|
1103
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1104
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
906
1105
|
}).strict();
|
|
907
1106
|
var gridLayoutConfigSchema = z2.object({
|
|
908
1107
|
mode: z2.literal("grid"),
|
|
@@ -910,13 +1109,17 @@ var gridLayoutConfigSchema = z2.object({
|
|
|
910
1109
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
911
1110
|
cardMinHeight: z2.number().int().min(32).max(4096).optional(),
|
|
912
1111
|
cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
|
|
913
|
-
equalHeight: z2.boolean().default(false)
|
|
1112
|
+
equalHeight: z2.boolean().default(false),
|
|
1113
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1114
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
914
1115
|
}).strict();
|
|
915
1116
|
var stackLayoutConfigSchema = z2.object({
|
|
916
1117
|
mode: z2.literal("stack"),
|
|
917
1118
|
direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
|
|
918
1119
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
919
|
-
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
|
|
1120
|
+
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
|
|
1121
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1122
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
920
1123
|
}).strict();
|
|
921
1124
|
var manualPositionSchema = z2.object({
|
|
922
1125
|
x: z2.number().int(),
|
|
@@ -926,7 +1129,9 @@ var manualPositionSchema = z2.object({
|
|
|
926
1129
|
}).strict();
|
|
927
1130
|
var manualLayoutConfigSchema = z2.object({
|
|
928
1131
|
mode: z2.literal("manual"),
|
|
929
|
-
positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
|
|
1132
|
+
positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
|
|
1133
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1134
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
930
1135
|
}).strict();
|
|
931
1136
|
var layoutConfigSchema = z2.discriminatedUnion("mode", [
|
|
932
1137
|
autoLayoutConfigSchema,
|
|
@@ -978,6 +1183,31 @@ var canvasSchema = z2.object({
|
|
|
978
1183
|
padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
|
|
979
1184
|
}).strict();
|
|
980
1185
|
var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
|
|
1186
|
+
var diagramPositionSchema = z2.object({
|
|
1187
|
+
x: z2.number(),
|
|
1188
|
+
y: z2.number(),
|
|
1189
|
+
width: z2.number().positive(),
|
|
1190
|
+
height: z2.number().positive()
|
|
1191
|
+
}).strict();
|
|
1192
|
+
var diagramElementSchema = z2.discriminatedUnion("type", [
|
|
1193
|
+
flowNodeElementSchema,
|
|
1194
|
+
connectionElementSchema
|
|
1195
|
+
]);
|
|
1196
|
+
var diagramLayoutSchema = z2.object({
|
|
1197
|
+
mode: z2.enum(["manual", "auto"]).default("manual"),
|
|
1198
|
+
positions: z2.record(z2.string(), diagramPositionSchema).optional(),
|
|
1199
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
1200
|
+
}).strict();
|
|
1201
|
+
var diagramSpecSchema = z2.object({
|
|
1202
|
+
version: z2.literal(1),
|
|
1203
|
+
canvas: z2.object({
|
|
1204
|
+
width: z2.number().int().min(320).max(4096).default(1200),
|
|
1205
|
+
height: z2.number().int().min(180).max(4096).default(675)
|
|
1206
|
+
}).default({ width: 1200, height: 675 }),
|
|
1207
|
+
theme: themeSchema.optional(),
|
|
1208
|
+
elements: z2.array(diagramElementSchema).min(1),
|
|
1209
|
+
layout: diagramLayoutSchema.default({ mode: "manual" })
|
|
1210
|
+
}).strict();
|
|
981
1211
|
var designSpecSchema = z2.object({
|
|
982
1212
|
version: z2.literal(2).default(2),
|
|
983
1213
|
canvas: canvasSchema.default(defaultCanvas),
|
|
@@ -1002,6 +1232,9 @@ function deriveSafeFrame(spec) {
|
|
|
1002
1232
|
height: spec.canvas.height - spec.canvas.padding * 2
|
|
1003
1233
|
};
|
|
1004
1234
|
}
|
|
1235
|
+
function parseDiagramSpec(input) {
|
|
1236
|
+
return diagramSpecSchema.parse(input);
|
|
1237
|
+
}
|
|
1005
1238
|
function parseDesignSpec(input) {
|
|
1006
1239
|
return designSpecSchema.parse(input);
|
|
1007
1240
|
}
|
|
@@ -1051,7 +1284,7 @@ async function runQa(options) {
|
|
|
1051
1284
|
const imagePath = resolve(options.imagePath);
|
|
1052
1285
|
const expectedSafeFrame = deriveSafeFrame(spec);
|
|
1053
1286
|
const expectedCanvas = canvasRect(spec);
|
|
1054
|
-
const imageMetadata = await
|
|
1287
|
+
const imageMetadata = await sharp2(imagePath).metadata();
|
|
1055
1288
|
const issues = [];
|
|
1056
1289
|
const expectedScale = options.metadata?.canvas.scale ?? resolveRenderScale(spec);
|
|
1057
1290
|
const expectedWidth = spec.canvas.width * expectedScale;
|
|
@@ -1202,6 +1435,31 @@ async function runQa(options) {
|
|
|
1202
1435
|
});
|
|
1203
1436
|
}
|
|
1204
1437
|
}
|
|
1438
|
+
let referenceResult;
|
|
1439
|
+
if (options.referencePath) {
|
|
1440
|
+
const { compareImages: compareImages2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
|
|
1441
|
+
const comparison = await compareImages2(options.referencePath, imagePath);
|
|
1442
|
+
referenceResult = {
|
|
1443
|
+
similarity: comparison.similarity,
|
|
1444
|
+
verdict: comparison.verdict,
|
|
1445
|
+
regions: comparison.regions.map((region) => ({
|
|
1446
|
+
label: region.label,
|
|
1447
|
+
similarity: region.similarity
|
|
1448
|
+
}))
|
|
1449
|
+
};
|
|
1450
|
+
if (comparison.verdict === "mismatch") {
|
|
1451
|
+
const severity = comparison.similarity < 0.5 ? "error" : "warning";
|
|
1452
|
+
issues.push({
|
|
1453
|
+
code: "REFERENCE_MISMATCH",
|
|
1454
|
+
severity,
|
|
1455
|
+
message: `Reference image comparison ${severity === "error" ? "failed" : "warned"}: similarity ${comparison.similarity.toFixed(4)} with verdict "${comparison.verdict}".`,
|
|
1456
|
+
details: {
|
|
1457
|
+
similarity: comparison.similarity,
|
|
1458
|
+
verdict: comparison.verdict
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1205
1463
|
const footerSpacingPx = options.metadata?.layout.elements ? (() => {
|
|
1206
1464
|
const footer = options.metadata.layout.elements.find((element) => element.id === "footer");
|
|
1207
1465
|
if (!footer) {
|
|
@@ -1234,7 +1492,8 @@ async function runQa(options) {
|
|
|
1234
1492
|
...imageMetadata.height !== void 0 ? { height: imageMetadata.height } : {},
|
|
1235
1493
|
...footerSpacingPx !== void 0 ? { footerSpacingPx } : {}
|
|
1236
1494
|
},
|
|
1237
|
-
issues
|
|
1495
|
+
issues,
|
|
1496
|
+
...referenceResult ? { reference: referenceResult } : {}
|
|
1238
1497
|
};
|
|
1239
1498
|
}
|
|
1240
1499
|
|
|
@@ -1301,9 +1560,9 @@ function drawRoundedRect(ctx, rect, radius, fill, stroke) {
|
|
|
1301
1560
|
roundRectPath(ctx, rect, radius);
|
|
1302
1561
|
fillAndStroke(ctx, fill, stroke);
|
|
1303
1562
|
}
|
|
1304
|
-
function drawCircle(ctx,
|
|
1563
|
+
function drawCircle(ctx, center, radius, fill, stroke) {
|
|
1305
1564
|
ctx.beginPath();
|
|
1306
|
-
ctx.arc(
|
|
1565
|
+
ctx.arc(center.x, center.y, Math.max(0, radius), 0, Math.PI * 2);
|
|
1307
1566
|
ctx.closePath();
|
|
1308
1567
|
fillAndStroke(ctx, fill, stroke);
|
|
1309
1568
|
}
|
|
@@ -1547,15 +1806,34 @@ function renderFlowNode(ctx, node, bounds, theme) {
|
|
|
1547
1806
|
const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
|
|
1548
1807
|
ctx.save();
|
|
1549
1808
|
ctx.lineWidth = borderWidth;
|
|
1809
|
+
if (node.shadow) {
|
|
1810
|
+
const shadowColor = node.shadow.color ?? borderColor ?? theme.accent;
|
|
1811
|
+
ctx.shadowColor = withAlpha(shadowColor, node.shadow.opacity);
|
|
1812
|
+
ctx.shadowBlur = node.shadow.blur;
|
|
1813
|
+
ctx.shadowOffsetX = node.shadow.offsetX;
|
|
1814
|
+
ctx.shadowOffsetY = node.shadow.offsetY;
|
|
1815
|
+
}
|
|
1550
1816
|
if (fillOpacity < 1) {
|
|
1551
1817
|
ctx.globalAlpha = node.opacity * fillOpacity;
|
|
1552
1818
|
drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
|
|
1819
|
+
if (node.shadow) {
|
|
1820
|
+
ctx.shadowColor = "transparent";
|
|
1821
|
+
ctx.shadowBlur = 0;
|
|
1822
|
+
ctx.shadowOffsetX = 0;
|
|
1823
|
+
ctx.shadowOffsetY = 0;
|
|
1824
|
+
}
|
|
1553
1825
|
ctx.globalAlpha = node.opacity;
|
|
1554
1826
|
drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
|
|
1555
1827
|
} else {
|
|
1556
1828
|
ctx.globalAlpha = node.opacity;
|
|
1557
1829
|
drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
|
|
1558
1830
|
}
|
|
1831
|
+
if (node.shadow) {
|
|
1832
|
+
ctx.shadowColor = "transparent";
|
|
1833
|
+
ctx.shadowBlur = 0;
|
|
1834
|
+
ctx.shadowOffsetX = 0;
|
|
1835
|
+
ctx.shadowOffsetY = 0;
|
|
1836
|
+
}
|
|
1559
1837
|
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
1560
1838
|
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
1561
1839
|
const monoFont = resolveFont(theme.fonts.mono, "mono");
|
|
@@ -2227,7 +2505,7 @@ function parseHexColor2(color) {
|
|
|
2227
2505
|
a: normalized.length === 8 ? parseChannel2(6) / 255 : 1
|
|
2228
2506
|
};
|
|
2229
2507
|
}
|
|
2230
|
-
function
|
|
2508
|
+
function withAlpha2(color, alpha) {
|
|
2231
2509
|
const parsed = parseHexColor2(color);
|
|
2232
2510
|
const effectiveAlpha = clamp01(parsed.a * alpha);
|
|
2233
2511
|
return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${effectiveAlpha})`;
|
|
@@ -2284,9 +2562,9 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
|
|
|
2284
2562
|
centerY,
|
|
2285
2563
|
outerRadius
|
|
2286
2564
|
);
|
|
2287
|
-
vignette.addColorStop(0,
|
|
2288
|
-
vignette.addColorStop(0.6,
|
|
2289
|
-
vignette.addColorStop(1,
|
|
2565
|
+
vignette.addColorStop(0, withAlpha2(color, 0));
|
|
2566
|
+
vignette.addColorStop(0.6, withAlpha2(color, 0));
|
|
2567
|
+
vignette.addColorStop(1, withAlpha2(color, clamp01(intensity)));
|
|
2290
2568
|
ctx.save();
|
|
2291
2569
|
ctx.fillStyle = vignette;
|
|
2292
2570
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -2417,12 +2695,12 @@ var MACOS_DOTS = [
|
|
|
2417
2695
|
{ fill: "#27C93F", stroke: "#1AAB29" }
|
|
2418
2696
|
];
|
|
2419
2697
|
function drawMacosDots(ctx, x, y) {
|
|
2420
|
-
for (const [index,
|
|
2698
|
+
for (const [index, dot2] of MACOS_DOTS.entries()) {
|
|
2421
2699
|
ctx.beginPath();
|
|
2422
2700
|
ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
|
|
2423
2701
|
ctx.closePath();
|
|
2424
|
-
ctx.fillStyle =
|
|
2425
|
-
ctx.strokeStyle =
|
|
2702
|
+
ctx.fillStyle = dot2.fill;
|
|
2703
|
+
ctx.strokeStyle = dot2.stroke;
|
|
2426
2704
|
ctx.lineWidth = DOT_STROKE_WIDTH;
|
|
2427
2705
|
ctx.fill();
|
|
2428
2706
|
ctx.stroke();
|
|
@@ -2843,25 +3121,203 @@ function drawOrthogonalPath(ctx, from, to, style) {
|
|
|
2843
3121
|
}
|
|
2844
3122
|
|
|
2845
3123
|
// src/renderers/connection.ts
|
|
2846
|
-
|
|
3124
|
+
var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
|
|
3125
|
+
function rectCenter(rect) {
|
|
2847
3126
|
return {
|
|
2848
3127
|
x: rect.x + rect.width / 2,
|
|
2849
3128
|
y: rect.y + rect.height / 2
|
|
2850
3129
|
};
|
|
2851
3130
|
}
|
|
2852
|
-
function edgeAnchor(
|
|
2853
|
-
const c =
|
|
3131
|
+
function edgeAnchor(bounds, target) {
|
|
3132
|
+
const c = rectCenter(bounds);
|
|
2854
3133
|
const dx = target.x - c.x;
|
|
2855
3134
|
const dy = target.y - c.y;
|
|
2856
|
-
if (
|
|
2857
|
-
return {
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
3135
|
+
if (dx === 0 && dy === 0) {
|
|
3136
|
+
return { x: c.x, y: c.y - bounds.height / 2 };
|
|
3137
|
+
}
|
|
3138
|
+
const hw = bounds.width / 2;
|
|
3139
|
+
const hh = bounds.height / 2;
|
|
3140
|
+
const absDx = Math.abs(dx);
|
|
3141
|
+
const absDy = Math.abs(dy);
|
|
3142
|
+
const t = absDx * hh > absDy * hw ? hw / absDx : hh / absDy;
|
|
3143
|
+
return { x: c.x + dx * t, y: c.y + dy * t };
|
|
3144
|
+
}
|
|
3145
|
+
function resolveAnchor(bounds, anchor, fallbackTarget) {
|
|
3146
|
+
if (!anchor) return edgeAnchor(bounds, fallbackTarget);
|
|
3147
|
+
if (typeof anchor === "string") {
|
|
3148
|
+
const c2 = rectCenter(bounds);
|
|
3149
|
+
switch (anchor) {
|
|
3150
|
+
case "top":
|
|
3151
|
+
return { x: c2.x, y: bounds.y };
|
|
3152
|
+
case "bottom":
|
|
3153
|
+
return { x: c2.x, y: bounds.y + bounds.height };
|
|
3154
|
+
case "left":
|
|
3155
|
+
return { x: bounds.x, y: c2.y };
|
|
3156
|
+
case "right":
|
|
3157
|
+
return { x: bounds.x + bounds.width, y: c2.y };
|
|
3158
|
+
case "center":
|
|
3159
|
+
return c2;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
const c = rectCenter(bounds);
|
|
3163
|
+
return {
|
|
3164
|
+
x: c.x + anchor.x * (bounds.width / 2),
|
|
3165
|
+
y: c.y + anchor.y * (bounds.height / 2)
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
function anchorNormal(anchor, point, diagramCenter) {
|
|
3169
|
+
if (typeof anchor === "string") {
|
|
3170
|
+
switch (anchor) {
|
|
3171
|
+
case "top":
|
|
3172
|
+
return { x: 0, y: -1 };
|
|
3173
|
+
case "bottom":
|
|
3174
|
+
return { x: 0, y: 1 };
|
|
3175
|
+
case "left":
|
|
3176
|
+
return { x: -1, y: 0 };
|
|
3177
|
+
case "right":
|
|
3178
|
+
return { x: 1, y: 0 };
|
|
3179
|
+
case "center":
|
|
3180
|
+
return outwardNormal(point, diagramCenter);
|
|
3181
|
+
}
|
|
2861
3182
|
}
|
|
3183
|
+
return outwardNormal(point, diagramCenter);
|
|
3184
|
+
}
|
|
3185
|
+
function outwardNormal(point, diagramCenter) {
|
|
3186
|
+
const dx = point.x - diagramCenter.x;
|
|
3187
|
+
const dy = point.y - diagramCenter.y;
|
|
3188
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
3189
|
+
return { x: dx / len, y: dy / len };
|
|
3190
|
+
}
|
|
3191
|
+
function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
|
|
3192
|
+
const fromCenter = rectCenter(fromBounds);
|
|
3193
|
+
const toCenter = rectCenter(toBounds);
|
|
3194
|
+
const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
|
|
3195
|
+
const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
|
|
3196
|
+
const dist = Math.hypot(p3.x - p0.x, p3.y - p0.y);
|
|
3197
|
+
const offset = dist * tension;
|
|
3198
|
+
const n0 = anchorNormal(fromAnchor, p0, diagramCenter);
|
|
3199
|
+
const n3 = anchorNormal(toAnchor, p3, diagramCenter);
|
|
3200
|
+
const cp1 = { x: p0.x + n0.x * offset, y: p0.y + n0.y * offset };
|
|
3201
|
+
const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
|
|
3202
|
+
return [p0, cp1, cp2, p3];
|
|
3203
|
+
}
|
|
3204
|
+
function dot(a, b) {
|
|
3205
|
+
return a.x * b.x + a.y * b.y;
|
|
3206
|
+
}
|
|
3207
|
+
function localToWorld(origin, axisX, axisY, local) {
|
|
2862
3208
|
return {
|
|
2863
|
-
x:
|
|
2864
|
-
y:
|
|
3209
|
+
x: origin.x + axisX.x * local.x + axisY.x * local.y,
|
|
3210
|
+
y: origin.y + axisX.y * local.x + axisY.y * local.y
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
|
|
3214
|
+
const fromCenter = rectCenter(fromBounds);
|
|
3215
|
+
const toCenter = rectCenter(toBounds);
|
|
3216
|
+
const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
|
|
3217
|
+
const end = resolveAnchor(toBounds, toAnchor, fromCenter);
|
|
3218
|
+
const chord = { x: end.x - start.x, y: end.y - start.y };
|
|
3219
|
+
const chordLength = Math.hypot(chord.x, chord.y);
|
|
3220
|
+
if (chordLength < 1e-6) {
|
|
3221
|
+
const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
3222
|
+
return [
|
|
3223
|
+
[start, start, mid, mid],
|
|
3224
|
+
[mid, mid, end, end]
|
|
3225
|
+
];
|
|
3226
|
+
}
|
|
3227
|
+
const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
|
|
3228
|
+
let axisY = { x: -axisX.y, y: axisX.x };
|
|
3229
|
+
const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
3230
|
+
const outwardHint = outwardNormal(midpoint, diagramCenter);
|
|
3231
|
+
if (dot(axisY, outwardHint) < 0) {
|
|
3232
|
+
axisY = { x: -axisY.x, y: -axisY.y };
|
|
3233
|
+
}
|
|
3234
|
+
const semiMajor = chordLength / 2;
|
|
3235
|
+
const semiMinor = Math.max(12, chordLength * tension * 0.75);
|
|
3236
|
+
const p0Local = { x: -semiMajor, y: 0 };
|
|
3237
|
+
const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
|
|
3238
|
+
const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
|
|
3239
|
+
const pMidLocal = { x: 0, y: semiMinor };
|
|
3240
|
+
const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
|
|
3241
|
+
const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
|
|
3242
|
+
const p3Local = { x: semiMajor, y: 0 };
|
|
3243
|
+
const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
|
|
3244
|
+
const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
|
|
3245
|
+
const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
|
|
3246
|
+
const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
|
|
3247
|
+
const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
|
|
3248
|
+
const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
|
|
3249
|
+
const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
|
|
3250
|
+
return [
|
|
3251
|
+
[p0, cp1, cp2, pMid],
|
|
3252
|
+
[pMid, cp3, cp4, p3]
|
|
3253
|
+
];
|
|
3254
|
+
}
|
|
3255
|
+
function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
|
|
3256
|
+
const fromC = rectCenter(fromBounds);
|
|
3257
|
+
const toC = rectCenter(toBounds);
|
|
3258
|
+
const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
|
|
3259
|
+
const p3 = resolveAnchor(toBounds, toAnchor, fromC);
|
|
3260
|
+
const midX = (p0.x + p3.x) / 2;
|
|
3261
|
+
return [p0, { x: midX, y: p0.y }, { x: midX, y: p3.y }, p3];
|
|
3262
|
+
}
|
|
3263
|
+
function bezierPointAt(p0, cp1, cp2, p3, t) {
|
|
3264
|
+
const mt = 1 - t;
|
|
3265
|
+
return {
|
|
3266
|
+
x: mt * mt * mt * p0.x + 3 * mt * mt * t * cp1.x + 3 * mt * t * t * cp2.x + t * t * t * p3.x,
|
|
3267
|
+
y: mt * mt * mt * p0.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * p3.y
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
function bezierTangentAt(p0, cp1, cp2, p3, t) {
|
|
3271
|
+
const mt = 1 - t;
|
|
3272
|
+
return {
|
|
3273
|
+
x: 3 * mt * mt * (cp1.x - p0.x) + 6 * mt * t * (cp2.x - cp1.x) + 3 * t * t * (p3.x - cp2.x),
|
|
3274
|
+
y: 3 * mt * mt * (cp1.y - p0.y) + 6 * mt * t * (cp2.y - cp1.y) + 3 * t * t * (p3.y - cp2.y)
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
function isInsideRect(point, rect) {
|
|
3278
|
+
return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
|
|
3279
|
+
}
|
|
3280
|
+
function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
|
|
3281
|
+
const step = 5e-3;
|
|
3282
|
+
if (searchFromEnd) {
|
|
3283
|
+
for (let t = 0.95; t >= 0.5; t -= step) {
|
|
3284
|
+
const pt = bezierPointAt(p0, cp1, cp2, p3, t);
|
|
3285
|
+
if (!isInsideRect(pt, targetRect)) {
|
|
3286
|
+
return t;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
} else {
|
|
3290
|
+
for (let t = 0.05; t <= 0.5; t += step) {
|
|
3291
|
+
const pt = bezierPointAt(p0, cp1, cp2, p3, t);
|
|
3292
|
+
if (!isInsideRect(pt, targetRect)) {
|
|
3293
|
+
return t;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
return void 0;
|
|
3298
|
+
}
|
|
3299
|
+
function pointAlongArc(route, t) {
|
|
3300
|
+
const [first, second] = route;
|
|
3301
|
+
if (t <= 0.5) {
|
|
3302
|
+
const localT2 = Math.max(0, Math.min(1, t * 2));
|
|
3303
|
+
return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
|
|
3304
|
+
}
|
|
3305
|
+
const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
|
|
3306
|
+
return bezierPointAt(second[0], second[1], second[2], second[3], localT);
|
|
3307
|
+
}
|
|
3308
|
+
function computeDiagramCenter(nodeBounds, canvasCenter) {
|
|
3309
|
+
if (nodeBounds.length === 0) {
|
|
3310
|
+
return canvasCenter ?? { x: 0, y: 0 };
|
|
3311
|
+
}
|
|
3312
|
+
let totalX = 0;
|
|
3313
|
+
let totalY = 0;
|
|
3314
|
+
for (const bounds of nodeBounds) {
|
|
3315
|
+
totalX += bounds.x + bounds.width / 2;
|
|
3316
|
+
totalY += bounds.y + bounds.height / 2;
|
|
3317
|
+
}
|
|
3318
|
+
return {
|
|
3319
|
+
x: totalX / nodeBounds.length,
|
|
3320
|
+
y: totalY / nodeBounds.length
|
|
2865
3321
|
};
|
|
2866
3322
|
}
|
|
2867
3323
|
function dashFromStyle(style) {
|
|
@@ -2945,51 +3401,148 @@ function polylineBounds(points) {
|
|
|
2945
3401
|
height: Math.max(1, maxY - minY)
|
|
2946
3402
|
};
|
|
2947
3403
|
}
|
|
2948
|
-
function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute) {
|
|
2949
|
-
const
|
|
2950
|
-
const
|
|
2951
|
-
const
|
|
2952
|
-
const
|
|
2953
|
-
const dash = dashFromStyle(
|
|
3404
|
+
function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
|
|
3405
|
+
const routing = conn.routing ?? "auto";
|
|
3406
|
+
const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
|
|
3407
|
+
const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
|
|
3408
|
+
const tension = conn.tension ?? 0.35;
|
|
3409
|
+
const dash = dashFromStyle(strokeStyle);
|
|
2954
3410
|
const style = {
|
|
2955
3411
|
color: conn.color ?? theme.borderMuted,
|
|
2956
|
-
width:
|
|
3412
|
+
width: strokeWidth,
|
|
2957
3413
|
headSize: conn.arrowSize ?? 10,
|
|
2958
3414
|
...dash ? { dash } : {}
|
|
2959
3415
|
};
|
|
2960
|
-
const
|
|
2961
|
-
const
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
let
|
|
2965
|
-
let
|
|
3416
|
+
const labelT = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
|
|
3417
|
+
const diagramCenter = options?.diagramCenter ?? computeDiagramCenter([fromBounds, toBounds]);
|
|
3418
|
+
let linePoints;
|
|
3419
|
+
let startPoint;
|
|
3420
|
+
let endPoint;
|
|
3421
|
+
let startAngle;
|
|
3422
|
+
let endAngle;
|
|
3423
|
+
let labelPoint;
|
|
3424
|
+
ctx.save();
|
|
3425
|
+
ctx.globalAlpha = conn.opacity;
|
|
3426
|
+
const arrowPlacement = conn.arrowPlacement ?? "endpoint";
|
|
3427
|
+
if (routing === "curve") {
|
|
3428
|
+
const [p0, cp1, cp2, p3] = curveRoute(
|
|
3429
|
+
fromBounds,
|
|
3430
|
+
toBounds,
|
|
3431
|
+
diagramCenter,
|
|
3432
|
+
tension,
|
|
3433
|
+
conn.fromAnchor,
|
|
3434
|
+
conn.toAnchor
|
|
3435
|
+
);
|
|
3436
|
+
ctx.strokeStyle = style.color;
|
|
3437
|
+
ctx.lineWidth = style.width;
|
|
3438
|
+
ctx.setLineDash(style.dash ?? []);
|
|
3439
|
+
ctx.beginPath();
|
|
3440
|
+
ctx.moveTo(p0.x, p0.y);
|
|
3441
|
+
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, p3.x, p3.y);
|
|
3442
|
+
ctx.stroke();
|
|
3443
|
+
linePoints = [p0, cp1, cp2, p3];
|
|
3444
|
+
startPoint = p0;
|
|
3445
|
+
endPoint = p3;
|
|
3446
|
+
startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
|
|
3447
|
+
endAngle = Math.atan2(p3.y - cp2.y, p3.x - cp2.x);
|
|
3448
|
+
labelPoint = bezierPointAt(p0, cp1, cp2, p3, labelT);
|
|
3449
|
+
if (arrowPlacement === "boundary") {
|
|
3450
|
+
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
3451
|
+
const tEnd = findBoundaryIntersection(p0, cp1, cp2, p3, toBounds, true);
|
|
3452
|
+
if (tEnd !== void 0) {
|
|
3453
|
+
endPoint = bezierPointAt(p0, cp1, cp2, p3, tEnd);
|
|
3454
|
+
const tangent = bezierTangentAt(p0, cp1, cp2, p3, tEnd);
|
|
3455
|
+
endAngle = Math.atan2(tangent.y, tangent.x);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
3459
|
+
const tStart = findBoundaryIntersection(p0, cp1, cp2, p3, fromBounds, false);
|
|
3460
|
+
if (tStart !== void 0) {
|
|
3461
|
+
startPoint = bezierPointAt(p0, cp1, cp2, p3, tStart);
|
|
3462
|
+
const tangent = bezierTangentAt(p0, cp1, cp2, p3, tStart);
|
|
3463
|
+
startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
} else if (routing === "arc") {
|
|
3468
|
+
const [first, second] = arcRoute(
|
|
3469
|
+
fromBounds,
|
|
3470
|
+
toBounds,
|
|
3471
|
+
diagramCenter,
|
|
3472
|
+
tension,
|
|
3473
|
+
conn.fromAnchor,
|
|
3474
|
+
conn.toAnchor
|
|
3475
|
+
);
|
|
3476
|
+
const [p0, cp1, cp2, pMid] = first;
|
|
3477
|
+
const [, cp3, cp4, p3] = second;
|
|
3478
|
+
ctx.strokeStyle = style.color;
|
|
3479
|
+
ctx.lineWidth = style.width;
|
|
3480
|
+
ctx.setLineDash(style.dash ?? []);
|
|
3481
|
+
ctx.beginPath();
|
|
3482
|
+
ctx.moveTo(p0.x, p0.y);
|
|
3483
|
+
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
|
|
3484
|
+
ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
|
|
3485
|
+
ctx.stroke();
|
|
3486
|
+
linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
|
|
3487
|
+
startPoint = p0;
|
|
3488
|
+
endPoint = p3;
|
|
3489
|
+
startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
|
|
3490
|
+
endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
|
|
3491
|
+
labelPoint = pointAlongArc([first, second], labelT);
|
|
3492
|
+
if (arrowPlacement === "boundary") {
|
|
3493
|
+
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
3494
|
+
const [, s_cp3, s_cp4, s_p3] = second;
|
|
3495
|
+
const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
|
|
3496
|
+
if (tEnd !== void 0) {
|
|
3497
|
+
endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
|
|
3498
|
+
const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
|
|
3499
|
+
endAngle = Math.atan2(tangent.y, tangent.x);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
3503
|
+
const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
|
|
3504
|
+
if (tStart !== void 0) {
|
|
3505
|
+
startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
|
|
3506
|
+
const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
|
|
3507
|
+
startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
} else {
|
|
3512
|
+
const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
|
|
3513
|
+
const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
|
|
3514
|
+
linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor) : orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
|
|
3515
|
+
startPoint = linePoints[0];
|
|
3516
|
+
const startSegment = linePoints[1] ?? linePoints[0];
|
|
3517
|
+
const endStart = linePoints[linePoints.length - 2] ?? linePoints[0];
|
|
3518
|
+
endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
|
|
3519
|
+
startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
|
|
3520
|
+
endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
|
|
3521
|
+
if (useElkRoute) {
|
|
3522
|
+
drawCubicInterpolatedPath(ctx, linePoints, style);
|
|
3523
|
+
} else {
|
|
3524
|
+
drawOrthogonalPath(ctx, startPoint, endPoint, style);
|
|
3525
|
+
}
|
|
3526
|
+
labelPoint = pointAlongPolyline(linePoints, labelT);
|
|
3527
|
+
}
|
|
2966
3528
|
if (!Number.isFinite(startAngle)) {
|
|
2967
3529
|
startAngle = 0;
|
|
2968
3530
|
}
|
|
2969
3531
|
if (!Number.isFinite(endAngle)) {
|
|
2970
3532
|
endAngle = 0;
|
|
2971
3533
|
}
|
|
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
3534
|
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
2982
|
-
drawArrowhead(ctx,
|
|
3535
|
+
drawArrowhead(ctx, startPoint, startAngle, style.headSize, style.color);
|
|
2983
3536
|
}
|
|
2984
3537
|
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
2985
|
-
drawArrowhead(ctx,
|
|
3538
|
+
drawArrowhead(ctx, endPoint, endAngle, style.headSize, style.color);
|
|
2986
3539
|
}
|
|
2987
3540
|
ctx.restore();
|
|
2988
3541
|
const elements = [
|
|
2989
3542
|
{
|
|
2990
3543
|
id: `connection-${conn.from}-${conn.to}`,
|
|
2991
3544
|
kind: "connection",
|
|
2992
|
-
bounds: polylineBounds(
|
|
3545
|
+
bounds: polylineBounds(linePoints),
|
|
2993
3546
|
foregroundColor: style.color
|
|
2994
3547
|
}
|
|
2995
3548
|
];
|
|
@@ -3615,6 +4168,36 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
3615
4168
|
});
|
|
3616
4169
|
break;
|
|
3617
4170
|
}
|
|
4171
|
+
case "grid": {
|
|
4172
|
+
const canvasWidth = ctx.canvas.width;
|
|
4173
|
+
const canvasHeight = ctx.canvas.height;
|
|
4174
|
+
withOpacity(ctx, command.opacity, () => {
|
|
4175
|
+
ctx.strokeStyle = command.color;
|
|
4176
|
+
ctx.lineWidth = command.width;
|
|
4177
|
+
const startX = command.offsetX % command.spacing;
|
|
4178
|
+
for (let x = startX; x <= canvasWidth; x += command.spacing) {
|
|
4179
|
+
ctx.beginPath();
|
|
4180
|
+
ctx.moveTo(x, 0);
|
|
4181
|
+
ctx.lineTo(x, canvasHeight);
|
|
4182
|
+
ctx.stroke();
|
|
4183
|
+
}
|
|
4184
|
+
const startY = command.offsetY % command.spacing;
|
|
4185
|
+
for (let y = startY; y <= canvasHeight; y += command.spacing) {
|
|
4186
|
+
ctx.beginPath();
|
|
4187
|
+
ctx.moveTo(0, y);
|
|
4188
|
+
ctx.lineTo(canvasWidth, y);
|
|
4189
|
+
ctx.stroke();
|
|
4190
|
+
}
|
|
4191
|
+
});
|
|
4192
|
+
rendered.push({
|
|
4193
|
+
id,
|
|
4194
|
+
kind: "draw",
|
|
4195
|
+
bounds: { x: 0, y: 0, width: canvasWidth, height: canvasHeight },
|
|
4196
|
+
foregroundColor: command.color,
|
|
4197
|
+
allowOverlap: true
|
|
4198
|
+
});
|
|
4199
|
+
break;
|
|
4200
|
+
}
|
|
3618
4201
|
}
|
|
3619
4202
|
}
|
|
3620
4203
|
return rendered;
|
|
@@ -4203,6 +4786,10 @@ async function renderDesign(input, options = {}) {
|
|
|
4203
4786
|
break;
|
|
4204
4787
|
}
|
|
4205
4788
|
}
|
|
4789
|
+
const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
|
|
4790
|
+
spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
|
|
4791
|
+
{ x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
|
|
4792
|
+
);
|
|
4206
4793
|
for (const element of spec.elements) {
|
|
4207
4794
|
if (element.type !== "connection") {
|
|
4208
4795
|
continue;
|
|
@@ -4215,7 +4802,9 @@ async function renderDesign(input, options = {}) {
|
|
|
4215
4802
|
);
|
|
4216
4803
|
}
|
|
4217
4804
|
const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
|
|
4218
|
-
elements.push(
|
|
4805
|
+
elements.push(
|
|
4806
|
+
...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
|
|
4807
|
+
);
|
|
4219
4808
|
}
|
|
4220
4809
|
if (footerRect && spec.footer) {
|
|
4221
4810
|
const footerText = spec.footer.tagline ? `${spec.footer.text} \u2022 ${spec.footer.tagline}` : spec.footer.text;
|
|
@@ -4578,6 +5167,36 @@ var renderOutputSchema = z3.object({
|
|
|
4578
5167
|
)
|
|
4579
5168
|
})
|
|
4580
5169
|
});
|
|
5170
|
+
var compareOutputSchema = z3.object({
|
|
5171
|
+
targetPath: z3.string(),
|
|
5172
|
+
renderedPath: z3.string(),
|
|
5173
|
+
targetDimensions: z3.object({
|
|
5174
|
+
width: z3.number().int().positive(),
|
|
5175
|
+
height: z3.number().int().positive()
|
|
5176
|
+
}),
|
|
5177
|
+
renderedDimensions: z3.object({
|
|
5178
|
+
width: z3.number().int().positive(),
|
|
5179
|
+
height: z3.number().int().positive()
|
|
5180
|
+
}),
|
|
5181
|
+
normalizedDimensions: z3.object({
|
|
5182
|
+
width: z3.number().int().positive(),
|
|
5183
|
+
height: z3.number().int().positive()
|
|
5184
|
+
}),
|
|
5185
|
+
dimensionMismatch: z3.boolean(),
|
|
5186
|
+
grid: z3.number().int().positive(),
|
|
5187
|
+
threshold: z3.number(),
|
|
5188
|
+
closeThreshold: z3.number(),
|
|
5189
|
+
similarity: z3.number(),
|
|
5190
|
+
verdict: z3.enum(["match", "close", "mismatch"]),
|
|
5191
|
+
regions: z3.array(
|
|
5192
|
+
z3.object({
|
|
5193
|
+
label: z3.string(),
|
|
5194
|
+
row: z3.number().int().nonnegative(),
|
|
5195
|
+
column: z3.number().int().nonnegative(),
|
|
5196
|
+
similarity: z3.number()
|
|
5197
|
+
})
|
|
5198
|
+
)
|
|
5199
|
+
});
|
|
4581
5200
|
async function readJson(path) {
|
|
4582
5201
|
if (path === "-") {
|
|
4583
5202
|
const chunks = [];
|
|
@@ -4680,6 +5299,44 @@ cli.command("render", {
|
|
|
4680
5299
|
return c.ok(runReport);
|
|
4681
5300
|
}
|
|
4682
5301
|
});
|
|
5302
|
+
cli.command("compare", {
|
|
5303
|
+
description: "Compare a rendered design against a target image using structural similarity scoring.",
|
|
5304
|
+
options: z3.object({
|
|
5305
|
+
target: z3.string().describe("Path to target image (baseline)"),
|
|
5306
|
+
rendered: z3.string().describe("Path to rendered image to evaluate"),
|
|
5307
|
+
grid: z3.number().int().positive().default(3).describe("Grid size for per-region scoring"),
|
|
5308
|
+
threshold: z3.number().min(0).max(1).default(0.8).describe("Minimum similarity score required for a match verdict")
|
|
5309
|
+
}),
|
|
5310
|
+
output: compareOutputSchema,
|
|
5311
|
+
examples: [
|
|
5312
|
+
{
|
|
5313
|
+
options: {
|
|
5314
|
+
target: "./designs/target.png",
|
|
5315
|
+
rendered: "./output/design-v2-g0.4.0-sabc123.png",
|
|
5316
|
+
grid: 3,
|
|
5317
|
+
threshold: 0.8
|
|
5318
|
+
},
|
|
5319
|
+
description: "Compare two images and report overall + per-region similarity scores"
|
|
5320
|
+
}
|
|
5321
|
+
],
|
|
5322
|
+
async run(c) {
|
|
5323
|
+
try {
|
|
5324
|
+
return c.ok(
|
|
5325
|
+
await compareImages(c.options.target, c.options.rendered, {
|
|
5326
|
+
grid: c.options.grid,
|
|
5327
|
+
threshold: c.options.threshold
|
|
5328
|
+
})
|
|
5329
|
+
);
|
|
5330
|
+
} catch (error) {
|
|
5331
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5332
|
+
return c.error({
|
|
5333
|
+
code: "COMPARE_FAILED",
|
|
5334
|
+
message: `Unable to compare images: ${message}`,
|
|
5335
|
+
retryable: false
|
|
5336
|
+
});
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
});
|
|
4683
5340
|
var template = Cli.create("template", {
|
|
4684
5341
|
description: "Generate common design templates and run the full render \u2192 QA pipeline."
|
|
4685
5342
|
});
|
|
@@ -4921,7 +5578,8 @@ cli.command("qa", {
|
|
|
4921
5578
|
options: z3.object({
|
|
4922
5579
|
in: z3.string().describe("Path to rendered PNG"),
|
|
4923
5580
|
spec: z3.string().describe("Path to normalized DesignSpec JSON"),
|
|
4924
|
-
meta: z3.string().optional().describe("Optional sidecar metadata path (.meta.json)")
|
|
5581
|
+
meta: z3.string().optional().describe("Optional sidecar metadata path (.meta.json)"),
|
|
5582
|
+
reference: z3.string().optional().describe("Optional reference image path for visual comparison")
|
|
4925
5583
|
}),
|
|
4926
5584
|
output: z3.object({
|
|
4927
5585
|
pass: z3.boolean(),
|
|
@@ -4935,7 +5593,18 @@ cli.command("qa", {
|
|
|
4935
5593
|
message: z3.string(),
|
|
4936
5594
|
elementId: z3.string().optional()
|
|
4937
5595
|
})
|
|
4938
|
-
)
|
|
5596
|
+
),
|
|
5597
|
+
reference: z3.object({
|
|
5598
|
+
similarity: z3.number(),
|
|
5599
|
+
verdict: z3.enum(["match", "close", "mismatch"]),
|
|
5600
|
+
regions: z3.array(
|
|
5601
|
+
z3.object({
|
|
5602
|
+
label: z3.string(),
|
|
5603
|
+
similarity: z3.number(),
|
|
5604
|
+
description: z3.string().optional()
|
|
5605
|
+
})
|
|
5606
|
+
)
|
|
5607
|
+
}).optional()
|
|
4939
5608
|
}),
|
|
4940
5609
|
examples: [
|
|
4941
5610
|
{
|
|
@@ -4958,14 +5627,16 @@ cli.command("qa", {
|
|
|
4958
5627
|
const report = await runQa({
|
|
4959
5628
|
imagePath: c.options.in,
|
|
4960
5629
|
spec,
|
|
4961
|
-
...metadata ? { metadata } : {}
|
|
5630
|
+
...metadata ? { metadata } : {},
|
|
5631
|
+
...c.options.reference ? { referencePath: c.options.reference } : {}
|
|
4962
5632
|
});
|
|
4963
5633
|
const response = {
|
|
4964
5634
|
pass: report.pass,
|
|
4965
5635
|
checkedAt: report.checkedAt,
|
|
4966
5636
|
imagePath: report.imagePath,
|
|
4967
5637
|
issueCount: report.issues.length,
|
|
4968
|
-
issues: report.issues
|
|
5638
|
+
issues: report.issues,
|
|
5639
|
+
...report.reference ? { reference: report.reference } : {}
|
|
4969
5640
|
};
|
|
4970
5641
|
if (!report.pass) {
|
|
4971
5642
|
return c.error({
|
|
@@ -5099,9 +5770,14 @@ var isMain = (() => {
|
|
|
5099
5770
|
if (isMain) {
|
|
5100
5771
|
cli.serve();
|
|
5101
5772
|
}
|
|
5773
|
+
|
|
5774
|
+
// src/index.ts
|
|
5775
|
+
init_compare();
|
|
5102
5776
|
export {
|
|
5103
5777
|
DEFAULT_GENERATOR_VERSION,
|
|
5104
5778
|
DEFAULT_RAINBOW_COLORS,
|
|
5779
|
+
arcRoute,
|
|
5780
|
+
bezierPointAt,
|
|
5105
5781
|
buildCardsSpec,
|
|
5106
5782
|
buildCodeSpec,
|
|
5107
5783
|
buildFlowchartSpec,
|
|
@@ -5109,7 +5785,11 @@ export {
|
|
|
5109
5785
|
builtInThemeBackgrounds,
|
|
5110
5786
|
builtInThemes,
|
|
5111
5787
|
cli,
|
|
5788
|
+
compareImages,
|
|
5789
|
+
computeDiagramCenter,
|
|
5112
5790
|
computeSpecHash,
|
|
5791
|
+
connectionElementSchema,
|
|
5792
|
+
curveRoute,
|
|
5113
5793
|
defaultAutoLayout,
|
|
5114
5794
|
defaultCanvas,
|
|
5115
5795
|
defaultConstraints,
|
|
@@ -5119,19 +5799,29 @@ export {
|
|
|
5119
5799
|
defaultTheme,
|
|
5120
5800
|
deriveSafeFrame,
|
|
5121
5801
|
designSpecSchema,
|
|
5802
|
+
diagramElementSchema,
|
|
5803
|
+
diagramLayoutSchema,
|
|
5804
|
+
diagramSpecSchema,
|
|
5122
5805
|
disposeHighlighter,
|
|
5123
5806
|
drawGradientRect,
|
|
5124
5807
|
drawRainbowRule,
|
|
5125
5808
|
drawVignette,
|
|
5809
|
+
edgeAnchor,
|
|
5810
|
+
flowNodeElementSchema,
|
|
5126
5811
|
highlightCode,
|
|
5127
5812
|
inferLayout,
|
|
5128
5813
|
inferSidecarPath,
|
|
5129
5814
|
initHighlighter,
|
|
5130
5815
|
loadFonts,
|
|
5816
|
+
orthogonalRoute,
|
|
5817
|
+
outwardNormal,
|
|
5131
5818
|
parseDesignSpec,
|
|
5819
|
+
parseDiagramSpec,
|
|
5132
5820
|
publishToGist,
|
|
5133
5821
|
publishToGitHub,
|
|
5134
5822
|
readMetadata,
|
|
5823
|
+
rectCenter,
|
|
5824
|
+
renderConnection,
|
|
5135
5825
|
renderDesign,
|
|
5136
5826
|
renderDrawCommands,
|
|
5137
5827
|
resolveShikiTheme,
|