@spectratools/graphic-designer-cli 0.4.0 → 0.7.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
@@ -115,9 +115,9 @@ function drawRoundedRect(ctx, rect, radius, fill, stroke) {
115
115
  roundRectPath(ctx, rect, radius);
116
116
  fillAndStroke(ctx, fill, stroke);
117
117
  }
118
- function drawCircle(ctx, center2, radius, fill, stroke) {
118
+ function drawCircle(ctx, center, radius, fill, stroke) {
119
119
  ctx.beginPath();
120
- ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
120
+ ctx.arc(center.x, center.y, Math.max(0, radius), 0, Math.PI * 2);
121
121
  ctx.closePath();
122
122
  fillAndStroke(ctx, fill, stroke);
123
123
  }
@@ -339,6 +339,10 @@ function relativeLuminance(hexColor) {
339
339
  const b = srgbToLinear(rgb.b);
340
340
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
341
341
  }
342
+ function withAlpha(hexColor, opacity) {
343
+ const rgb = parseHexColor(hexColor);
344
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`;
345
+ }
342
346
  function blendColorWithOpacity(foreground, background, opacity) {
343
347
  const fg = parseHexColor(foreground);
344
348
  const bg = parseHexColor(background);
@@ -441,15 +445,34 @@ function renderFlowNode(ctx, node, bounds, theme) {
441
445
  const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
442
446
  ctx.save();
443
447
  ctx.lineWidth = borderWidth;
448
+ if (node.shadow) {
449
+ const shadowColor = node.shadow.color ?? borderColor ?? theme.accent;
450
+ ctx.shadowColor = withAlpha(shadowColor, node.shadow.opacity);
451
+ ctx.shadowBlur = node.shadow.blur;
452
+ ctx.shadowOffsetX = node.shadow.offsetX;
453
+ ctx.shadowOffsetY = node.shadow.offsetY;
454
+ }
444
455
  if (fillOpacity < 1) {
445
456
  ctx.globalAlpha = node.opacity * fillOpacity;
446
457
  drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
458
+ if (node.shadow) {
459
+ ctx.shadowColor = "transparent";
460
+ ctx.shadowBlur = 0;
461
+ ctx.shadowOffsetX = 0;
462
+ ctx.shadowOffsetY = 0;
463
+ }
447
464
  ctx.globalAlpha = node.opacity;
448
465
  drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
449
466
  } else {
450
467
  ctx.globalAlpha = node.opacity;
451
468
  drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
452
469
  }
470
+ if (node.shadow) {
471
+ ctx.shadowColor = "transparent";
472
+ ctx.shadowBlur = 0;
473
+ ctx.shadowOffsetX = 0;
474
+ ctx.shadowOffsetY = 0;
475
+ }
453
476
  const headingFont = resolveFont(theme.fonts.heading, "heading");
454
477
  const bodyFont = resolveFont(theme.fonts.body, "body");
455
478
  const monoFont = resolveFont(theme.fonts.mono, "mono");
@@ -1121,7 +1144,7 @@ function parseHexColor2(color) {
1121
1144
  a: normalized.length === 8 ? parseChannel2(6) / 255 : 1
1122
1145
  };
1123
1146
  }
1124
- function withAlpha(color, alpha) {
1147
+ function withAlpha2(color, alpha) {
1125
1148
  const parsed = parseHexColor2(color);
1126
1149
  const effectiveAlpha = clamp01(parsed.a * alpha);
1127
1150
  return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${effectiveAlpha})`;
@@ -1178,9 +1201,9 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
1178
1201
  centerY,
1179
1202
  outerRadius
1180
1203
  );
1181
- vignette.addColorStop(0, withAlpha(color, 0));
1182
- vignette.addColorStop(0.6, withAlpha(color, 0));
1183
- vignette.addColorStop(1, withAlpha(color, clamp01(intensity)));
1204
+ vignette.addColorStop(0, withAlpha2(color, 0));
1205
+ vignette.addColorStop(0.6, withAlpha2(color, 0));
1206
+ vignette.addColorStop(1, withAlpha2(color, clamp01(intensity)));
1184
1207
  ctx.save();
1185
1208
  ctx.fillStyle = vignette;
1186
1209
  ctx.fillRect(0, 0, width, height);
@@ -1311,12 +1334,12 @@ var MACOS_DOTS = [
1311
1334
  { fill: "#27C93F", stroke: "#1AAB29" }
1312
1335
  ];
1313
1336
  function drawMacosDots(ctx, x, y) {
1314
- for (const [index, dot] of MACOS_DOTS.entries()) {
1337
+ for (const [index, dot2] of MACOS_DOTS.entries()) {
1315
1338
  ctx.beginPath();
1316
1339
  ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
1317
1340
  ctx.closePath();
1318
- ctx.fillStyle = dot.fill;
1319
- ctx.strokeStyle = dot.stroke;
1341
+ ctx.fillStyle = dot2.fill;
1342
+ ctx.strokeStyle = dot2.stroke;
1320
1343
  ctx.lineWidth = DOT_STROKE_WIDTH;
1321
1344
  ctx.fill();
1322
1345
  ctx.stroke();
@@ -1965,25 +1988,203 @@ function drawOrthogonalPath(ctx, from, to, style) {
1965
1988
  }
1966
1989
 
1967
1990
  // src/renderers/connection.ts
1968
- function center(rect) {
1991
+ var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
1992
+ function rectCenter(rect) {
1969
1993
  return {
1970
1994
  x: rect.x + rect.width / 2,
1971
1995
  y: rect.y + rect.height / 2
1972
1996
  };
1973
1997
  }
1974
- function edgeAnchor(rect, target) {
1975
- const c = center(rect);
1998
+ function edgeAnchor(bounds, target) {
1999
+ const c = rectCenter(bounds);
1976
2000
  const dx = target.x - c.x;
1977
2001
  const dy = target.y - c.y;
1978
- if (Math.abs(dx) >= Math.abs(dy)) {
1979
- return {
1980
- x: dx >= 0 ? rect.x + rect.width : rect.x,
1981
- y: c.y
1982
- };
2002
+ if (dx === 0 && dy === 0) {
2003
+ return { x: c.x, y: c.y - bounds.height / 2 };
2004
+ }
2005
+ const hw = bounds.width / 2;
2006
+ const hh = bounds.height / 2;
2007
+ const absDx = Math.abs(dx);
2008
+ const absDy = Math.abs(dy);
2009
+ const t = absDx * hh > absDy * hw ? hw / absDx : hh / absDy;
2010
+ return { x: c.x + dx * t, y: c.y + dy * t };
2011
+ }
2012
+ function resolveAnchor(bounds, anchor, fallbackTarget) {
2013
+ if (!anchor) return edgeAnchor(bounds, fallbackTarget);
2014
+ if (typeof anchor === "string") {
2015
+ const c2 = rectCenter(bounds);
2016
+ switch (anchor) {
2017
+ case "top":
2018
+ return { x: c2.x, y: bounds.y };
2019
+ case "bottom":
2020
+ return { x: c2.x, y: bounds.y + bounds.height };
2021
+ case "left":
2022
+ return { x: bounds.x, y: c2.y };
2023
+ case "right":
2024
+ return { x: bounds.x + bounds.width, y: c2.y };
2025
+ case "center":
2026
+ return c2;
2027
+ }
2028
+ }
2029
+ const c = rectCenter(bounds);
2030
+ return {
2031
+ x: c.x + anchor.x * (bounds.width / 2),
2032
+ y: c.y + anchor.y * (bounds.height / 2)
2033
+ };
2034
+ }
2035
+ function anchorNormal(anchor, point, diagramCenter) {
2036
+ if (typeof anchor === "string") {
2037
+ switch (anchor) {
2038
+ case "top":
2039
+ return { x: 0, y: -1 };
2040
+ case "bottom":
2041
+ return { x: 0, y: 1 };
2042
+ case "left":
2043
+ return { x: -1, y: 0 };
2044
+ case "right":
2045
+ return { x: 1, y: 0 };
2046
+ case "center":
2047
+ return outwardNormal(point, diagramCenter);
2048
+ }
1983
2049
  }
2050
+ return outwardNormal(point, diagramCenter);
2051
+ }
2052
+ function outwardNormal(point, diagramCenter) {
2053
+ const dx = point.x - diagramCenter.x;
2054
+ const dy = point.y - diagramCenter.y;
2055
+ const len = Math.hypot(dx, dy) || 1;
2056
+ return { x: dx / len, y: dy / len };
2057
+ }
2058
+ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
2059
+ const fromCenter = rectCenter(fromBounds);
2060
+ const toCenter = rectCenter(toBounds);
2061
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
2062
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
2063
+ const dist = Math.hypot(p3.x - p0.x, p3.y - p0.y);
2064
+ const offset = dist * tension;
2065
+ const n0 = anchorNormal(fromAnchor, p0, diagramCenter);
2066
+ const n3 = anchorNormal(toAnchor, p3, diagramCenter);
2067
+ const cp1 = { x: p0.x + n0.x * offset, y: p0.y + n0.y * offset };
2068
+ const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
2069
+ return [p0, cp1, cp2, p3];
2070
+ }
2071
+ function dot(a, b) {
2072
+ return a.x * b.x + a.y * b.y;
2073
+ }
2074
+ function localToWorld(origin, axisX, axisY, local) {
2075
+ return {
2076
+ x: origin.x + axisX.x * local.x + axisY.x * local.y,
2077
+ y: origin.y + axisX.y * local.x + axisY.y * local.y
2078
+ };
2079
+ }
2080
+ function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
2081
+ const fromCenter = rectCenter(fromBounds);
2082
+ const toCenter = rectCenter(toBounds);
2083
+ const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
2084
+ const end = resolveAnchor(toBounds, toAnchor, fromCenter);
2085
+ const chord = { x: end.x - start.x, y: end.y - start.y };
2086
+ const chordLength = Math.hypot(chord.x, chord.y);
2087
+ if (chordLength < 1e-6) {
2088
+ const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2089
+ return [
2090
+ [start, start, mid, mid],
2091
+ [mid, mid, end, end]
2092
+ ];
2093
+ }
2094
+ const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
2095
+ let axisY = { x: -axisX.y, y: axisX.x };
2096
+ const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2097
+ const outwardHint = outwardNormal(midpoint, diagramCenter);
2098
+ if (dot(axisY, outwardHint) < 0) {
2099
+ axisY = { x: -axisY.x, y: -axisY.y };
2100
+ }
2101
+ const semiMajor = chordLength / 2;
2102
+ const semiMinor = Math.max(12, chordLength * tension * 0.75);
2103
+ const p0Local = { x: -semiMajor, y: 0 };
2104
+ const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2105
+ const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2106
+ const pMidLocal = { x: 0, y: semiMinor };
2107
+ const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2108
+ const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2109
+ const p3Local = { x: semiMajor, y: 0 };
2110
+ const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
2111
+ const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
2112
+ const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
2113
+ const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
2114
+ const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
2115
+ const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
2116
+ const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
2117
+ return [
2118
+ [p0, cp1, cp2, pMid],
2119
+ [pMid, cp3, cp4, p3]
2120
+ ];
2121
+ }
2122
+ function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
2123
+ const fromC = rectCenter(fromBounds);
2124
+ const toC = rectCenter(toBounds);
2125
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
2126
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
2127
+ const midX = (p0.x + p3.x) / 2;
2128
+ return [p0, { x: midX, y: p0.y }, { x: midX, y: p3.y }, p3];
2129
+ }
2130
+ function bezierPointAt(p0, cp1, cp2, p3, t) {
2131
+ const mt = 1 - t;
2132
+ return {
2133
+ x: mt * mt * mt * p0.x + 3 * mt * mt * t * cp1.x + 3 * mt * t * t * cp2.x + t * t * t * p3.x,
2134
+ y: mt * mt * mt * p0.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * p3.y
2135
+ };
2136
+ }
2137
+ function bezierTangentAt(p0, cp1, cp2, p3, t) {
2138
+ const mt = 1 - t;
1984
2139
  return {
1985
- x: c.x,
1986
- y: dy >= 0 ? rect.y + rect.height : rect.y
2140
+ x: 3 * mt * mt * (cp1.x - p0.x) + 6 * mt * t * (cp2.x - cp1.x) + 3 * t * t * (p3.x - cp2.x),
2141
+ y: 3 * mt * mt * (cp1.y - p0.y) + 6 * mt * t * (cp2.y - cp1.y) + 3 * t * t * (p3.y - cp2.y)
2142
+ };
2143
+ }
2144
+ function isInsideRect(point, rect) {
2145
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
2146
+ }
2147
+ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
2148
+ const step = 5e-3;
2149
+ if (searchFromEnd) {
2150
+ for (let t = 0.95; t >= 0.5; t -= step) {
2151
+ const pt = bezierPointAt(p0, cp1, cp2, p3, t);
2152
+ if (!isInsideRect(pt, targetRect)) {
2153
+ return t;
2154
+ }
2155
+ }
2156
+ } else {
2157
+ for (let t = 0.05; t <= 0.5; t += step) {
2158
+ const pt = bezierPointAt(p0, cp1, cp2, p3, t);
2159
+ if (!isInsideRect(pt, targetRect)) {
2160
+ return t;
2161
+ }
2162
+ }
2163
+ }
2164
+ return void 0;
2165
+ }
2166
+ function pointAlongArc(route, t) {
2167
+ const [first, second] = route;
2168
+ if (t <= 0.5) {
2169
+ const localT2 = Math.max(0, Math.min(1, t * 2));
2170
+ return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
2171
+ }
2172
+ const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
2173
+ return bezierPointAt(second[0], second[1], second[2], second[3], localT);
2174
+ }
2175
+ function computeDiagramCenter(nodeBounds, canvasCenter) {
2176
+ if (nodeBounds.length === 0) {
2177
+ return canvasCenter ?? { x: 0, y: 0 };
2178
+ }
2179
+ let totalX = 0;
2180
+ let totalY = 0;
2181
+ for (const bounds of nodeBounds) {
2182
+ totalX += bounds.x + bounds.width / 2;
2183
+ totalY += bounds.y + bounds.height / 2;
2184
+ }
2185
+ return {
2186
+ x: totalX / nodeBounds.length,
2187
+ y: totalY / nodeBounds.length
1987
2188
  };
1988
2189
  }
1989
2190
  function dashFromStyle(style) {
@@ -2067,51 +2268,148 @@ function polylineBounds(points) {
2067
2268
  height: Math.max(1, maxY - minY)
2068
2269
  };
2069
2270
  }
2070
- function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute) {
2071
- const fromCenter = center(fromBounds);
2072
- const toCenter = center(toBounds);
2073
- const from = edgeAnchor(fromBounds, toCenter);
2074
- const to = edgeAnchor(toBounds, fromCenter);
2075
- const dash = dashFromStyle(conn.style);
2271
+ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
2272
+ const routing = conn.routing ?? "auto";
2273
+ const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
2274
+ const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
2275
+ const tension = conn.tension ?? 0.35;
2276
+ const dash = dashFromStyle(strokeStyle);
2076
2277
  const style = {
2077
2278
  color: conn.color ?? theme.borderMuted,
2078
- width: conn.width ?? 2,
2279
+ width: strokeWidth,
2079
2280
  headSize: conn.arrowSize ?? 10,
2080
2281
  ...dash ? { dash } : {}
2081
2282
  };
2082
- const points = edgeRoute && edgeRoute.points.length >= 2 ? edgeRoute.points : [from, { x: (from.x + to.x) / 2, y: from.y }, { x: (from.x + to.x) / 2, y: to.y }, to];
2083
- const startSegment = points[1] ?? points[0];
2084
- const endStart = points[points.length - 2] ?? points[0];
2085
- const end = points[points.length - 1] ?? points[0];
2086
- let startAngle = Math.atan2(startSegment.y - points[0].y, startSegment.x - points[0].x) + Math.PI;
2087
- let endAngle = Math.atan2(end.y - endStart.y, end.x - endStart.x);
2283
+ const labelT = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
2284
+ const diagramCenter = options?.diagramCenter ?? computeDiagramCenter([fromBounds, toBounds]);
2285
+ let linePoints;
2286
+ let startPoint;
2287
+ let endPoint;
2288
+ let startAngle;
2289
+ let endAngle;
2290
+ let labelPoint;
2291
+ ctx.save();
2292
+ ctx.globalAlpha = conn.opacity;
2293
+ const arrowPlacement = conn.arrowPlacement ?? "endpoint";
2294
+ if (routing === "curve") {
2295
+ const [p0, cp1, cp2, p3] = curveRoute(
2296
+ fromBounds,
2297
+ toBounds,
2298
+ diagramCenter,
2299
+ tension,
2300
+ conn.fromAnchor,
2301
+ conn.toAnchor
2302
+ );
2303
+ ctx.strokeStyle = style.color;
2304
+ ctx.lineWidth = style.width;
2305
+ ctx.setLineDash(style.dash ?? []);
2306
+ ctx.beginPath();
2307
+ ctx.moveTo(p0.x, p0.y);
2308
+ ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, p3.x, p3.y);
2309
+ ctx.stroke();
2310
+ linePoints = [p0, cp1, cp2, p3];
2311
+ startPoint = p0;
2312
+ endPoint = p3;
2313
+ startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
2314
+ endAngle = Math.atan2(p3.y - cp2.y, p3.x - cp2.x);
2315
+ labelPoint = bezierPointAt(p0, cp1, cp2, p3, labelT);
2316
+ if (arrowPlacement === "boundary") {
2317
+ if (conn.arrow === "end" || conn.arrow === "both") {
2318
+ const tEnd = findBoundaryIntersection(p0, cp1, cp2, p3, toBounds, true);
2319
+ if (tEnd !== void 0) {
2320
+ endPoint = bezierPointAt(p0, cp1, cp2, p3, tEnd);
2321
+ const tangent = bezierTangentAt(p0, cp1, cp2, p3, tEnd);
2322
+ endAngle = Math.atan2(tangent.y, tangent.x);
2323
+ }
2324
+ }
2325
+ if (conn.arrow === "start" || conn.arrow === "both") {
2326
+ const tStart = findBoundaryIntersection(p0, cp1, cp2, p3, fromBounds, false);
2327
+ if (tStart !== void 0) {
2328
+ startPoint = bezierPointAt(p0, cp1, cp2, p3, tStart);
2329
+ const tangent = bezierTangentAt(p0, cp1, cp2, p3, tStart);
2330
+ startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
2331
+ }
2332
+ }
2333
+ }
2334
+ } else if (routing === "arc") {
2335
+ const [first, second] = arcRoute(
2336
+ fromBounds,
2337
+ toBounds,
2338
+ diagramCenter,
2339
+ tension,
2340
+ conn.fromAnchor,
2341
+ conn.toAnchor
2342
+ );
2343
+ const [p0, cp1, cp2, pMid] = first;
2344
+ const [, cp3, cp4, p3] = second;
2345
+ ctx.strokeStyle = style.color;
2346
+ ctx.lineWidth = style.width;
2347
+ ctx.setLineDash(style.dash ?? []);
2348
+ ctx.beginPath();
2349
+ ctx.moveTo(p0.x, p0.y);
2350
+ ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
2351
+ ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
2352
+ ctx.stroke();
2353
+ linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
2354
+ startPoint = p0;
2355
+ endPoint = p3;
2356
+ startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
2357
+ endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
2358
+ labelPoint = pointAlongArc([first, second], labelT);
2359
+ if (arrowPlacement === "boundary") {
2360
+ if (conn.arrow === "end" || conn.arrow === "both") {
2361
+ const [, s_cp3, s_cp4, s_p3] = second;
2362
+ const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
2363
+ if (tEnd !== void 0) {
2364
+ endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
2365
+ const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
2366
+ endAngle = Math.atan2(tangent.y, tangent.x);
2367
+ }
2368
+ }
2369
+ if (conn.arrow === "start" || conn.arrow === "both") {
2370
+ const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
2371
+ if (tStart !== void 0) {
2372
+ startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
2373
+ const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
2374
+ startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
2375
+ }
2376
+ }
2377
+ }
2378
+ } else {
2379
+ const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
2380
+ const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
2381
+ linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor) : orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
2382
+ startPoint = linePoints[0];
2383
+ const startSegment = linePoints[1] ?? linePoints[0];
2384
+ const endStart = linePoints[linePoints.length - 2] ?? linePoints[0];
2385
+ endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
2386
+ startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
2387
+ endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
2388
+ if (useElkRoute) {
2389
+ drawCubicInterpolatedPath(ctx, linePoints, style);
2390
+ } else {
2391
+ drawOrthogonalPath(ctx, startPoint, endPoint, style);
2392
+ }
2393
+ labelPoint = pointAlongPolyline(linePoints, labelT);
2394
+ }
2088
2395
  if (!Number.isFinite(startAngle)) {
2089
2396
  startAngle = 0;
2090
2397
  }
2091
2398
  if (!Number.isFinite(endAngle)) {
2092
2399
  endAngle = 0;
2093
2400
  }
2094
- const t = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
2095
- const labelPoint = pointAlongPolyline(points, t);
2096
- ctx.save();
2097
- ctx.globalAlpha = conn.opacity;
2098
- if (edgeRoute && edgeRoute.points.length >= 2) {
2099
- drawCubicInterpolatedPath(ctx, points, style);
2100
- } else {
2101
- drawOrthogonalPath(ctx, points[0], points[points.length - 1], style);
2102
- }
2103
2401
  if (conn.arrow === "start" || conn.arrow === "both") {
2104
- drawArrowhead(ctx, points[0], startAngle, style.headSize, style.color);
2402
+ drawArrowhead(ctx, startPoint, startAngle, style.headSize, style.color);
2105
2403
  }
2106
2404
  if (conn.arrow === "end" || conn.arrow === "both") {
2107
- drawArrowhead(ctx, end, endAngle, style.headSize, style.color);
2405
+ drawArrowhead(ctx, endPoint, endAngle, style.headSize, style.color);
2108
2406
  }
2109
2407
  ctx.restore();
2110
2408
  const elements = [
2111
2409
  {
2112
2410
  id: `connection-${conn.from}-${conn.to}`,
2113
2411
  kind: "connection",
2114
- bounds: polylineBounds(points),
2412
+ bounds: polylineBounds(linePoints),
2115
2413
  foregroundColor: style.color
2116
2414
  }
2117
2415
  ];
@@ -2737,6 +3035,36 @@ function renderDrawCommands(ctx, commands, theme) {
2737
3035
  });
