@spectratools/graphic-designer-cli 0.10.0 → 0.12.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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Cli } from 'incur';
2
- import { T as ThemeInput, D as DesignSpec, a as Rect$1, b as DrawCommand, c as Theme, d as RenderedElement, A as AnchorHint, C as ConnectionElement } from './spec.schema-B6sXTTou.js';
3
- export { e as AutoLayoutConfig, B as BuiltInTheme, f as CardElement, g as CodeBlockElement, h as ConstraintSpec, i as DEFAULT_GENERATOR_VERSION, j as DEFAULT_RAINBOW_COLORS, k as Decorator, l as DesignCardSpec, m as DesignSafeFrame, n as DesignTheme, o as DiagramElement, p as DiagramLayout, q as DiagramSpec, r as DrawArc, s as DrawBadge, t as DrawBezier, u as DrawCircle, v as DrawFontFamily, w as DrawGradientRect, x as DrawLine, y as DrawPath, z as DrawPoint, E as DrawRect, F as DrawShadow, G as DrawText, H as DrawTextRow, I as DrawTextRowSegment, J as Element, K as FlowNodeElement, L as Gradient, M as GradientOverlayDecorator, N as GradientSpec, O as GradientStop, P as GridLayoutConfig, Q as ImageElement, S as IterationMeta, U as LayoutConfig, V as LayoutSnapshot, W as ManualLayoutConfig, X as RainbowRuleDecorator, Y as RenderDesignOptions, R as RenderMetadata, Z as RenderResult, _ as ShapeElement, $ as StackLayoutConfig, a0 as TerminalElement, a1 as TextElement, a2 as ThemeInput, a3 as VignetteDecorator, a4 as WrittenArtifacts, a5 as builtInThemeBackgrounds, a6 as builtInThemes, a7 as computeSpecHash, a8 as connectionElementSchema, a9 as defaultAutoLayout, aa as defaultCanvas, ab as defaultConstraints, ac as defaultGridLayout, ad as defaultLayout, ae as defaultStackLayout, af as defaultTheme, ag as deriveSafeFrame, ah as designSpecSchema, ai as diagramElementSchema, aj as diagramLayoutSchema, ak as diagramSpecSchema, al as drawGradientRect, am as drawRainbowRule, an as drawVignette, ao as flowNodeElementSchema, ap as inferLayout, aq as inferSidecarPath, ar as parseDesignSpec, as as parseDiagramSpec, at as renderDesign, au as resolveTheme, av as writeRenderArtifacts } from './spec.schema-B6sXTTou.js';
2
+ import { T as ThemeInput, D as DesignSpec, a as Rect$1, b as DrawCommand, c as Theme, d as RenderedElement, A as AnchorHint, C as ConnectionElement } from './spec.schema-BkbcnVcm.js';
3
+ export { e as AutoLayoutConfig, B as BuiltInTheme, f as CardElement, g as CodeBlockElement, h as ConstraintSpec, i as DEFAULT_GENERATOR_VERSION, j as DEFAULT_RAINBOW_COLORS, k as Decorator, l as DesignCardSpec, m as DesignSafeFrame, n as DesignTheme, o as DiagramElement, p as DiagramLayout, q as DiagramSpec, r as DrawArc, s as DrawBadge, t as DrawBezier, u as DrawCircle, v as DrawFontFamily, w as DrawGradientRect, x as DrawLine, y as DrawPath, z as DrawPoint, E as DrawRect, F as DrawShadow, G as DrawText, H as DrawTextRow, I as DrawTextRowSegment, J as Element, K as EllipseLayoutConfig, L as FlowNodeElement, M as Gradient, N as GradientOverlayDecorator, O as GradientSpec, P as GradientStop, Q as GridLayoutConfig, S as ImageElement, U as IterationMeta, V as LayoutConfig, W as LayoutSnapshot, X as ManualLayoutConfig, Y as RainbowRuleDecorator, Z as RenderDesignOptions, R as RenderMetadata, _ as RenderResult, $ as ShapeElement, a0 as StackLayoutConfig, a1 as TerminalElement, a2 as TextElement, a3 as ThemeInput, a4 as VignetteDecorator, a5 as WrittenArtifacts, a6 as builtInThemeBackgrounds, a7 as builtInThemes, a8 as computeSpecHash, a9 as connectionElementSchema, aa as defaultAutoLayout, ab as defaultCanvas, ac as defaultConstraints, ad as defaultGridLayout, ae as defaultLayout, af as defaultStackLayout, ag as defaultTheme, ah as deriveSafeFrame, ai as designSpecSchema, aj as diagramElementSchema, ak as diagramLayoutSchema, al as diagramSpecSchema, am as drawGradientRect, an as drawRainbowRule, ao as drawVignette, ap as flowNodeElementSchema, aq as inferLayout, ar as inferSidecarPath, as as parseDesignSpec, at as parseDiagramSpec, au as renderDesign, av as resolveTheme, aw as writeRenderArtifacts } from './spec.schema-BkbcnVcm.js';
4
4
  import { SKRSContext2D } from '@napi-rs/canvas';
