@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/README.md +42 -1
- package/dist/cli.js +262 -124
- package/dist/index.d.ts +44 -4
- package/dist/index.js +266 -74
- package/dist/qa.d.ts +1 -1
- package/dist/qa.js +50 -7
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +262 -124
- package/dist/{spec.schema-B6sXTTou.d.ts → spec.schema-BkbcnVcm.d.ts} +1790 -1267
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +50 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,7 @@ Plus a **freestyle draw layer** with 8 draw command types: `rect`, `circle`, `te
|
|
|
125
125
|
- **auto** — ELK.js layout engine (layered, stress, force, radial, box algorithms)
|
|
126
126
|
- **grid** — column-based grid layout
|
|
127
127
|
- **stack** — vertical or horizontal stack
|
|
128
|
+
- **ellipse** — evenly spaced nodes on a configurable ellipse
|
|
128
129
|
- **manual** — explicit x/y coordinates
|
|
129
130
|
|
|
130
131
|
### Connection Routing Modes
|
|
@@ -138,6 +139,36 @@ Per-connection `routing` supports:
|
|
|
138
139
|
|
|
139
140
|
`layout.diagramCenter` can optionally override the center point used by `curve` and `arc` routing. When omitted, center is derived from the laid-out element centroid (fallback: canvas center).
|
|
140
141
|
|
|
142
|
+
### Hexagonal Preset via Ellipse Layout
|
|
143
|
+
|
|
144
|
+
A six-node hexagonal arrangement is just an ellipse layout with equal angular spacing and matching routing hints:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const spec = parseDesignSpec({
|
|
148
|
+
version: 2,
|
|
149
|
+
theme: 'dark',
|
|
150
|
+
elements: [
|
|
151
|
+
{ type: 'flow-node', id: 'triage', label: 'Triage', shape: 'rounded-box' },
|
|
152
|
+
{ type: 'flow-node', id: 'design', label: 'Design', shape: 'rounded-box' },
|
|
153
|
+
{ type: 'flow-node', id: 'build', label: 'Build', shape: 'rounded-box' },
|
|
154
|
+
{ type: 'flow-node', id: 'test', label: 'Test', shape: 'rounded-box' },
|
|
155
|
+
{ type: 'flow-node', id: 'deploy', label: 'Deploy', shape: 'rounded-box' },
|
|
156
|
+
{ type: 'flow-node', id: 'observe', label: 'Observe', shape: 'rounded-box' },
|
|
157
|
+
],
|
|
158
|
+
layout: {
|
|
159
|
+
mode: 'ellipse',
|
|
160
|
+
cx: 600,
|
|
161
|
+
cy: 340,
|
|
162
|
+
rx: 280,
|
|
163
|
+
ry: 180,
|
|
164
|
+
startAngle: -90,
|
|
165
|
+
diagramCenter: { x: 600, y: 340 },
|
|
166
|
+
ellipseRx: 280,
|
|
167
|
+
ellipseRy: 180,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
141
172
|
## Programmatic Usage
|
|
142
173
|
|
|
143
174
|
```ts
|
|
@@ -157,7 +188,17 @@ const spec = parseDesignSpec({
|
|
|
157
188
|
{ type: 'flow-node', id: 'b', label: 'End', shape: 'rounded-box', color: '#059669' },
|
|
158
189
|
{ type: 'connection', from: 'a', to: 'b', label: 'next', routing: 'arc' },
|
|
159
190
|
],
|
|
160
|
-
layout: {
|
|
191
|
+
layout: {
|
|
192
|
+
mode: 'ellipse',
|
|
193
|
+
cx: 600,
|
|
194
|
+
cy: 340,
|
|
195
|
+
rx: 280,
|
|
196
|
+
ry: 180,
|
|
197
|
+
startAngle: -90,
|
|
198
|
+
diagramCenter: { x: 600, y: 340 },
|
|
199
|
+
ellipseRx: 280,
|
|
200
|
+
ellipseRy: 180,
|
|
201
|
+
},
|
|
161
202
|
});
|
|
162
203
|
|
|
163
204
|
const render = await renderDesign(spec, { generatorVersion: '0.3.0' });
|
package/dist/cli.js
CHANGED
|
@@ -766,6 +766,10 @@ var drawShadowSchema = z2.object({
|
|
|
766
766
|
offsetY: z2.number().default(4)
|
|
767
767
|
}).strict();
|
|
768
768
|
var drawFontFamilySchema = z2.enum(["heading", "body", "mono"]);
|
|
769
|
+
var strokeGradientSchema = z2.object({
|
|
770
|
+
from: colorHexSchema2,
|
|
771
|
+
to: colorHexSchema2
|
|
772
|
+
}).strict();
|
|
769
773
|
var drawRectSchema = z2.object({
|
|
770
774
|
type: z2.literal("rect"),
|
|
771
775
|
x: z2.number(),
|
|
@@ -813,6 +817,7 @@ var drawLineSchema = z2.object({
|
|
|
813
817
|
x2: z2.number(),
|
|
814
818
|
y2: z2.number(),
|
|
815
819
|
color: colorHexSchema2.default("#FFFFFF"),
|
|
820
|
+
strokeGradient: strokeGradientSchema.optional(),
|
|
816
821
|
width: z2.number().min(0.5).max(32).default(2),
|
|
817
822
|
dash: z2.array(z2.number()).max(6).optional(),
|
|
818
823
|
arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
|
|
@@ -843,6 +848,7 @@ var drawBezierSchema = z2.object({
|
|
|
843
848
|
type: z2.literal("bezier"),
|
|
844
849
|
points: z2.array(drawPointSchema).min(2).max(20),
|
|
845
850
|
color: colorHexSchema2.default("#FFFFFF"),
|
|
851
|
+
strokeGradient: strokeGradientSchema.optional(),
|
|
846
852
|
width: z2.number().min(0.5).max(32).default(2),
|
|
847
853
|
dash: z2.array(z2.number()).max(6).optional(),
|
|
848
854
|
arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
|
|
@@ -1050,7 +1056,8 @@ var connectionElementSchema = z2.object({
|
|
|
1050
1056
|
from: z2.string().min(1).max(120),
|
|
1051
1057
|
to: z2.string().min(1).max(120),
|
|
1052
1058
|
style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
1053
|
-
|
|
1059
|
+
/** @deprecated Use `style` instead. */
|
|
1060
|
+
strokeStyle: z2.enum(["solid", "dashed", "dotted"]).optional(),
|
|
1054
1061
|
arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
|
|
1055
1062
|
label: z2.string().min(1).max(200).optional(),
|
|
1056
1063
|
labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
|
|
@@ -1062,7 +1069,8 @@ var connectionElementSchema = z2.object({
|
|
|
1062
1069
|
arrowSize: z2.number().min(4).max(32).optional(),
|
|
1063
1070
|
arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
|
|
1064
1071
|
opacity: z2.number().min(0).max(1).default(1),
|
|
1065
|
-
routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
|
|
1072
|
+
routing: z2.enum(["auto", "orthogonal", "curve", "arc", "straight"]).default("auto"),
|
|
1073
|
+
curveMode: z2.enum(["normal", "ellipse"]).default("normal"),
|
|
1066
1074
|
tension: z2.number().min(0.1).max(0.8).default(0.35),
|
|
1067
1075
|
fromAnchor: anchorHintSchema.optional(),
|
|
1068
1076
|
toAnchor: anchorHintSchema.optional()
|
|
@@ -1155,7 +1163,11 @@ var autoLayoutConfigSchema = z2.object({
|
|
|
1155
1163
|
/** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
|
|
1156
1164
|
radialSortBy: z2.enum(["id", "connections"]).optional(),
|
|
1157
1165
|
/** Explicit center used by curve/arc connection routing. */
|
|
1158
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1166
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1167
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1168
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1169
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1170
|
+
ellipseRy: z2.number().positive().optional()
|
|
1159
1171
|
}).strict();
|
|
1160
1172
|
var gridLayoutConfigSchema = z2.object({
|
|
1161
1173
|
mode: z2.literal("grid"),
|
|
@@ -1165,7 +1177,11 @@ var gridLayoutConfigSchema = z2.object({
|
|
|
1165
1177
|
cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
|
|
1166
1178
|
equalHeight: z2.boolean().default(false),
|
|
1167
1179
|
/** Explicit center used by curve/arc connection routing. */
|
|
1168
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1180
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1181
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1182
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1183
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1184
|
+
ellipseRy: z2.number().positive().optional()
|
|
1169
1185
|
}).strict();
|
|
1170
1186
|
var stackLayoutConfigSchema = z2.object({
|
|
1171
1187
|
mode: z2.literal("stack"),
|
|
@@ -1173,7 +1189,25 @@ var stackLayoutConfigSchema = z2.object({
|
|
|
1173
1189
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
1174
1190
|
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
|
|
1175
1191
|
/** Explicit center used by curve/arc connection routing. */
|
|
1176
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1192
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1193
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1194
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1195
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1196
|
+
ellipseRy: z2.number().positive().optional()
|
|
1197
|
+
}).strict();
|
|
1198
|
+
var ellipseLayoutConfigSchema = z2.object({
|
|
1199
|
+
mode: z2.literal("ellipse"),
|
|
1200
|
+
cx: z2.number().optional(),
|
|
1201
|
+
cy: z2.number().optional(),
|
|
1202
|
+
rx: z2.number().positive(),
|
|
1203
|
+
ry: z2.number().positive(),
|
|
1204
|
+
startAngle: z2.number().default(-90),
|
|
1205
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
1206
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1207
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1208
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1209
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1210
|
+
ellipseRy: z2.number().positive().optional()
|
|
1177
1211
|
}).strict();
|
|
1178
1212
|
var manualPositionSchema = z2.object({
|
|
1179
1213
|
x: z2.number().int(),
|
|
@@ -1185,12 +1219,17 @@ var manualLayoutConfigSchema = z2.object({
|
|
|
1185
1219
|
mode: z2.literal("manual"),
|
|
1186
1220
|
positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
|
|
1187
1221
|
/** Explicit center used by curve/arc connection routing. */
|
|
1188
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1222
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1223
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1224
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1225
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1226
|
+
ellipseRy: z2.number().positive().optional()
|
|
1189
1227
|
}).strict();
|
|
1190
1228
|
var layoutConfigSchema = z2.discriminatedUnion("mode", [
|
|
1191
1229
|
autoLayoutConfigSchema,
|
|
1192
1230
|
gridLayoutConfigSchema,
|
|
1193
1231
|
stackLayoutConfigSchema,
|
|
1232
|
+
ellipseLayoutConfigSchema,
|
|
1194
1233
|
manualLayoutConfigSchema
|
|
1195
1234
|
]);
|
|
1196
1235
|
var constraintsSchema = z2.object({
|
|
@@ -1250,7 +1289,11 @@ var diagramElementSchema = z2.discriminatedUnion("type", [
|
|
|
1250
1289
|
var diagramLayoutSchema = z2.object({
|
|
1251
1290
|
mode: z2.enum(["manual", "auto"]).default("manual"),
|
|
1252
1291
|
positions: z2.record(z2.string(), diagramPositionSchema).optional(),
|
|
1253
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1292
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1293
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1294
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1295
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1296
|
+
ellipseRy: z2.number().positive().optional()
|
|
1254
1297
|
}).strict();
|
|
1255
1298
|
var diagramSpecSchema = z2.object({
|
|
1256
1299
|
version: z2.literal(1),
|
|
@@ -2417,6 +2460,35 @@ async function computeElkLayout(elements, config, safeFrame) {
|
|
|
2417
2460
|
};
|
|
2418
2461
|
}
|
|
2419
2462
|
|
|
2463
|
+
// src/layout/ellipse.ts
|
|
2464
|
+
function clampDimension(estimated, max) {
|
|
2465
|
+
return Math.max(1, Math.min(max, Math.floor(estimated)));
|
|
2466
|
+
}
|
|
2467
|
+
function computeEllipseLayout(elements, config, safeFrame) {
|
|
2468
|
+
const placeable = elements.filter((element) => element.type !== "connection");
|
|
2469
|
+
const positions = /* @__PURE__ */ new Map();
|
|
2470
|
+
if (placeable.length === 0) {
|
|
2471
|
+
return { positions };
|
|
2472
|
+
}
|
|
2473
|
+
const cx = config.cx ?? safeFrame.x + safeFrame.width / 2;
|
|
2474
|
+
const cy = config.cy ?? safeFrame.y + safeFrame.height / 2;
|
|
2475
|
+
const stepDegrees = 360 / placeable.length;
|
|
2476
|
+
for (const [index, element] of placeable.entries()) {
|
|
2477
|
+
const angleRadians = (config.startAngle + index * stepDegrees) * Math.PI / 180;
|
|
2478
|
+
const centerX = cx + config.rx * Math.cos(angleRadians);
|
|
2479
|
+
const centerY = cy + config.ry * Math.sin(angleRadians);
|
|
2480
|
+
const width = clampDimension(estimateElementWidth(element), safeFrame.width);
|
|
2481
|
+
const height = clampDimension(estimateElementHeight(element), safeFrame.height);
|
|
2482
|
+
positions.set(element.id, {
|
|
2483
|
+
x: Math.round(centerX - width / 2),
|
|
2484
|
+
y: Math.round(centerY - height / 2),
|
|
2485
|
+
width,
|
|
2486
|
+
height
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
return { positions };
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2420
2492
|
// src/layout/grid.ts
|
|
2421
2493
|
function computeGridLayout(elements, config, safeFrame) {
|
|
2422
2494
|
const placeable = elements.filter((element) => element.type !== "connection");
|
|
@@ -2512,6 +2584,8 @@ async function computeLayout(elements, layout, safeFrame) {
|
|
|
2512
2584
|
return computeGridLayout(elements, layout, safeFrame);
|
|
2513
2585
|
case "stack":
|
|
2514
2586
|
return computeStackLayout(elements, layout, safeFrame);
|
|
2587
|
+
case "ellipse":
|
|
2588
|
+
return computeEllipseLayout(elements, layout, safeFrame);
|
|
2515
2589
|
case "manual":
|
|
2516
2590
|
return computeManualLayout(elements, layout, safeFrame);
|
|
2517
2591
|
default:
|
|
@@ -2778,12 +2852,12 @@ var MACOS_DOTS = [
|
|
|
2778
2852
|
{ fill: "#27C93F", stroke: "#1AAB29" }
|
|
2779
2853
|
];
|
|
2780
2854
|
function drawMacosDots(ctx, x, y) {
|
|
2781
|
-
for (const [index,
|
|
2855
|
+
for (const [index, dot] of MACOS_DOTS.entries()) {
|
|
2782
2856
|
ctx.beginPath();
|
|
2783
2857
|
ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
|
|
2784
2858
|
ctx.closePath();
|
|
2785
|
-
ctx.fillStyle =
|
|
2786
|
-
ctx.strokeStyle =
|
|
2859
|
+
ctx.fillStyle = dot.fill;
|
|
2860
|
+
ctx.strokeStyle = dot.stroke;
|
|
2787
2861
|
ctx.lineWidth = DOT_STROKE_WIDTH;
|
|
2788
2862
|
ctx.fill();
|
|
2789
2863
|
ctx.stroke();
|
|
@@ -3270,56 +3344,71 @@ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, to
|
|
|
3270
3344
|
const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
|
|
3271
3345
|
return [p0, cp1, cp2, p3];
|
|
3272
3346
|
}
|
|
3273
|
-
function
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3347
|
+
function inferEllipseParams(nodeBounds, explicitCenter, explicitRx, explicitRy) {
|
|
3348
|
+
if (nodeBounds.length === 0) {
|
|
3349
|
+
return {
|
|
3350
|
+
cx: explicitCenter?.x ?? 0,
|
|
3351
|
+
cy: explicitCenter?.y ?? 0,
|
|
3352
|
+
rx: explicitRx ?? 1,
|
|
3353
|
+
ry: explicitRy ?? 1
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
const centers = nodeBounds.map(rectCenter);
|
|
3357
|
+
const cx = explicitCenter?.x ?? centers.reduce((sum, c) => sum + c.x, 0) / centers.length;
|
|
3358
|
+
const cy = explicitCenter?.y ?? centers.reduce((sum, c) => sum + c.y, 0) / centers.length;
|
|
3359
|
+
const rx = explicitRx ?? Math.max(1, ...centers.map((c) => Math.abs(c.x - cx)));
|
|
3360
|
+
const ry = explicitRy ?? Math.max(1, ...centers.map((c) => Math.abs(c.y - cy)));
|
|
3361
|
+
return { cx, cy, rx, ry };
|
|
3281
3362
|
}
|
|
3282
|
-
function
|
|
3363
|
+
function ellipseRoute(fromBounds, toBounds, ellipse, fromAnchor, toAnchor) {
|
|
3283
3364
|
const fromCenter = rectCenter(fromBounds);
|
|
3284
3365
|
const toCenter = rectCenter(toBounds);
|
|
3285
|
-
const
|
|
3286
|
-
const
|
|
3287
|
-
const
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
const
|
|
3299
|
-
const
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
const
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
const
|
|
3309
|
-
const
|
|
3310
|
-
const
|
|
3311
|
-
const
|
|
3312
|
-
const
|
|
3313
|
-
const
|
|
3314
|
-
const
|
|
3315
|
-
const
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3366
|
+
const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
|
|
3367
|
+
const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
|
|
3368
|
+
const theta1 = Math.atan2(
|
|
3369
|
+
(fromCenter.y - ellipse.cy) / ellipse.ry,
|
|
3370
|
+
(fromCenter.x - ellipse.cx) / ellipse.rx
|
|
3371
|
+
);
|
|
3372
|
+
const theta2 = Math.atan2(
|
|
3373
|
+
(toCenter.y - ellipse.cy) / ellipse.ry,
|
|
3374
|
+
(toCenter.x - ellipse.cx) / ellipse.rx
|
|
3375
|
+
);
|
|
3376
|
+
let angularSpan = theta2 - theta1;
|
|
3377
|
+
while (angularSpan > Math.PI) angularSpan -= 2 * Math.PI;
|
|
3378
|
+
while (angularSpan <= -Math.PI) angularSpan += 2 * Math.PI;
|
|
3379
|
+
const absSpan = Math.abs(angularSpan);
|
|
3380
|
+
const kappa = absSpan < 1e-6 ? 0 : 4 / 3 * Math.tan(absSpan / 4);
|
|
3381
|
+
const tangent1 = {
|
|
3382
|
+
x: -ellipse.rx * Math.sin(theta1),
|
|
3383
|
+
y: ellipse.ry * Math.cos(theta1)
|
|
3384
|
+
};
|
|
3385
|
+
const tangent2 = {
|
|
3386
|
+
x: -ellipse.rx * Math.sin(theta2),
|
|
3387
|
+
y: ellipse.ry * Math.cos(theta2)
|
|
3388
|
+
};
|
|
3389
|
+
const len1 = Math.hypot(tangent1.x, tangent1.y) || 1;
|
|
3390
|
+
const len2 = Math.hypot(tangent2.x, tangent2.y) || 1;
|
|
3391
|
+
const norm1 = { x: tangent1.x / len1, y: tangent1.y / len1 };
|
|
3392
|
+
const norm2 = { x: tangent2.x / len2, y: tangent2.y / len2 };
|
|
3393
|
+
const chordLength = Math.hypot(p3.x - p0.x, p3.y - p0.y);
|
|
3394
|
+
const cpDistance = chordLength * kappa * 0.5;
|
|
3395
|
+
const sign = angularSpan >= 0 ? 1 : -1;
|
|
3396
|
+
const cp1 = {
|
|
3397
|
+
x: p0.x + norm1.x * cpDistance * sign,
|
|
3398
|
+
y: p0.y + norm1.y * cpDistance * sign
|
|
3399
|
+
};
|
|
3400
|
+
const cp2 = {
|
|
3401
|
+
x: p3.x - norm2.x * cpDistance * sign,
|
|
3402
|
+
y: p3.y - norm2.y * cpDistance * sign
|
|
3403
|
+
};
|
|
3404
|
+
return [p0, cp1, cp2, p3];
|
|
3405
|
+
}
|
|
3406
|
+
function straightRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
|
|
3407
|
+
const fromC = rectCenter(fromBounds);
|
|
3408
|
+
const toC = rectCenter(toBounds);
|
|
3409
|
+
const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
|
|
3410
|
+
const p3 = resolveAnchor(toBounds, toAnchor, fromC);
|
|
3411
|
+
return [p0, p3];
|
|
3323
3412
|
}
|
|
3324
3413
|
function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
|
|
3325
3414
|
const fromC = rectCenter(fromBounds);
|
|
@@ -3365,15 +3454,6 @@ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
|
|
|
3365
3454
|
}
|
|
3366
3455
|
return void 0;
|
|
3367
3456
|
}
|
|
3368
|
-
function pointAlongArc(route, t) {
|
|
3369
|
-
const [first, second] = route;
|
|
3370
|
-
if (t <= 0.5) {
|
|
3371
|
-
const localT2 = Math.max(0, Math.min(1, t * 2));
|
|
3372
|
-
return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
|
|
3373
|
-
}
|
|
3374
|
-
const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
|
|
3375
|
-
return bezierPointAt(second[0], second[1], second[2], second[3], localT);
|
|
3376
|
-
}
|
|
3377
3457
|
function computeDiagramCenter(nodeBounds, canvasCenter) {
|
|
3378
3458
|
if (nodeBounds.length === 0) {
|
|
3379
3459
|
return canvasCenter ?? { x: 0, y: 0 };
|
|
@@ -3496,8 +3576,19 @@ function polylineBounds(points) {
|
|
|
3496
3576
|
};
|
|
3497
3577
|
}
|
|
3498
3578
|
function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
|
|
3499
|
-
|
|
3500
|
-
|
|
3579
|
+
let routing = conn.routing ?? "auto";
|
|
3580
|
+
let curveMode = conn.curveMode ?? "normal";
|
|
3581
|
+
if (conn.strokeStyle !== void 0) {
|
|
3582
|
+
console.warn("connection.strokeStyle is deprecated, use style instead");
|
|
3583
|
+
}
|
|
3584
|
+
if (routing === "arc") {
|
|
3585
|
+
console.warn(
|
|
3586
|
+
"connection routing: 'arc' is deprecated. Use routing: 'curve' with curveMode: 'ellipse' instead."
|
|
3587
|
+
);
|
|
3588
|
+
routing = "curve";
|
|
3589
|
+
curveMode = "ellipse";
|
|
3590
|
+
}
|
|
3591
|
+
const strokeStyle = conn.style ?? conn.strokeStyle ?? "solid";
|
|
3501
3592
|
const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
|
|
3502
3593
|
const tension = conn.tension ?? 0.35;
|
|
3503
3594
|
const dash = dashFromStyle(strokeStyle);
|
|
@@ -3519,14 +3610,29 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
|
|
|
3519
3610
|
ctx.globalAlpha = conn.opacity;
|
|
3520
3611
|
const arrowPlacement = conn.arrowPlacement ?? "endpoint";
|
|
3521
3612
|
if (routing === "curve") {
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3613
|
+
let p0;
|
|
3614
|
+
let cp1;
|
|
3615
|
+
let cp2;
|
|
3616
|
+
let p3;
|
|
3617
|
+
if (curveMode === "ellipse") {
|
|
3618
|
+
const ellipse = options?.ellipseParams ?? inferEllipseParams([fromBounds, toBounds]);
|
|
3619
|
+
[p0, cp1, cp2, p3] = ellipseRoute(
|
|
3620
|
+
fromBounds,
|
|
3621
|
+
toBounds,
|
|
3622
|
+
ellipse,
|
|
3623
|
+
conn.fromAnchor,
|
|
3624
|
+
conn.toAnchor
|
|
3625
|
+
);
|
|
3626
|
+
} else {
|
|
3627
|
+
[p0, cp1, cp2, p3] = curveRoute(
|
|
3628
|
+
fromBounds,
|
|
3629
|
+
toBounds,
|
|
3630
|
+
diagramCenter,
|
|
3631
|
+
tension,
|
|
3632
|
+
conn.fromAnchor,
|
|
3633
|
+
conn.toAnchor
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3530
3636
|
const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
|
|
3531
3637
|
ctx.strokeStyle = stroke;
|
|
3532
3638
|
ctx.lineWidth = style.width;
|
|
@@ -3559,51 +3665,22 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
|
|
|
3559
3665
|
}
|
|
3560
3666
|
}
|
|
3561
3667
|
}
|
|
3562
|
-
} else if (routing === "
|
|
3563
|
-
const [
|
|
3564
|
-
fromBounds,
|
|
3565
|
-
toBounds,
|
|
3566
|
-
diagramCenter,
|
|
3567
|
-
tension,
|
|
3568
|
-
conn.fromAnchor,
|
|
3569
|
-
conn.toAnchor
|
|
3570
|
-
);
|
|
3571
|
-
const [p0, cp1, cp2, pMid] = first;
|
|
3572
|
-
const [, cp3, cp4, p3] = second;
|
|
3668
|
+
} else if (routing === "straight") {
|
|
3669
|
+
const [p0, p3] = straightRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
|
|
3573
3670
|
const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
|
|
3574
3671
|
ctx.strokeStyle = stroke;
|
|
3575
3672
|
ctx.lineWidth = style.width;
|
|
3576
3673
|
ctx.setLineDash(style.dash ?? []);
|
|
3577
3674
|
ctx.beginPath();
|
|
3578
3675
|
ctx.moveTo(p0.x, p0.y);
|
|
3579
|
-
ctx.
|
|
3580
|
-
ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
|
|
3676
|
+
ctx.lineTo(p3.x, p3.y);
|
|
3581
3677
|
ctx.stroke();
|
|
3582
|
-
linePoints = [p0,
|
|
3678
|
+
linePoints = [p0, p3];
|
|
3583
3679
|
startPoint = p0;
|
|
3584
3680
|
endPoint = p3;
|
|
3585
|
-
startAngle = Math.atan2(p0.y -
|
|
3586
|
-
endAngle = Math.atan2(p3.y -
|
|
3587
|
-
labelPoint =
|
|
3588
|
-
if (arrowPlacement === "boundary") {
|
|
3589
|
-
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
3590
|
-
const [, s_cp3, s_cp4, s_p3] = second;
|
|
3591
|
-
const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
|
|
3592
|
-
if (tEnd !== void 0) {
|
|
3593
|
-
endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
|
|
3594
|
-
const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
|
|
3595
|
-
endAngle = Math.atan2(tangent.y, tangent.x);
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
|
-
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
3599
|
-
const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
|
|
3600
|
-
if (tStart !== void 0) {
|
|
3601
|
-
startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
|
|
3602
|
-
const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
|
|
3603
|
-
startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
}
|
|
3681
|
+
startAngle = Math.atan2(p0.y - p3.y, p0.x - p3.x);
|
|
3682
|
+
endAngle = Math.atan2(p3.y - p0.y, p3.x - p0.x);
|
|
3683
|
+
labelPoint = pointAlongPolyline(linePoints, labelT);
|
|
3607
3684
|
} else {
|
|
3608
3685
|
const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
|
|
3609
3686
|
const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
|
|
@@ -3869,6 +3946,24 @@ function fromPoints(points) {
|
|
|
3869
3946
|
function resolveDrawFont(theme, family) {
|
|
3870
3947
|
return resolveFont(theme.fonts[family], family);
|
|
3871
3948
|
}
|
|
3949
|
+
function createDrawStrokeGradient(ctx, start, end, strokeGradient) {
|
|
3950
|
+
const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
|
|
3951
|
+
gradient.addColorStop(0, strokeGradient.from);
|
|
3952
|
+
gradient.addColorStop(1, strokeGradient.to);
|
|
3953
|
+
return gradient;
|
|
3954
|
+
}
|
|
3955
|
+
function resolveDrawStroke(ctx, start, end, color, strokeGradient) {
|
|
3956
|
+
if (!strokeGradient) {
|
|
3957
|
+
return color;
|
|
3958
|
+
}
|
|
3959
|
+
return createDrawStrokeGradient(ctx, start, end, strokeGradient);
|
|
3960
|
+
}
|
|
3961
|
+
function resolveArrowFill(color, strokeGradient, position) {
|
|
3962
|
+
if (!strokeGradient) {
|
|
3963
|
+
return color;
|
|
3964
|
+
}
|
|
3965
|
+
return position === "start" ? strokeGradient.from : strokeGradient.to;
|
|
3966
|
+
}
|
|
3872
3967
|
function measureSpacedTextWidth(ctx, text, letterSpacing) {
|
|
3873
3968
|
const chars = [...text];
|
|
3874
3969
|
if (chars.length === 0) {
|
|
@@ -4147,18 +4242,31 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
4147
4242
|
const from = { x: command.x1, y: command.y1 };
|
|
4148
4243
|
const to = { x: command.x2, y: command.y2 };
|
|
4149
4244
|
const lineAngle = angleBetween(from, to);
|
|
4245
|
+
const stroke = resolveDrawStroke(ctx, from, to, command.color, command.strokeGradient);
|
|
4150
4246
|
withOpacity(ctx, command.opacity, () => {
|
|
4151
4247
|
applyDrawShadow(ctx, command.shadow);
|
|
4152
4248
|
drawLine(ctx, from, to, {
|
|
4153
|
-
color:
|
|
4249
|
+
color: stroke,
|
|
4154
4250
|
width: command.width,
|
|
4155
4251
|
...command.dash ? { dash: command.dash } : {}
|
|
4156
4252
|
});
|
|
4157
4253
|
if (command.arrow === "end" || command.arrow === "both") {
|
|
4158
|
-
drawArrowhead(
|
|
4254
|
+
drawArrowhead(
|
|
4255
|
+
ctx,
|
|
4256
|
+
to,
|
|
4257
|
+
lineAngle,
|
|
4258
|
+
command.arrowSize,
|
|
4259
|
+
resolveArrowFill(command.color, command.strokeGradient, "end")
|
|
4260
|
+
);
|
|
4159
4261
|
}
|
|
4160
4262
|
if (command.arrow === "start" || command.arrow === "both") {
|
|
4161
|
-
drawArrowhead(
|
|
4263
|
+
drawArrowhead(
|
|
4264
|
+
ctx,
|
|
4265
|
+
from,
|
|
4266
|
+
lineAngle + Math.PI,
|
|
4267
|
+
command.arrowSize,
|
|
4268
|
+
resolveArrowFill(command.color, command.strokeGradient, "start")
|
|
4269
|
+
);
|
|
4162
4270
|
}
|
|
4163
4271
|
});
|
|
4164
4272
|
const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
|
|
@@ -4166,7 +4274,7 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
4166
4274
|
id,
|
|
4167
4275
|
kind: "draw",
|
|
4168
4276
|
bounds: expandRect(fromPoints([from, to]), Math.max(command.width / 2, arrowPadding)),
|
|
4169
|
-
foregroundColor: command.color
|
|
4277
|
+
foregroundColor: command.strokeGradient?.from ?? command.color
|
|
4170
4278
|
});
|
|
4171
4279
|
break;
|
|
4172
4280
|
}
|
|
@@ -4200,10 +4308,17 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
4200
4308
|
}
|
|
4201
4309
|
case "bezier": {
|
|
4202
4310
|
const points = command.points;
|
|
4311
|
+
const stroke = resolveDrawStroke(
|
|
4312
|
+
ctx,
|
|
4313
|
+
points[0],
|
|
4314
|
+
points[points.length - 1],
|
|
4315
|
+
command.color,
|
|
4316
|
+
command.strokeGradient
|
|
4317
|
+
);
|
|
4203
4318
|
withOpacity(ctx, command.opacity, () => {
|
|
4204
4319
|
applyDrawShadow(ctx, command.shadow);
|
|
4205
4320
|
drawBezier(ctx, points, {
|
|
4206
|
-
color:
|
|
4321
|
+
color: stroke,
|
|
4207
4322
|
width: command.width,
|
|
4208
4323
|
...command.dash ? { dash: command.dash } : {}
|
|
4209
4324
|
});
|
|
@@ -4215,11 +4330,17 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
4215
4330
|
points[points.length - 1],
|
|
4216
4331
|
endAngle,
|
|
4217
4332
|
command.arrowSize,
|
|
4218
|
-
command.color
|
|
4333
|
+
resolveArrowFill(command.color, command.strokeGradient, "end")
|
|
4219
4334
|
);
|
|
4220
4335
|
}
|
|
4221
4336
|
if (command.arrow === "start" || command.arrow === "both") {
|
|
4222
|
-
drawArrowhead(
|
|
4337
|
+
drawArrowhead(
|
|
4338
|
+
ctx,
|
|
4339
|
+
points[0],
|
|
4340
|
+
startAngle + Math.PI,
|
|
4341
|
+
command.arrowSize,
|
|
4342
|
+
resolveArrowFill(command.color, command.strokeGradient, "start")
|
|
4343
|
+
);
|
|
4223
4344
|
}
|
|
4224
4345
|
});
|
|
4225
4346
|
const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
|
|
@@ -4227,7 +4348,7 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
4227
4348
|
id,
|
|
4228
4349
|
kind: "draw",
|
|
4229
4350
|
bounds: expandRect(fromPoints(points), Math.max(command.width / 2, arrowPadding)),
|
|
4230
|
-
foregroundColor: command.color
|
|
4351
|
+
foregroundColor: command.strokeGradient?.from ?? command.color
|
|
4231
4352
|
});
|
|
4232
4353
|
break;
|
|
4233
4354
|
}
|
|
@@ -5032,10 +5153,19 @@ async function renderDesign(input, options = {}) {
|
|
|
5032
5153
|
break;
|
|
5033
5154
|
}
|
|
5034
5155
|
}
|
|
5035
|
-
const
|
|
5036
|
-
|
|
5037
|
-
|
|
5156
|
+
const nodeBounds = spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null);
|
|
5157
|
+
const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(nodeBounds, { x: spec.canvas.width / 2, y: spec.canvas.height / 2 });
|
|
5158
|
+
const layoutEllipseRx = "ellipseRx" in spec.layout ? spec.layout.ellipseRx : void 0;
|
|
5159
|
+
const layoutEllipseRy = "ellipseRy" in spec.layout ? spec.layout.ellipseRy : void 0;
|
|
5160
|
+
const hasEllipseConnections = spec.elements.some(
|
|
5161
|
+
(el) => el.type === "connection" && (el.curveMode === "ellipse" || el.routing === "arc")
|
|
5038
5162
|
);
|
|
5163
|
+
const ellipseParams = hasEllipseConnections ? inferEllipseParams(
|
|
5164
|
+
nodeBounds,
|
|
5165
|
+
spec.layout.diagramCenter ?? diagramCenter,
|
|
5166
|
+
layoutEllipseRx,
|
|
5167
|
+
layoutEllipseRy
|
|
5168
|
+
) : void 0;
|
|
5039
5169
|
for (const element of spec.elements) {
|
|
5040
5170
|
if (element.type !== "connection") {
|
|
5041
5171
|
continue;
|
|
@@ -5049,7 +5179,15 @@ async function renderDesign(input, options = {}) {
|
|
|
5049
5179
|
}
|
|
5050
5180
|
const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
|
|
5051
5181
|
elements.push(
|
|
5052
|
-
...renderConnection(
|
|
5182
|
+
...renderConnection(
|
|
5183
|
+
ctx,
|
|
5184
|
+
element,
|
|
5185
|
+
fromRect,
|
|
5186
|
+
toRect,
|
|
5187
|
+
theme,
|
|
5188
|
+
edgeRoute,
|
|
5189
|
+
ellipseParams ? { diagramCenter, ellipseParams } : { diagramCenter }
|
|
5190
|
+
)
|
|
5053
5191
|
);
|
|
5054
5192
|
}
|
|
5055
5193
|
if (footerRect && spec.footer) {
|