2738
3036
  break;
2739
3037
  }
3038
+ case "grid": {
3039
+ const canvasWidth = ctx.canvas.width;
3040
+ const canvasHeight = ctx.canvas.height;
3041
+ withOpacity(ctx, command.opacity, () => {
3042
+ ctx.strokeStyle = command.color;
3043
+ ctx.lineWidth = command.width;
3044
+ const startX = command.offsetX % command.spacing;
3045
+ for (let x = startX; x <= canvasWidth; x += command.spacing) {
3046
+ ctx.beginPath();
3047
+ ctx.moveTo(x, 0);
3048
+ ctx.lineTo(x, canvasHeight);
3049
+ ctx.stroke();
3050
+ }
3051
+ const startY = command.offsetY % command.spacing;
3052
+ for (let y = startY; y <= canvasHeight; y += command.spacing) {
3053
+ ctx.beginPath();
3054
+ ctx.moveTo(0, y);
3055
+ ctx.lineTo(canvasWidth, y);
3056
+ ctx.stroke();
3057
+ }
3058
+ });
3059
+ rendered.push({
3060
+ id,
3061
+ kind: "draw",
3062
+ bounds: { x: 0, y: 0, width: canvasWidth, height: canvasHeight },
3063
+ foregroundColor: command.color,
3064
+ allowOverlap: true
3065
+ });
3066
+ break;
3067
+ }
2740
3068
  }
