@spectratools/graphic-designer-cli 0.10.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/renderer.js CHANGED
@@ -1366,12 +1366,12 @@ var MACOS_DOTS = [
1366
1366
  { fill: "#27C93F", stroke: "#1AAB29" }
1367
1367
  ];
1368
1368
  function drawMacosDots(ctx, x, y) {
1369
- for (const [index, dot2] of MACOS_DOTS.entries()) {
1369
+ for (const [index, dot] of MACOS_DOTS.entries()) {
1370
1370
  ctx.beginPath();
1371
1371
  ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
1372
1372
  ctx.closePath();
1373
- ctx.fillStyle = dot2.fill;
1374
- ctx.strokeStyle = dot2.stroke;
1373
+ ctx.fillStyle = dot.fill;
1374
+ ctx.strokeStyle = dot.stroke;
1375
1375
  ctx.lineWidth = DOT_STROKE_WIDTH;
1376
1376
  ctx.fill();
1377
1377
  ctx.stroke();
@@ -2090,56 +2090,71 @@ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, to
2090
2090
  const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
2091
2091
  return [p0, cp1, cp2, p3];
2092
2092
  }
2093
- function dot(a, b) {
2094
- return a.x * b.x + a.y * b.y;
2095
- }
2096
- function localToWorld(origin, axisX, axisY, local) {
2097
- return {
2098
- x: origin.x + axisX.x * local.x + axisY.x * local.y,
2099
- y: origin.y + axisX.y * local.x + axisY.y * local.y
2100
- };
2093
+ function inferEllipseParams(nodeBounds, explicitCenter, explicitRx, explicitRy) {
2094
+ if (nodeBounds.length === 0) {
2095
+ return {
2096
+ cx: explicitCenter?.x ?? 0,
2097
+ cy: explicitCenter?.y ?? 0,
2098
+ rx: explicitRx ?? 1,
2099
+ ry: explicitRy ?? 1
2100
+ };
2101
+ }
2102
+ const centers = nodeBounds.map(rectCenter);
2103
+ const cx = explicitCenter?.x ?? centers.reduce((sum, c) => sum + c.x, 0) / centers.length;
2104
+ const cy = explicitCenter?.y ?? centers.reduce((sum, c) => sum + c.y, 0) / centers.length;
2105
+ const rx = explicitRx ?? Math.max(1, ...centers.map((c) => Math.abs(c.x - cx)));
2106
+ const ry = explicitRy ?? Math.max(1, ...centers.map((c) => Math.abs(c.y - cy)));
2107
+ return { cx, cy, rx, ry };
2101
2108
  }
2102
- function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
2109
+ function ellipseRoute(fromBounds, toBounds, ellipse, fromAnchor, toAnchor) {
2103
2110
  const fromCenter = rectCenter(fromBounds);
2104
2111
  const toCenter = rectCenter(toBounds);
2105
- const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
2106
- const end = resolveAnchor(toBounds, toAnchor, fromCenter);
2107
- const chord = { x: end.x - start.x, y: end.y - start.y };
2108
- const chordLength = Math.hypot(chord.x, chord.y);
2109
- if (chordLength < 1e-6) {
2110
- const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2111
- return [
2112
- [start, start, mid, mid],
2113
- [mid, mid, end, end]
2114
- ];
2115
- }
2116
- const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
2117
- let axisY = { x: -axisX.y, y: axisX.x };
2118
- const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2119
- const outwardHint = outwardNormal(midpoint, diagramCenter);
2120
- if (dot(axisY, outwardHint) < 0) {
2121
- axisY = { x: -axisY.x, y: -axisY.y };
2122
- }
2123
- const semiMajor = chordLength / 2;
2124
- const semiMinor = Math.max(12, chordLength * tension * 0.75);
2125
- const p0Local = { x: -semiMajor, y: 0 };
2126
- const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2127
- const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2128
- const pMidLocal = { x: 0, y: semiMinor };
2129
- const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2130
- const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2131
- const p3Local = { x: semiMajor, y: 0 };
2132
- const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
2133
- const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
2134
- const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
2135
- const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
2136
- const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
2137
- const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
2138
- const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
2139
- return [
2140
- [p0, cp1, cp2, pMid],
2141
- [pMid, cp3, cp4, p3]
2142
- ];
2112
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
2113
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
2114
+ const theta1 = Math.atan2(
2115
+ (fromCenter.y - ellipse.cy) / ellipse.ry,
2116
+ (fromCenter.x - ellipse.cx) / ellipse.rx
2117
+ );
2118
+ const theta2 = Math.atan2(
2119
+ (toCenter.y - ellipse.cy) / ellipse.ry,
2120
+ (toCenter.x - ellipse.cx) / ellipse.rx
2121
+ );
2122
+ let angularSpan = theta2 - theta1;
2123
+ while (angularSpan > Math.PI) angularSpan -= 2 * Math.PI;
2124
+ while (angularSpan <= -Math.PI) angularSpan += 2 * Math.PI;
2125
+ const absSpan = Math.abs(angularSpan);
2126
+ const kappa = absSpan < 1e-6 ? 0 : 4 / 3 * Math.tan(absSpan / 4);
2127
+ const tangent1 = {
2128
+ x: -ellipse.rx * Math.sin(theta1),
2129
+ y: ellipse.ry * Math.cos(theta1)
2130
+ };
2131
+ const tangent2 = {
2132
+ x: -ellipse.rx * Math.sin(theta2),
2133
+ y: ellipse.ry * Math.cos(theta2)
2134
+ };
2135
+ const len1 = Math.hypot(tangent1.x, tangent1.y) || 1;
2136
+ const len2 = Math.hypot(tangent2.x, tangent2.y) || 1;
2137
+ const norm1 = { x: tangent1.x / len1, y: tangent1.y / len1 };
2138
+ const norm2 = { x: tangent2.x / len2, y: tangent2.y / len2 };
2139
+ const chordLength = Math.hypot(p3.x - p0.x, p3.y - p0.y);
2140
+ const cpDistance = chordLength * kappa * 0.5;
2141
+ const sign = angularSpan >= 0 ? 1 : -1;
2142
+ const cp1 = {
2143
+ x: p0.x + norm1.x * cpDistance * sign,
2144
+ y: p0.y + norm1.y * cpDistance * sign
2145
+ };
2146
+ const cp2 = {
2147
+ x: p3.x - norm2.x * cpDistance * sign,
2148
+ y: p3.y - norm2.y * cpDistance * sign
2149
+ };
2150
+ return [p0, cp1, cp2, p3];
2151
+ }
2152
+ function straightRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
2153
+ const fromC = rectCenter(fromBounds);
2154
+ const toC = rectCenter(toBounds);
2155
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
2156
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
2157
+ return [p0, p3];
2143
2158
  }
2144
2159
  function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
2145
2160
  const fromC = rectCenter(fromBounds);
@@ -2185,15 +2200,6 @@ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
2185
2200
  }
2186
2201
  return void 0;
2187
2202
  }
