@spectratools/graphic-designer-cli 0.3.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/dist/cli.js +1252 -517
- package/dist/index.d.ts +105 -5
- package/dist/index.js +1271 -513
- package/dist/qa.d.ts +14 -3
- package/dist/qa.js +361 -50
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +909 -431
- package/dist/{spec.schema-DhAI-tE8.d.ts → spec.schema-Dm_wOLTd.d.ts} +2028 -599
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +158 -10
- package/package.json +1 -1
package/dist/qa.d.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { R as RenderMetadata, D as DesignSpec } from './spec.schema-
|
|
1
|
+
import { R as RenderMetadata, D as DesignSpec } from './spec.schema-Dm_wOLTd.js';
|
|
2
2
|
import 'zod';
|
|
3
3
|
import '@napi-rs/canvas';
|
|
4
4
|
|
|
5
5
|
type QaSeverity = 'error' | 'warning';
|
|
6
6
|
type QaIssue = {
|
|
7
|
-
code: 'DIMENSIONS_MISMATCH' | 'ELEMENT_CLIPPED' | 'ELEMENT_OVERLAP' | 'LOW_CONTRAST' | 'FOOTER_SPACING' | 'TEXT_TRUNCATED' | 'MISSING_LAYOUT' | 'DRAW_OUT_OF_BOUNDS';
|
|
7
|
+
code: 'DIMENSIONS_MISMATCH' | 'ELEMENT_CLIPPED' | 'ELEMENT_OVERLAP' | 'LOW_CONTRAST' | 'FOOTER_SPACING' | 'TEXT_TRUNCATED' | 'MISSING_LAYOUT' | 'DRAW_OUT_OF_BOUNDS' | 'REFERENCE_MISMATCH';
|
|
8
8
|
severity: QaSeverity;
|
|
9
9
|
message: string;
|
|
10
10
|
elementId?: string;
|
|
11
11
|
details?: Record<string, number | string | boolean>;
|
|
12
12
|
};
|
|
13
|
+
type QaReferenceResult = {
|
|
14
|
+
similarity: number;
|
|
15
|
+
verdict: 'match' | 'close' | 'mismatch';
|
|
16
|
+
regions: Array<{
|
|
17
|
+
label: string;
|
|
18
|
+
similarity: number;
|
|
19
|
+
description?: string;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
13
22
|
type QaReport = {
|
|
14
23
|
pass: boolean;
|
|
15
24
|
checkedAt: string;
|
|
@@ -27,6 +36,7 @@ type QaReport = {
|
|
|
27
36
|
footerSpacingPx?: number;
|
|
28
37
|
};
|
|
29
38
|
issues: QaIssue[];
|
|
39
|
+
reference?: QaReferenceResult;
|
|
30
40
|
};
|
|
31
41
|
/**
|
|
32
42
|
* Read and parse a sidecar `.meta.json` file produced by
|
|
@@ -57,6 +67,7 @@ declare function runQa(options: {
|
|
|
57
67
|
imagePath: string;
|
|
58
68
|
spec: DesignSpec;
|
|
59
69
|
metadata?: RenderMetadata;
|
|
70
|
+
referencePath?: string;
|
|
60
71
|
}): Promise<QaReport>;
|
|
61
72
|
|
|
62
|
-
export { type QaIssue, type QaReport, type QaSeverity, readMetadata, runQa };
|
|
73
|
+
export { type QaIssue, type QaReferenceResult, type QaReport, type QaSeverity, readMetadata, runQa };
|
package/dist/qa.js
CHANGED
|
@@ -1,7 +1,154 @@
|
|
|
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/qa.ts
|
|
2
149
|
import { readFile } from "fs/promises";
|
|
3
150
|
import { resolve } from "path";
|
|
4
|
-
import
|
|
151
|
+
import sharp2 from "sharp";
|
|
5
152
|
|
|
6
153
|
// src/code-style.ts
|
|
7
154
|
var CARBON_SURROUND_COLOR = "rgba(171, 184, 195, 1)";
|
|
@@ -62,7 +209,98 @@ import { z as z2 } from "zod";
|
|
|
62
209
|
|
|
63
210
|
// src/themes/builtin.ts
|
|
64
211
|
import { z } from "zod";
|
|
65
|
-
|
|
212
|
+
|
|
213
|
+
// src/utils/color.ts
|
|
214
|
+
function parseChannel(hex, offset) {
|
|
215
|
+
return Number.parseInt(hex.slice(offset, offset + 2), 16);
|
|
216
|
+
}
|
|
217
|
+
function parseHexColor(hexColor) {
|
|
218
|
+
const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
|
|
219
|
+
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
220
|
+
throw new Error(`Unsupported color format: ${hexColor}`);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
r: parseChannel(normalized, 0),
|
|
224
|
+
g: parseChannel(normalized, 2),
|
|
225
|
+
b: parseChannel(normalized, 4)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
var rgbaRegex = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([01](?:\.\d+)?|0?\.\d+)\s*)?\)$/;
|
|
229
|
+
var hexColorRegex = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
230
|
+
function toHex(n) {
|
|
231
|
+
return n.toString(16).padStart(2, "0");
|
|
232
|
+
}
|
|
233
|
+
function parseRgbaToHex(color) {
|
|
234
|
+
const match = rgbaRegex.exec(color);
|
|
235
|
+
if (!match) {
|
|
236
|
+
throw new Error(`Invalid rgb/rgba color: ${color}`);
|
|
237
|
+
}
|
|
238
|
+
const r = Number.parseInt(match[1], 10);
|
|
239
|
+
const g = Number.parseInt(match[2], 10);
|
|
240
|
+
const b = Number.parseInt(match[3], 10);
|
|
241
|
+
if (r > 255 || g > 255 || b > 255) {
|
|
242
|
+
throw new Error(`RGB channel values must be 0-255, got: ${color}`);
|
|
243
|
+
}
|
|
244
|
+
if (match[4] !== void 0) {
|
|
245
|
+
const a = Number.parseFloat(match[4]);
|
|
246
|
+
if (a < 0 || a > 1) {
|
|
247
|
+
throw new Error(`Alpha value must be 0-1, got: ${a}`);
|
|
248
|
+
}
|
|
249
|
+
const alphaByte = Math.round(a * 255);
|
|
250
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alphaByte)}`;
|
|
251
|
+
}
|
|
252
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
253
|
+
}
|
|
254
|
+
function isRgbaColor(color) {
|
|
255
|
+
return rgbaRegex.test(color);
|
|
256
|
+
}
|
|
257
|
+
function isHexColor(color) {
|
|
258
|
+
return hexColorRegex.test(color);
|
|
259
|
+
}
|
|
260
|
+
function normalizeColor(color) {
|
|
261
|
+
if (isHexColor(color)) {
|
|
262
|
+
return color;
|
|
263
|
+
}
|
|
264
|
+
if (isRgbaColor(color)) {
|
|
265
|
+
return parseRgbaToHex(color);
|
|
266
|
+
}
|
|
267
|
+
throw new Error(`Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color, got: ${color}`);
|
|
268
|
+
}
|
|
269
|
+
function srgbToLinear(channel) {
|
|
270
|
+
const normalized = channel / 255;
|
|
271
|
+
if (normalized <= 0.03928) {
|
|
272
|
+
return normalized / 12.92;
|
|
273
|
+
}
|
|
274
|
+
return ((normalized + 0.055) / 1.055) ** 2.4;
|
|
275
|
+
}
|
|
276
|
+
function relativeLuminance(hexColor) {
|
|
277
|
+
const normalized = isRgbaColor(hexColor) ? parseRgbaToHex(hexColor) : hexColor;
|
|
278
|
+
const rgb = parseHexColor(normalized);
|
|
279
|
+
const r = srgbToLinear(rgb.r);
|
|
280
|
+
const g = srgbToLinear(rgb.g);
|
|
281
|
+
const b = srgbToLinear(rgb.b);
|
|
282
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
283
|
+
}
|
|
284
|
+
function contrastRatio(foreground, background) {
|
|
285
|
+
const fg = relativeLuminance(foreground);
|
|
286
|
+
const bg = relativeLuminance(background);
|
|
287
|
+
const lighter = Math.max(fg, bg);
|
|
288
|
+
const darker = Math.min(fg, bg);
|
|
289
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/themes/builtin.ts
|
|
293
|
+
var colorHexSchema = z.string().refine(
|
|
294
|
+
(v) => {
|
|
295
|
+
try {
|
|
296
|
+
normalizeColor(v);
|
|
297
|
+
return true;
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{ message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
|
|
303
|
+
).transform((v) => normalizeColor(v));
|
|
66
304
|
var fontFamilySchema = z.string().min(1).max(120);
|
|
67
305
|
var codeThemeSchema = z.object({
|
|
68
306
|
background: colorHexSchema,
|
|
@@ -235,7 +473,17 @@ var builtInThemes = {
|
|
|
235
473
|
var defaultTheme = builtInThemes.dark;
|
|
236
474
|
|
|
237
475
|
// src/spec.schema.ts
|
|
238
|
-
var colorHexSchema2 = z2.string().
|
|
476
|
+
var colorHexSchema2 = z2.string().refine(
|
|
477
|
+
(v) => {
|
|
478
|
+
try {
|
|
479
|
+
normalizeColor(v);
|
|
480
|
+
return true;
|
|
481
|
+
} catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
{ message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
|
|
486
|
+
).transform((v) => normalizeColor(v));
|
|
239
487
|
var gradientStopSchema = z2.object({
|
|
240
488
|
offset: z2.number().min(0).max(1),
|
|
241
489
|
color: colorHexSchema2
|
|
@@ -419,13 +667,32 @@ var cardElementSchema = z2.object({
|
|
|
419
667
|
tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
|
|
420
668
|
icon: z2.string().min(1).max(64).optional()
|
|
421
669
|
}).strict();
|
|
670
|
+
var flowNodeShadowSchema = z2.object({
|
|
671
|
+
color: colorHexSchema2.optional(),
|
|
672
|
+
blur: z2.number().min(0).max(64).default(8),
|
|
673
|
+
offsetX: z2.number().min(-32).max(32).default(0),
|
|
674
|
+
offsetY: z2.number().min(-32).max(32).default(0),
|
|
675
|
+
opacity: z2.number().min(0).max(1).default(0.3)
|
|
676
|
+
}).strict();
|
|
422
677
|
var flowNodeElementSchema = z2.object({
|
|
423
678
|
type: z2.literal("flow-node"),
|
|
424
679
|
id: z2.string().min(1).max(120),
|
|
425
|
-
shape: z2.enum([
|
|
680
|
+
shape: z2.enum([
|
|
681
|
+
"box",
|
|
682
|
+
"rounded-box",
|
|
683
|
+
"diamond",
|
|
684
|
+
"circle",
|
|
685
|
+
"pill",
|
|
686
|
+
"cylinder",
|
|
687
|
+
"parallelogram",
|
|
688
|
+
"hexagon"
|
|
689
|
+
]).default("rounded-box"),
|
|
426
690
|
label: z2.string().min(1).max(200),
|
|
427
691
|
sublabel: z2.string().min(1).max(300).optional(),
|
|
428
692
|
sublabelColor: colorHexSchema2.optional(),
|
|
693
|
+
sublabel2: z2.string().min(1).max(300).optional(),
|
|
694
|
+
sublabel2Color: colorHexSchema2.optional(),
|
|
695
|
+
sublabel2FontSize: z2.number().min(8).max(32).optional(),
|
|
429
696
|
labelColor: colorHexSchema2.optional(),
|
|
430
697
|
labelFontSize: z2.number().min(10).max(48).optional(),
|
|
431
698
|
color: colorHexSchema2.optional(),
|
|
@@ -434,20 +701,30 @@ var flowNodeElementSchema = z2.object({
|
|
|
434
701
|
cornerRadius: z2.number().min(0).max(64).optional(),
|
|
435
702
|
width: z2.number().int().min(40).max(800).optional(),
|
|
436
703
|
height: z2.number().int().min(30).max(600).optional(),
|
|
437
|
-
|
|
704
|
+
fillOpacity: z2.number().min(0).max(1).default(1),
|
|
705
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
706
|
+
badgeText: z2.string().min(1).max(32).optional(),
|
|
707
|
+
badgeColor: colorHexSchema2.optional(),
|
|
708
|
+
badgeBackground: colorHexSchema2.optional(),
|
|
709
|
+
badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
|
|
710
|
+
shadow: flowNodeShadowSchema.optional()
|
|
438
711
|
}).strict();
|
|
439
712
|
var connectionElementSchema = z2.object({
|
|
440
713
|
type: z2.literal("connection"),
|
|
441
714
|
from: z2.string().min(1).max(120),
|
|
442
715
|
to: z2.string().min(1).max(120),
|
|
443
716
|
style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
717
|
+
strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
444
718
|
arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
|
|
445
719
|
label: z2.string().min(1).max(200).optional(),
|
|
446
720
|
labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
|
|
447
721
|
color: colorHexSchema2.optional(),
|
|
448
|
-
width: z2.number().min(0.5).max(
|
|
722
|
+
width: z2.number().min(0.5).max(10).optional(),
|
|
723
|
+
strokeWidth: z2.number().min(0.5).max(10).default(2),
|
|
449
724
|
arrowSize: z2.number().min(4).max(32).optional(),
|
|
450
|
-
opacity: z2.number().min(0).max(1).default(1)
|
|
725
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
726
|
+
routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
|
|
727
|
+
tension: z2.number().min(0.1).max(0.8).default(0.35)
|
|
451
728
|
}).strict();
|
|
452
729
|
var codeBlockStyleSchema = z2.object({
|
|
453
730
|
paddingVertical: z2.number().min(0).max(128).default(56),
|
|
@@ -516,6 +793,10 @@ var elementSchema = z2.discriminatedUnion("type", [
|
|
|
516
793
|
shapeElementSchema,
|
|
517
794
|
imageElementSchema
|
|
518
795
|
]);
|
|
796
|
+
var diagramCenterSchema = z2.object({
|
|
797
|
+
x: z2.number(),
|
|
798
|
+
y: z2.number()
|
|
799
|
+
}).strict();
|
|
519
800
|
var autoLayoutConfigSchema = z2.object({
|
|
520
801
|
mode: z2.literal("auto"),
|
|
521
802
|
algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
|
|
@@ -523,7 +804,17 @@ var autoLayoutConfigSchema = z2.object({
|
|
|
523
804
|
nodeSpacing: z2.number().int().min(0).max(512).default(80),
|
|
524
805
|
rankSpacing: z2.number().int().min(0).max(512).default(120),
|
|
525
806
|
edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
|
|
526
|
-
aspectRatio: z2.number().min(0.5).max(3).optional()
|
|
807
|
+
aspectRatio: z2.number().min(0.5).max(3).optional(),
|
|
808
|
+
/** ID of the root node for radial layout. Only relevant when algorithm is 'radial'. */
|
|
809
|
+
radialRoot: z2.string().min(1).max(120).optional(),
|
|
810
|
+
/** Fixed radius in pixels for radial layout. Only relevant when algorithm is 'radial'. */
|
|
811
|
+
radialRadius: z2.number().positive().optional(),
|
|
812
|
+
/** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
|
|
813
|
+
radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
|
|
814
|
+
/** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
|
|
815
|
+
radialSortBy: z2.enum(["id", "connections"]).optional(),
|
|
816
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
817
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
527
818
|
}).strict();
|
|
528
819
|
var gridLayoutConfigSchema = z2.object({
|
|
529
820
|
mode: z2.literal("grid"),
|
|
@@ -531,13 +822,17 @@ var gridLayoutConfigSchema = z2.object({
|
|
|
531
822
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
532
823
|
cardMinHeight: z2.number().int().min(32).max(4096).optional(),
|
|
533
824
|
cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
|
|
534
|
-
equalHeight: z2.boolean().default(false)
|
|
825
|
+
equalHeight: z2.boolean().default(false),
|
|
826
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
827
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
535
828
|
}).strict();
|
|
536
829
|
var stackLayoutConfigSchema = z2.object({
|
|
537
830
|
mode: z2.literal("stack"),
|
|
538
831
|
direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
|
|
539
832
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
540
|
-
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
|
|
833
|
+
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
|
|
834
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
835
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
541
836
|
}).strict();
|
|
542
837
|
var manualPositionSchema = z2.object({
|
|
543
838
|
x: z2.number().int(),
|
|
@@ -547,7 +842,9 @@ var manualPositionSchema = z2.object({
|
|
|
547
842
|
}).strict();
|
|
548
843
|
var manualLayoutConfigSchema = z2.object({
|
|
549
844
|
mode: z2.literal("manual"),
|
|
550
|
-
positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
|
|
845
|
+
positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
|
|
846
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
847
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
551
848
|
}).strict();
|
|
552
849
|
var layoutConfigSchema = z2.discriminatedUnion("mode", [
|
|
553
850
|
autoLayoutConfigSchema,
|
|
@@ -599,6 +896,31 @@ var canvasSchema = z2.object({
|
|
|
599
896
|
padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
|
|
600
897
|
}).strict();
|
|
601
898
|
var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
|
|
899
|
+
var diagramPositionSchema = z2.object({
|
|
900
|
+
x: z2.number(),
|
|
901
|
+
y: z2.number(),
|
|
902
|
+
width: z2.number().positive(),
|
|
903
|
+
height: z2.number().positive()
|
|
904
|
+
}).strict();
|
|
905
|
+
var diagramElementSchema = z2.discriminatedUnion("type", [
|
|
906
|
+
flowNodeElementSchema,
|
|
907
|
+
connectionElementSchema
|
|
908
|
+
]);
|
|
909
|
+
var diagramLayoutSchema = z2.object({
|
|
910
|
+
mode: z2.enum(["manual", "auto"]).default("manual"),
|
|
911
|
+
positions: z2.record(z2.string(), diagramPositionSchema).optional(),
|
|
912
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
913
|
+
}).strict();
|
|
914
|
+
var diagramSpecSchema = z2.object({
|
|
915
|
+
version: z2.literal(1),
|
|
916
|
+
canvas: z2.object({
|
|
917
|
+
width: z2.number().int().min(320).max(4096).default(1200),
|
|
918
|
+
height: z2.number().int().min(180).max(4096).default(675)
|
|
919
|
+
}).default({ width: 1200, height: 675 }),
|
|
920
|
+
theme: themeSchema.optional(),
|
|
921
|
+
elements: z2.array(diagramElementSchema).min(1),
|
|
922
|
+
layout: diagramLayoutSchema.default({ mode: "manual" })
|
|
923
|
+
}).strict();
|
|
602
924
|
var designSpecSchema = z2.object({
|
|
603
925
|
version: z2.literal(2).default(2),
|
|
604
926
|
canvas: canvasSchema.default(defaultCanvas),
|
|
@@ -627,43 +949,6 @@ function parseDesignSpec(input) {
|
|
|
627
949
|
return designSpecSchema.parse(input);
|
|
628
950
|
}
|
|
629
951
|
|
|
630
|
-
// src/utils/color.ts
|
|
631
|
-
function parseChannel(hex, offset) {
|
|
632
|
-
return Number.parseInt(hex.slice(offset, offset + 2), 16);
|
|
633
|
-
}
|
|
634
|
-
function parseHexColor(hexColor) {
|
|
635
|
-
const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
|
|
636
|
-
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
637
|
-
throw new Error(`Unsupported color format: ${hexColor}`);
|
|
638
|
-
}
|
|
639
|
-
return {
|
|
640
|
-
r: parseChannel(normalized, 0),
|
|
641
|
-
g: parseChannel(normalized, 2),
|
|
642
|
-
b: parseChannel(normalized, 4)
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
function srgbToLinear(channel) {
|
|
646
|
-
const normalized = channel / 255;
|
|
647
|
-
if (normalized <= 0.03928) {
|
|
648
|
-
return normalized / 12.92;
|
|
649
|
-
}
|
|
650
|
-
return ((normalized + 0.055) / 1.055) ** 2.4;
|
|
651
|
-
}
|
|
652
|
-
function relativeLuminance(hexColor) {
|
|
653
|
-
const rgb = parseHexColor(hexColor);
|
|
654
|
-
const r = srgbToLinear(rgb.r);
|
|
655
|
-
const g = srgbToLinear(rgb.g);
|
|
656
|
-
const b = srgbToLinear(rgb.b);
|
|
657
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
658
|
-
}
|
|
659
|
-
function contrastRatio(foreground, background) {
|
|
660
|
-
const fg = relativeLuminance(foreground);
|
|
661
|
-
const bg = relativeLuminance(background);
|
|
662
|
-
const lighter = Math.max(fg, bg);
|
|
663
|
-
const darker = Math.min(fg, bg);
|
|
664
|
-
return (lighter + 0.05) / (darker + 0.05);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
952
|
// src/qa.ts
|
|
668
953
|
function rectWithin(outer, inner) {
|
|
669
954
|
return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
|
|
@@ -709,7 +994,7 @@ async function runQa(options) {
|
|
|
709
994
|
const imagePath = resolve(options.imagePath);
|
|
710
995
|
const expectedSafeFrame = deriveSafeFrame(spec);
|
|
711
996
|
const expectedCanvas = canvasRect(spec);
|
|
712
|
-
const imageMetadata = await
|
|
997
|
+
const imageMetadata = await sharp2(imagePath).metadata();
|
|
713
998
|
const issues = [];
|
|
714
999
|
const expectedScale = options.metadata?.canvas.scale ?? resolveRenderScale(spec);
|
|
715
1000
|
const expectedWidth = spec.canvas.width * expectedScale;
|
|
@@ -860,6 +1145,31 @@ async function runQa(options) {
|
|
|
860
1145
|
});
|
|
861
1146
|
}
|
|
862
1147
|
}
|
|
1148
|
+
let referenceResult;
|
|
1149
|
+
if (options.referencePath) {
|
|
1150
|
+
const { compareImages: compareImages2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
|
|
1151
|
+
const comparison = await compareImages2(options.referencePath, imagePath);
|
|
1152
|
+
referenceResult = {
|
|
1153
|
+
similarity: comparison.similarity,
|
|
1154
|
+
verdict: comparison.verdict,
|
|
1155
|
+
regions: comparison.regions.map((region) => ({
|
|
1156
|
+
label: region.label,
|
|
1157
|
+
similarity: region.similarity
|
|
1158
|
+
}))
|
|
1159
|
+
};
|
|
1160
|
+
if (comparison.verdict === "mismatch") {
|
|
1161
|
+
const severity = comparison.similarity < 0.5 ? "error" : "warning";
|
|
1162
|
+
issues.push({
|
|
1163
|
+
code: "REFERENCE_MISMATCH",
|
|
1164
|
+
severity,
|
|
1165
|
+
message: `Reference image comparison ${severity === "error" ? "failed" : "warned"}: similarity ${comparison.similarity.toFixed(4)} with verdict "${comparison.verdict}".`,
|
|
1166
|
+
details: {
|
|
1167
|
+
similarity: comparison.similarity,
|
|
1168
|
+
verdict: comparison.verdict
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
863
1173
|
const footerSpacingPx = options.metadata?.layout.elements ? (() => {
|
|
864
1174
|
const footer = options.metadata.layout.elements.find((element) => element.id === "footer");
|
|
865
1175
|
if (!footer) {
|
|
@@ -892,7 +1202,8 @@ async function runQa(options) {
|
|
|
892
1202
|
...imageMetadata.height !== void 0 ? { height: imageMetadata.height } : {},
|
|
893
1203
|
...footerSpacingPx !== void 0 ? { footerSpacingPx } : {}
|
|
894
1204
|
},
|
|
895
|
-
issues
|
|
1205
|
+
issues,
|
|
1206
|
+
...referenceResult ? { reference: referenceResult } : {}
|
|
896
1207
|
};
|
|
897
1208
|
}
|
|
898
1209
|
export {
|
package/dist/renderer.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { h as DEFAULT_GENERATOR_VERSION,
|
|
1
|
+
export { h as DEFAULT_GENERATOR_VERSION, N as LayoutSnapshot, a as Rect, R as RenderMetadata, Q as RenderResult, d as RenderedElement, Z as WrittenArtifacts, a0 as computeSpecHash, aj as inferSidecarPath, am as renderDesign, ao as writeRenderArtifacts } from './spec.schema-Dm_wOLTd.js';
|
|
2
2
|
import 'zod';
|
|
3
3
|
import '@napi-rs/canvas';
|