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