@spectratools/graphic-designer-cli 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +559 -319
- package/dist/index.d.ts +2 -2
- package/dist/index.js +559 -319
- package/dist/qa.d.ts +1 -1
- package/dist/qa.js +121 -41
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +627 -389
- package/dist/{spec.schema-DhAI-tE8.d.ts → spec.schema-BUTof436.d.ts} +625 -457
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +85 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -257,7 +257,106 @@ import { z as z2 } from "zod";
|
|
|
257
257
|
|
|
258
258
|
// src/themes/builtin.ts
|
|
259
259
|
import { z } from "zod";
|
|
260
|
-
|
|
260
|
+
|
|
261
|
+
// src/utils/color.ts
|
|
262
|
+
function parseChannel(hex, offset) {
|
|
263
|
+
return Number.parseInt(hex.slice(offset, offset + 2), 16);
|
|
264
|
+
}
|
|
265
|
+
function parseHexColor(hexColor) {
|
|
266
|
+
const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
|
|
267
|
+
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
268
|
+
throw new Error(`Unsupported color format: ${hexColor}`);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
r: parseChannel(normalized, 0),
|
|
272
|
+
g: parseChannel(normalized, 2),
|
|
273
|
+
b: parseChannel(normalized, 4)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
var rgbaRegex = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([01](?:\.\d+)?|0?\.\d+)\s*)?\)$/;
|
|
277
|
+
var hexColorRegex = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
278
|
+
function toHex(n) {
|
|
279
|
+
return n.toString(16).padStart(2, "0");
|
|
280
|
+
}
|
|
281
|
+
function parseRgbaToHex(color) {
|
|
282
|
+
const match = rgbaRegex.exec(color);
|
|
283
|
+
if (!match) {
|
|
284
|
+
throw new Error(`Invalid rgb/rgba color: ${color}`);
|
|
285
|
+
}
|
|
286
|
+
const r = Number.parseInt(match[1], 10);
|
|
287
|
+
const g = Number.parseInt(match[2], 10);
|
|
288
|
+
const b = Number.parseInt(match[3], 10);
|
|
289
|
+
if (r > 255 || g > 255 || b > 255) {
|
|
290
|
+
throw new Error(`RGB channel values must be 0-255, got: ${color}`);
|
|
291
|
+
}
|
|
292
|
+
if (match[4] !== void 0) {
|
|
293
|
+
const a = Number.parseFloat(match[4]);
|
|
294
|
+
if (a < 0 || a > 1) {
|
|
295
|
+
throw new Error(`Alpha value must be 0-1, got: ${a}`);
|
|
296
|
+
}
|
|
297
|
+
const alphaByte = Math.round(a * 255);
|
|
298
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alphaByte)}`;
|
|
299
|
+
}
|
|
300
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
301
|
+
}
|
|
302
|
+
function isRgbaColor(color) {
|
|
303
|
+
return rgbaRegex.test(color);
|
|
304
|
+
}
|
|
305
|
+
function isHexColor(color) {
|
|
306
|
+
return hexColorRegex.test(color);
|
|
307
|
+
}
|
|
308
|
+
function normalizeColor(color) {
|
|
309
|
+
if (isHexColor(color)) {
|
|
310
|
+
return color;
|
|
311
|
+
}
|
|
312
|
+
if (isRgbaColor(color)) {
|
|
313
|
+
return parseRgbaToHex(color);
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color, got: ${color}`);
|
|
316
|
+
}
|
|
317
|
+
function srgbToLinear(channel) {
|
|
318
|
+
const normalized = channel / 255;
|
|
319
|
+
if (normalized <= 0.03928) {
|
|
320
|
+
return normalized / 12.92;
|
|
321
|
+
}
|
|
322
|
+
return ((normalized + 0.055) / 1.055) ** 2.4;
|
|
323
|
+
}
|
|
324
|
+
function relativeLuminance(hexColor) {
|
|
325
|
+
const normalized = isRgbaColor(hexColor) ? parseRgbaToHex(hexColor) : hexColor;
|
|
326
|
+
const rgb = parseHexColor(normalized);
|
|
327
|
+
const r = srgbToLinear(rgb.r);
|
|
328
|
+
const g = srgbToLinear(rgb.g);
|
|
329
|
+
const b = srgbToLinear(rgb.b);
|
|
330
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
331
|
+
}
|
|
332
|
+
function contrastRatio(foreground, background) {
|
|
333
|
+
const fg = relativeLuminance(foreground);
|
|
334
|
+
const bg = relativeLuminance(background);
|
|
335
|
+
const lighter = Math.max(fg, bg);
|
|
336
|
+
const darker = Math.min(fg, bg);
|
|
337
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
338
|
+
}
|
|
339
|
+
function blendColorWithOpacity(foreground, background, opacity) {
|
|
340
|
+
const fg = parseHexColor(foreground);
|
|
341
|
+
const bg = parseHexColor(background);
|
|
342
|
+
const r = Math.round(fg.r * opacity + bg.r * (1 - opacity));
|
|
343
|
+
const g = Math.round(fg.g * opacity + bg.g * (1 - opacity));
|
|
344
|
+
const b = Math.round(fg.b * opacity + bg.b * (1 - opacity));
|
|
345
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/themes/builtin.ts
|
|
349
|
+
var colorHexSchema = z.string().refine(
|
|
350
|
+
(v) => {
|
|
351
|
+
try {
|
|
352
|
+
normalizeColor(v);
|
|
353
|
+
return true;
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
{ message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
|
|
359
|
+
).transform((v) => normalizeColor(v));
|
|
261
360
|
var fontFamilySchema = z.string().min(1).max(120);
|
|
262
361
|
var codeThemeSchema = z.object({
|
|
263
362
|
background: colorHexSchema,
|
|
@@ -488,7 +587,17 @@ function resolveTheme(theme) {
|
|
|
488
587
|
}
|
|
489
588
|
|
|
490
589
|
// src/spec.schema.ts
|
|
491
|
-
var colorHexSchema2 = z2.string().
|
|
590
|
+
var colorHexSchema2 = z2.string().refine(
|
|
591
|
+
(v) => {
|
|
592
|
+
try {
|
|
593
|
+
normalizeColor(v);
|
|
594
|
+
return true;
|
|
595
|
+
} catch {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
{ message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
|
|
600
|
+
).transform((v) => normalizeColor(v));
|
|
492
601
|
var gradientStopSchema = z2.object({
|
|
493
602
|
offset: z2.number().min(0).max(1),
|
|
494
603
|
color: colorHexSchema2
|
|
@@ -680,6 +789,9 @@ var flowNodeElementSchema = z2.object({
|
|
|
680
789
|
label: z2.string().min(1).max(200),
|
|
681
790
|
sublabel: z2.string().min(1).max(300).optional(),
|
|
682
791
|
sublabelColor: colorHexSchema2.optional(),
|
|
792
|
+
sublabel2: z2.string().min(1).max(300).optional(),
|
|
793
|
+
sublabel2Color: colorHexSchema2.optional(),
|
|
794
|
+
sublabel2FontSize: z2.number().min(8).max(32).optional(),
|
|
683
795
|
labelColor: colorHexSchema2.optional(),
|
|
684
796
|
labelFontSize: z2.number().min(10).max(48).optional(),
|
|
685
797
|
color: colorHexSchema2.optional(),
|
|
@@ -688,7 +800,12 @@ var flowNodeElementSchema = z2.object({
|
|
|
688
800
|
cornerRadius: z2.number().min(0).max(64).optional(),
|
|
689
801
|
width: z2.number().int().min(40).max(800).optional(),
|
|
690
802
|
height: z2.number().int().min(30).max(600).optional(),
|
|
691
|
-
|
|
803
|
+
fillOpacity: z2.number().min(0).max(1).default(1),
|
|
804
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
805
|
+
badgeText: z2.string().min(1).max(32).optional(),
|
|
806
|
+
badgeColor: colorHexSchema2.optional(),
|
|
807
|
+
badgeBackground: colorHexSchema2.optional(),
|
|
808
|
+
badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
|
|
692
809
|
}).strict();
|
|
693
810
|
var connectionElementSchema = z2.object({
|
|
694
811
|
type: z2.literal("connection"),
|
|
@@ -777,7 +894,15 @@ var autoLayoutConfigSchema = z2.object({
|
|
|
777
894
|
nodeSpacing: z2.number().int().min(0).max(512).default(80),
|
|
778
895
|
rankSpacing: z2.number().int().min(0).max(512).default(120),
|
|
779
896
|
edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
|
|
780
|
-
aspectRatio: z2.number().min(0.5).max(3).optional()
|
|
897
|
+
aspectRatio: z2.number().min(0.5).max(3).optional(),
|
|
898
|
+
/** ID of the root node for radial layout. Only relevant when algorithm is 'radial'. */
|
|
899
|
+
radialRoot: z2.string().min(1).max(120).optional(),
|
|
900
|
+
/** Fixed radius in pixels for radial layout. Only relevant when algorithm is 'radial'. */
|
|
901
|
+
radialRadius: z2.number().positive().optional(),
|
|
902
|
+
/** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
|
|
903
|
+
radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
|
|
904
|
+
/** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
|
|
905
|
+
radialSortBy: z2.enum(["id", "connections"]).optional()
|
|
781
906
|
}).strict();
|
|
782
907
|
var gridLayoutConfigSchema = z2.object({
|
|
783
908
|
mode: z2.literal("grid"),
|
|
@@ -881,43 +1006,6 @@ function parseDesignSpec(input) {
|
|
|
881
1006
|
return designSpecSchema.parse(input);
|
|
882
1007
|
}
|
|
883
1008
|
|
|
884
|
-
// src/utils/color.ts
|
|
885
|
-
function parseChannel(hex, offset) {
|
|
886
|
-
return Number.parseInt(hex.slice(offset, offset + 2), 16);
|
|
887
|
-
}
|
|
888
|
-
function parseHexColor(hexColor) {
|
|
889
|
-
const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
|
|
890
|
-
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
891
|
-
throw new Error(`Unsupported color format: ${hexColor}`);
|
|
892
|
-
}
|
|
893
|
-
return {
|
|
894
|
-
r: parseChannel(normalized, 0),
|
|
895
|
-
g: parseChannel(normalized, 2),
|
|
896
|
-
b: parseChannel(normalized, 4)
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
function srgbToLinear(channel) {
|
|
900
|
-
const normalized = channel / 255;
|
|
901
|
-
if (normalized <= 0.03928) {
|
|
902
|
-
return normalized / 12.92;
|
|
903
|
-
}
|
|
904
|
-
return ((normalized + 0.055) / 1.055) ** 2.4;
|
|
905
|
-
}
|
|
906
|
-
function relativeLuminance(hexColor) {
|
|
907
|
-
const rgb = parseHexColor(hexColor);
|
|
908
|
-
const r = srgbToLinear(rgb.r);
|
|
909
|
-
const g = srgbToLinear(rgb.g);
|
|
910
|
-
const b = srgbToLinear(rgb.b);
|
|
911
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
912
|
-
}
|
|
913
|
-
function contrastRatio(foreground, background) {
|
|
914
|
-
const fg = relativeLuminance(foreground);
|
|
915
|
-
const bg = relativeLuminance(background);
|
|
916
|
-
const lighter = Math.max(fg, bg);
|
|
917
|
-
const darker = Math.min(fg, bg);
|
|
918
|
-
return (lighter + 0.05) / (darker + 0.05);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
1009
|
// src/qa.ts
|
|
922
1010
|
function rectWithin(outer, inner) {
|
|
923
1011
|
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;
|
|
@@ -1184,6 +1272,382 @@ function loadFonts() {
|
|
|
1184
1272
|
// src/layout/elk.ts
|
|
1185
1273
|
import ELK from "elkjs";
|
|
1186
1274
|
|
|
1275
|
+
// src/primitives/shapes.ts
|
|
1276
|
+
function roundRectPath(ctx, rect, radius) {
|
|
1277
|
+
const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
|
|
1278
|
+
const right = rect.x + rect.width;
|
|
1279
|
+
const bottom = rect.y + rect.height;
|
|
1280
|
+
ctx.beginPath();
|
|
1281
|
+
ctx.moveTo(rect.x + r, rect.y);
|
|
1282
|
+
ctx.lineTo(right - r, rect.y);
|
|
1283
|
+
ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
|
|
1284
|
+
ctx.lineTo(right, bottom - r);
|
|
1285
|
+
ctx.quadraticCurveTo(right, bottom, right - r, bottom);
|
|
1286
|
+
ctx.lineTo(rect.x + r, bottom);
|
|
1287
|
+
ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
|
|
1288
|
+
ctx.lineTo(rect.x, rect.y + r);
|
|
1289
|
+
ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
|
|
1290
|
+
ctx.closePath();
|
|
1291
|
+
}
|
|
1292
|
+
function fillAndStroke(ctx, fill, stroke) {
|
|
1293
|
+
ctx.fillStyle = fill;
|
|
1294
|
+
ctx.fill();
|
|
1295
|
+
if (stroke) {
|
|
1296
|
+
ctx.strokeStyle = stroke;
|
|
1297
|
+
ctx.stroke();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function drawRoundedRect(ctx, rect, radius, fill, stroke) {
|
|
1301
|
+
roundRectPath(ctx, rect, radius);
|
|
1302
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1303
|
+
}
|
|
1304
|
+
function drawCircle(ctx, center2, radius, fill, stroke) {
|
|
1305
|
+
ctx.beginPath();
|
|
1306
|
+
ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
|
|
1307
|
+
ctx.closePath();
|
|
1308
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1309
|
+
}
|
|
1310
|
+
function drawDiamond(ctx, bounds, fill, stroke) {
|
|
1311
|
+
const cx = bounds.x + bounds.width / 2;
|
|
1312
|
+
const cy = bounds.y + bounds.height / 2;
|
|
1313
|
+
ctx.beginPath();
|
|
1314
|
+
ctx.moveTo(cx, bounds.y);
|
|
1315
|
+
ctx.lineTo(bounds.x + bounds.width, cy);
|
|
1316
|
+
ctx.lineTo(cx, bounds.y + bounds.height);
|
|
1317
|
+
ctx.lineTo(bounds.x, cy);
|
|
1318
|
+
ctx.closePath();
|
|
1319
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1320
|
+
}
|
|
1321
|
+
function drawPill(ctx, bounds, fill, stroke) {
|
|
1322
|
+
drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
|
|
1323
|
+
}
|
|
1324
|
+
function drawEllipse(ctx, bounds, fill, stroke) {
|
|
1325
|
+
const cx = bounds.x + bounds.width / 2;
|
|
1326
|
+
const cy = bounds.y + bounds.height / 2;
|
|
1327
|
+
ctx.beginPath();
|
|
1328
|
+
ctx.ellipse(
|
|
1329
|
+
cx,
|
|
1330
|
+
cy,
|
|
1331
|
+
Math.max(0, bounds.width / 2),
|
|
1332
|
+
Math.max(0, bounds.height / 2),
|
|
1333
|
+
0,
|
|
1334
|
+
0,
|
|
1335
|
+
Math.PI * 2
|
|
1336
|
+
);
|
|
1337
|
+
ctx.closePath();
|
|
1338
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1339
|
+
}
|
|
1340
|
+
function drawCylinder(ctx, bounds, fill, stroke) {
|
|
1341
|
+
const rx = Math.max(2, bounds.width / 2);
|
|
1342
|
+
const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
|
|
1343
|
+
const cx = bounds.x + bounds.width / 2;
|
|
1344
|
+
const topCy = bounds.y + ry;
|
|
1345
|
+
const bottomCy = bounds.y + bounds.height - ry;
|
|
1346
|
+
ctx.beginPath();
|
|
1347
|
+
ctx.moveTo(bounds.x, topCy);
|
|
1348
|
+
ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
|
|
1349
|
+
ctx.lineTo(bounds.x + bounds.width, bottomCy);
|
|
1350
|
+
ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
|
|
1351
|
+
ctx.closePath();
|
|
1352
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1353
|
+
if (stroke) {
|
|
1354
|
+
ctx.beginPath();
|
|
1355
|
+
ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
|
|
1356
|
+
ctx.closePath();
|
|
1357
|
+
ctx.strokeStyle = stroke;
|
|
1358
|
+
ctx.stroke();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function drawParallelogram(ctx, bounds, fill, stroke, skew) {
|
|
1362
|
+
const maxSkew = bounds.width * 0.45;
|
|
1363
|
+
const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
|
|
1364
|
+
ctx.beginPath();
|
|
1365
|
+
ctx.moveTo(bounds.x + skewX, bounds.y);
|
|
1366
|
+
ctx.lineTo(bounds.x + bounds.width, bounds.y);
|
|
1367
|
+
ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
|
|
1368
|
+
ctx.lineTo(bounds.x, bounds.y + bounds.height);
|
|
1369
|
+
ctx.closePath();
|
|
1370
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/primitives/text.ts
|
|
1374
|
+
var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
|
|
1375
|
+
function resolveFont(requested, role) {
|
|
1376
|
+
if (SUPPORTED_FONT_FAMILIES.has(requested)) {
|
|
1377
|
+
return requested;
|
|
1378
|
+
}
|
|
1379
|
+
if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
|
|
1380
|
+
return "JetBrains Mono";
|
|
1381
|
+
}
|
|
1382
|
+
if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
|
|
1383
|
+
return "Space Grotesk";
|
|
1384
|
+
}
|
|
1385
|
+
return "Inter";
|
|
1386
|
+
}
|
|
1387
|
+
function applyFont(ctx, options) {
|
|
1388
|
+
ctx.font = `${options.weight} ${options.size}px ${options.family}`;
|
|
1389
|
+
}
|
|
1390
|
+
function wrapText(ctx, text, maxWidth, maxLines) {
|
|
1391
|
+
const trimmed = text.trim();
|
|
1392
|
+
if (!trimmed) {
|
|
1393
|
+
return { lines: [], truncated: false };
|
|
1394
|
+
}
|
|
1395
|
+
const words = trimmed.split(/\s+/u);
|
|
1396
|
+
const lines = [];
|
|
1397
|
+
let current = "";
|
|
1398
|
+
for (const word of words) {
|
|
1399
|
+
const trial = current.length > 0 ? `${current} ${word}` : word;
|
|
1400
|
+
if (ctx.measureText(trial).width <= maxWidth) {
|
|
1401
|
+
current = trial;
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
if (current.length > 0) {
|
|
1405
|
+
lines.push(current);
|
|
1406
|
+
current = word;
|
|
1407
|
+
} else {
|
|
1408
|
+
lines.push(word);
|
|
1409
|
+
current = "";
|
|
1410
|
+
}
|
|
1411
|
+
if (lines.length >= maxLines) {
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (lines.length < maxLines && current.length > 0) {
|
|
1416
|
+
lines.push(current);
|
|
1417
|
+
}
|
|
1418
|
+
const wasTruncated = lines.length >= maxLines && words.join(" ") !== lines.join(" ");
|
|
1419
|
+
if (!wasTruncated) {
|
|
1420
|
+
return { lines, truncated: false };
|
|
1421
|
+
}
|
|
1422
|
+
const lastIndex = lines.length - 1;
|
|
1423
|
+
let truncatedLine = `${lines[lastIndex]}\u2026`;
|
|
1424
|
+
while (truncatedLine.length > 1 && ctx.measureText(truncatedLine).width > maxWidth) {
|
|
1425
|
+
truncatedLine = `${truncatedLine.slice(0, -2)}\u2026`;
|
|
1426
|
+
}
|
|
1427
|
+
lines[lastIndex] = truncatedLine;
|
|
1428
|
+
return { lines, truncated: true };
|
|
1429
|
+
}
|
|
1430
|
+
function drawTextBlock(ctx, options) {
|
|
1431
|
+
applyFont(ctx, { size: options.fontSize, weight: options.fontWeight, family: options.family });
|
|
1432
|
+
const wrapped = wrapText(ctx, options.text, options.maxWidth, options.maxLines);
|
|
1433
|
+
ctx.fillStyle = options.color;
|
|
1434
|
+
for (const [index, line] of wrapped.lines.entries()) {
|
|
1435
|
+
ctx.fillText(line, options.x, options.y + index * options.lineHeight);
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
height: wrapped.lines.length * options.lineHeight,
|
|
1439
|
+
truncated: wrapped.truncated
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function drawTextLabel(ctx, text, position, options) {
|
|
1443
|
+
applyFont(ctx, { size: options.fontSize, weight: 600, family: options.fontFamily });
|
|
1444
|
+
const textWidth = Math.ceil(ctx.measureText(text).width);
|
|
1445
|
+
const rect = {
|
|
1446
|
+
x: Math.round(position.x - (textWidth + options.padding * 2) / 2),
|
|
1447
|
+
y: Math.round(position.y - (options.fontSize + options.padding * 2) / 2),
|
|
1448
|
+
width: textWidth + options.padding * 2,
|
|
1449
|
+
height: options.fontSize + options.padding * 2
|
|
1450
|
+
};
|
|
1451
|
+
drawRoundedRect(ctx, rect, options.borderRadius, options.backgroundColor);
|
|
1452
|
+
ctx.fillStyle = options.color;
|
|
1453
|
+
ctx.fillText(text, rect.x + options.padding, rect.y + rect.height - options.padding);
|
|
1454
|
+
return rect;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/renderers/flow-node.ts
|
|
1458
|
+
var BADGE_FONT_SIZE = 10;
|
|
1459
|
+
var BADGE_FONT_WEIGHT = 600;
|
|
1460
|
+
var BADGE_LETTER_SPACING = 1;
|
|
1461
|
+
var BADGE_PADDING_X = 8;
|
|
1462
|
+
var BADGE_PADDING_Y = 3;
|
|
1463
|
+
var BADGE_BORDER_RADIUS = 12;
|
|
1464
|
+
var BADGE_DEFAULT_COLOR = "#FFFFFF";
|
|
1465
|
+
var BADGE_PILL_HEIGHT = BADGE_FONT_SIZE + BADGE_PADDING_Y * 2;
|
|
1466
|
+
var BADGE_INSIDE_TOP_EXTRA = BADGE_PILL_HEIGHT + 6;
|
|
1467
|
+
function drawNodeShape(ctx, shape, bounds, fill, stroke, cornerRadius) {
|
|
1468
|
+
switch (shape) {
|
|
1469
|
+
case "box":
|
|
1470
|
+
drawRoundedRect(ctx, bounds, 0, fill, stroke);
|
|
1471
|
+
break;
|
|
1472
|
+
case "rounded-box":
|
|
1473
|
+
drawRoundedRect(ctx, bounds, cornerRadius, fill, stroke);
|
|
1474
|
+
break;
|
|
1475
|
+
case "diamond":
|
|
1476
|
+
drawDiamond(ctx, bounds, fill, stroke);
|
|
1477
|
+
break;
|
|
1478
|
+
case "circle": {
|
|
1479
|
+
const radius = Math.min(bounds.width, bounds.height) / 2;
|
|
1480
|
+
drawCircle(
|
|
1481
|
+
ctx,
|
|
1482
|
+
{ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
|
|
1483
|
+
radius,
|
|
1484
|
+
fill,
|
|
1485
|
+
stroke
|
|
1486
|
+
);
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
case "pill":
|
|
1490
|
+
drawPill(ctx, bounds, fill, stroke);
|
|
1491
|
+
break;
|
|
1492
|
+
case "cylinder":
|
|
1493
|
+
drawCylinder(ctx, bounds, fill, stroke);
|
|
1494
|
+
break;
|
|
1495
|
+
case "parallelogram":
|
|
1496
|
+
drawParallelogram(ctx, bounds, fill, stroke);
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function measureSpacedText(ctx, text, letterSpacing) {
|
|
1501
|
+
const base = ctx.measureText(text).width;
|
|
1502
|
+
const extraChars = [...text].length - 1;
|
|
1503
|
+
return extraChars > 0 ? base + extraChars * letterSpacing : base;
|
|
1504
|
+
}
|
|
1505
|
+
function drawSpacedText(ctx, text, centerX, centerY, letterSpacing) {
|
|
1506
|
+
const chars = [...text];
|
|
1507
|
+
if (chars.length === 0) return;
|
|
1508
|
+
const totalWidth = measureSpacedText(ctx, text, letterSpacing);
|
|
1509
|
+
let cursorX = centerX - totalWidth / 2;
|
|
1510
|
+
ctx.textAlign = "left";
|
|
1511
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1512
|
+
ctx.fillText(chars[i], cursorX, centerY);
|
|
1513
|
+
cursorX += ctx.measureText(chars[i]).width + (i < chars.length - 1 ? letterSpacing : 0);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
function renderBadgePill(ctx, centerX, centerY, text, textColor, background, monoFont) {
|
|
1517
|
+
ctx.save();
|
|
1518
|
+
applyFont(ctx, { size: BADGE_FONT_SIZE, weight: BADGE_FONT_WEIGHT, family: monoFont });
|
|
1519
|
+
const textWidth = measureSpacedText(ctx, text, BADGE_LETTER_SPACING);
|
|
1520
|
+
const pillWidth = textWidth + BADGE_PADDING_X * 2;
|
|
1521
|
+
const pillHeight = BADGE_PILL_HEIGHT;
|
|
1522
|
+
const pillX = centerX - pillWidth / 2;
|
|
1523
|
+
const pillY = centerY - pillHeight / 2;
|
|
1524
|
+
ctx.fillStyle = background;
|
|
1525
|
+
ctx.beginPath();
|
|
1526
|
+
ctx.roundRect(pillX, pillY, pillWidth, pillHeight, BADGE_BORDER_RADIUS);
|
|
1527
|
+
ctx.fill();
|
|
1528
|
+
ctx.fillStyle = textColor;
|
|
1529
|
+
ctx.textBaseline = "middle";
|
|
1530
|
+
applyFont(ctx, { size: BADGE_FONT_SIZE, weight: BADGE_FONT_WEIGHT, family: monoFont });
|
|
1531
|
+
drawSpacedText(ctx, text, centerX, centerY, BADGE_LETTER_SPACING);
|
|
1532
|
+
ctx.restore();
|
|
1533
|
+
return pillWidth;
|
|
1534
|
+
}
|
|
1535
|
+
function renderFlowNode(ctx, node, bounds, theme) {
|
|
1536
|
+
const fillColor = node.color ?? theme.surfaceElevated;
|
|
1537
|
+
const borderColor = node.borderColor ?? theme.border;
|
|
1538
|
+
const borderWidth = node.borderWidth ?? 2;
|
|
1539
|
+
const cornerRadius = node.cornerRadius ?? 16;
|
|
1540
|
+
const labelColor = node.labelColor ?? theme.text;
|
|
1541
|
+
const sublabelColor = node.sublabelColor ?? theme.textMuted;
|
|
1542
|
+
const labelFontSize = node.labelFontSize ?? 20;
|
|
1543
|
+
const fillOpacity = node.fillOpacity ?? 1;
|
|
1544
|
+
const hasBadge = !!node.badgeText;
|
|
1545
|
+
const badgePosition = node.badgePosition ?? "inside-top";
|
|
1546
|
+
const badgeColor = node.badgeColor ?? BADGE_DEFAULT_COLOR;
|
|
1547
|
+
const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
|
|
1548
|
+
ctx.save();
|
|
1549
|
+
ctx.lineWidth = borderWidth;
|
|
1550
|
+
if (fillOpacity < 1) {
|
|
1551
|
+
ctx.globalAlpha = node.opacity * fillOpacity;
|
|
1552
|
+
drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
|
|
1553
|
+
ctx.globalAlpha = node.opacity;
|
|
1554
|
+
drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
|
|
1555
|
+
} else {
|
|
1556
|
+
ctx.globalAlpha = node.opacity;
|
|
1557
|
+
drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
|
|
1558
|
+
}
|
|
1559
|
+
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
1560
|
+
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
1561
|
+
const monoFont = resolveFont(theme.fonts.mono, "mono");
|
|
1562
|
+
const centerX = bounds.x + bounds.width / 2;
|
|
1563
|
+
const centerY = bounds.y + bounds.height / 2;
|
|
1564
|
+
const insideTopShift = hasBadge && badgePosition === "inside-top" ? BADGE_INSIDE_TOP_EXTRA / 2 : 0;
|
|
1565
|
+
const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
|
|
1566
|
+
const sublabel2FontSize = node.sublabel2FontSize ?? 11;
|
|
1567
|
+
const sublabel2Color = node.sublabel2Color ?? sublabelColor;
|
|
1568
|
+
const lineCount = node.sublabel2 ? 3 : node.sublabel ? 2 : 1;
|
|
1569
|
+
const labelToSublabelGap = Math.max(20, sublabelFontSize + 6);
|
|
1570
|
+
const sublabelToSublabel2Gap = sublabel2FontSize + 4;
|
|
1571
|
+
let textBlockHeight;
|
|
1572
|
+
if (lineCount === 1) {
|
|
1573
|
+
textBlockHeight = labelFontSize;
|
|
1574
|
+
} else if (lineCount === 2) {
|
|
1575
|
+
textBlockHeight = labelFontSize + labelToSublabelGap;
|
|
1576
|
+
} else {
|
|
1577
|
+
textBlockHeight = labelFontSize + labelToSublabelGap + sublabelToSublabel2Gap;
|
|
1578
|
+
}
|
|
1579
|
+
const labelY = lineCount === 1 ? centerY + labelFontSize * 0.3 + insideTopShift : centerY - textBlockHeight / 2 + labelFontSize * 0.8 + insideTopShift;
|
|
1580
|
+
ctx.textAlign = "center";
|
|
1581
|
+
applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
|
|
1582
|
+
ctx.fillStyle = labelColor;
|
|
1583
|
+
ctx.fillText(node.label, centerX, labelY);
|
|
1584
|
+
let textBoundsY = bounds.y + bounds.height / 2 - 18;
|
|
1585
|
+
let textBoundsHeight = 36;
|
|
1586
|
+
if (node.sublabel) {
|
|
1587
|
+
applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
|
|
1588
|
+
ctx.fillStyle = sublabelColor;
|
|
1589
|
+
ctx.fillText(node.sublabel, centerX, labelY + labelToSublabelGap);
|
|
1590
|
+
textBoundsY = bounds.y + bounds.height / 2 - 24;
|
|
1591
|
+
textBoundsHeight = 56;
|
|
1592
|
+
}
|
|
1593
|
+
if (node.sublabel2) {
|
|
1594
|
+
applyFont(ctx, { size: sublabel2FontSize, weight: 500, family: bodyFont });
|
|
1595
|
+
ctx.fillStyle = sublabel2Color;
|
|
1596
|
+
const sublabel2Y = node.sublabel ? labelY + labelToSublabelGap + sublabelToSublabel2Gap : labelY + labelToSublabelGap;
|
|
1597
|
+
ctx.fillText(node.sublabel2, centerX, sublabel2Y);
|
|
1598
|
+
textBoundsY = bounds.y + bounds.height / 2 - 30;
|
|
1599
|
+
textBoundsHeight = 72;
|
|
1600
|
+
}
|
|
1601
|
+
if (hasBadge && node.badgeText) {
|
|
1602
|
+
if (badgePosition === "inside-top") {
|
|
1603
|
+
const badgeCenterY = bounds.y + BADGE_PILL_HEIGHT / 2 + 8;
|
|
1604
|
+
renderBadgePill(
|
|
1605
|
+
ctx,
|
|
1606
|
+
centerX,
|
|
1607
|
+
badgeCenterY,
|
|
1608
|
+
node.badgeText,
|
|
1609
|
+
badgeColor,
|
|
1610
|
+
badgeBackground,
|
|
1611
|
+
monoFont
|
|
1612
|
+
);
|
|
1613
|
+
} else {
|
|
1614
|
+
const badgeCenterY = bounds.y - BADGE_PILL_HEIGHT / 2 - 4;
|
|
1615
|
+
renderBadgePill(
|
|
1616
|
+
ctx,
|
|
1617
|
+
centerX,
|
|
1618
|
+
badgeCenterY,
|
|
1619
|
+
node.badgeText,
|
|
1620
|
+
badgeColor,
|
|
1621
|
+
badgeBackground,
|
|
1622
|
+
monoFont
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
ctx.restore();
|
|
1627
|
+
const effectiveBg = fillOpacity < 1 ? blendColorWithOpacity(fillColor, theme.background, fillOpacity) : fillColor;
|
|
1628
|
+
return [
|
|
1629
|
+
{
|
|
1630
|
+
id: `flow-node-${node.id}`,
|
|
1631
|
+
kind: "flow-node",
|
|
1632
|
+
bounds,
|
|
1633
|
+
foregroundColor: labelColor,
|
|
1634
|
+
backgroundColor: effectiveBg
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
id: `flow-node-${node.id}-label`,
|
|
1638
|
+
kind: "text",
|
|
1639
|
+
bounds: {
|
|
1640
|
+
x: bounds.x + 8,
|
|
1641
|
+
y: textBoundsY,
|
|
1642
|
+
width: bounds.width - 16,
|
|
1643
|
+
height: textBoundsHeight
|
|
1644
|
+
},
|
|
1645
|
+
foregroundColor: labelColor,
|
|
1646
|
+
backgroundColor: effectiveBg
|
|
1647
|
+
}
|
|
1648
|
+
];
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1187
1651
|
// src/layout/estimates.ts
|
|
1188
1652
|
function estimateElementHeight(element) {
|
|
1189
1653
|
switch (element.type) {
|
|
@@ -1282,33 +1746,37 @@ function computeStackLayout(elements, config, safeFrame) {
|
|
|
1282
1746
|
|
|
1283
1747
|
// src/layout/elk.ts
|
|
1284
1748
|
function estimateFlowNodeSize(node) {
|
|
1749
|
+
const badgeExtra = node.badgeText && (node.badgePosition ?? "inside-top") === "inside-top" ? BADGE_INSIDE_TOP_EXTRA : 0;
|
|
1750
|
+
const sublabel2Extra = node.sublabel2 ? (node.sublabel2FontSize ?? 11) + 4 : 0;
|
|
1751
|
+
const extra = badgeExtra + sublabel2Extra;
|
|
1285
1752
|
if (node.width && node.height) {
|
|
1286
|
-
return { width: node.width, height: node.height };
|
|
1753
|
+
return { width: node.width, height: node.height + extra };
|
|
1287
1754
|
}
|
|
1288
1755
|
if (node.width) {
|
|
1756
|
+
const baseHeight = node.shape === "diamond" || node.shape === "circle" ? node.width : 60;
|
|
1289
1757
|
return {
|
|
1290
1758
|
width: node.width,
|
|
1291
|
-
height:
|
|
1759
|
+
height: baseHeight + extra
|
|
1292
1760
|
};
|
|
1293
1761
|
}
|
|
1294
1762
|
if (node.height) {
|
|
1295
1763
|
return {
|
|
1296
1764
|
width: node.shape === "diamond" || node.shape === "circle" ? node.height : 160,
|
|
1297
|
-
height: node.height
|
|
1765
|
+
height: node.height + extra
|
|
1298
1766
|
};
|
|
1299
1767
|
}
|
|
1300
1768
|
switch (node.shape) {
|
|
1301
1769
|
case "diamond":
|
|
1302
1770
|
case "circle":
|
|
1303
|
-
return { width: 100, height: 100 };
|
|
1771
|
+
return { width: 100 + extra, height: 100 + extra };
|
|
1304
1772
|
case "pill":
|
|
1305
|
-
return { width: 180, height: 56 };
|
|
1773
|
+
return { width: 180, height: 56 + extra };
|
|
1306
1774
|
case "cylinder":
|
|
1307
|
-
return { width: 140, height: 92 };
|
|
1775
|
+
return { width: 140, height: 92 + extra };
|
|
1308
1776
|
case "parallelogram":
|
|
1309
|
-
return { width: 180, height: 72 };
|
|
1777
|
+
return { width: 180, height: 72 + extra };
|
|
1310
1778
|
default:
|
|
1311
|
-
return { width: 170, height: 64 };
|
|
1779
|
+
return { width: 170, height: 64 + extra };
|
|
1312
1780
|
}
|
|
1313
1781
|
}
|
|
1314
1782
|
function splitLayoutFrames(safeFrame, direction, hasAuxiliary) {
|
|
@@ -1426,6 +1894,40 @@ function directionToElk(direction) {
|
|
|
1426
1894
|
return "DOWN";
|
|
1427
1895
|
}
|
|
1428
1896
|
}
|
|
1897
|
+
function radialCompactionToElk(compaction) {
|
|
1898
|
+
switch (compaction) {
|
|
1899
|
+
case "radial":
|
|
1900
|
+
return "RADIAL_COMPACTION";
|
|
1901
|
+
case "wedge":
|
|
1902
|
+
return "WEDGE_COMPACTION";
|
|
1903
|
+
default:
|
|
1904
|
+
return "NONE";
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function radialSortByToElk(sortBy) {
|
|
1908
|
+
switch (sortBy) {
|
|
1909
|
+
case "connections":
|
|
1910
|
+
return "POLAR_COORDINATE";
|
|
1911
|
+
default:
|
|
1912
|
+
return "ID";
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
function buildRadialOptions(config) {
|
|
1916
|
+
const options = {};
|
|
1917
|
+
if (config.radialRoot) {
|
|
1918
|
+
options["elk.radial.centerOnRoot"] = "true";
|
|
1919
|
+
}
|
|
1920
|
+
if (config.radialRadius != null) {
|
|
1921
|
+
options["elk.radial.radius"] = String(config.radialRadius);
|
|
1922
|
+
}
|
|
1923
|
+
if (config.radialCompaction) {
|
|
1924
|
+
options["elk.radial.compaction.strategy"] = radialCompactionToElk(config.radialCompaction);
|
|
1925
|
+
}
|
|
1926
|
+
if (config.radialSortBy) {
|
|
1927
|
+
options["elk.radial.orderId"] = radialSortByToElk(config.radialSortBy);
|
|
1928
|
+
}
|
|
1929
|
+
return options;
|
|
1930
|
+
}
|
|
1429
1931
|
function fallbackForNoFlowNodes(nonFlow, safeFrame) {
|
|
1430
1932
|
const fallbackConfig = {
|
|
1431
1933
|
mode: "stack",
|
|
@@ -1461,6 +1963,11 @@ async function computeElkLayout(elements, config, safeFrame) {
|
|
|
1461
1963
|
elkNodeSizes.set(node.id, estimateFlowNodeSize(node));
|
|
1462
1964
|
}
|
|
1463
1965
|
const edgeIdToRouteKey = /* @__PURE__ */ new Map();
|
|
1966
|
+
const radialOptions = config.algorithm === "radial" ? buildRadialOptions(config) : {};
|
|
1967
|
+
const orderedFlowNodes = config.radialRoot && config.algorithm === "radial" ? [
|
|
1968
|
+
...flowNodes.filter((node) => node.id === config.radialRoot),
|
|
1969
|
+
...flowNodes.filter((node) => node.id !== config.radialRoot)
|
|
1970
|
+
] : flowNodes;
|
|
1464
1971
|
const elkGraph = {
|
|
1465
1972
|
id: "root",
|
|
1466
1973
|
layoutOptions: {
|
|
@@ -1470,9 +1977,10 @@ async function computeElkLayout(elements, config, safeFrame) {
|
|
|
1470
1977
|
"elk.layered.spacing.nodeNodeBetweenLayers": String(config.rankSpacing),
|
|
1471
1978
|
"elk.edgeRouting": edgeRoutingToElk(config.edgeRouting),
|
|
1472
1979
|
...config.aspectRatio ? { "elk.aspectRatio": String(config.aspectRatio) } : {},
|
|
1473
|
-
...config.algorithm === "stress" ? { "elk.stress.desiredEdgeLength": String(config.rankSpacing + config.nodeSpacing) } : {}
|
|
1980
|
+
...config.algorithm === "stress" ? { "elk.stress.desiredEdgeLength": String(config.rankSpacing + config.nodeSpacing) } : {},
|
|
1981
|
+
...radialOptions
|
|
1474
1982
|
},
|
|
1475
|
-
children:
|
|
1983
|
+
children: orderedFlowNodes.map((node) => {
|
|
1476
1984
|
const size = elkNodeSizes.get(node.id) ?? { width: 160, height: 60 };
|
|
1477
1985
|
return {
|
|
1478
1986
|
id: node.id,
|
|
@@ -1785,188 +2293,6 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
|
|
|
1785
2293
|
ctx.restore();
|
|
1786
2294
|
}
|
|
1787
2295
|
|
|
1788
|
-
// src/primitives/shapes.ts
|
|
1789
|
-
function roundRectPath(ctx, rect, radius) {
|
|
1790
|
-
const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
|
|
1791
|
-
const right = rect.x + rect.width;
|
|
1792
|
-
const bottom = rect.y + rect.height;
|
|
1793
|
-
ctx.beginPath();
|
|
1794
|
-
ctx.moveTo(rect.x + r, rect.y);
|
|
1795
|
-
ctx.lineTo(right - r, rect.y);
|
|
1796
|
-
ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
|
|
1797
|
-
ctx.lineTo(right, bottom - r);
|
|
1798
|
-
ctx.quadraticCurveTo(right, bottom, right - r, bottom);
|
|
1799
|
-
ctx.lineTo(rect.x + r, bottom);
|
|
1800
|
-
ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
|
|
1801
|
-
ctx.lineTo(rect.x, rect.y + r);
|
|
1802
|
-
ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
|
|
1803
|
-
ctx.closePath();
|
|
1804
|
-
}
|
|
1805
|
-
function fillAndStroke(ctx, fill, stroke) {
|
|
1806
|
-
ctx.fillStyle = fill;
|
|
1807
|
-
ctx.fill();
|
|
1808
|
-
if (stroke) {
|
|
1809
|
-
ctx.strokeStyle = stroke;
|
|
1810
|
-
ctx.stroke();
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
function drawRoundedRect(ctx, rect, radius, fill, stroke) {
|
|
1814
|
-
roundRectPath(ctx, rect, radius);
|
|
1815
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1816
|
-
}
|
|
1817
|
-
function drawCircle(ctx, center2, radius, fill, stroke) {
|
|
1818
|
-
ctx.beginPath();
|
|
1819
|
-
ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
|
|
1820
|
-
ctx.closePath();
|
|
1821
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1822
|
-
}
|
|
1823
|
-
function drawDiamond(ctx, bounds, fill, stroke) {
|
|
1824
|
-
const cx = bounds.x + bounds.width / 2;
|
|
1825
|
-
const cy = bounds.y + bounds.height / 2;
|
|
1826
|
-
ctx.beginPath();
|
|
1827
|
-
ctx.moveTo(cx, bounds.y);
|
|
1828
|
-
ctx.lineTo(bounds.x + bounds.width, cy);
|
|
1829
|
-
ctx.lineTo(cx, bounds.y + bounds.height);
|
|
1830
|
-
ctx.lineTo(bounds.x, cy);
|
|
1831
|
-
ctx.closePath();
|
|
1832
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1833
|
-
}
|
|
1834
|
-
function drawPill(ctx, bounds, fill, stroke) {
|
|
1835
|
-
drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
|
|
1836
|
-
}
|
|
1837
|
-
function drawEllipse(ctx, bounds, fill, stroke) {
|
|
1838
|
-
const cx = bounds.x + bounds.width / 2;
|
|
1839
|
-
const cy = bounds.y + bounds.height / 2;
|
|
1840
|
-
ctx.beginPath();
|
|
1841
|
-
ctx.ellipse(
|
|
1842
|
-
cx,
|
|
1843
|
-
cy,
|
|
1844
|
-
Math.max(0, bounds.width / 2),
|
|
1845
|
-
Math.max(0, bounds.height / 2),
|
|
1846
|
-
0,
|
|
1847
|
-
0,
|
|
1848
|
-
Math.PI * 2
|
|
1849
|
-
);
|
|
1850
|
-
ctx.closePath();
|
|
1851
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1852
|
-
}
|
|
1853
|
-
function drawCylinder(ctx, bounds, fill, stroke) {
|
|
1854
|
-
const rx = Math.max(2, bounds.width / 2);
|
|
1855
|
-
const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
|
|
1856
|
-
const cx = bounds.x + bounds.width / 2;
|
|
1857
|
-
const topCy = bounds.y + ry;
|
|
1858
|
-
const bottomCy = bounds.y + bounds.height - ry;
|
|
1859
|
-
ctx.beginPath();
|
|
1860
|
-
ctx.moveTo(bounds.x, topCy);
|
|
1861
|
-
ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
|
|
1862
|
-
ctx.lineTo(bounds.x + bounds.width, bottomCy);
|
|
1863
|
-
ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
|
|
1864
|
-
ctx.closePath();
|
|
1865
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1866
|
-
if (stroke) {
|
|
1867
|
-
ctx.beginPath();
|
|
1868
|
-
ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
|
|
1869
|
-
ctx.closePath();
|
|
1870
|
-
ctx.strokeStyle = stroke;
|
|
1871
|
-
ctx.stroke();
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
function drawParallelogram(ctx, bounds, fill, stroke, skew) {
|
|
1875
|
-
const maxSkew = bounds.width * 0.45;
|
|
1876
|
-
const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
|
|
1877
|
-
ctx.beginPath();
|
|
1878
|
-
ctx.moveTo(bounds.x + skewX, bounds.y);
|
|
1879
|
-
ctx.lineTo(bounds.x + bounds.width, bounds.y);
|
|
1880
|
-
ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
|
|
1881
|
-
ctx.lineTo(bounds.x, bounds.y + bounds.height);
|
|
1882
|
-
ctx.closePath();
|
|
1883
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
// src/primitives/text.ts
|
|
1887
|
-
var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
|
|
1888
|
-
function resolveFont(requested, role) {
|
|
1889
|
-
if (SUPPORTED_FONT_FAMILIES.has(requested)) {
|
|
1890
|
-
return requested;
|
|
1891
|
-
}
|
|
1892
|
-
if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
|
|
1893
|
-
return "JetBrains Mono";
|
|
1894
|
-
}
|
|
1895
|
-
if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
|
|
1896
|
-
return "Space Grotesk";
|
|
1897
|
-
}
|
|
1898
|
-
return "Inter";
|
|
1899
|
-
}
|
|
1900
|
-
function applyFont(ctx, options) {
|
|
1901
|
-
ctx.font = `${options.weight} ${options.size}px ${options.family}`;
|
|
1902
|
-
}
|
|
1903
|
-
function wrapText(ctx, text, maxWidth, maxLines) {
|
|
1904
|
-
const trimmed = text.trim();
|
|
1905
|
-
if (!trimmed) {
|
|
1906
|
-
return { lines: [], truncated: false };
|
|
1907
|
-
}
|
|
1908
|
-
const words = trimmed.split(/\s+/u);
|
|
1909
|
-
const lines = [];
|
|
1910
|
-
let current = "";
|
|
1911
|
-
for (const word of words) {
|
|
1912
|
-
const trial = current.length > 0 ? `${current} ${word}` : word;
|
|
1913
|
-
if (ctx.measureText(trial).width <= maxWidth) {
|
|
1914
|
-
current = trial;
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
if (current.length > 0) {
|
|
1918
|
-
lines.push(current);
|
|
1919
|
-
current = word;
|
|
1920
|
-
} else {
|
|
1921
|
-
lines.push(word);
|
|
1922
|
-
current = "";
|
|
1923
|
-
}
|
|
1924
|
-
if (lines.length >= maxLines) {
|
|
1925
|
-
break;
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
if (lines.length < maxLines && current.length > 0) {
|
|
1929
|
-
lines.push(current);
|
|
1930
|
-
}
|
|
1931
|
-
const wasTruncated = lines.length >= maxLines && words.join(" ") !== lines.join(" ");
|
|
1932
|
-
if (!wasTruncated) {
|
|
1933
|
-
return { lines, truncated: false };
|
|
1934
|
-
}
|
|
1935
|
-
const lastIndex = lines.length - 1;
|
|
1936
|
-
let truncatedLine = `${lines[lastIndex]}\u2026`;
|
|
1937
|
-
while (truncatedLine.length > 1 && ctx.measureText(truncatedLine).width > maxWidth) {
|
|
1938
|
-
truncatedLine = `${truncatedLine.slice(0, -2)}\u2026`;
|
|
1939
|
-
}
|
|
1940
|
-
lines[lastIndex] = truncatedLine;
|
|
1941
|
-
return { lines, truncated: true };
|
|
1942
|
-
}
|
|
1943
|
-
function drawTextBlock(ctx, options) {
|
|
1944
|
-
applyFont(ctx, { size: options.fontSize, weight: options.fontWeight, family: options.family });
|
|
1945
|
-
const wrapped = wrapText(ctx, options.text, options.maxWidth, options.maxLines);
|
|
1946
|
-
ctx.fillStyle = options.color;
|
|
1947
|
-
for (const [index, line] of wrapped.lines.entries()) {
|
|
1948
|
-
ctx.fillText(line, options.x, options.y + index * options.lineHeight);
|
|
1949
|
-
}
|
|
1950
|
-
return {
|
|
1951
|
-
height: wrapped.lines.length * options.lineHeight,
|
|
1952
|
-
truncated: wrapped.truncated
|
|
1953
|
-
};
|
|
1954
|
-
}
|
|
1955
|
-
function drawTextLabel(ctx, text, position, options) {
|
|
1956
|
-
applyFont(ctx, { size: options.fontSize, weight: 600, family: options.fontFamily });
|
|
1957
|
-
const textWidth = Math.ceil(ctx.measureText(text).width);
|
|
1958
|
-
const rect = {
|
|
1959
|
-
x: Math.round(position.x - (textWidth + options.padding * 2) / 2),
|
|
1960
|
-
y: Math.round(position.y - (options.fontSize + options.padding * 2) / 2),
|
|
1961
|
-
width: textWidth + options.padding * 2,
|
|
1962
|
-
height: options.fontSize + options.padding * 2
|
|
1963
|
-
};
|
|
1964
|
-
drawRoundedRect(ctx, rect, options.borderRadius, options.backgroundColor);
|
|
1965
|
-
ctx.fillStyle = options.color;
|
|
1966
|
-
ctx.fillText(text, rect.x + options.padding, rect.y + rect.height - options.padding);
|
|
1967
|
-
return rect;
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
2296
|
// src/renderers/card.ts
|
|
1971
2297
|
var TONE_BADGE_COLORS = {
|
|
1972
2298
|
neutral: "#334B83",
|
|
@@ -3294,92 +3620,6 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
3294
3620
|
return rendered;
|
|
3295
3621
|
}
|
|
3296
3622
|
|
|
3297
|
-
// src/renderers/flow-node.ts
|
|
3298
|
-
function renderFlowNode(ctx, node, bounds, theme) {
|
|
3299
|
-
const fillColor = node.color ?? theme.surfaceElevated;
|
|
3300
|
-
const borderColor = node.borderColor ?? theme.border;
|
|
3301
|
-
const borderWidth = node.borderWidth ?? 2;
|
|
3302
|
-
const cornerRadius = node.cornerRadius ?? 16;
|
|
3303
|
-
const labelColor = node.labelColor ?? theme.text;
|
|
3304
|
-
const sublabelColor = node.sublabelColor ?? theme.textMuted;
|
|
3305
|
-
const labelFontSize = node.labelFontSize ?? 20;
|
|
3306
|
-
ctx.save();
|
|
3307
|
-
ctx.globalAlpha = node.opacity;
|
|
3308
|
-
ctx.lineWidth = borderWidth;
|
|
3309
|
-
switch (node.shape) {
|
|
3310
|
-
case "box":
|
|
3311
|
-
drawRoundedRect(ctx, bounds, 0, fillColor, borderColor);
|
|
3312
|
-
break;
|
|
3313
|
-
case "rounded-box":
|
|
3314
|
-
drawRoundedRect(ctx, bounds, cornerRadius, fillColor, borderColor);
|
|
3315
|
-
break;
|
|
3316
|
-
case "diamond":
|
|
3317
|
-
drawDiamond(ctx, bounds, fillColor, borderColor);
|
|
3318
|
-
break;
|
|
3319
|
-
case "circle": {
|
|
3320
|
-
const radius = Math.min(bounds.width, bounds.height) / 2;
|
|
3321
|
-
drawCircle(
|
|
3322
|
-
ctx,
|
|
3323
|
-
{ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
|
|
3324
|
-
radius,
|
|
3325
|
-
fillColor,
|
|
3326
|
-
borderColor
|
|
3327
|
-
);
|
|
3328
|
-
break;
|
|
3329
|
-
}
|
|
3330
|
-
case "pill":
|
|
3331
|
-
drawPill(ctx, bounds, fillColor, borderColor);
|
|
3332
|
-
break;
|
|
3333
|
-
case "cylinder":
|
|
3334
|
-
drawCylinder(ctx, bounds, fillColor, borderColor);
|
|
3335
|
-
break;
|
|
3336
|
-
case "parallelogram":
|
|
3337
|
-
drawParallelogram(ctx, bounds, fillColor, borderColor);
|
|
3338
|
-
break;
|
|
3339
|
-
}
|
|
3340
|
-
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
3341
|
-
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
3342
|
-
const centerX = bounds.x + bounds.width / 2;
|
|
3343
|
-
const centerY = bounds.y + bounds.height / 2;
|
|
3344
|
-
const labelY = node.sublabel ? centerY - Math.max(4, labelFontSize * 0.2) : centerY + labelFontSize * 0.3;
|
|
3345
|
-
ctx.textAlign = "center";
|
|
3346
|
-
applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
|
|
3347
|
-
ctx.fillStyle = labelColor;
|
|
3348
|
-
ctx.fillText(node.label, centerX, labelY);
|
|
3349
|
-
let textBoundsY = bounds.y + bounds.height / 2 - 18;
|
|
3350
|
-
let textBoundsHeight = 36;
|
|
3351
|
-
if (node.sublabel) {
|
|
3352
|
-
const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
|
|
3353
|
-
applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
|
|
3354
|
-
ctx.fillStyle = sublabelColor;
|
|
3355
|
-
ctx.fillText(node.sublabel, centerX, labelY + Math.max(20, sublabelFontSize + 6));
|
|
3356
|
-
textBoundsY = bounds.y + bounds.height / 2 - 24;
|
|
3357
|
-
textBoundsHeight = 56;
|
|
3358
|
-
}
|
|
3359
|
-
ctx.restore();
|
|
3360
|
-
return [
|
|
3361
|
-
{
|
|
3362
|
-
id: `flow-node-${node.id}`,
|
|
3363
|
-
kind: "flow-node",
|
|
3364
|
-
bounds,
|
|
3365
|
-
foregroundColor: labelColor,
|
|
3366
|
-
backgroundColor: fillColor
|
|
3367
|
-
},
|
|
3368
|
-
{
|
|
3369
|
-
id: `flow-node-${node.id}-label`,
|
|
3370
|
-
kind: "text",
|
|
3371
|
-
bounds: {
|
|
3372
|
-
x: bounds.x + 8,
|
|
3373
|
-
y: textBoundsY,
|
|
3374
|
-
width: bounds.width - 16,
|
|
3375
|
-
height: textBoundsHeight
|
|
3376
|
-
},
|
|
3377
|
-
foregroundColor: labelColor,
|
|
3378
|
-
backgroundColor: fillColor
|
|
3379
|
-
}
|
|
3380
|
-
];
|
|
3381
|
-
}
|
|
3382
|
-
|
|
3383
3623
|
// src/renderers/image.ts
|
|
3384
3624
|
import { loadImage } from "@napi-rs/canvas";
|
|
3385
3625
|
function roundedRectPath2(ctx, bounds, radius) {
|