2741
3069
  }
2742
3070
  return rendered;
@@ -3118,6 +3446,15 @@ var drawGradientRectSchema = z2.object({
3118
3446
  radius: z2.number().min(0).max(256).default(0),
3119
3447
  opacity: z2.number().min(0).max(1).default(1)
3120
3448
  }).strict();
3449
+ var drawGridSchema = z2.object({
3450
+ type: z2.literal("grid"),
3451
+ spacing: z2.number().min(5).max(200).default(40),
3452
+ color: colorHexSchema2.default("#1E2D4A"),
3453
+ width: z2.number().min(0.1).max(4).default(0.5),
3454
+ opacity: z2.number().min(0).max(1).default(0.2),
3455
+ offsetX: z2.number().default(0),
3456
+ offsetY: z2.number().default(0)
3457
+ }).strict();
3121
3458
  var drawCommandSchema = z2.discriminatedUnion("type", [
3122
3459
  drawRectSchema,
3123
3460
  drawCircleSchema,
@@ -3126,7 +3463,8 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
3126
3463
  drawBezierSchema,
3127
3464
  drawPathSchema,
3128
3465
  drawBadgeSchema,
3129
- drawGradientRectSchema
3466
+ drawGradientRectSchema,
3467
+ drawGridSchema
3130
3468
  ]);