5
5
  export { QaIssue, QaReferenceResult, QaReport, QaSeverity, readMetadata, runQa } from './qa.js';
6
6
  import { Highlighter } from 'shiki';
@@ -245,9 +245,16 @@ type Point$1 = {
245
245
 
246
246
  type Point = Point$1;
247
247
  type Rect = Rect$1;
248
- type ConnectionRouting = 'auto' | 'orthogonal' | 'curve' | 'arc';
248
+ type ConnectionRouting = 'auto' | 'orthogonal' | 'curve' | 'arc' | 'straight';
249
+ type ConnectionCurveMode = 'normal' | 'ellipse';
249
250
  type ConnectionArrow = 'none' | 'end' | 'start' | 'both';
250
251
  type ConnectionStrokeStyle = 'solid' | 'dashed' | 'dotted';
252
+ type EllipseParams = {
253
+ cx: number;
254
+ cy: number;
255
+ rx: number;
256
+ ry: number;
257
+ };
251
258
  type ConnectionRenderOptions = {
252
259
  fromBounds: Rect;
253
260
  toBounds: Rect;
@@ -292,6 +299,38 @@ declare function curveRoute(fromBounds: Rect, toBounds: Rect, diagramCenter: Poi
292
299
  * automatic edge anchor calculation.
293
300
  */
294
301
  declare function arcRoute(fromBounds: Rect, toBounds: Rect, diagramCenter: Point, tension: number, fromAnchor?: AnchorHint, toAnchor?: AnchorHint): [CubicBezierSegment, CubicBezierSegment];
302
+ /**
303
+ * Infer ellipse parameters from the bounding boxes of all flow-node elements.
304
+ *
305
+ * When explicit `ellipseRx`/`ellipseRy` are provided they are used directly.
306
+ * Otherwise the ellipse is fitted by computing the centroid of all node centers
307
+ * as the center, with `rx` and `ry` derived from the maximum horizontal and
308
+ * vertical distance from the centroid to any node center.
309
+ */
310
+ declare function inferEllipseParams(nodeBounds: Rect[], explicitCenter?: Point, explicitRx?: number, explicitRy?: number): EllipseParams;
311
+ /**
312
+ * Compute a cubic bezier curve that traces an arc on a shared global ellipse.
313
+ *
314
+ * Uses the generalized kappa formula: `κ = (4/3) × tan(angularSpan / 4)`
315
+ * to produce control points from the ellipse tangent vectors at the source
316
+ * and target angles.
317
+ *
318
+ * The source and target points are the edge anchors of the respective node
319
+ * bounding boxes (not points on the ellipse itself), but the control points
320
+ * are derived from the ellipse tangent at the angle each node center makes
321
+ * with the ellipse center. This produces curves that follow the global
322
+ * ellipse arc while connecting node boundaries correctly.
323
+ *
324
+ * @returns `[startPoint, controlPoint1, controlPoint2, endPoint]`
325
+ */
326
+ declare function ellipseRoute(fromBounds: Rect, toBounds: Rect, ellipse: EllipseParams, fromAnchor?: AnchorHint, toAnchor?: AnchorHint): [Point, Point, Point, Point];
327
+ /**
328
+ * Compute a straight-line route between two rectangles.
329
+ *
330
+ * When `fromAnchor` or `toAnchor` hints are provided, they override the
331
+ * automatic edge anchor calculation.
332
+ */
333
+ declare function straightRoute(fromBounds: Rect, toBounds: Rect, fromAnchor?: AnchorHint, toAnchor?: AnchorHint): [Point, Point];
295
334
  /**
296
335
  * Compute an orthogonal (right-angle) path between two rectangles.
297
336
  * Returns an array of waypoints forming a 3-segment path.
@@ -309,6 +348,7 @@ declare function bezierPointAt(p0: Point, cp1: Point, cp2: Point, p3: Point, t:
309
348
  declare function computeDiagramCenter(nodeBounds: Rect[], canvasCenter?: Point): Point;
310
349
  declare function renderConnection(ctx: SKRSContext2D, conn: ConnectionElement, fromBounds: Rect, toBounds: Rect, theme: Theme, edgeRoute?: EdgeRoute, options?: {
311
350
  diagramCenter?: Point;
351
+ ellipseParams?: EllipseParams;
312
352
  }): RenderedElement[];
313
353
 
314
354
  /**
@@ -366,4 +406,4 @@ declare function highlightCode(code: string, language: string, themeName: string
366
406
  */
367
407
  declare function disposeHighlighter(): void;
368
408
 
369
- export { type CompareImagesOptions, type CompareImagesReport, type CompareRegionScore, type CompareVerdict, type ConnectionArrow, ConnectionElement, type ConnectionRenderOptions, type ConnectionRouting, type ConnectionStrokeStyle, type CubicBezierSegment, DesignSpec, DrawCommand, type EdgeRoute, type ElkLayoutResult, type HighlightedLine, type LayoutResult, type Point, Rect$1 as Rect, RenderedElement, Theme, arcRoute, bezierPointAt, buildCardsSpec, buildCodeSpec, buildFlowchartSpec, buildTerminalSpec, cli, compareImages, computeDiagramCenter, curveRoute, disposeHighlighter, edgeAnchor, highlightCode, initHighlighter, loadFonts, orthogonalRoute, outwardNormal, rectCenter, renderConnection, renderDrawCommands, resolveShikiTheme, themeToShikiMap };
409
+ export { type CompareImagesOptions, type CompareImagesReport, type CompareRegionScore, type CompareVerdict, type ConnectionArrow, type ConnectionCurveMode, ConnectionElement, type ConnectionRenderOptions, type ConnectionRouting, type ConnectionStrokeStyle, type CubicBezierSegment, DesignSpec, DrawCommand, type EdgeRoute, type ElkLayoutResult, type EllipseParams, type HighlightedLine, type LayoutResult, type Point, Rect$1 as Rect, RenderedElement, Theme, arcRoute, bezierPointAt, buildCardsSpec, buildCodeSpec, buildFlowchartSpec, buildTerminalSpec, cli, compareImages, computeDiagramCenter, curveRoute, disposeHighlighter, edgeAnchor, ellipseRoute, highlightCode, inferEllipseParams, initHighlighter, loadFonts, orthogonalRoute, outwardNormal, rectCenter, renderConnection, renderDrawCommands, resolveShikiTheme, straightRoute, themeToShikiMap };
package/dist/index.js CHANGED
@@ -775,6 +775,10 @@ var drawShadowSchema = z2.object({
775
775
  offsetY: z2.number().default(4)
776
776
  }).strict();
777
777
  var drawFontFamilySchema = z2.enum(["heading", "body", "mono"]);
778
+ var strokeGradientSchema = z2.object({
779
+ from: colorHexSchema2,
780
+ to: colorHexSchema2
781
+ }).strict();
778
782
  var drawRectSchema = z2.object({
779
783
  type: z2.literal("rect"),
780
784
  x: z2.number(),
@@ -822,6 +826,7 @@ var drawLineSchema = z2.object({
822
826
  x2: z2.number(),
823
827
  y2: z2.number(),
824
828
  color: colorHexSchema2.default("#FFFFFF"),
829
+ strokeGradient: strokeGradientSchema.optional(),
825
830
  width: z2.number().min(0.5).max(32).default(2),
826
831
  dash: z2.array(z2.number()).max(6).optional(),
827
832
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -852,6 +857,7 @@ var drawBezierSchema = z2.object({
852
857
  type: z2.literal("bezier"),
853
858
  points: z2.array(drawPointSchema).min(2).max(20),
854
859
  color: colorHexSchema2.default("#FFFFFF"),
860
+ strokeGradient: strokeGradientSchema.optional(),
855
861
  width: z2.number().min(0.5).max(32).default(2),
856
862
  dash: z2.array(z2.number()).max(6).optional(),
857
863
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -1060,7 +1066,8 @@ var connectionElementSchema = z2.object({
1060
1066
  from: z2.string().min(1).max(120),
1061
1067
  to: z2.string().min(1).max(120),
1062
1068
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
1063
- strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
1069
+ /** @deprecated Use `style` instead. */
1070
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).optional(),
1064
1071
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
1065
1072
  label: z2.string().min(1).max(200).optional(),
1066
1073
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
@@ -1072,7 +1079,8 @@ var connectionElementSchema = z2.object({
1072
1079
  arrowSize: z2.number().min(4).max(32).optional(),
1073
1080
  arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
1074
1081
  opacity: z2.number().min(0).max(1).default(1),
1075
- routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
1082
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc", "straight"]).default("auto"),
1083
+ curveMode: z2.enum(["normal", "ellipse"]).default("normal"),
1076
1084
  tension: z2.number().min(0.1).max(0.8).default(0.35),
1077
1085
  fromAnchor: anchorHintSchema.optional(),
1078
1086
  toAnchor: anchorHintSchema.optional()
@@ -1165,7 +1173,11 @@ var autoLayoutConfigSchema = z2.object({
1165
1173
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
1166
1174
  radialSortBy: z2.enum(["id", "connections"]).optional(),
1167
1175
  /** Explicit center used by curve/arc connection routing. */
1168
- diagramCenter: diagramCenterSchema.optional()
1176
+ diagramCenter: diagramCenterSchema.optional(),
1177
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1178
+ ellipseRx: z2.number().positive().optional(),
1179
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1180
+ ellipseRy: z2.number().positive().optional()
1169
1181
  }).strict();
1170
1182
  var gridLayoutConfigSchema = z2.object({
1171
1183
  mode: z2.literal("grid"),
@@ -1175,7 +1187,11 @@ var gridLayoutConfigSchema = z2.object({
1175
1187
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
1176
1188
  equalHeight: z2.boolean().default(false),
1177
1189
  /** Explicit center used by curve/arc connection routing. */
1178
- diagramCenter: diagramCenterSchema.optional()
1190
+ diagramCenter: diagramCenterSchema.optional(),
1191
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1192
+ ellipseRx: z2.number().positive().optional(),
1193
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1194
+ ellipseRy: z2.number().positive().optional()
1179
1195
  }).strict();
1180
1196
  var stackLayoutConfigSchema = z2.object({
1181
1197
  mode: z2.literal("stack"),
@@ -1183,7 +1199,25 @@ var stackLayoutConfigSchema = z2.object({
1183
1199
  gap: z2.number().int().min(0).max(256).default(24),
1184
1200
  alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
1185
1201
  /** Explicit center used by curve/arc connection routing. */
1186
- 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()
1207
+ }).strict();
1208
+ var ellipseLayoutConfigSchema = z2.object({
1209
+ mode: z2.literal("ellipse"),
1210
+ cx: z2.number().optional(),
1211
+ cy: z2.number().optional(),
1212
+ rx: z2.number().positive(),
1213
+ ry: z2.number().positive(),
1214
+ startAngle: z2.number().default(-90),
1215
+ /** Explicit center used by curve/arc connection routing. */
1216
+ diagramCenter: diagramCenterSchema.optional(),
1217
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1218
+ ellipseRx: z2.number().positive().optional(),
1219
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1220
+ ellipseRy: z2.number().positive().optional()
1187
1221
  }).strict();
1188
1222
  var manualPositionSchema = z2.object({
1189
1223
  x: z2.number().int(),
@@ -1195,12 +1229,17 @@ var manualLayoutConfigSchema = z2.object({
1195
1229
  mode: z2.literal("manual"),
1196
1230
  positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
1197
1231
  /** Explicit center used by curve/arc connection routing. */
1198
- diagramCenter: diagramCenterSchema.optional()
1232
+ diagramCenter: diagramCenterSchema.optional(),
1233
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1234
+ ellipseRx: z2.number().positive().optional(),
1235
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1236
+ ellipseRy: z2.number().positive().optional()
1199
1237
  }).strict();
1200
1238
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
1201
1239
  autoLayoutConfigSchema,
1202
1240
  gridLayoutConfigSchema,
1203
1241
  stackLayoutConfigSchema,
1242
+ ellipseLayoutConfigSchema,
1204
1243
  manualLayoutConfigSchema
1205
1244
  ]);
1206
1245
  var constraintsSchema = z2.object({
@@ -1260,7 +1299,11 @@ var diagramElementSchema = z2.discriminatedUnion("type", [
1260
1299
  var diagramLayoutSchema = z2.object({
1261
1300
  mode: z2.enum(["manual", "auto"]).default("manual"),
1262
1301
  positions: z2.record(z2.string(), diagramPositionSchema).optional(),
1263
- diagramCenter: diagramCenterSchema.optional()
1302
+ diagramCenter: diagramCenterSchema.optional(),
1303
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1304
+ ellipseRx: z2.number().positive().optional(),
1305
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1306
+ ellipseRy: z2.number().positive().optional()
1264
1307
  }).strict();
1265
1308
  var diagramSpecSchema = z2.object({
1266
1309
  version: z2.literal(1),
@@ -2430,6 +2473,35 @@ async function computeElkLayout(elements, config, safeFrame) {
2430
2473
  };
2431
2474
  }
2432
2475
 
2476
+ // src/layout/ellipse.ts
2477
+ function clampDimension(estimated, max) {
2478
+ return Math.max(1, Math.min(max, Math.floor(estimated)));
2479
+ }
2480
+ function computeEllipseLayout(elements, config, safeFrame) {
2481
+ const placeable = elements.filter((element) => element.type !== "connection");
2482
+ const positions = /* @__PURE__ */ new Map();
2483
+ if (placeable.length === 0) {
2484
+ return { positions };
2485
+ }
2486
+ const cx = config.cx ?? safeFrame.x + safeFrame.width / 2;
2487
+ const cy = config.cy ?? safeFrame.y + safeFrame.height / 2;
2488
+ const stepDegrees = 360 / placeable.length;
2489
+ for (const [index, element] of placeable.entries()) {
2490
+ const angleRadians = (config.startAngle + index * stepDegrees) * Math.PI / 180;
2491
+ const centerX = cx + config.rx * Math.cos(angleRadians);
2492
+ const centerY = cy + config.ry * Math.sin(angleRadians);
2493
+ const width = clampDimension(estimateElementWidth(element), safeFrame.width);
2494
+ const height = clampDimension(estimateElementHeight(element), safeFrame.height);
2495
+ positions.set(element.id, {
2496
+ x: Math.round(centerX - width / 2),
2497
+ y: Math.round(centerY - height / 2),
2498
+ width,
2499
+ height
2500
+ });
2501
+ }
2502
+ return { positions };
2503
+ }
2504
+
2433
2505
  // src/layout/grid.ts
2434
2506
  function computeGridLayout(elements, config, safeFrame) {
2435
2507
  const placeable = elements.filter((element) => element.type !== "connection");
@@ -2525,6 +2597,8 @@ async function computeLayout(elements, layout, safeFrame) {
2525
2597
  return computeGridLayout(elements, layout, safeFrame);
2526
2598
  case "stack":
2527
2599
  return computeStackLayout(elements, layout, safeFrame);
2600
+ case "ellipse":
2601
+ return computeEllipseLayout(elements, layout, safeFrame);
2528
2602
  case "manual":
2529
2603
  return computeManualLayout(elements, layout, safeFrame);
2530
2604
  default:
@@ -3338,6 +3412,72 @@ function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAn
3338
3412
  [pMid, cp3, cp4, p3]
3339
3413
  ];
3340
3414
  }
3415
+ function inferEllipseParams(nodeBounds, explicitCenter, explicitRx, explicitRy) {
3416
+ if (nodeBounds.length === 0) {
3417
+ return {
3418
+ cx: explicitCenter?.x ?? 0,
3419
+ cy: explicitCenter?.y ?? 0,
3420
+ rx: explicitRx ?? 1,
3421
+ ry: explicitRy ?? 1
3422
+ };
3423
+ }
3424
+ const centers = nodeBounds.map(rectCenter);
3425
+ const cx = explicitCenter?.x ?? centers.reduce((sum, c) => sum + c.x, 0) / centers.length;
3426
+ const cy = explicitCenter?.y ?? centers.reduce((sum, c) => sum + c.y, 0) / centers.length;
3427
+ const rx = explicitRx ?? Math.max(1, ...centers.map((c) => Math.abs(c.x - cx)));
3428
+ const ry = explicitRy ?? Math.max(1, ...centers.map((c) => Math.abs(c.y - cy)));
3429
+ return { cx, cy, rx, ry };
3430
+ }
3431
+ function ellipseRoute(fromBounds, toBounds, ellipse, fromAnchor, toAnchor) {
3432
+ const fromCenter = rectCenter(fromBounds);
3433
+ const toCenter = rectCenter(toBounds);
3434
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
3435
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
3436
+ const theta1 = Math.atan2(
3437
+ (fromCenter.y - ellipse.cy) / ellipse.ry,
3438
+ (fromCenter.x - ellipse.cx) / ellipse.rx
3439
+ );
3440
+ const theta2 = Math.atan2(
3441
+ (toCenter.y - ellipse.cy) / ellipse.ry,
3442
+ (toCenter.x - ellipse.cx) / ellipse.rx
3443
+ );
3444
+ let angularSpan = theta2 - theta1;
3445
+ while (angularSpan > Math.PI) angularSpan -= 2 * Math.PI;
3446
+ while (angularSpan <= -Math.PI) angularSpan += 2 * Math.PI;
3447
+ const absSpan = Math.abs(angularSpan);
3448
+ const kappa = absSpan < 1e-6 ? 0 : 4 / 3 * Math.tan(absSpan / 4);
3449
+ const tangent1 = {
3450
+ x: -ellipse.rx * Math.sin(theta1),
3451
+ y: ellipse.ry * Math.cos(theta1)
3452
+ };
3453
+ const tangent2 = {
3454
+ x: -ellipse.rx * Math.sin(theta2),
3455
+ y: ellipse.ry * Math.cos(theta2)
3456
+ };
3457
+ const len1 = Math.hypot(tangent1.x, tangent1.y) || 1;
3458
+ const len2 = Math.hypot(tangent2.x, tangent2.y) || 1;
3459
+ const norm1 = { x: tangent1.x / len1, y: tangent1.y / len1 };
3460
+ const norm2 = { x: tangent2.x / len2, y: tangent2.y / len2 };
3461
+ const chordLength = Math.hypot(p3.x - p0.x, p3.y - p0.y);
3462
+ const cpDistance = chordLength * kappa * 0.5;
3463
+ const sign = angularSpan >= 0 ? 1 : -1;
3464
+ const cp1 = {
3465
+ x: p0.x + norm1.x * cpDistance * sign,
3466
+ y: p0.y + norm1.y * cpDistance * sign
3467
+ };
3468
+ const cp2 = {
3469
+ x: p3.x - norm2.x * cpDistance * sign,
3470
+ y: p3.y - norm2.y * cpDistance * sign
3471
+ };
3472
+ return [p0, cp1, cp2, p3];
3473
+ }
3474
+ function straightRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
3475
+ const fromC = rectCenter(fromBounds);
3476
+ const toC = rectCenter(toBounds);
3477
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
3478
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
3479
+ return [p0, p3];
3480
+ }
3341
3481
  function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
3342
3482
  const fromC = rectCenter(fromBounds);
3343
3483
  const toC = rectCenter(toBounds);
@@ -3382,15 +3522,6 @@ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
3382
3522
  }
3383
3523
  return void 0;
3384
3524
  }
3385
- function pointAlongArc(route, t) {
3386
- const [first, second] = route;
3387
- if (t <= 0.5) {
3388
- const localT2 = Math.max(0, Math.min(1, t * 2));
3389
- return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
3390
- }
3391
- const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
3392
- return bezierPointAt(second[0], second[1], second[2], second[3], localT);
3393
- }
3394
3525
  function computeDiagramCenter(nodeBounds, canvasCenter) {
3395
3526
  if (nodeBounds.length === 0) {
3396
3527
  return canvasCenter ?? { x: 0, y: 0 };
@@ -3513,8 +3644,19 @@ function polylineBounds(points) {
3513
3644
  };
3514
3645
  }
3515
3646
  function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
3516
- const routing = conn.routing ?? "auto";
3517
- const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
3647
+ let routing = conn.routing ?? "auto";
3648
+ let curveMode = conn.curveMode ?? "normal";
3649
+ if (conn.strokeStyle !== void 0) {
3650
+ console.warn("connection.strokeStyle is deprecated, use style instead");
3651
+ }
3652
+ if (routing === "arc") {
3653
+ console.warn(
3654
+ "connection routing: 'arc' is deprecated. Use routing: 'curve' with curveMode: 'ellipse' instead."
3655
+ );
3656
+ routing = "curve";
3657
+ curveMode = "ellipse";
3658
+ }
3659
+ const strokeStyle = conn.style ?? conn.strokeStyle ?? "solid";
3518
3660
  const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
3519
3661
  const tension = conn.tension ?? 0.35;
3520
3662
  const dash = dashFromStyle(strokeStyle);
@@ -3536,14 +3678,29 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3536
3678
  ctx.globalAlpha = conn.opacity;
3537
3679
  const arrowPlacement = conn.arrowPlacement ?? "endpoint";
3538
3680
  if (routing === "curve") {
3539
- const [p0, cp1, cp2, p3] = curveRoute(
3540
- fromBounds,
3541
- toBounds,
3542
- diagramCenter,
3543
- tension,
3544
- conn.fromAnchor,
3545
- conn.toAnchor
3546
- );
3681
+ let p0;
3682
+ let cp1;
3683
+ let cp2;
3684
+ let p3;
3685
+ if (curveMode === "ellipse") {
3686
+ const ellipse = options?.ellipseParams ?? inferEllipseParams([fromBounds, toBounds]);
3687
+ [p0, cp1, cp2, p3] = ellipseRoute(
3688
+ fromBounds,
3689
+ toBounds,
3690
+ ellipse,
3691
+ conn.fromAnchor,
3692
+ conn.toAnchor
3693
+ );
3694
+ } else {
3695
+ [p0, cp1, cp2, p3] = curveRoute(
3696
+ fromBounds,
3697
+ toBounds,
3698
+ diagramCenter,
3699
+ tension,
3700
+ conn.fromAnchor,
3701
+ conn.toAnchor
3702
+ );
3703
+ }
3547
3704
  const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
3548
3705
  ctx.strokeStyle = stroke;
3549
3706
  ctx.lineWidth = style.width;
@@ -3576,51 +3733,22 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3576
3733
  }
3577
3734
  }
3578
3735
  }
3579
- } else if (routing === "arc") {
3580
- const [first, second] = arcRoute(
3581
- fromBounds,
3582
- toBounds,
3583
- diagramCenter,
3584
- tension,
3585
- conn.fromAnchor,
3586
- conn.toAnchor
3587
- );
3588
- const [p0, cp1, cp2, pMid] = first;
3589
- const [, cp3, cp4, p3] = second;
3736
+ } else if (routing === "straight") {
3737
+ const [p0, p3] = straightRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
3590
3738
  const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
3591
3739
  ctx.strokeStyle = stroke;
3592
3740
  ctx.lineWidth = style.width;
3593
3741
  ctx.setLineDash(style.dash ?? []);
3594
3742
  ctx.beginPath();
3595
3743
  ctx.moveTo(p0.x, p0.y);
3596
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
3597
- ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
3744
+ ctx.lineTo(p3.x, p3.y);
3598
3745
  ctx.stroke();
3599
- linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
3746
+ linePoints = [p0, p3];
3600
3747
  startPoint = p0;
3601
3748
  endPoint = p3;
3602
- startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3603
- endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
3604
- labelPoint = pointAlongArc([first, second], labelT);
3605
- if (arrowPlacement === "boundary") {
3606
- if (conn.arrow === "end" || conn.arrow === "both") {
3607
- const [, s_cp3, s_cp4, s_p3] = second;
3608
- const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
3609
- if (tEnd !== void 0) {
3610
- endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3611
- const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3612
- endAngle = Math.atan2(tangent.y, tangent.x);
3613
- }
3614
- }
3615
- if (conn.arrow === "start" || conn.arrow === "both") {
3616
- const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
3617
- if (tStart !== void 0) {
3618
- startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
3619
- const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
3620
- startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
3621
- }
3622
- }
3623
- }
3749
+ startAngle = Math.atan2(p0.y - p3.y, p0.x - p3.x);
3750
+ endAngle = Math.atan2(p3.y - p0.y, p3.x - p0.x);
3751
+ labelPoint = pointAlongPolyline(linePoints, labelT);
3624
3752
  } else {
3625
3753
  const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
3626
3754
  const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
@@ -3886,6 +4014,24 @@ function fromPoints(points) {
3886
4014
  function resolveDrawFont(theme, family) {
3887
4015
  return resolveFont(theme.fonts[family], family);
3888
4016
  }
4017
+ function createDrawStrokeGradient(ctx, start, end, strokeGradient) {
4018
+ const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
4019
+ gradient.addColorStop(0, strokeGradient.from);
4020
+ gradient.addColorStop(1, strokeGradient.to);
4021
+ return gradient;
4022
+ }
4023
+ function resolveDrawStroke(ctx, start, end, color, strokeGradient) {
4024
+ if (!strokeGradient) {
4025
+ return color;
4026
+ }
4027
+ return createDrawStrokeGradient(ctx, start, end, strokeGradient);
4028
+ }
4029
+ function resolveArrowFill(color, strokeGradient, position) {
4030
+ if (!strokeGradient) {
4031
+ return color;
4032
+ }
4033
+ return position === "start" ? strokeGradient.from : strokeGradient.to;
4034
+ }
3889
4035
  function measureSpacedTextWidth(ctx, text, letterSpacing) {
3890
4036
  const chars = [...text];
3891
4037
  if (chars.length === 0) {
@@ -4164,18 +4310,31 @@ function renderDrawCommands(ctx, commands, theme) {
4164
4310
  const from = { x: command.x1, y: command.y1 };
4165
4311
  const to = { x: command.x2, y: command.y2 };
4166
4312
  const lineAngle = angleBetween(from, to);
4313
+ const stroke = resolveDrawStroke(ctx, from, to, command.color, command.strokeGradient);
4167
4314
  withOpacity(ctx, command.opacity, () => {
4168
4315
  applyDrawShadow(ctx, command.shadow);
4169
4316
  drawLine(ctx, from, to, {
4170
- color: command.color,
4317
+ color: stroke,
4171
4318
  width: command.width,
4172
4319
  ...command.dash ? { dash: command.dash } : {}
4173
4320
  });
4174
4321
  if (command.arrow === "end" || command.arrow === "both") {
4175
- drawArrowhead(ctx, to, lineAngle, command.arrowSize, command.color);
4322
+ drawArrowhead(
4323
+ ctx,
4324
+ to,
4325
+ lineAngle,
4326
+ command.arrowSize,
4327
+ resolveArrowFill(command.color, command.strokeGradient, "end")
4328
+ );
4176
4329
  }
4177
4330
  if (command.arrow === "start" || command.arrow === "both") {
4178
- drawArrowhead(ctx, from, lineAngle + Math.PI, command.arrowSize, command.color);
4331
+ drawArrowhead(
4332
+ ctx,
4333
+ from,
4334
+ lineAngle + Math.PI,
4335
+ command.arrowSize,
4336
+ resolveArrowFill(command.color, command.strokeGradient, "start")
4337
+ );
4179
4338
  }
4180
4339
  });
4181
4340
  const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
@@ -4183,7 +4342,7 @@ function renderDrawCommands(ctx, commands, theme) {
4183
4342
  id,
4184
4343
  kind: "draw",
4185
4344
  bounds: expandRect(fromPoints([from, to]), Math.max(command.width / 2, arrowPadding)),
4186
- foregroundColor: command.color
4345
+ foregroundColor: command.strokeGradient?.from ?? command.color
4187
4346
  });
4188
4347
  break;
4189
4348
  }
@@ -4217,10 +4376,17 @@ function renderDrawCommands(ctx, commands, theme) {
4217
4376
  }
4218
4377
  case "bezier": {
4219
4378
  const points = command.points;
4379
+ const stroke = resolveDrawStroke(
4380
+ ctx,
4381
+ points[0],
4382
+ points[points.length - 1],
4383
+ command.color,
4384
+ command.strokeGradient
4385
+ );
4220
4386
  withOpacity(ctx, command.opacity, () => {
4221
4387
  applyDrawShadow(ctx, command.shadow);
4222
4388
  drawBezier(ctx, points, {
4223
- color: command.color,
4389
+ color: stroke,
4224
4390
  width: command.width,
4225
4391
  ...command.dash ? { dash: command.dash } : {}
4226
4392
  });
@@ -4232,11 +4398,17 @@ function renderDrawCommands(ctx, commands, theme) {
4232
4398
  points[points.length - 1],
4233
4399
  endAngle,
4234
4400
  command.arrowSize,
4235
- command.color
4401
+ resolveArrowFill(command.color, command.strokeGradient, "end")
4236
4402
  );
4237
4403
  }
4238
4404
  if (command.arrow === "start" || command.arrow === "both") {
4239
- drawArrowhead(ctx, points[0], startAngle + Math.PI, command.arrowSize, command.color);
4405
+ drawArrowhead(
4406
+ ctx,
4407
+ points[0],
4408
+ startAngle + Math.PI,
4409
+ command.arrowSize,
4410
+ resolveArrowFill(command.color, command.strokeGradient, "start")
4411
+ );
4240
4412
  }
4241
4413
  });
4242
4414
  const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
@@ -4244,7 +4416,7 @@ function renderDrawCommands(ctx, commands, theme) {
4244
4416
  id,
4245
4417
  kind: "draw",
4246
4418
  bounds: expandRect(fromPoints(points), Math.max(command.width / 2, arrowPadding)),
4247
- foregroundColor: command.color
4419
+ foregroundColor: command.strokeGradient?.from ?? command.color
4248
4420
  });
4249
4421
  break;
4250
4422
  }
@@ -5049,10 +5221,19 @@ async function renderDesign(input, options = {}) {
5049
5221
  break;
5050
5222
  }
5051
5223
  }
5052
- const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
5053
- spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
5054
- { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
5224
+ const nodeBounds = spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null);
5225
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(nodeBounds, { x: spec.canvas.width / 2, y: spec.canvas.height / 2 });
5226
+ const layoutEllipseRx = "ellipseRx" in spec.layout ? spec.layout.ellipseRx : void 0;
5227
+ const layoutEllipseRy = "ellipseRy" in spec.layout ? spec.layout.ellipseRy : void 0;
5228
+ const hasEllipseConnections = spec.elements.some(
5229
+ (el) => el.type === "connection" && (el.curveMode === "ellipse" || el.routing === "arc")
5055
5230
  );
5231
+ const ellipseParams = hasEllipseConnections ? inferEllipseParams(
5232
+ nodeBounds,
5233
+ spec.layout.diagramCenter ?? diagramCenter,
5234
+ layoutEllipseRx,
5235
+ layoutEllipseRy
5236
+ ) : void 0;
5056
5237
  for (const element of spec.elements) {
5057
5238
  if (element.type !== "connection") {
5058
5239
  continue;
@@ -5066,7 +5247,15 @@ async function renderDesign(input, options = {}) {
5066
5247
  }
5067
5248
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
5068
5249
  elements.push(
5069
- ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
5250
+ ...renderConnection(
5251
+ ctx,
5252
+ element,
5253
+ fromRect,
5254
+ toRect,
5255
+ theme,
5256
+ edgeRoute,
5257
+ ellipseParams ? { diagramCenter, ellipseParams } : { diagramCenter }
5258
+ )
5070
5259
  );
5071
5260
  }
5072
5261
  if (footerRect && spec.footer) {
@@ -6128,8 +6317,10 @@ export {
6128
6317
  drawRainbowRule,
6129
6318
  drawVignette,
6130
6319
  edgeAnchor,
6320
+ ellipseRoute,
6131
6321
  flowNodeElementSchema,
6132
6322
  highlightCode,
6323
+ inferEllipseParams,
6133
6324
  inferLayout,
6134
6325
  inferSidecarPath,
6135
6326
  initHighlighter,
@@ -6148,6 +6339,7 @@ export {
6148
6339
  resolveShikiTheme,
6149
6340
  resolveTheme,
6150
6341
  runQa,
6342
+ straightRoute,
6151
6343
  themeToShikiMap,
6152
6344
  writeRenderArtifacts
6153
6345
  };
package/dist/qa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as RenderMetadata, D as DesignSpec } from './spec.schema-B6sXTTou.js';
1
+ import { R as RenderMetadata, D as DesignSpec } from './spec.schema-BkbcnVcm.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';
4
4