@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/index.js CHANGED
@@ -829,6 +829,21 @@ var drawLineSchema = z2.object({
829
829
  opacity: z2.number().min(0).max(1).default(1),
830
830
  shadow: drawShadowSchema.optional()
831
831
  }).strict();
832
+ var drawArcSchema = z2.object({
833
+ type: z2.literal("arc"),
834
+ center: z2.object({
835
+ x: z2.number(),
836
+ y: z2.number()
837
+ }).strict(),
838
+ radius: z2.number().positive(),
839
+ startAngle: z2.number(),
840
+ endAngle: z2.number(),
841
+ color: colorHexSchema2.default("#FFFFFF"),
842
+ width: z2.number().min(0.5).max(32).default(2),
843
+ dash: z2.array(z2.number()).max(6).optional(),
844
+ opacity: z2.number().min(0).max(1).default(1),
845
+ shadow: drawShadowSchema.optional()
846
+ }).strict();
832
847
  var drawPointSchema = z2.object({
833
848
  x: z2.number(),
834
849
  y: z2.number()
@@ -913,6 +928,7 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
913
928
  drawCircleSchema,
914
929
  drawTextSchema,
915
930
  drawLineSchema,
931
+ drawArcSchema,
916
932
  drawBezierSchema,
917
933
  drawPathSchema,
918
934
  drawBadgeSchema,
@@ -1044,17 +1060,21 @@ var connectionElementSchema = z2.object({
1044
1060
  from: z2.string().min(1).max(120),
1045
1061
  to: z2.string().min(1).max(120),
1046
1062
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
1047
- strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
1063
+ /** @deprecated Use `style` instead. */
1064
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).optional(),
1048
1065
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
1049
1066
  label: z2.string().min(1).max(200).optional(),
1050
1067
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
1051
1068
  color: colorHexSchema2.optional(),
1069
+ fromColor: colorHexSchema2.optional(),
1070
+ toColor: colorHexSchema2.optional(),
1052
1071
  width: z2.number().min(0.5).max(10).optional(),
1053
1072
  strokeWidth: z2.number().min(0.5).max(10).default(2),
1054
1073
  arrowSize: z2.number().min(4).max(32).optional(),
1055
1074
  arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
1056
1075
  opacity: z2.number().min(0).max(1).default(1),
1057
- routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
1076
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc", "straight"]).default("auto"),
1077
+ curveMode: z2.enum(["normal", "ellipse"]).default("normal"),
1058
1078
  tension: z2.number().min(0.1).max(0.8).default(0.35),
1059
1079
  fromAnchor: anchorHintSchema.optional(),
1060
1080
  toAnchor: anchorHintSchema.optional()
@@ -1147,7 +1167,11 @@ var autoLayoutConfigSchema = z2.object({
1147
1167
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
1148
1168
  radialSortBy: z2.enum(["id", "connections"]).optional(),
1149
1169
  /** Explicit center used by curve/arc connection routing. */
1150
- diagramCenter: diagramCenterSchema.optional()
1170
+ diagramCenter: diagramCenterSchema.optional(),
1171
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1172
+ ellipseRx: z2.number().positive().optional(),
1173
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1174
+ ellipseRy: z2.number().positive().optional()
1151
1175
  }).strict();
1152
1176
  var gridLayoutConfigSchema = z2.object({
1153
1177
  mode: z2.literal("grid"),
@@ -1157,7 +1181,11 @@ var gridLayoutConfigSchema = z2.object({
1157
1181
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
1158
1182
  equalHeight: z2.boolean().default(false),
1159
1183
  /** Explicit center used by curve/arc connection routing. */
1160
- diagramCenter: diagramCenterSchema.optional()
1184
+ diagramCenter: diagramCenterSchema.optional(),
1185
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1186
+ ellipseRx: z2.number().positive().optional(),
1187
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1188
+ ellipseRy: z2.number().positive().optional()
1161
1189
  }).strict();
1162
1190
  var stackLayoutConfigSchema = z2.object({
1163
1191
  mode: z2.literal("stack"),
@@ -1165,7 +1193,11 @@ var stackLayoutConfigSchema = z2.object({
1165
1193
  gap: z2.number().int().min(0).max(256).default(24),
1166
1194
  alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
1167
1195
  /** Explicit center used by curve/arc connection routing. */
1168
- diagramCenter: diagramCenterSchema.optional()
1196
+ diagramCenter: diagramCenterSchema.optional(),
1197
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1198
+ ellipseRx: z2.number().positive().optional(),
1199
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1200
+ ellipseRy: z2.number().positive().optional()
1169
1201
  }).strict();
1170
1202
  var manualPositionSchema = z2.object({
1171
1203
  x: z2.number().int(),
@@ -1177,7 +1209,11 @@ var manualLayoutConfigSchema = z2.object({
1177
1209
  mode: z2.literal("manual"),
1178
1210
  positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
1179
1211
  /** Explicit center used by curve/arc connection routing. */
1180
- diagramCenter: diagramCenterSchema.optional()
1212
+ diagramCenter: diagramCenterSchema.optional(),
1213
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1214
+ ellipseRx: z2.number().positive().optional(),
1215
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1216
+ ellipseRy: z2.number().positive().optional()
1181
1217
  }).strict();
1182
1218
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
1183
1219
  autoLayoutConfigSchema,
@@ -1242,7 +1278,11 @@ var diagramElementSchema = z2.discriminatedUnion("type", [
1242
1278
  var diagramLayoutSchema = z2.object({
1243
1279
  mode: z2.enum(["manual", "auto"]).default("manual"),
1244
1280
  positions: z2.record(z2.string(), diagramPositionSchema).optional(),
1245
- diagramCenter: diagramCenterSchema.optional()
1281
+ diagramCenter: diagramCenterSchema.optional(),
1282
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1283
+ ellipseRx: z2.number().positive().optional(),
1284
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1285
+ ellipseRy: z2.number().positive().optional()
1246
1286
  }).strict();
1247
1287
  var diagramSpecSchema = z2.object({
1248
1288
  version: z2.literal(1),
@@ -3187,16 +3227,6 @@ function drawBezier(ctx, points, style) {
3187
3227
  ctx.quadraticCurveTo(penultimate.x, penultimate.y, last.x, last.y);
3188
3228
  ctx.stroke();
3189
3229
  }
3190
- function drawOrthogonalPath(ctx, from, to, style) {
3191
- const midX = (from.x + to.x) / 2;
3192
- applyLineStyle(ctx, style);
3193
- ctx.beginPath();
3194
- ctx.moveTo(from.x, from.y);
3195
- ctx.lineTo(midX, from.y);
3196
- ctx.lineTo(midX, to.y);
3197
- ctx.lineTo(to.x, to.y);
3198
- ctx.stroke();
3199
- }
3200
3230
 
3201
3231
  // src/renderers/connection.ts
3202
3232
  var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
@@ -3330,6 +3360,72 @@ function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAn
3330
3360
  [pMid, cp3, cp4, p3]
3331
3361
  ];
3332
3362
  }
3363
+ function inferEllipseParams(nodeBounds, explicitCenter, explicitRx, explicitRy) {
3364
+ if (nodeBounds.length === 0) {
3365
+ return {
3366
+ cx: explicitCenter?.x ?? 0,
3367
+ cy: explicitCenter?.y ?? 0,
3368
+ rx: explicitRx ?? 1,
3369
+ ry: explicitRy ?? 1
3370
+ };
3371
+ }
3372
+ const centers = nodeBounds.map(rectCenter);
3373
+ const cx = explicitCenter?.x ?? centers.reduce((sum, c) => sum + c.x, 0) / centers.length;
3374
+ const cy = explicitCenter?.y ?? centers.reduce((sum, c) => sum + c.y, 0) / centers.length;
3375
+ const rx = explicitRx ?? Math.max(1, ...centers.map((c) => Math.abs(c.x - cx)));
3376
+ const ry = explicitRy ?? Math.max(1, ...centers.map((c) => Math.abs(c.y - cy)));
3377
+ return { cx, cy, rx, ry };
3378
+ }
3379
+ function ellipseRoute(fromBounds, toBounds, ellipse, fromAnchor, toAnchor) {
3380
+ const fromCenter = rectCenter(fromBounds);
3381
+ const toCenter = rectCenter(toBounds);
3382
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
3383
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
3384
+ const theta1 = Math.atan2(
3385
+ (fromCenter.y - ellipse.cy) / ellipse.ry,
3386
+ (fromCenter.x - ellipse.cx) / ellipse.rx
3387
+ );
3388
+ const theta2 = Math.atan2(
3389
+ (toCenter.y - ellipse.cy) / ellipse.ry,
3390
+ (toCenter.x - ellipse.cx) / ellipse.rx
3391
+ );
3392
+ let angularSpan = theta2 - theta1;
3393
+ while (angularSpan > Math.PI) angularSpan -= 2 * Math.PI;
3394
+ while (angularSpan <= -Math.PI) angularSpan += 2 * Math.PI;
3395
+ const absSpan = Math.abs(angularSpan);
3396
+ const kappa = absSpan < 1e-6 ? 0 : 4 / 3 * Math.tan(absSpan / 4);
3397
+ const tangent1 = {
3398
+ x: -ellipse.rx * Math.sin(theta1),
3399
+ y: ellipse.ry * Math.cos(theta1)
3400
+ };
3401
+ const tangent2 = {
3402
+ x: -ellipse.rx * Math.sin(theta2),
3403
+ y: ellipse.ry * Math.cos(theta2)
3404
+ };
3405
+ const len1 = Math.hypot(tangent1.x, tangent1.y) || 1;
3406
+ const len2 = Math.hypot(tangent2.x, tangent2.y) || 1;
3407
+ const norm1 = { x: tangent1.x / len1, y: tangent1.y / len1 };
3408
+ const norm2 = { x: tangent2.x / len2, y: tangent2.y / len2 };
3409
+ const chordLength = Math.hypot(p3.x - p0.x, p3.y - p0.y);
3410
+ const cpDistance = chordLength * kappa * 0.5;
3411
+ const sign = angularSpan >= 0 ? 1 : -1;
3412
+ const cp1 = {
3413
+ x: p0.x + norm1.x * cpDistance * sign,
3414
+ y: p0.y + norm1.y * cpDistance * sign
3415
+ };
3416
+ const cp2 = {
3417
+ x: p3.x - norm2.x * cpDistance * sign,
3418
+ y: p3.y - norm2.y * cpDistance * sign
3419
+ };
3420
+ return [p0, cp1, cp2, p3];
3421
+ }
3422
+ function straightRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
3423
+ const fromC = rectCenter(fromBounds);
3424
+ const toC = rectCenter(toBounds);
3425
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
3426
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
3427
+ return [p0, p3];
3428
+ }
3333
3429
  function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
3334
3430
  const fromC = rectCenter(fromBounds);
3335
3431
  const toC = rectCenter(toBounds);
@@ -3374,15 +3470,6 @@ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
3374
3470
  }
3375
3471
  return void 0;
3376
3472
  }
3377
- function pointAlongArc(route, t) {
3378
- const [first, second] = route;
3379
- if (t <= 0.5) {
3380
- const localT2 = Math.max(0, Math.min(1, t * 2));
3381
- return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
3382
- }
3383
- const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
3384
- return bezierPointAt(second[0], second[1], second[2], second[3], localT);
3385
- }
3386
3473
  function computeDiagramCenter(nodeBounds, canvasCenter) {
3387
3474
  if (nodeBounds.length === 0) {
3388
3475
  return canvasCenter ?? { x: 0, y: 0 };
@@ -3436,11 +3523,36 @@ function pointAlongPolyline(points, t) {
3436
3523
  }
3437
3524
  return points[points.length - 1];
3438
3525
  }
3439
- function drawCubicInterpolatedPath(ctx, points, style) {
3526
+ function createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor) {
3527
+ const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
3528
+ gradient.addColorStop(0, fromColor);
3529
+ gradient.addColorStop(0.5, baseColor);
3530
+ gradient.addColorStop(1, toColor);
3531
+ return gradient;
3532
+ }
3533
+ function resolveConnectionStroke(ctx, start, end, fromColor, baseColor, toColor) {
3534
+ if (!fromColor || !toColor) {
3535
+ return baseColor;
3536
+ }
3537
+ return createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor);
3538
+ }
3539
+ function drawOrthogonalPathWithStroke(ctx, from, to, style, stroke) {
3540
+ const midX = (from.x + to.x) / 2;
3541
+ ctx.strokeStyle = stroke;
3542
+ ctx.lineWidth = style.width;
3543
+ ctx.setLineDash(style.dash ?? []);
3544
+ ctx.beginPath();
3545
+ ctx.moveTo(from.x, from.y);
3546
+ ctx.lineTo(midX, from.y);
3547
+ ctx.lineTo(midX, to.y);
3548
+ ctx.lineTo(to.x, to.y);
3549
+ ctx.stroke();
3550
+ }
3551
+ function drawCubicInterpolatedPath(ctx, points, style, stroke) {
3440
3552
  if (points.length < 2) {
3441
3553
  return;
3442
3554
  }
3443
- ctx.strokeStyle = style.color;
3555
+ ctx.strokeStyle = stroke;
3444
3556
  ctx.lineWidth = style.width;
3445
3557
  ctx.setLineDash(style.dash ?? []);
3446
3558
  ctx.beginPath();
@@ -3480,8 +3592,19 @@ function polylineBounds(points) {
3480
3592
  };
3481
3593
  }
3482
3594
  function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
3483
- const routing = conn.routing ?? "auto";
3484
- const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
3595
+ let routing = conn.routing ?? "auto";
3596
+ let curveMode = conn.curveMode ?? "normal";
3597
+ if (conn.strokeStyle !== void 0) {
3598
+ console.warn("connection.strokeStyle is deprecated, use style instead");
3599
+ }
3600
+ if (routing === "arc") {
3601
+ console.warn(
3602
+ "connection routing: 'arc' is deprecated. Use routing: 'curve' with curveMode: 'ellipse' instead."
3603
+ );
3604
+ routing = "curve";
3605
+ curveMode = "ellipse";
3606
+ }
3607
+ const strokeStyle = conn.style ?? conn.strokeStyle ?? "solid";
3485
3608
  const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
3486
3609
  const tension = conn.tension ?? 0.35;
3487
3610
  const dash = dashFromStyle(strokeStyle);
@@ -3503,15 +3626,31 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3503
3626
  ctx.globalAlpha = conn.opacity;
3504
3627
  const arrowPlacement = conn.arrowPlacement ?? "endpoint";
3505
3628
  if (routing === "curve") {
3506
- const [p0, cp1, cp2, p3] = curveRoute(
3507
- fromBounds,
3508
- toBounds,
3509
- diagramCenter,
3510
- tension,
3511
- conn.fromAnchor,
3512
- conn.toAnchor
3513
- );
3514
- ctx.strokeStyle = style.color;
3629
+ let p0;
3630
+ let cp1;
3631
+ let cp2;
3632
+ let p3;
3633
+ if (curveMode === "ellipse") {
3634
+ const ellipse = options?.ellipseParams ?? inferEllipseParams([fromBounds, toBounds]);
3635
+ [p0, cp1, cp2, p3] = ellipseRoute(
3636
+ fromBounds,
3637
+ toBounds,
3638
+ ellipse,
3639
+ conn.fromAnchor,
3640
+ conn.toAnchor
3641
+ );
3642
+ } else {
3643
+ [p0, cp1, cp2, p3] = curveRoute(
3644
+ fromBounds,
3645
+ toBounds,
3646
+ diagramCenter,
3647
+ tension,
3648
+ conn.fromAnchor,
3649
+ conn.toAnchor
3650
+ );
3651
+ }
3652
+ const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
3653
+ ctx.strokeStyle = stroke;
3515
3654
  ctx.lineWidth = style.width;
3516
3655
  ctx.setLineDash(style.dash ?? []);
3517
3656
  ctx.beginPath();
@@ -3542,50 +3681,22 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3542
3681
  }
3543
3682
  }
3544
3683
  }
3545
- } else if (routing === "arc") {
3546
- const [first, second] = arcRoute(
3547
- fromBounds,
3548
- toBounds,
3549
- diagramCenter,
3550
- tension,
3551
- conn.fromAnchor,
3552
- conn.toAnchor
3553
- );
3554
- const [p0, cp1, cp2, pMid] = first;
3555
- const [, cp3, cp4, p3] = second;
3556
- ctx.strokeStyle = style.color;
3684
+ } else if (routing === "straight") {
3685
+ const [p0, p3] = straightRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
3686
+ const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
3687
+ ctx.strokeStyle = stroke;
3557
3688
  ctx.lineWidth = style.width;
3558
3689
  ctx.setLineDash(style.dash ?? []);
3559
3690
  ctx.beginPath();
3560
3691
  ctx.moveTo(p0.x, p0.y);
3561
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
3562
- ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
3692
+ ctx.lineTo(p3.x, p3.y);
3563
3693
  ctx.stroke();
3564
- linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
3694
+ linePoints = [p0, p3];
3565
3695
  startPoint = p0;
3566
3696
  endPoint = p3;
3567
- startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3568
- endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
3569
- labelPoint = pointAlongArc([first, second], labelT);
3570
- if (arrowPlacement === "boundary") {
3571
- if (conn.arrow === "end" || conn.arrow === "both") {
3572
- const [, s_cp3, s_cp4, s_p3] = second;
3573
- const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
3574
- if (tEnd !== void 0) {
3575
- endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3576
- const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3577
- endAngle = Math.atan2(tangent.y, tangent.x);
3578
- }
3579
- }
3580
- if (conn.arrow === "start" || conn.arrow === "both") {
3581
- const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
3582
- if (tStart !== void 0) {
3583
- startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
3584
- const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
3585
- startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
3586
- }
3587
- }
3588
- }
3697
+ startAngle = Math.atan2(p0.y - p3.y, p0.x - p3.x);
3698
+ endAngle = Math.atan2(p3.y - p0.y, p3.x - p0.x);
3699
+ labelPoint = pointAlongPolyline(linePoints, labelT);
3589
3700
  } else {
3590
3701
  const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
3591
3702
  const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
@@ -3596,10 +3707,18 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3596
3707
  endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
3597
3708
  startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
3598
3709
  endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
3710
+ const stroke = resolveConnectionStroke(
3711
+ ctx,
3712
+ startPoint,
3713
+ endPoint,
3714
+ conn.fromColor,
3715
+ style.color,
3716
+ conn.toColor
3717
+ );
3599
3718
  if (useElkRoute) {
3600
- drawCubicInterpolatedPath(ctx, linePoints, style);
3719
+ drawCubicInterpolatedPath(ctx, linePoints, style, stroke);
3601
3720
  } else {
3602
- drawOrthogonalPath(ctx, startPoint, endPoint, style);
3721
+ drawOrthogonalPathWithStroke(ctx, startPoint, endPoint, style, stroke);
3603
3722
  }
3604
3723
  labelPoint = pointAlongPolyline(linePoints, labelT);
3605
3724
  }
@@ -3904,6 +4023,9 @@ function measureTextBounds(ctx, options) {
3904
4023
  function angleBetween(from, to) {
3905
4024
  return Math.atan2(to.y - from.y, to.x - from.x);
3906
4025
  }
4026
+ function degreesToRadians(angle) {
4027
+ return angle * Math.PI / 180;
4028
+ }
3907
4029
  function pathBounds(operations) {
3908
4030
  let minX = Number.POSITIVE_INFINITY;
3909
4031
  let minY = Number.POSITIVE_INFINITY;
@@ -4141,6 +4263,34 @@ function renderDrawCommands(ctx, commands, theme) {
4141
4263
  });
4142
4264
  break;
4143
4265
  }
4266
+ case "arc": {
4267
+ const startAngle = degreesToRadians(command.startAngle);
4268
+ const endAngle = degreesToRadians(command.endAngle);
4269
+ withOpacity(ctx, command.opacity, () => {
4270
+ applyDrawShadow(ctx, command.shadow);
4271
+ ctx.beginPath();
4272
+ ctx.setLineDash(command.dash ?? []);
4273
+ ctx.lineWidth = command.width;
4274
+ ctx.strokeStyle = command.color;
4275
+ ctx.arc(command.center.x, command.center.y, command.radius, startAngle, endAngle);
4276
+ ctx.stroke();
4277
+ });
4278
+ rendered.push({
4279
+ id,
4280
+ kind: "draw",
4281
+ bounds: expandRect(
4282
+ {
4283
+ x: command.center.x - command.radius,
4284
+ y: command.center.y - command.radius,
4285
+ width: command.radius * 2,
4286
+ height: command.radius * 2
4287
+ },
4288
+ command.width / 2
4289
+ ),
4290
+ foregroundColor: command.color
4291
+ });
4292
+ break;
4293
+ }
4144
4294
  case "bezier": {
4145
4295
  const points = command.points;
4146
4296
  withOpacity(ctx, command.opacity, () => {
@@ -4784,6 +4934,18 @@ async function renderDesign(input, options = {}) {
4784
4934
  const specHash = computeSpecHash(spec);
4785
4935
  const generatorVersion = options.generatorVersion ?? DEFAULT_GENERATOR_VERSION;
4786
4936
  const renderedAt = options.renderedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4937
+ const iteration = options.iteration;
4938
+ if (iteration) {
4939
+ if (!Number.isInteger(iteration.iteration) || iteration.iteration <= 0) {
4940
+ throw new Error("Iteration metadata requires iteration to be a positive integer.");
4941
+ }
4942
+ if (iteration.maxIterations != null && (!Number.isInteger(iteration.maxIterations) || iteration.maxIterations <= 0)) {
4943
+ throw new Error("Iteration metadata requires maxIterations to be a positive integer.");
4944
+ }
4945
+ if (iteration.maxIterations != null && iteration.maxIterations < iteration.iteration) {
4946
+ throw new Error("Iteration metadata requires maxIterations to be >= iteration.");
4947
+ }
4948
+ }
4787
4949
  const renderScale = resolveRenderScale(spec);
4788
4950
  const canvas = createCanvas(spec.canvas.width * renderScale, spec.canvas.height * renderScale);
4789
4951
  const ctx = canvas.getContext("2d");
@@ -4963,10 +5125,19 @@ async function renderDesign(input, options = {}) {
4963
5125
  break;
4964
5126
  }
4965
5127
  }
4966
- const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
4967
- spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
4968
- { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
5128
+ const nodeBounds = spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null);
5129
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(nodeBounds, { x: spec.canvas.width / 2, y: spec.canvas.height / 2 });
5130
+ const layoutEllipseRx = "ellipseRx" in spec.layout ? spec.layout.ellipseRx : void 0;
5131
+ const layoutEllipseRy = "ellipseRy" in spec.layout ? spec.layout.ellipseRy : void 0;
5132
+ const hasEllipseConnections = spec.elements.some(
5133
+ (el) => el.type === "connection" && (el.curveMode === "ellipse" || el.routing === "arc")
4969
5134
  );
5135
+ const ellipseParams = hasEllipseConnections ? inferEllipseParams(
5136
+ nodeBounds,
5137
+ spec.layout.diagramCenter ?? diagramCenter,
5138
+ layoutEllipseRx,
5139
+ layoutEllipseRy
5140
+ ) : void 0;
4970
5141
  for (const element of spec.elements) {
4971
5142
  if (element.type !== "connection") {
4972
5143
  continue;
@@ -4980,7 +5151,15 @@ async function renderDesign(input, options = {}) {
4980
5151
  }
4981
5152
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
4982
5153
  elements.push(
4983
- ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
5154
+ ...renderConnection(
5155
+ ctx,
5156
+ element,
5157
+ fromRect,
5158
+ toRect,
5159
+ theme,
5160
+ edgeRoute,
5161
+ ellipseParams ? { diagramCenter, ellipseParams } : { diagramCenter }
5162
+ )
4984
5163
  );
4985
5164
  }
4986
5165
  if (footerRect && spec.footer) {
@@ -5024,7 +5203,8 @@ async function renderDesign(input, options = {}) {
5024
5203
  layout: {
5025
5204
  safeFrame,
5026
5205
  elements
5027
- }
5206
+ },
5207
+ ...iteration ? { iteration } : {}
5028
5208
  };
5029
5209
  return {
5030
5210
  png: pngBuffer,
@@ -5331,6 +5511,12 @@ var renderOutputSchema = z3.object({
5331
5511
  artifactHash: z3.string(),
5332
5512
  specHash: z3.string(),
5333
5513
  layoutMode: z3.string(),
5514
+ iteration: z3.object({
5515
+ current: z3.number().int().positive(),
5516
+ max: z3.number().int().positive(),
5517
+ isLast: z3.boolean(),
5518
+ notes: z3.string().optional()
5519
+ }).optional(),
5334
5520
  qa: z3.object({
5335
5521
  pass: z3.boolean(),
5336
5522
  issueCount: z3.number(),
@@ -5417,8 +5603,30 @@ function readCodeRange(code, start, end) {
5417
5603
  const lines = code.split(/\r?\n/u);
5418
5604
  return lines.slice(start - 1, end).join("\n");
5419
5605
  }
5606
+ function parseIterationMeta(options) {
5607
+ if (options.iteration == null) {
5608
+ if (options.maxIterations != null || options.iterationNotes || options.previousHash) {
5609
+ throw new Error(
5610
+ "--iteration is required when using --max-iterations, --iteration-notes, or --previous-hash."
5611
+ );
5612
+ }
5613
+ return void 0;
5614
+ }
5615
+ if (options.maxIterations != null && options.maxIterations < options.iteration) {
5616
+ throw new Error("--max-iterations must be greater than or equal to --iteration.");
5617
+ }
5618
+ return {
5619
+ iteration: options.iteration,
5620
+ ...options.maxIterations != null ? { maxIterations: options.maxIterations } : {},
5621
+ ...options.iterationNotes ? { notes: options.iterationNotes } : {},
5622
+ ...options.previousHash ? { previousHash: options.previousHash } : {}
5623
+ };
5624
+ }
5420
5625
  async function runRenderPipeline(spec, options) {
5421
- const renderResult = await renderDesign(spec, { generatorVersion: pkg.version });
5626
+ const renderResult = await renderDesign(spec, {
5627
+ generatorVersion: pkg.version,
5628
+ ...options.iteration ? { iteration: options.iteration } : {}
5629
+ });
5422
5630
  const written = await writeRenderArtifacts(renderResult, options.out);
5423
5631
  const specPath = options.specOut ? resolve4(options.specOut) : specPathFor(written.metadataPath);
5424
5632
  await mkdir2(dirname3(specPath), { recursive: true });
@@ -5435,6 +5643,14 @@ async function runRenderPipeline(spec, options) {
5435
5643
  artifactHash: written.metadata.artifactHash,
5436
5644
  specHash: written.metadata.specHash,
5437
5645
  layoutMode: spec.layout.mode,
5646
+ ...written.metadata.iteration ? {
5647
+ iteration: {
5648
+ current: written.metadata.iteration.iteration,
5649
+ max: written.metadata.iteration.maxIterations ?? written.metadata.iteration.iteration,
5650
+ isLast: (written.metadata.iteration.maxIterations ?? written.metadata.iteration.iteration) === written.metadata.iteration.iteration,
5651
+ ...written.metadata.iteration.notes ? { notes: written.metadata.iteration.notes } : {}
5652
+ }
5653
+ } : {},
5438
5654
  qa: {
5439
5655
  pass: qa.pass,
5440
5656
  issueCount: qa.issues.length,
@@ -5448,6 +5664,10 @@ cli.command("render", {
5448
5664
  spec: z3.string().describe('Path to DesignSpec JSON file (or "-" to read JSON from stdin)'),
5449
5665
  out: z3.string().describe("Output file path (.png) or output directory"),
5450
5666
  specOut: z3.string().optional().describe("Optional explicit output path for normalized spec JSON"),
5667
+ iteration: z3.number().int().positive().optional().describe("Optional iteration number for iterative workflows (1-indexed)"),
5668
+ iterationNotes: z3.string().optional().describe("Optional notes for the current iteration metadata"),
5669
+ maxIterations: z3.number().int().positive().optional().describe("Optional maximum planned iteration count"),
5670
+ previousHash: z3.string().optional().describe("Optional artifact hash from the previous iteration"),
5451
5671
  allowQaFail: z3.boolean().default(false).describe("Allow render success even if QA fails")
5452
5672
  }),
5453
5673
  output: renderOutputSchema,
@@ -5462,9 +5682,26 @@ cli.command("render", {
5462
5682
  ],
5463
5683
  async run(c) {
5464
5684
  const spec = parseDesignSpec(await readJson(c.options.spec));
5685
+ let iteration;
5686
+ try {
5687
+ iteration = parseIterationMeta({
5688
+ ...c.options.iteration != null ? { iteration: c.options.iteration } : {},
5689
+ ...c.options.maxIterations != null ? { maxIterations: c.options.maxIterations } : {},
5690
+ ...c.options.iterationNotes ? { iterationNotes: c.options.iterationNotes } : {},
5691
+ ...c.options.previousHash ? { previousHash: c.options.previousHash } : {}
5692
+ });
5693
+ } catch (error) {
5694
+ const message = error instanceof Error ? error.message : String(error);
5695
+ return c.error({
5696
+ code: "INVALID_ITERATION_OPTIONS",
5697
+ message,
5698
+ retryable: false
5699
+ });
5700
+ }
5465
5701
  const runReport = await runRenderPipeline(spec, {
5466
5702
  out: c.options.out,
5467
- ...c.options.specOut ? { specOut: c.options.specOut } : {}
5703
+ ...c.options.specOut ? { specOut: c.options.specOut } : {},
5704
+ ...iteration ? { iteration } : {}
5468
5705
  });
5469
5706
  if (!runReport.qa.pass && !c.options.allowQaFail) {
5470
5707
  return c.error({
@@ -5984,8 +6221,10 @@ export {
5984
6221
  drawRainbowRule,
5985
6222
  drawVignette,
5986
6223
  edgeAnchor,
6224
+ ellipseRoute,
5987
6225
  flowNodeElementSchema,
5988
6226
  highlightCode,
6227
+ inferEllipseParams,
5989
6228
  inferLayout,
5990
6229
  inferSidecarPath,
5991
6230
  initHighlighter,
@@ -6004,6 +6243,7 @@ export {
6004
6243
  resolveShikiTheme,
6005
6244
  resolveTheme,
6006
6245
  runQa,
6246
+ straightRoute,
6007
6247
  themeToShikiMap,
6008
6248
  writeRenderArtifacts
6009
6249
  };
package/dist/qa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as RenderMetadata, D as DesignSpec } from './spec.schema-B_Z-KNqt.js';
1
+ import { R as RenderMetadata, D as DesignSpec } from './spec.schema-CYlOLxmK.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';
4
4