3131
3469
  var defaultCanvas = {
3132
3470
  width: 1200,
@@ -3190,10 +3528,26 @@ var cardElementSchema = z2.object({
3190
3528
  tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
3191
3529
  icon: z2.string().min(1).max(64).optional()
3192
3530
  }).strict();
3531
+ var flowNodeShadowSchema = z2.object({
3532
+ color: colorHexSchema2.optional(),
3533
+ blur: z2.number().min(0).max(64).default(8),
3534
+ offsetX: z2.number().min(-32).max(32).default(0),
3535
+ offsetY: z2.number().min(-32).max(32).default(0),
3536
+ opacity: z2.number().min(0).max(1).default(0.3)
3537
+ }).strict();
3193
3538
  var flowNodeElementSchema = z2.object({
3194
3539
  type: z2.literal("flow-node"),
3195
3540
  id: z2.string().min(1).max(120),
3196
- shape: z2.enum(["box", "rounded-box", "diamond", "circle", "pill", "cylinder", "parallelogram"]),
3541
+ shape: z2.enum([
3542
+ "box",
3543
+ "rounded-box",
3544
+ "diamond",
3545
+ "circle",
3546
+ "pill",
3547
+ "cylinder",
3548
+ "parallelogram",
3549
+ "hexagon"
3550
+ ]).default("rounded-box"),
3197
3551
  label: z2.string().min(1).max(200),
3198
3552
  sublabel: z2.string().min(1).max(300).optional(),
3199
3553
  sublabelColor: colorHexSchema2.optional(),
@@ -3213,20 +3567,35 @@ var flowNodeElementSchema = z2.object({
3213
3567
  badgeText: z2.string().min(1).max(32).optional(),
3214
3568
  badgeColor: colorHexSchema2.optional(),
3215
3569
  badgeBackground: colorHexSchema2.optional(),
3216
- badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
3570
+ badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
3571
+ shadow: flowNodeShadowSchema.optional()
3217
3572
  }).strict();
3573
+ var anchorHintSchema = z2.union([
3574
+ z2.enum(["top", "bottom", "left", "right", "center"]),
3575
+ z2.object({
3576
+ x: z2.number().min(-1).max(1),
3577
+ y: z2.number().min(-1).max(1)
3578
+ }).strict()
3579
+ ]);
3218
3580
  var connectionElementSchema = z2.object({
3219
3581
  type: z2.literal("connection"),
3220
3582
  from: z2.string().min(1).max(120),
3221
3583
  to: z2.string().min(1).max(120),
3222
3584
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3585
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3223
3586
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
3224
3587
  label: z2.string().min(1).max(200).optional(),
3225
3588
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
3226
3589
  color: colorHexSchema2.optional(),
3227
- width: z2.number().min(0.5).max(8).optional(),
3590
+ width: z2.number().min(0.5).max(10).optional(),
3591
+ strokeWidth: z2.number().min(0.5).max(10).default(2),
3228
3592
  arrowSize: z2.number().min(4).max(32).optional(),
3229
- opacity: z2.number().min(0).max(1).default(1)
3593
+ arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
3594
+ opacity: z2.number().min(0).max(1).default(1),
3595
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
3596
+ tension: z2.number().min(0.1).max(0.8).default(0.35),
3597
+ fromAnchor: anchorHintSchema.optional(),
3598
+ toAnchor: anchorHintSchema.optional()
3230
3599
  }).strict();
3231
3600
  var codeBlockStyleSchema = z2.object({
3232
3601
  paddingVertical: z2.number().min(0).max(128).default(56),
@@ -3295,6 +3664,10 @@ var elementSchema = z2.discriminatedUnion("type", [
3295
3664
  shapeElementSchema,
3296
3665
  imageElementSchema
3297
3666
  ]);
3667
+ var diagramCenterSchema = z2.object({
3668
+ x: z2.number(),
3669
+ y: z2.number()
3670
+ }).strict();
3298
3671
  var autoLayoutConfigSchema = z2.object({
3299
3672
  mode: z2.literal("auto"),
3300
3673
  algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
@@ -3310,7 +3683,9 @@ var autoLayoutConfigSchema = z2.object({
3310
3683
  /** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
3311
3684
  radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
3312
3685
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
3313
- radialSortBy: z2.enum(["id", "connections"]).optional()
3686
+ radialSortBy: z2.enum(["id", "connections"]).optional(),
3687
+ /** Explicit center used by curve/arc connection routing. */
3688
+ diagramCenter: diagramCenterSchema.optional()
3314
3689
  }).strict();
3315
3690
  var gridLayoutConfigSchema = z2.object({
3316
3691
  mode: z2.literal("grid"),
@@ -3318,13 +3693,17 @@ var gridLayoutConfigSchema = z2.object({
3318
3693
  gap: z2.number().int().min(0).max(256).default(24),
3319
3694
  cardMinHeight: z2.number().int().min(32).max(4096).optional(),
3320
3695
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
3321
- equalHeight: z2.boolean().default(false)
3696
+ equalHeight: z2.boolean().default(false),
3697
+ /** Explicit center used by curve/arc connection routing. */
3698
+ diagramCenter: diagramCenterSchema.optional()
3322
3699
  }).strict();
3323
3700
  var stackLayoutConfigSchema = z2.object({
3324
3701
  mode: z2.literal("stack"),
3325
3702
  direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
3326
3703
  gap: z2.number().int().min(0).max(256).default(24),
3327
- alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
3704
+ alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
3705
+ /** Explicit center used by curve/arc connection routing. */
3706
+ diagramCenter: diagramCenterSchema.optional()
3328
3707
  }).strict();
3329
3708
  var manualPositionSchema = z2.object({
3330
3709
  x: z2.number().int(),
@@ -3334,7 +3713,9 @@ var manualPositionSchema = z2.object({
3334
3713
  }).strict();
3335
3714
  var manualLayoutConfigSchema = z2.object({
3336
3715
  mode: z2.literal("manual"),
3337
- positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
3716
+ positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
3717
+ /** Explicit center used by curve/arc connection routing. */
3718
+ diagramCenter: diagramCenterSchema.optional()
3338
3719
  }).strict();
3339
3720
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
3340
3721
  autoLayoutConfigSchema,
@@ -3386,6 +3767,31 @@ var canvasSchema = z2.object({
3386
3767
  padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
3387
3768
  }).strict();
3388
3769
  var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
3770
+ var diagramPositionSchema = z2.object({
3771
+ x: z2.number(),
3772
+ y: z2.number(),
3773
+ width: z2.number().positive(),
3774
+ height: z2.number().positive()
3775
+ }).strict();
3776
+ var diagramElementSchema = z2.discriminatedUnion("type", [
3777
+ flowNodeElementSchema,
3778
+ connectionElementSchema
3779
+ ]);
3780
+ var diagramLayoutSchema = z2.object({
3781
+ mode: z2.enum(["manual", "auto"]).default("manual"),
3782
+ positions: z2.record(z2.string(), diagramPositionSchema).optional(),
3783
+ diagramCenter: diagramCenterSchema.optional()
3784
+ }).strict();
3785
+ var diagramSpecSchema = z2.object({
3786
+ version: z2.literal(1),
3787
+ canvas: z2.object({
3788
+ width: z2.number().int().min(320).max(4096).default(1200),
3789
+ height: z2.number().int().min(180).max(4096).default(675)
3790
+ }).default({ width: 1200, height: 675 }),
3791
+ theme: themeSchema.optional(),
3792
+ elements: z2.array(diagramElementSchema).min(1),
3793
+ layout: diagramLayoutSchema.default({ mode: "manual" })
3794
+ }).strict();
3389
3795
  var designSpecSchema = z2.object({
3390
3796
  version: z2.literal(2).default(2),
3391
3797
  canvas: canvasSchema.default(defaultCanvas),
@@ -3745,6 +4151,10 @@ async function renderDesign(input, options = {}) {
3745
4151
  break;
3746
4152
  }
3747
4153
  }
4154
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
4155
+ spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
4156
+ { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
4157
+ );
3748
4158
  for (const element of spec.elements) {
3749
4159
  if (element.type !== "connection") {
3750
4160
  continue;
@@ -3757,7 +4167,9 @@ async function renderDesign(input, options = {}) {
3757
4167
  );
3758
4168
  }
3759
4169
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
3760
- elements.push(...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute));
4170
+ elements.push(
4171
+ ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
4172
+ );
3761
4173
  }
3762
4174
  if (footerRect && spec.footer) {
3763
4175
  const footerText = spec.footer.tagline ? `${spec.footer.text} \u2022 ${spec.footer.tagline}` : spec.footer.text;