@spectratools/graphic-designer-cli 0.9.0 → 0.11.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 CHANGED
@@ -820,6 +820,21 @@ var drawLineSchema = z2.object({
820
820
  opacity: z2.number().min(0).max(1).default(1),
821
821
  shadow: drawShadowSchema.optional()
822
822
  }).strict();
823
+ var drawArcSchema = z2.object({
824
+ type: z2.literal("arc"),
825
+ center: z2.object({
826
+ x: z2.number(),
827
+ y: z2.number()
828
+ }).strict(),
829
+ radius: z2.number().positive(),
830
+ startAngle: z2.number(),
831
+ endAngle: z2.number(),
832
+ color: colorHexSchema2.default("#FFFFFF"),
833
+ width: z2.number().min(0.5).max(32).default(2),
834
+ dash: z2.array(z2.number()).max(6).optional(),
835
+ opacity: z2.number().min(0).max(1).default(1),
836
+ shadow: drawShadowSchema.optional()
837
+ }).strict();
823
838
  var drawPointSchema = z2.object({
824
839
  x: z2.number(),
825
840
  y: z2.number()
@@ -904,6 +919,7 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
904
919
  drawCircleSchema,
905
920
  drawTextSchema,
906
921
  drawLineSchema,
922
+ drawArcSchema,
907
923
  drawBezierSchema,
908
924
  drawPathSchema,
909
925
  drawBadgeSchema,
@@ -1034,17 +1050,21 @@ var connectionElementSchema = z2.object({
1034
1050
  from: z2.string().min(1).max(120),
1035
1051
  to: z2.string().min(1).max(120),
1036
1052
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
1037
- strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
1053
+ /** @deprecated Use `style` instead. */
1054
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).optional(),
1038
1055
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
1039
1056
  label: z2.string().min(1).max(200).optional(),
1040
1057
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
1041
1058
  color: colorHexSchema2.optional(),
1059
+ fromColor: colorHexSchema2.optional(),
1060
+ toColor: colorHexSchema2.optional(),
1042
1061
  width: z2.number().min(0.5).max(10).optional(),
1043
1062
  strokeWidth: z2.number().min(0.5).max(10).default(2),
1044
1063
  arrowSize: z2.number().min(4).max(32).optional(),
1045
1064
  arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
1046
1065
  opacity: z2.number().min(0).max(1).default(1),
1047
- routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
1066
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc", "straight"]).default("auto"),
1067
+ curveMode: z2.enum(["normal", "ellipse"]).default("normal"),
1048
1068
  tension: z2.number().min(0.1).max(0.8).default(0.35),
1049
1069
  fromAnchor: anchorHintSchema.optional(),
1050
1070
  toAnchor: anchorHintSchema.optional()
@@ -1137,7 +1157,11 @@ var autoLayoutConfigSchema = z2.object({
1137
1157
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
1138
1158
  radialSortBy: z2.enum(["id", "connections"]).optional(),
1139
1159
  /** Explicit center used by curve/arc connection routing. */
1140
- diagramCenter: diagramCenterSchema.optional()
1160
+ diagramCenter: diagramCenterSchema.optional(),
1161
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1162
+ ellipseRx: z2.number().positive().optional(),
1163
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1164
+ ellipseRy: z2.number().positive().optional()
1141
1165
  }).strict();
1142
1166
  var gridLayoutConfigSchema = z2.object({
1143
1167
  mode: z2.literal("grid"),
@@ -1147,7 +1171,11 @@ var gridLayoutConfigSchema = z2.object({
1147
1171
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
1148
1172
  equalHeight: z2.boolean().default(false),
1149
1173
  /** Explicit center used by curve/arc connection routing. */
1150
- diagramCenter: diagramCenterSchema.optional()
1174
+ diagramCenter: diagramCenterSchema.optional(),
1175
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1176
+ ellipseRx: z2.number().positive().optional(),
1177
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1178
+ ellipseRy: z2.number().positive().optional()
1151
1179
  }).strict();
1152
1180
  var stackLayoutConfigSchema = z2.object({
1153
1181
  mode: z2.literal("stack"),
@@ -1155,7 +1183,11 @@ var stackLayoutConfigSchema = z2.object({
1155
1183
  gap: z2.number().int().min(0).max(256).default(24),
1156
1184
  alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
1157
1185
  /** Explicit center used by curve/arc connection routing. */
1158
- diagramCenter: diagramCenterSchema.optional()
1186
+ diagramCenter: diagramCenterSchema.optional(),
1187
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1188
+ ellipseRx: z2.number().positive().optional(),
1189
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1190
+ ellipseRy: z2.number().positive().optional()
1159
1191
  }).strict();
1160
1192
  var manualPositionSchema = z2.object({
1161
1193
  x: z2.number().int(),
@@ -1167,7 +1199,11 @@ var manualLayoutConfigSchema = z2.object({
1167
1199
  mode: z2.literal("manual"),
1168
1200
  positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
1169
1201
  /** Explicit center used by curve/arc connection routing. */
1170
- diagramCenter: diagramCenterSchema.optional()
1202
+ diagramCenter: diagramCenterSchema.optional(),
1203
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1204
+ ellipseRx: z2.number().positive().optional(),
1205
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1206
+ ellipseRy: z2.number().positive().optional()
1171
1207
  }).strict();
1172
1208
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
1173
1209
  autoLayoutConfigSchema,
@@ -1232,7 +1268,11 @@ var diagramElementSchema = z2.discriminatedUnion("type", [
1232
1268
  var diagramLayoutSchema = z2.object({
1233
1269
  mode: z2.enum(["manual", "auto"]).default("manual"),
1234
1270
  positions: z2.record(z2.string(), diagramPositionSchema).optional(),
1235
- diagramCenter: diagramCenterSchema.optional()
1271
+ diagramCenter: diagramCenterSchema.optional(),
1272
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1273
+ ellipseRx: z2.number().positive().optional(),
1274
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1275
+ ellipseRy: z2.number().positive().optional()
1236
1276
  }).strict();
1237
1277
  var diagramSpecSchema = z2.object({
1238
1278
  version: z2.literal(1),
@@ -2760,12 +2800,12 @@ var MACOS_DOTS = [
2760
2800
  { fill: "#27C93F", stroke: "#1AAB29" }
2761
2801
  ];
2762
2802
  function drawMacosDots(ctx, x, y) {
2763
- for (const [index, dot2] of MACOS_DOTS.entries()) {
2803
+ for (const [index, dot] of MACOS_DOTS.entries()) {
2764
2804
  ctx.beginPath();
2765
2805
  ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
2766
2806
  ctx.closePath();
2767
- ctx.fillStyle = dot2.fill;
2768
- ctx.strokeStyle = dot2.stroke;
2807
+ ctx.fillStyle = dot.fill;
2808
+ ctx.strokeStyle = dot.stroke;
2769
2809
  ctx.lineWidth = DOT_STROKE_WIDTH;
2770
2810
  ctx.fill();
2771
2811
  ctx.stroke();
@@ -3170,16 +3210,6 @@ function drawBezier(ctx, points, style) {
3170
3210
  ctx.quadraticCurveTo(penultimate.x, penultimate.y, last.x, last.y);
3171
3211
  ctx.stroke();
3172
3212
  }
3173
- function drawOrthogonalPath(ctx, from, to, style) {
3174
- const midX = (from.x + to.x) / 2;
3175
- applyLineStyle(ctx, style);
3176
- ctx.beginPath();
3177
- ctx.moveTo(from.x, from.y);
3178
- ctx.lineTo(midX, from.y);
3179
- ctx.lineTo(midX, to.y);
3180
- ctx.lineTo(to.x, to.y);
3181
- ctx.stroke();
3182
- }
3183
3213
 
3184
3214
  // src/renderers/connection.ts
3185
3215
  var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
@@ -3262,56 +3292,71 @@ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, to
3262
3292
  const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
3263
3293
  return [p0, cp1, cp2, p3];
3264
3294
  }
3265
- function dot(a, b) {
3266
- return a.x * b.x + a.y * b.y;
3267
- }
3268
- function localToWorld(origin, axisX, axisY, local) {
3269
- return {
3270
- x: origin.x + axisX.x * local.x + axisY.x * local.y,
3271
- y: origin.y + axisX.y * local.x + axisY.y * local.y
3272
- };
3295
+ function inferEllipseParams(nodeBounds, explicitCenter, explicitRx, explicitRy) {
3296
+ if (nodeBounds.length === 0) {
3297
+ return {
3298
+ cx: explicitCenter?.x ?? 0,
3299
+ cy: explicitCenter?.y ?? 0,
3300
+ rx: explicitRx ?? 1,
3301
+ ry: explicitRy ?? 1
3302
+ };
3303
+ }
3304
+ const centers = nodeBounds.map(rectCenter);
3305
+ const cx = explicitCenter?.x ?? centers.reduce((sum, c) => sum + c.x, 0) / centers.length;
3306
+ const cy = explicitCenter?.y ?? centers.reduce((sum, c) => sum + c.y, 0) / centers.length;
3307
+ const rx = explicitRx ?? Math.max(1, ...centers.map((c) => Math.abs(c.x - cx)));
3308
+ const ry = explicitRy ?? Math.max(1, ...centers.map((c) => Math.abs(c.y - cy)));
3309
+ return { cx, cy, rx, ry };
3273
3310
  }
3274
- function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
3311
+ function ellipseRoute(fromBounds, toBounds, ellipse, fromAnchor, toAnchor) {
3275
3312
  const fromCenter = rectCenter(fromBounds);
3276
3313
  const toCenter = rectCenter(toBounds);
3277
- const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
3278
- const end = resolveAnchor(toBounds, toAnchor, fromCenter);
3279
- const chord = { x: end.x - start.x, y: end.y - start.y };
3280
- const chordLength = Math.hypot(chord.x, chord.y);
3281
- if (chordLength < 1e-6) {
3282
- const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
3283
- return [
3284
- [start, start, mid, mid],
3285
- [mid, mid, end, end]
3286
- ];
3287
- }
3288
- const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
3289
- let axisY = { x: -axisX.y, y: axisX.x };
3290
- const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
3291
- const outwardHint = outwardNormal(midpoint, diagramCenter);
3292
- if (dot(axisY, outwardHint) < 0) {
3293
- axisY = { x: -axisY.x, y: -axisY.y };
3294
- }
3295
- const semiMajor = chordLength / 2;
3296
- const semiMinor = Math.max(12, chordLength * tension * 0.75);
3297
- const p0Local = { x: -semiMajor, y: 0 };
3298
- const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
3299
- const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
3300
- const pMidLocal = { x: 0, y: semiMinor };
3301
- const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
3302
- const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
3303
- const p3Local = { x: semiMajor, y: 0 };
3304
- const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
3305
- const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
3306
- const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
3307
- const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
3308
- const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
3309
- const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
3310
- const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
3311
- return [
3312
- [p0, cp1, cp2, pMid],
3313
- [pMid, cp3, cp4, p3]
3314
- ];
3314
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
3315
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
3316
+ const theta1 = Math.atan2(
3317
+ (fromCenter.y - ellipse.cy) / ellipse.ry,
3318
+ (fromCenter.x - ellipse.cx) / ellipse.rx
3319
+ );
3320
+ const theta2 = Math.atan2(
3321
+ (toCenter.y - ellipse.cy) / ellipse.ry,
3322
+ (toCenter.x - ellipse.cx) / ellipse.rx
3323
+ );
3324
+ let angularSpan = theta2 - theta1;
3325
+ while (angularSpan > Math.PI) angularSpan -= 2 * Math.PI;
3326
+ while (angularSpan <= -Math.PI) angularSpan += 2 * Math.PI;
3327
+ const absSpan = Math.abs(angularSpan);
3328
+ const kappa = absSpan < 1e-6 ? 0 : 4 / 3 * Math.tan(absSpan / 4);
3329
+ const tangent1 = {
3330
+ x: -ellipse.rx * Math.sin(theta1),
3331
+ y: ellipse.ry * Math.cos(theta1)
3332
+ };
3333
+ const tangent2 = {
3334
+ x: -ellipse.rx * Math.sin(theta2),
3335
+ y: ellipse.ry * Math.cos(theta2)
3336
+ };
3337
+ const len1 = Math.hypot(tangent1.x, tangent1.y) || 1;
3338
+ const len2 = Math.hypot(tangent2.x, tangent2.y) || 1;
3339
+ const norm1 = { x: tangent1.x / len1, y: tangent1.y / len1 };
3340
+ const norm2 = { x: tangent2.x / len2, y: tangent2.y / len2 };
3341
+ const chordLength = Math.hypot(p3.x - p0.x, p3.y - p0.y);
3342
+ const cpDistance = chordLength * kappa * 0.5;
3343
+ const sign = angularSpan >= 0 ? 1 : -1;
3344
+ const cp1 = {
3345
+ x: p0.x + norm1.x * cpDistance * sign,
3346
+ y: p0.y + norm1.y * cpDistance * sign
3347
+ };
3348
+ const cp2 = {
3349
+ x: p3.x - norm2.x * cpDistance * sign,
3350
+ y: p3.y - norm2.y * cpDistance * sign
3351
+ };
3352
+ return [p0, cp1, cp2, p3];
3353
+ }
3354
+ function straightRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
3355
+ const fromC = rectCenter(fromBounds);
3356
+ const toC = rectCenter(toBounds);
3357
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
3358
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
3359
+ return [p0, p3];
3315
3360
  }
3316
3361
  function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
3317
3362
  const fromC = rectCenter(fromBounds);
@@ -3357,15 +3402,6 @@ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
3357
3402
  }
3358
3403
  return void 0;
3359
3404
  }
3360
- function pointAlongArc(route, t) {
3361
- const [first, second] = route;
3362
- if (t <= 0.5) {
3363
- const localT2 = Math.max(0, Math.min(1, t * 2));
3364
- return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
3365
- }
3366
- const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
3367
- return bezierPointAt(second[0], second[1], second[2], second[3], localT);
3368
- }
3369
3405
  function computeDiagramCenter(nodeBounds, canvasCenter) {
3370
3406
  if (nodeBounds.length === 0) {
3371
3407
  return canvasCenter ?? { x: 0, y: 0 };
@@ -3419,11 +3455,36 @@ function pointAlongPolyline(points, t) {
3419
3455
  }
3420
3456
  return points[points.length - 1];
3421
3457
  }
3422
- function drawCubicInterpolatedPath(ctx, points, style) {
3458
+ function createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor) {
3459
+ const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
3460
+ gradient.addColorStop(0, fromColor);
3461
+ gradient.addColorStop(0.5, baseColor);
3462
+ gradient.addColorStop(1, toColor);
3463
+ return gradient;
3464
+ }
3465
+ function resolveConnectionStroke(ctx, start, end, fromColor, baseColor, toColor) {
3466
+ if (!fromColor || !toColor) {
3467
+ return baseColor;
3468
+ }
3469
+ return createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor);
3470
+ }
3471
+ function drawOrthogonalPathWithStroke(ctx, from, to, style, stroke) {
3472
+ const midX = (from.x + to.x) / 2;
3473
+ ctx.strokeStyle = stroke;
3474
+ ctx.lineWidth = style.width;
3475
+ ctx.setLineDash(style.dash ?? []);
3476
+ ctx.beginPath();
3477
+ ctx.moveTo(from.x, from.y);
3478
+ ctx.lineTo(midX, from.y);
3479
+ ctx.lineTo(midX, to.y);
3480
+ ctx.lineTo(to.x, to.y);
3481
+ ctx.stroke();
3482
+ }
3483
+ function drawCubicInterpolatedPath(ctx, points, style, stroke) {
3423
3484
  if (points.length < 2) {
3424
3485
  return;
3425
3486
  }
3426
- ctx.strokeStyle = style.color;
3487
+ ctx.strokeStyle = stroke;
3427
3488
  ctx.lineWidth = style.width;
3428
3489
  ctx.setLineDash(style.dash ?? []);
3429
3490
  ctx.beginPath();
@@ -3463,8 +3524,19 @@ function polylineBounds(points) {
3463
3524
  };
3464
3525
  }
3465
3526
  function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
3466
- const routing = conn.routing ?? "auto";
3467
- const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
3527
+ let routing = conn.routing ?? "auto";
3528
+ let curveMode = conn.curveMode ?? "normal";
3529
+ if (conn.strokeStyle !== void 0) {
3530
+ console.warn("connection.strokeStyle is deprecated, use style instead");
3531
+ }
3532
+ if (routing === "arc") {
3533
+ console.warn(
3534
+ "connection routing: 'arc' is deprecated. Use routing: 'curve' with curveMode: 'ellipse' instead."
3535
+ );
3536
+ routing = "curve";
3537
+ curveMode = "ellipse";
3538
+ }
3539
+ const strokeStyle = conn.style ?? conn.strokeStyle ?? "solid";
3468
3540
  const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
3469
3541
  const tension = conn.tension ?? 0.35;
3470
3542
  const dash = dashFromStyle(strokeStyle);
@@ -3486,15 +3558,31 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3486
3558
  ctx.globalAlpha = conn.opacity;
3487
3559
  const arrowPlacement = conn.arrowPlacement ?? "endpoint";
3488
3560
  if (routing === "curve") {
3489
- const [p0, cp1, cp2, p3] = curveRoute(
3490
- fromBounds,
3491
- toBounds,
3492
- diagramCenter,
3493
- tension,
3494
- conn.fromAnchor,
3495
- conn.toAnchor
3496
- );
3497
- ctx.strokeStyle = style.color;
3561
+ let p0;
3562
+ let cp1;
3563
+ let cp2;
3564
+ let p3;
3565
+ if (curveMode === "ellipse") {
3566
+ const ellipse = options?.ellipseParams ?? inferEllipseParams([fromBounds, toBounds]);
3567
+ [p0, cp1, cp2, p3] = ellipseRoute(
3568
+ fromBounds,
3569
+ toBounds,
3570
+ ellipse,
3571
+ conn.fromAnchor,
3572
+ conn.toAnchor
3573
+ );
3574
+ } else {
3575
+ [p0, cp1, cp2, p3] = curveRoute(
3576
+ fromBounds,
3577
+ toBounds,
3578
+ diagramCenter,
3579
+ tension,
3580
+ conn.fromAnchor,
3581
+ conn.toAnchor
3582
+ );
3583
+ }
3584
+ const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
3585
+ ctx.strokeStyle = stroke;
3498
3586
  ctx.lineWidth = style.width;
3499
3587
  ctx.setLineDash(style.dash ?? []);
3500
3588
  ctx.beginPath();
@@ -3525,50 +3613,22 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3525
3613
  }
3526
3614
  }
3527
3615
  }
3528
- } else if (routing === "arc") {
3529
- const [first, second] = arcRoute(
3530
- fromBounds,
3531
- toBounds,
3532
- diagramCenter,
3533
- tension,
3534
- conn.fromAnchor,
3535
- conn.toAnchor
3536
- );
3537
- const [p0, cp1, cp2, pMid] = first;
3538
- const [, cp3, cp4, p3] = second;
3539
- ctx.strokeStyle = style.color;
3616
+ } else if (routing === "straight") {
3617
+ const [p0, p3] = straightRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
3618
+ const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
3619
+ ctx.strokeStyle = stroke;
3540
3620
  ctx.lineWidth = style.width;
3541
3621
  ctx.setLineDash(style.dash ?? []);
3542
3622
  ctx.beginPath();
3543
3623
  ctx.moveTo(p0.x, p0.y);
3544
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
3545
- ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
3624
+ ctx.lineTo(p3.x, p3.y);
3546
3625
  ctx.stroke();
3547
- linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
3626
+ linePoints = [p0, p3];
3548
3627
  startPoint = p0;
3549
3628
  endPoint = p3;
3550
- startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3551
- endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
3552
- labelPoint = pointAlongArc([first, second], labelT);
3553
- if (arrowPlacement === "boundary") {
3554
- if (conn.arrow === "end" || conn.arrow === "both") {
3555
- const [, s_cp3, s_cp4, s_p3] = second;
3556
- const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
3557
- if (tEnd !== void 0) {
3558
- endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3559
- const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3560
- endAngle = Math.atan2(tangent.y, tangent.x);
3561
- }
3562
- }
3563
- if (conn.arrow === "start" || conn.arrow === "both") {
3564
- const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
3565
- if (tStart !== void 0) {
3566
- startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
3567
- const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
3568
- startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
3569
- }
3570
- }
3571
- }
3629
+ startAngle = Math.atan2(p0.y - p3.y, p0.x - p3.x);
3630
+ endAngle = Math.atan2(p3.y - p0.y, p3.x - p0.x);
3631
+ labelPoint = pointAlongPolyline(linePoints, labelT);
3572
3632
  } else {
3573
3633
  const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
3574
3634
  const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
@@ -3579,10 +3639,18 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3579
3639
  endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
3580
3640
  startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
3581
3641
  endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
3642
+ const stroke = resolveConnectionStroke(
3643
+ ctx,
3644
+ startPoint,
3645
+ endPoint,
3646
+ conn.fromColor,
3647
+ style.color,
3648
+ conn.toColor
3649
+ );
3582
3650
  if (useElkRoute) {
3583
- drawCubicInterpolatedPath(ctx, linePoints, style);
3651
+ drawCubicInterpolatedPath(ctx, linePoints, style, stroke);
3584
3652
  } else {
3585
- drawOrthogonalPath(ctx, startPoint, endPoint, style);
3653
+ drawOrthogonalPathWithStroke(ctx, startPoint, endPoint, style, stroke);
3586
3654
  }
3587
3655
  labelPoint = pointAlongPolyline(linePoints, labelT);
3588
3656
  }
@@ -3887,6 +3955,9 @@ function measureTextBounds(ctx, options) {
3887
3955
  function angleBetween(from, to) {
3888
3956
  return Math.atan2(to.y - from.y, to.x - from.x);
3889
3957
  }
3958
+ function degreesToRadians(angle) {
3959
+ return angle * Math.PI / 180;
3960
+ }
3890
3961
  function pathBounds(operations) {
3891
3962
  let minX = Number.POSITIVE_INFINITY;
3892
3963
  let minY = Number.POSITIVE_INFINITY;
@@ -4124,6 +4195,34 @@ function renderDrawCommands(ctx, commands, theme) {
4124
4195
  });
4125
4196
  break;
4126
4197
  }
4198
+ case "arc": {
4199
+ const startAngle = degreesToRadians(command.startAngle);
4200
+ const endAngle = degreesToRadians(command.endAngle);
4201
+ withOpacity(ctx, command.opacity, () => {
4202
+ applyDrawShadow(ctx, command.shadow);
4203
+ ctx.beginPath();
4204
+ ctx.setLineDash(command.dash ?? []);
4205
+ ctx.lineWidth = command.width;
4206
+ ctx.strokeStyle = command.color;
4207
+ ctx.arc(command.center.x, command.center.y, command.radius, startAngle, endAngle);
4208
+ ctx.stroke();
4209
+ });
4210
+ rendered.push({
4211
+ id,
4212
+ kind: "draw",
4213
+ bounds: expandRect(
4214
+ {
4215
+ x: command.center.x - command.radius,
4216
+ y: command.center.y - command.radius,
4217
+ width: command.radius * 2,
4218
+ height: command.radius * 2
4219
+ },
4220
+ command.width / 2
4221
+ ),
4222
+ foregroundColor: command.color
4223
+ });
4224
+ break;
4225
+ }
4127
4226
  case "bezier": {
4128
4227
  const points = command.points;
4129
4228
  withOpacity(ctx, command.opacity, () => {
@@ -4767,6 +4866,18 @@ async function renderDesign(input, options = {}) {
4767
4866
  const specHash = computeSpecHash(spec);
4768
4867
  const generatorVersion = options.generatorVersion ?? DEFAULT_GENERATOR_VERSION;
4769
4868
  const renderedAt = options.renderedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4869
+ const iteration = options.iteration;
4870
+ if (iteration) {
4871
+ if (!Number.isInteger(iteration.iteration) || iteration.iteration <= 0) {
4872
+ throw new Error("Iteration metadata requires iteration to be a positive integer.");
4873
+ }
4874
+ if (iteration.maxIterations != null && (!Number.isInteger(iteration.maxIterations) || iteration.maxIterations <= 0)) {
4875
+ throw new Error("Iteration metadata requires maxIterations to be a positive integer.");
4876
+ }
4877
+ if (iteration.maxIterations != null && iteration.maxIterations < iteration.iteration) {
4878
+ throw new Error("Iteration metadata requires maxIterations to be >= iteration.");
4879
+ }
4880
+ }
4770
4881
  const renderScale = resolveRenderScale(spec);
4771
4882
  const canvas = createCanvas(spec.canvas.width * renderScale, spec.canvas.height * renderScale);
4772
4883
  const ctx = canvas.getContext("2d");
@@ -4946,10 +5057,19 @@ async function renderDesign(input, options = {}) {
4946
5057
  break;
4947
5058
  }
4948
5059
  }
4949
- const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
4950
- spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
4951
- { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
5060
+ const nodeBounds = spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null);
5061
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(nodeBounds, { x: spec.canvas.width / 2, y: spec.canvas.height / 2 });
5062
+ const layoutEllipseRx = "ellipseRx" in spec.layout ? spec.layout.ellipseRx : void 0;
5063
+ const layoutEllipseRy = "ellipseRy" in spec.layout ? spec.layout.ellipseRy : void 0;
5064
+ const hasEllipseConnections = spec.elements.some(
5065
+ (el) => el.type === "connection" && (el.curveMode === "ellipse" || el.routing === "arc")
4952
5066
  );
5067
+ const ellipseParams = hasEllipseConnections ? inferEllipseParams(
5068
+ nodeBounds,
5069
+ spec.layout.diagramCenter ?? diagramCenter,
5070
+ layoutEllipseRx,
5071
+ layoutEllipseRy
5072
+ ) : void 0;
4953
5073
  for (const element of spec.elements) {
4954
5074
  if (element.type !== "connection") {
4955
5075
  continue;
@@ -4963,7 +5083,15 @@ async function renderDesign(input, options = {}) {
4963
5083
  }
4964
5084
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
4965
5085
  elements.push(
4966
- ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
5086
+ ...renderConnection(
5087
+ ctx,
5088
+ element,
5089
+ fromRect,
5090
+ toRect,
5091
+ theme,
5092
+ edgeRoute,
5093
+ ellipseParams ? { diagramCenter, ellipseParams } : { diagramCenter }
5094
+ )
4967
5095
  );
4968
5096
  }
4969
5097
  if (footerRect && spec.footer) {
@@ -5007,7 +5135,8 @@ async function renderDesign(input, options = {}) {
5007
5135
  layout: {
5008
5136
  safeFrame,
5009
5137
  elements
5010
- }
5138
+ },
5139
+ ...iteration ? { iteration } : {}
5011
5140
  };
5012
5141
  return {
5013
5142
  png: pngBuffer,
@@ -5314,6 +5443,12 @@ var renderOutputSchema = z3.object({
5314
5443
  artifactHash: z3.string(),
5315
5444
  specHash: z3.string(),
5316
5445
  layoutMode: z3.string(),
5446
+ iteration: z3.object({
5447
+ current: z3.number().int().positive(),
5448
+ max: z3.number().int().positive(),
5449
+ isLast: z3.boolean(),
5450
+ notes: z3.string().optional()
5451
+ }).optional(),
5317
5452
  qa: z3.object({
5318
5453
  pass: z3.boolean(),
5319
5454
  issueCount: z3.number(),
@@ -5400,8 +5535,30 @@ function readCodeRange(code, start, end) {
5400
5535
  const lines = code.split(/\r?\n/u);
5401
5536
  return lines.slice(start - 1, end).join("\n");
5402
5537
  }
5538
+ function parseIterationMeta(options) {
5539
+ if (options.iteration == null) {
5540
+ if (options.maxIterations != null || options.iterationNotes || options.previousHash) {
5541
+ throw new Error(
5542
+ "--iteration is required when using --max-iterations, --iteration-notes, or --previous-hash."
5543
+ );
5544
+ }
5545
+ return void 0;
5546
+ }
5547
+ if (options.maxIterations != null && options.maxIterations < options.iteration) {
5548
+ throw new Error("--max-iterations must be greater than or equal to --iteration.");
5549
+ }
5550
+ return {
5551
+ iteration: options.iteration,
5552
+ ...options.maxIterations != null ? { maxIterations: options.maxIterations } : {},
5553
+ ...options.iterationNotes ? { notes: options.iterationNotes } : {},
5554
+ ...options.previousHash ? { previousHash: options.previousHash } : {}
5555
+ };
5556
+ }
5403
5557
  async function runRenderPipeline(spec, options) {
5404
- const renderResult = await renderDesign(spec, { generatorVersion: pkg.version });
5558
+ const renderResult = await renderDesign(spec, {
5559
+ generatorVersion: pkg.version,
5560
+ ...options.iteration ? { iteration: options.iteration } : {}
5561
+ });
5405
5562
  const written = await writeRenderArtifacts(renderResult, options.out);
5406
5563
  const specPath = options.specOut ? resolve4(options.specOut) : specPathFor(written.metadataPath);
5407
5564
  await mkdir2(dirname3(specPath), { recursive: true });
@@ -5418,6 +5575,14 @@ async function runRenderPipeline(spec, options) {
5418
5575
  artifactHash: written.metadata.artifactHash,
5419
5576
  specHash: written.metadata.specHash,
5420
5577
  layoutMode: spec.layout.mode,
5578
+ ...written.metadata.iteration ? {
5579
+ iteration: {
5580
+ current: written.metadata.iteration.iteration,
5581
+ max: written.metadata.iteration.maxIterations ?? written.metadata.iteration.iteration,
5582
+ isLast: (written.metadata.iteration.maxIterations ?? written.metadata.iteration.iteration) === written.metadata.iteration.iteration,
5583
+ ...written.metadata.iteration.notes ? { notes: written.metadata.iteration.notes } : {}
5584
+ }
5585
+ } : {},
5421
5586
  qa: {
5422
5587
  pass: qa.pass,
5423
5588
  issueCount: qa.issues.length,
@@ -5431,6 +5596,10 @@ cli.command("render", {
5431
5596
  spec: z3.string().describe('Path to DesignSpec JSON file (or "-" to read JSON from stdin)'),
5432
5597
  out: z3.string().describe("Output file path (.png) or output directory"),
5433
5598
  specOut: z3.string().optional().describe("Optional explicit output path for normalized spec JSON"),
5599
+ iteration: z3.number().int().positive().optional().describe("Optional iteration number for iterative workflows (1-indexed)"),
5600
+ iterationNotes: z3.string().optional().describe("Optional notes for the current iteration metadata"),
5601
+ maxIterations: z3.number().int().positive().optional().describe("Optional maximum planned iteration count"),
5602
+ previousHash: z3.string().optional().describe("Optional artifact hash from the previous iteration"),
5434
5603
  allowQaFail: z3.boolean().default(false).describe("Allow render success even if QA fails")
5435
5604
  }),
5436
5605
  output: renderOutputSchema,
@@ -5445,9 +5614,26 @@ cli.command("render", {
5445
5614
  ],
5446
5615
  async run(c) {
5447
5616
  const spec = parseDesignSpec(await readJson(c.options.spec));
5617
+ let iteration;
5618
+ try {
5619
+ iteration = parseIterationMeta({
5620
+ ...c.options.iteration != null ? { iteration: c.options.iteration } : {},
5621
+ ...c.options.maxIterations != null ? { maxIterations: c.options.maxIterations } : {},
5622
+ ...c.options.iterationNotes ? { iterationNotes: c.options.iterationNotes } : {},
5623
+ ...c.options.previousHash ? { previousHash: c.options.previousHash } : {}
5624
+ });
5625
+ } catch (error) {
5626
+ const message = error instanceof Error ? error.message : String(error);
5627
+ return c.error({
5628
+ code: "INVALID_ITERATION_OPTIONS",
5629
+ message,
5630
+ retryable: false
5631
+ });
5632
+ }
5448
5633
  const runReport = await runRenderPipeline(spec, {
5449
5634
  out: c.options.out,
5450
- ...c.options.specOut ? { specOut: c.options.specOut } : {}
5635
+ ...c.options.specOut ? { specOut: c.options.specOut } : {},
5636
+ ...iteration ? { iteration } : {}
5451
5637
  });
5452
5638
  if (!runReport.qa.pass && !c.options.allowQaFail) {
5453
5639
  return c.error({