2188
- function pointAlongArc(route, t) {
2189
- const [first, second] = route;
2190
- if (t <= 0.5) {
2191
- const localT2 = Math.max(0, Math.min(1, t * 2));
2192
- return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
2193
- }
2194
- const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
2195
- return bezierPointAt(second[0], second[1], second[2], second[3], localT);
2196
- }
2197
2203
  function computeDiagramCenter(nodeBounds, canvasCenter) {
2198
2204
  if (nodeBounds.length === 0) {
2199
2205
  return canvasCenter ?? { x: 0, y: 0 };
@@ -2316,8 +2322,19 @@ function polylineBounds(points) {
2316
2322
  };
2317
2323
  }
2318
2324
  function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
2319
- const routing = conn.routing ?? "auto";
2320
- const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
2325
+ let routing = conn.routing ?? "auto";
2326
+ let curveMode = conn.curveMode ?? "normal";
2327
+ if (conn.strokeStyle !== void 0) {
2328
+ console.warn("connection.strokeStyle is deprecated, use style instead");
2329
+ }
2330
+ if (routing === "arc") {
2331
+ console.warn(
2332
+ "connection routing: 'arc' is deprecated. Use routing: 'curve' with curveMode: 'ellipse' instead."
2333
+ );
2334
+ routing = "curve";
2335
+ curveMode = "ellipse";
2336
+ }
2337
+ const strokeStyle = conn.style ?? conn.strokeStyle ?? "solid";
2321
2338
  const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
2322
2339
  const tension = conn.tension ?? 0.35;
2323
2340
  const dash = dashFromStyle(strokeStyle);
@@ -2339,14 +2356,29 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2339
2356
  ctx.globalAlpha = conn.opacity;
2340
2357
  const arrowPlacement = conn.arrowPlacement ?? "endpoint";
2341
2358
  if (routing === "curve") {
2342
- const [p0, cp1, cp2, p3] = curveRoute(
2343
- fromBounds,
2344
- toBounds,
2345
- diagramCenter,
2346
- tension,
2347
- conn.fromAnchor,
2348
- conn.toAnchor
2349
- );
2359
+ let p0;
2360
+ let cp1;
2361
+ let cp2;
2362
+ let p3;
2363
+ if (curveMode === "ellipse") {
2364
+ const ellipse = options?.ellipseParams ?? inferEllipseParams([fromBounds, toBounds]);
2365
+ [p0, cp1, cp2, p3] = ellipseRoute(
2366
+ fromBounds,
2367
+ toBounds,
2368
+ ellipse,
2369
+ conn.fromAnchor,
2370
+ conn.toAnchor
2371
+ );
2372
+ } else {
2373
+ [p0, cp1, cp2, p3] = curveRoute(
2374
+ fromBounds,
2375
+ toBounds,
2376
+ diagramCenter,
2377
+ tension,
2378
+ conn.fromAnchor,
2379
+ conn.toAnchor
2380
+ );
2381
+ }
2350
2382
  const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
2351
2383
  ctx.strokeStyle = stroke;
2352
2384
  ctx.lineWidth = style.width;
@@ -2379,51 +2411,22 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2379
2411
  }
2380
2412
  }
2381
2413
  }
2382
- } else if (routing === "arc") {
2383
- const [first, second] = arcRoute(
2384
- fromBounds,
2385
- toBounds,
2386
- diagramCenter,
2387
- tension,
2388
- conn.fromAnchor,
2389
- conn.toAnchor
2390
- );
2391
- const [p0, cp1, cp2, pMid] = first;
2392
- const [, cp3, cp4, p3] = second;
2414
+ } else if (routing === "straight") {
2415
+ const [p0, p3] = straightRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
2393
2416
  const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
2394
2417
  ctx.strokeStyle = stroke;
2395
2418
  ctx.lineWidth = style.width;
2396
2419
  ctx.setLineDash(style.dash ?? []);
2397
2420
  ctx.beginPath();
2398
2421
  ctx.moveTo(p0.x, p0.y);
2399
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
2400
- ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
2422
+ ctx.lineTo(p3.x, p3.y);
2401
2423
  ctx.stroke();
2402
- linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
2424
+ linePoints = [p0, p3];
2403
2425
  startPoint = p0;
2404
2426
  endPoint = p3;
2405
- startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
2406
- endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
2407
- labelPoint = pointAlongArc([first, second], labelT);
2408
- if (arrowPlacement === "boundary") {
2409
- if (conn.arrow === "end" || conn.arrow === "both") {
2410
- const [, s_cp3, s_cp4, s_p3] = second;
2411
- const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
2412
- if (tEnd !== void 0) {
2413
- endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
2414
- const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
2415
- endAngle = Math.atan2(tangent.y, tangent.x);
2416
- }
2417
- }
2418
- if (conn.arrow === "start" || conn.arrow === "both") {
2419
- const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
2420
- if (tStart !== void 0) {
2421
- startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
2422
- const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
2423
- startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
2424
- }
2425
- }
2426
- }
2427
+ startAngle = Math.atan2(p0.y - p3.y, p0.x - p3.x);
2428
+ endAngle = Math.atan2(p3.y - p0.y, p3.x - p0.x);
2429
+ labelPoint = pointAlongPolyline(linePoints, labelT);
2427
2430
  } else {
2428
2431
  const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
2429
2432
  const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
@@ -3831,7 +3834,8 @@ var connectionElementSchema = z2.object({
3831
3834
  from: z2.string().min(1).max(120),
3832
3835
  to: z2.string().min(1).max(120),
3833
3836
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3834
- strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3837
+ /** @deprecated Use `style` instead. */
3838
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).optional(),
3835
3839
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
3836
3840
  label: z2.string().min(1).max(200).optional(),
3837
3841
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
@@ -3843,7 +3847,8 @@ var connectionElementSchema = z2.object({
3843
3847
  arrowSize: z2.number().min(4).max(32).optional(),
3844
3848
  arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
3845
3849
  opacity: z2.number().min(0).max(1).default(1),
3846
- routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
3850
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc", "straight"]).default("auto"),
3851
+ curveMode: z2.enum(["normal", "ellipse"]).default("normal"),
3847
3852
  tension: z2.number().min(0.1).max(0.8).default(0.35),
3848
3853
  fromAnchor: anchorHintSchema.optional(),
3849
3854
  toAnchor: anchorHintSchema.optional()
@@ -3936,7 +3941,11 @@ var autoLayoutConfigSchema = z2.object({
3936
3941
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
3937
3942
  radialSortBy: z2.enum(["id", "connections"]).optional(),
3938
3943
  /** Explicit center used by curve/arc connection routing. */
3939
- diagramCenter: diagramCenterSchema.optional()
3944
+ diagramCenter: diagramCenterSchema.optional(),
3945
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
3946
+ ellipseRx: z2.number().positive().optional(),
3947
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
3948
+ ellipseRy: z2.number().positive().optional()
3940
3949
  }).strict();
3941
3950
  var gridLayoutConfigSchema = z2.object({
3942
3951
  mode: z2.literal("grid"),
@@ -3946,7 +3955,11 @@ var gridLayoutConfigSchema = z2.object({
3946
3955
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
3947
3956
  equalHeight: z2.boolean().default(false),
3948
3957
  /** Explicit center used by curve/arc connection routing. */
3949
- diagramCenter: diagramCenterSchema.optional()
3958
+ diagramCenter: diagramCenterSchema.optional(),
3959
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
3960
+ ellipseRx: z2.number().positive().optional(),
3961
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
3962
+ ellipseRy: z2.number().positive().optional()
3950
3963
  }).strict();
3951
3964
  var stackLayoutConfigSchema = z2.object({
3952
3965
  mode: z2.literal("stack"),
@@ -3954,7 +3967,11 @@ var stackLayoutConfigSchema = z2.object({
3954
3967
  gap: z2.number().int().min(0).max(256).default(24),
3955
3968
  alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
3956
3969
  /** Explicit center used by curve/arc connection routing. */
3957
- diagramCenter: diagramCenterSchema.optional()
3970
+ diagramCenter: diagramCenterSchema.optional(),
3971
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
3972
+ ellipseRx: z2.number().positive().optional(),
3973
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
3974
+ ellipseRy: z2.number().positive().optional()
3958
3975
  }).strict();
3959
3976
  var manualPositionSchema = z2.object({
3960
3977
  x: z2.number().int(),
@@ -3966,7 +3983,11 @@ var manualLayoutConfigSchema = z2.object({
3966
3983
  mode: z2.literal("manual"),
3967
3984
  positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
3968
3985
  /** Explicit center used by curve/arc connection routing. */
3969
- diagramCenter: diagramCenterSchema.optional()
3986
+ diagramCenter: diagramCenterSchema.optional(),
3987
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
3988
+ ellipseRx: z2.number().positive().optional(),
3989
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
3990
+ ellipseRy: z2.number().positive().optional()
3970
3991
  }).strict();
3971
3992
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
3972
3993
  autoLayoutConfigSchema,
@@ -4031,7 +4052,11 @@ var diagramElementSchema = z2.discriminatedUnion("type", [
4031
4052
  var diagramLayoutSchema = z2.object({
4032
4053
  mode: z2.enum(["manual", "auto"]).default("manual"),
4033
4054
  positions: z2.record(z2.string(), diagramPositionSchema).optional(),
4034
- diagramCenter: diagramCenterSchema.optional()
4055
+ diagramCenter: diagramCenterSchema.optional(),
4056
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
4057
+ ellipseRx: z2.number().positive().optional(),
4058
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
4059
+ ellipseRy: z2.number().positive().optional()
4035
4060
  }).strict();
4036
4061
  var diagramSpecSchema = z2.object({
4037
4062
  version: z2.literal(1),
@@ -4414,10 +4439,19 @@ async function renderDesign(input, options = {}) {
4414
4439
  break;
4415
4440
  }
4416
4441
  }
4417
- const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
4418
- spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
4419
- { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
4442
+ const nodeBounds = spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null);
4443
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(nodeBounds, { x: spec.canvas.width / 2, y: spec.canvas.height / 2 });
4444
+ const layoutEllipseRx = "ellipseRx" in spec.layout ? spec.layout.ellipseRx : void 0;
4445
+ const layoutEllipseRy = "ellipseRy" in spec.layout ? spec.layout.ellipseRy : void 0;
4446
+ const hasEllipseConnections = spec.elements.some(
4447
+ (el) => el.type === "connection" && (el.curveMode === "ellipse" || el.routing === "arc")
4420
4448
  );
4449
+ const ellipseParams = hasEllipseConnections ? inferEllipseParams(
4450
+ nodeBounds,
4451
+ spec.layout.diagramCenter ?? diagramCenter,
4452
+ layoutEllipseRx,
4453
+ layoutEllipseRy
4454
+ ) : void 0;
4421
4455
  for (const element of spec.elements) {
4422
4456
  if (element.type !== "connection") {
4423
4457
  continue;
@@ -4431,7 +4465,15 @@ async function renderDesign(input, options = {}) {
4431
4465
  }
4432
4466
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
4433
4467
  elements.push(
4434
- ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
4468
+ ...renderConnection(
4469
+ ctx,
4470
+ element,
4471
+ fromRect,
4472
+ toRect,
4473
+ theme,
4474
+ edgeRoute,
4475
+ ellipseParams ? { diagramCenter, ellipseParams } : { diagramCenter }
4476
+ )
4435
4477
  );
4436
4478
  }
4437
4479
  if (footerRect && spec.footer) {