sketchmark 1.1.6 → 1.2.1
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 +200 -152
- package/dist/animation/index.d.ts +2 -0
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/ast/types.d.ts +3 -0
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/index.cjs +711 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +711 -105
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.ts +8 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/parser/index.d.ts +2 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/tokenizer.d.ts.map +1 -1
- package/dist/plugins.d.ts +12 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/render.d.ts +2 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/renderer/shapes/path-geometry.d.ts +8 -0
- package/dist/renderer/shapes/path-geometry.d.ts.map +1 -0
- package/dist/renderer/shapes/path.d.ts.map +1 -1
- package/dist/renderer/shared.d.ts +1 -1
- package/dist/renderer/shared.d.ts.map +1 -1
- package/dist/renderer/svg/index.d.ts.map +1 -1
- package/dist/scene/index.d.ts +2 -0
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +711 -105
- package/dist/ui/canvas.d.ts +2 -0
- package/dist/ui/canvas.d.ts.map +1 -1
- package/dist/ui/embed.d.ts +2 -0
- package/dist/ui/embed.d.ts.map +1 -1
- package/package.json +20 -1
package/dist/index.cjs
CHANGED
|
@@ -146,10 +146,16 @@ function tokenize$1(src) {
|
|
|
146
146
|
val += "\n";
|
|
147
147
|
else if (esc === "t")
|
|
148
148
|
val += "\t";
|
|
149
|
+
else if (esc === "r")
|
|
150
|
+
val += "\r";
|
|
149
151
|
else if (esc === "\\")
|
|
150
152
|
val += "\\";
|
|
153
|
+
else if (esc === q)
|
|
154
|
+
val += q;
|
|
155
|
+
else if (esc)
|
|
156
|
+
val += `\\${esc}`;
|
|
151
157
|
else
|
|
152
|
-
val +=
|
|
158
|
+
val += "\\";
|
|
153
159
|
}
|
|
154
160
|
else
|
|
155
161
|
val += src[i];
|
|
@@ -226,6 +232,47 @@ function tokenize$1(src) {
|
|
|
226
232
|
return tokens;
|
|
227
233
|
}
|
|
228
234
|
|
|
235
|
+
function pluginMessage(plugin, stage, error) {
|
|
236
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
237
|
+
return `Plugin "${plugin.name}" ${stage} failed: ${detail}`;
|
|
238
|
+
}
|
|
239
|
+
function applyPluginPreprocessors(source, plugins = []) {
|
|
240
|
+
let nextSource = source;
|
|
241
|
+
for (const plugin of plugins) {
|
|
242
|
+
if (!plugin.preprocess)
|
|
243
|
+
continue;
|
|
244
|
+
try {
|
|
245
|
+
const transformed = plugin.preprocess(nextSource);
|
|
246
|
+
if (typeof transformed !== "string") {
|
|
247
|
+
throw new Error("preprocess must return a string");
|
|
248
|
+
}
|
|
249
|
+
nextSource = transformed;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
throw new Error(pluginMessage(plugin, "preprocess", error));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return nextSource;
|
|
256
|
+
}
|
|
257
|
+
function applyPluginAstTransforms(ast, plugins = []) {
|
|
258
|
+
let nextAst = ast;
|
|
259
|
+
for (const plugin of plugins) {
|
|
260
|
+
if (!plugin.transformAst)
|
|
261
|
+
continue;
|
|
262
|
+
try {
|
|
263
|
+
const transformed = plugin.transformAst(nextAst);
|
|
264
|
+
if (!transformed || transformed.kind !== "diagram") {
|
|
265
|
+
throw new Error('transformAst must return a DiagramAST with kind="diagram"');
|
|
266
|
+
}
|
|
267
|
+
nextAst = transformed;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
throw new Error(pluginMessage(plugin, "transformAst", error));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return nextAst;
|
|
274
|
+
}
|
|
275
|
+
|
|
229
276
|
// ============================================================
|
|
230
277
|
// sketchmark - Parser (Tokens -> DiagramAST)
|
|
231
278
|
// ============================================================
|
|
@@ -312,9 +359,10 @@ function isValueToken(t) {
|
|
|
312
359
|
function isPropKeyToken(t) {
|
|
313
360
|
return !!t && (t.type === "IDENT" || t.type === "KEYWORD");
|
|
314
361
|
}
|
|
315
|
-
function parse(src) {
|
|
362
|
+
function parse(src, options = {}) {
|
|
316
363
|
resetUid();
|
|
317
|
-
const
|
|
364
|
+
const preparedSource = applyPluginPreprocessors(src, options.plugins);
|
|
365
|
+
const tokens = tokenize$1(preparedSource).filter((t) => t.type !== "NEWLINE" || t.value === "\n");
|
|
318
366
|
const flat = [];
|
|
319
367
|
let lastNL = false;
|
|
320
368
|
for (const t of tokens) {
|
|
@@ -497,6 +545,7 @@ function parse(src) {
|
|
|
497
545
|
const toks = lineTokens();
|
|
498
546
|
const id = requireExplicitId(keywordTok, toks);
|
|
499
547
|
const props = parseSimpleProps(toks, 1);
|
|
548
|
+
const meta = extractNodeMeta(props);
|
|
500
549
|
const node = {
|
|
501
550
|
kind: "node",
|
|
502
551
|
id,
|
|
@@ -511,6 +560,7 @@ function parse(src) {
|
|
|
511
560
|
...(props.dy ? { dy: parseFloat(props.dy) } : {}),
|
|
512
561
|
...(props.factor ? { factor: parseFloat(props.factor) } : {}),
|
|
513
562
|
...(props.theme ? { theme: props.theme } : {}),
|
|
563
|
+
...(meta ? { meta } : {}),
|
|
514
564
|
style: propsToStyle(props),
|
|
515
565
|
};
|
|
516
566
|
if (props.url)
|
|
@@ -535,12 +585,14 @@ function parse(src) {
|
|
|
535
585
|
j = 2;
|
|
536
586
|
}
|
|
537
587
|
Object.assign(props, parseSimpleProps(toks, j));
|
|
588
|
+
const meta = extractNodeMeta(props);
|
|
538
589
|
return {
|
|
539
590
|
kind: "node",
|
|
540
591
|
id,
|
|
541
592
|
shape: "note",
|
|
542
593
|
label: (props.label ?? "").replace(/\\n/g, "\n"),
|
|
543
594
|
theme: props.theme,
|
|
595
|
+
...(meta ? { meta } : {}),
|
|
544
596
|
style: propsToStyle(props),
|
|
545
597
|
...(props.width ? { width: parseFloat(props.width) } : {}),
|
|
546
598
|
...(props.height ? { height: parseFloat(props.height) } : {}),
|
|
@@ -552,6 +604,13 @@ function parse(src) {
|
|
|
552
604
|
...(props.factor ? { factor: parseFloat(props.factor) } : {}),
|
|
553
605
|
};
|
|
554
606
|
}
|
|
607
|
+
function extractNodeMeta(props) {
|
|
608
|
+
const meta = {};
|
|
609
|
+
if (props["animation-parent"]) {
|
|
610
|
+
meta.animationParent = props["animation-parent"];
|
|
611
|
+
}
|
|
612
|
+
return Object.keys(meta).length ? meta : undefined;
|
|
613
|
+
}
|
|
555
614
|
function parseGroup() {
|
|
556
615
|
const keywordTok = cur();
|
|
557
616
|
skip();
|
|
@@ -624,6 +683,8 @@ function parse(src) {
|
|
|
624
683
|
to: toTok.value,
|
|
625
684
|
connector: connector,
|
|
626
685
|
label: props.label,
|
|
686
|
+
fromAnchor: props["anchor-from"],
|
|
687
|
+
toAnchor: props["anchor-to"],
|
|
627
688
|
dashed,
|
|
628
689
|
bidirectional,
|
|
629
690
|
style: propsToStyle(props),
|
|
@@ -937,6 +998,7 @@ function parse(src) {
|
|
|
937
998
|
registerAuthoredId(grp.id, "group", t);
|
|
938
999
|
if (isBare) {
|
|
939
1000
|
grp.label = "";
|
|
1001
|
+
grp.padding = grp.padding ?? 0;
|
|
940
1002
|
grp.style = {
|
|
941
1003
|
...grp.style,
|
|
942
1004
|
fill: grp.style?.fill ?? "none",
|
|
@@ -1107,7 +1169,7 @@ function parse(src) {
|
|
|
1107
1169
|
node.style = { ...ast.styles[node.id], ...node.style };
|
|
1108
1170
|
}
|
|
1109
1171
|
}
|
|
1110
|
-
return ast;
|
|
1172
|
+
return applyPluginAstTransforms(ast, options.plugins);
|
|
1111
1173
|
}
|
|
1112
1174
|
|
|
1113
1175
|
// ============================================================
|
|
@@ -3554,6 +3616,8 @@ function buildSceneGraph(ast) {
|
|
|
3554
3616
|
to: e.to,
|
|
3555
3617
|
connector: e.connector,
|
|
3556
3618
|
label: e.label,
|
|
3619
|
+
fromAnchor: e.fromAnchor,
|
|
3620
|
+
toAnchor: e.toAnchor,
|
|
3557
3621
|
dashed: e.dashed ?? false,
|
|
3558
3622
|
bidirectional: e.bidirectional ?? false,
|
|
3559
3623
|
style: e.style ?? {},
|
|
@@ -4131,28 +4195,13 @@ function connMeta(connector) {
|
|
|
4131
4195
|
return { arrowAt: "start", dashed };
|
|
4132
4196
|
return { arrowAt: "end", dashed };
|
|
4133
4197
|
}
|
|
4134
|
-
// ── Generic rect connection point ────────────────────────────────────────
|
|
4135
|
-
function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
|
|
4136
|
-
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
4137
|
-
const dx = ox - cx, dy = oy - cy;
|
|
4138
|
-
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
4139
|
-
return [cx, cy];
|
|
4140
|
-
const hw = rw / 2 - 2, hh = rh / 2 - 2;
|
|
4141
|
-
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
4142
|
-
const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
|
|
4143
|
-
const t = Math.min(tx, ty);
|
|
4144
|
-
return [cx + t * dx, cy + t * dy];
|
|
4145
|
-
}
|
|
4146
4198
|
// ── Resolve an endpoint entity by ID across all maps ─────────────────────
|
|
4147
4199
|
function resolveEndpoint(id, nm, tm, gm, cm) {
|
|
4148
4200
|
return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? null;
|
|
4149
4201
|
}
|
|
4150
4202
|
// ── Get connection point for any entity ──────────────────────────────────
|
|
4151
|
-
function getConnPoint(src, dstCX, dstCY) {
|
|
4152
|
-
|
|
4153
|
-
return connPoint(src, { x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
|
|
4154
|
-
}
|
|
4155
|
-
return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
4203
|
+
function getConnPoint(src, dstCX, dstCY, anchor) {
|
|
4204
|
+
return anchoredConnPoint(src, anchor, dstCX, dstCY);
|
|
4156
4205
|
}
|
|
4157
4206
|
// ── Group depth (for paint order) ────────────────────────────────────────
|
|
4158
4207
|
function groupDepth(g, gm) {
|
|
@@ -4250,37 +4299,510 @@ const lineShape = {
|
|
|
4250
4299
|
},
|
|
4251
4300
|
};
|
|
4252
4301
|
|
|
4302
|
+
const COMMAND_RE = /^[AaCcHhLlMmQqSsTtVvZz]$/;
|
|
4303
|
+
const TOKEN_RE = /[AaCcHhLlMmQqSsTtVvZz]|[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?/g;
|
|
4304
|
+
const EPSILON = 1e-6;
|
|
4305
|
+
const PARAM_COUNTS = {
|
|
4306
|
+
A: 7,
|
|
4307
|
+
C: 6,
|
|
4308
|
+
H: 1,
|
|
4309
|
+
L: 2,
|
|
4310
|
+
M: 2,
|
|
4311
|
+
Q: 4,
|
|
4312
|
+
S: 4,
|
|
4313
|
+
T: 2,
|
|
4314
|
+
V: 1,
|
|
4315
|
+
Z: 0,
|
|
4316
|
+
};
|
|
4317
|
+
function isCommandToken(token) {
|
|
4318
|
+
return COMMAND_RE.test(token);
|
|
4319
|
+
}
|
|
4320
|
+
function formatNumber(value) {
|
|
4321
|
+
const rounded = Math.abs(value) < EPSILON ? 0 : Number(value.toFixed(3));
|
|
4322
|
+
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
4323
|
+
}
|
|
4324
|
+
function parseRawSegments(pathData) {
|
|
4325
|
+
const tokens = pathData.match(TOKEN_RE) ?? [];
|
|
4326
|
+
if (!tokens.length)
|
|
4327
|
+
return [];
|
|
4328
|
+
const segments = [];
|
|
4329
|
+
let index = 0;
|
|
4330
|
+
let currentCommand = null;
|
|
4331
|
+
while (index < tokens.length) {
|
|
4332
|
+
const token = tokens[index];
|
|
4333
|
+
if (isCommandToken(token)) {
|
|
4334
|
+
currentCommand = token;
|
|
4335
|
+
index += 1;
|
|
4336
|
+
if (token === "Z" || token === "z") {
|
|
4337
|
+
segments.push({ command: "Z", values: [] });
|
|
4338
|
+
}
|
|
4339
|
+
continue;
|
|
4340
|
+
}
|
|
4341
|
+
if (!currentCommand)
|
|
4342
|
+
break;
|
|
4343
|
+
const upper = currentCommand.toUpperCase();
|
|
4344
|
+
const paramCount = PARAM_COUNTS[upper];
|
|
4345
|
+
if (!paramCount) {
|
|
4346
|
+
index += 1;
|
|
4347
|
+
continue;
|
|
4348
|
+
}
|
|
4349
|
+
let isFirstMove = upper === "M";
|
|
4350
|
+
while (index < tokens.length && !isCommandToken(tokens[index])) {
|
|
4351
|
+
if (index + paramCount > tokens.length)
|
|
4352
|
+
return segments;
|
|
4353
|
+
const values = tokens
|
|
4354
|
+
.slice(index, index + paramCount)
|
|
4355
|
+
.map((value) => Number(value));
|
|
4356
|
+
if (values.some((value) => Number.isNaN(value))) {
|
|
4357
|
+
return segments;
|
|
4358
|
+
}
|
|
4359
|
+
if (upper === "M") {
|
|
4360
|
+
const moveCommand = isFirstMove
|
|
4361
|
+
? currentCommand
|
|
4362
|
+
: currentCommand === "m"
|
|
4363
|
+
? "l"
|
|
4364
|
+
: "L";
|
|
4365
|
+
segments.push({ command: moveCommand, values });
|
|
4366
|
+
isFirstMove = false;
|
|
4367
|
+
}
|
|
4368
|
+
else {
|
|
4369
|
+
segments.push({ command: currentCommand, values });
|
|
4370
|
+
}
|
|
4371
|
+
index += paramCount;
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
return segments;
|
|
4375
|
+
}
|
|
4376
|
+
function reflect(control, around) {
|
|
4377
|
+
return {
|
|
4378
|
+
x: around.x * 2 - control.x,
|
|
4379
|
+
y: around.y * 2 - control.y,
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
function toAbsoluteSegments(rawSegments) {
|
|
4383
|
+
const segments = [];
|
|
4384
|
+
let current = { x: 0, y: 0 };
|
|
4385
|
+
let subpathStart = { x: 0, y: 0 };
|
|
4386
|
+
let previousCubicControl = null;
|
|
4387
|
+
let previousQuadraticControl = null;
|
|
4388
|
+
for (const segment of rawSegments) {
|
|
4389
|
+
const isRelative = segment.command === segment.command.toLowerCase();
|
|
4390
|
+
const command = segment.command.toUpperCase();
|
|
4391
|
+
const values = segment.values;
|
|
4392
|
+
switch (command) {
|
|
4393
|
+
case "M": {
|
|
4394
|
+
const x = isRelative ? current.x + values[0] : values[0];
|
|
4395
|
+
const y = isRelative ? current.y + values[1] : values[1];
|
|
4396
|
+
current = { x, y };
|
|
4397
|
+
subpathStart = { x, y };
|
|
4398
|
+
previousCubicControl = null;
|
|
4399
|
+
previousQuadraticControl = null;
|
|
4400
|
+
segments.push({ command: "M", values: [x, y] });
|
|
4401
|
+
break;
|
|
4402
|
+
}
|
|
4403
|
+
case "L": {
|
|
4404
|
+
const x = isRelative ? current.x + values[0] : values[0];
|
|
4405
|
+
const y = isRelative ? current.y + values[1] : values[1];
|
|
4406
|
+
current = { x, y };
|
|
4407
|
+
previousCubicControl = null;
|
|
4408
|
+
previousQuadraticControl = null;
|
|
4409
|
+
segments.push({ command: "L", values: [x, y] });
|
|
4410
|
+
break;
|
|
4411
|
+
}
|
|
4412
|
+
case "H": {
|
|
4413
|
+
const x = isRelative ? current.x + values[0] : values[0];
|
|
4414
|
+
current = { x, y: current.y };
|
|
4415
|
+
previousCubicControl = null;
|
|
4416
|
+
previousQuadraticControl = null;
|
|
4417
|
+
segments.push({ command: "L", values: [x, current.y] });
|
|
4418
|
+
break;
|
|
4419
|
+
}
|
|
4420
|
+
case "V": {
|
|
4421
|
+
const y = isRelative ? current.y + values[0] : values[0];
|
|
4422
|
+
current = { x: current.x, y };
|
|
4423
|
+
previousCubicControl = null;
|
|
4424
|
+
previousQuadraticControl = null;
|
|
4425
|
+
segments.push({ command: "L", values: [current.x, y] });
|
|
4426
|
+
break;
|
|
4427
|
+
}
|
|
4428
|
+
case "C": {
|
|
4429
|
+
const x1 = isRelative ? current.x + values[0] : values[0];
|
|
4430
|
+
const y1 = isRelative ? current.y + values[1] : values[1];
|
|
4431
|
+
const x2 = isRelative ? current.x + values[2] : values[2];
|
|
4432
|
+
const y2 = isRelative ? current.y + values[3] : values[3];
|
|
4433
|
+
const x = isRelative ? current.x + values[4] : values[4];
|
|
4434
|
+
const y = isRelative ? current.y + values[5] : values[5];
|
|
4435
|
+
current = { x, y };
|
|
4436
|
+
previousCubicControl = { x: x2, y: y2 };
|
|
4437
|
+
previousQuadraticControl = null;
|
|
4438
|
+
segments.push({ command: "C", values: [x1, y1, x2, y2, x, y] });
|
|
4439
|
+
break;
|
|
4440
|
+
}
|
|
4441
|
+
case "S": {
|
|
4442
|
+
const control1 = previousCubicControl
|
|
4443
|
+
? reflect(previousCubicControl, current)
|
|
4444
|
+
: { ...current };
|
|
4445
|
+
const x2 = isRelative ? current.x + values[0] : values[0];
|
|
4446
|
+
const y2 = isRelative ? current.y + values[1] : values[1];
|
|
4447
|
+
const x = isRelative ? current.x + values[2] : values[2];
|
|
4448
|
+
const y = isRelative ? current.y + values[3] : values[3];
|
|
4449
|
+
current = { x, y };
|
|
4450
|
+
previousCubicControl = { x: x2, y: y2 };
|
|
4451
|
+
previousQuadraticControl = null;
|
|
4452
|
+
segments.push({
|
|
4453
|
+
command: "C",
|
|
4454
|
+
values: [control1.x, control1.y, x2, y2, x, y],
|
|
4455
|
+
});
|
|
4456
|
+
break;
|
|
4457
|
+
}
|
|
4458
|
+
case "Q": {
|
|
4459
|
+
const x1 = isRelative ? current.x + values[0] : values[0];
|
|
4460
|
+
const y1 = isRelative ? current.y + values[1] : values[1];
|
|
4461
|
+
const x = isRelative ? current.x + values[2] : values[2];
|
|
4462
|
+
const y = isRelative ? current.y + values[3] : values[3];
|
|
4463
|
+
current = { x, y };
|
|
4464
|
+
previousCubicControl = null;
|
|
4465
|
+
previousQuadraticControl = { x: x1, y: y1 };
|
|
4466
|
+
segments.push({ command: "Q", values: [x1, y1, x, y] });
|
|
4467
|
+
break;
|
|
4468
|
+
}
|
|
4469
|
+
case "T": {
|
|
4470
|
+
const control = previousQuadraticControl
|
|
4471
|
+
? reflect(previousQuadraticControl, current)
|
|
4472
|
+
: { ...current };
|
|
4473
|
+
const x = isRelative ? current.x + values[0] : values[0];
|
|
4474
|
+
const y = isRelative ? current.y + values[1] : values[1];
|
|
4475
|
+
current = { x, y };
|
|
4476
|
+
previousCubicControl = null;
|
|
4477
|
+
previousQuadraticControl = control;
|
|
4478
|
+
segments.push({ command: "Q", values: [control.x, control.y, x, y] });
|
|
4479
|
+
break;
|
|
4480
|
+
}
|
|
4481
|
+
case "A": {
|
|
4482
|
+
const rx = Math.abs(values[0]);
|
|
4483
|
+
const ry = Math.abs(values[1]);
|
|
4484
|
+
const rotation = values[2];
|
|
4485
|
+
const largeArc = values[3];
|
|
4486
|
+
const sweep = values[4];
|
|
4487
|
+
const x = isRelative ? current.x + values[5] : values[5];
|
|
4488
|
+
const y = isRelative ? current.y + values[6] : values[6];
|
|
4489
|
+
current = { x, y };
|
|
4490
|
+
previousCubicControl = null;
|
|
4491
|
+
previousQuadraticControl = null;
|
|
4492
|
+
segments.push({
|
|
4493
|
+
command: "A",
|
|
4494
|
+
values: [rx, ry, rotation, largeArc, sweep, x, y],
|
|
4495
|
+
});
|
|
4496
|
+
break;
|
|
4497
|
+
}
|
|
4498
|
+
case "Z": {
|
|
4499
|
+
current = { ...subpathStart };
|
|
4500
|
+
previousCubicControl = null;
|
|
4501
|
+
previousQuadraticControl = null;
|
|
4502
|
+
segments.push({ command: "Z", values: [] });
|
|
4503
|
+
break;
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
return segments;
|
|
4508
|
+
}
|
|
4509
|
+
function cubicAt(p0, p1, p2, p3, t) {
|
|
4510
|
+
const mt = 1 - t;
|
|
4511
|
+
return (mt * mt * mt * p0 +
|
|
4512
|
+
3 * mt * mt * t * p1 +
|
|
4513
|
+
3 * mt * t * t * p2 +
|
|
4514
|
+
t * t * t * p3);
|
|
4515
|
+
}
|
|
4516
|
+
function quadraticAt(p0, p1, p2, t) {
|
|
4517
|
+
const mt = 1 - t;
|
|
4518
|
+
return mt * mt * p0 + 2 * mt * t * p1 + t * t * p2;
|
|
4519
|
+
}
|
|
4520
|
+
function cubicExtrema(p0, p1, p2, p3) {
|
|
4521
|
+
const a = -p0 + 3 * p1 - 3 * p2 + p3;
|
|
4522
|
+
const b = 3 * p0 - 6 * p1 + 3 * p2;
|
|
4523
|
+
const c = -3 * p0 + 3 * p1;
|
|
4524
|
+
if (Math.abs(a) < EPSILON) {
|
|
4525
|
+
if (Math.abs(b) < EPSILON)
|
|
4526
|
+
return [];
|
|
4527
|
+
return [-c / (2 * b)].filter((t) => t > 0 && t < 1);
|
|
4528
|
+
}
|
|
4529
|
+
const discriminant = 4 * b * b - 12 * a * c;
|
|
4530
|
+
if (discriminant < 0)
|
|
4531
|
+
return [];
|
|
4532
|
+
const sqrtDiscriminant = Math.sqrt(discriminant);
|
|
4533
|
+
return [
|
|
4534
|
+
(-2 * b + sqrtDiscriminant) / (6 * a),
|
|
4535
|
+
(-2 * b - sqrtDiscriminant) / (6 * a),
|
|
4536
|
+
].filter((t) => t > 0 && t < 1);
|
|
4537
|
+
}
|
|
4538
|
+
function quadraticExtrema(p0, p1, p2) {
|
|
4539
|
+
const denominator = p0 - 2 * p1 + p2;
|
|
4540
|
+
if (Math.abs(denominator) < EPSILON)
|
|
4541
|
+
return [];
|
|
4542
|
+
const t = (p0 - p1) / denominator;
|
|
4543
|
+
return t > 0 && t < 1 ? [t] : [];
|
|
4544
|
+
}
|
|
4545
|
+
function angleBetween(u, v) {
|
|
4546
|
+
const magnitude = Math.hypot(u.x, u.y) * Math.hypot(v.x, v.y);
|
|
4547
|
+
if (magnitude < EPSILON)
|
|
4548
|
+
return 0;
|
|
4549
|
+
const sign = u.x * v.y - u.y * v.x < 0 ? -1 : 1;
|
|
4550
|
+
const cosine = Math.min(1, Math.max(-1, (u.x * v.x + u.y * v.y) / magnitude));
|
|
4551
|
+
return sign * Math.acos(cosine);
|
|
4552
|
+
}
|
|
4553
|
+
function sampleArc(start, values) {
|
|
4554
|
+
let [rx, ry, rotation, largeArcFlag, sweepFlag, endX, endY] = values;
|
|
4555
|
+
if ((Math.abs(start.x - endX) < EPSILON && Math.abs(start.y - endY) < EPSILON) || rx < EPSILON || ry < EPSILON) {
|
|
4556
|
+
return [start, { x: endX, y: endY }];
|
|
4557
|
+
}
|
|
4558
|
+
rx = Math.abs(rx);
|
|
4559
|
+
ry = Math.abs(ry);
|
|
4560
|
+
const phi = (rotation * Math.PI) / 180;
|
|
4561
|
+
const cosPhi = Math.cos(phi);
|
|
4562
|
+
const sinPhi = Math.sin(phi);
|
|
4563
|
+
const dx2 = (start.x - endX) / 2;
|
|
4564
|
+
const dy2 = (start.y - endY) / 2;
|
|
4565
|
+
const x1p = cosPhi * dx2 + sinPhi * dy2;
|
|
4566
|
+
const y1p = -sinPhi * dx2 + cosPhi * dy2;
|
|
4567
|
+
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
4568
|
+
if (lambda > 1) {
|
|
4569
|
+
const scale = Math.sqrt(lambda);
|
|
4570
|
+
rx *= scale;
|
|
4571
|
+
ry *= scale;
|
|
4572
|
+
}
|
|
4573
|
+
const rx2 = rx * rx;
|
|
4574
|
+
const ry2 = ry * ry;
|
|
4575
|
+
const x1p2 = x1p * x1p;
|
|
4576
|
+
const y1p2 = y1p * y1p;
|
|
4577
|
+
const numerator = rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2;
|
|
4578
|
+
const denominator = rx2 * y1p2 + ry2 * x1p2;
|
|
4579
|
+
const factor = denominator < EPSILON ? 0 : Math.sqrt(Math.max(0, numerator / denominator));
|
|
4580
|
+
const sign = largeArcFlag === sweepFlag ? -1 : 1;
|
|
4581
|
+
const cxp = sign * factor * ((rx * y1p) / ry);
|
|
4582
|
+
const cyp = sign * factor * (-(ry * x1p) / rx);
|
|
4583
|
+
const cx = cosPhi * cxp - sinPhi * cyp + (start.x + endX) / 2;
|
|
4584
|
+
const cy = sinPhi * cxp + cosPhi * cyp + (start.y + endY) / 2;
|
|
4585
|
+
const startVector = {
|
|
4586
|
+
x: (x1p - cxp) / rx,
|
|
4587
|
+
y: (y1p - cyp) / ry,
|
|
4588
|
+
};
|
|
4589
|
+
const endVector = {
|
|
4590
|
+
x: (-x1p - cxp) / rx,
|
|
4591
|
+
y: (-y1p - cyp) / ry,
|
|
4592
|
+
};
|
|
4593
|
+
let deltaTheta = angleBetween(startVector, endVector);
|
|
4594
|
+
if (!sweepFlag && deltaTheta > 0)
|
|
4595
|
+
deltaTheta -= Math.PI * 2;
|
|
4596
|
+
if (sweepFlag && deltaTheta < 0)
|
|
4597
|
+
deltaTheta += Math.PI * 2;
|
|
4598
|
+
const theta1 = angleBetween({ x: 1, y: 0 }, startVector);
|
|
4599
|
+
const steps = Math.max(12, Math.ceil(Math.abs(deltaTheta) / (Math.PI / 8)));
|
|
4600
|
+
const points = [];
|
|
4601
|
+
for (let index = 0; index <= steps; index += 1) {
|
|
4602
|
+
const theta = theta1 + (deltaTheta * index) / steps;
|
|
4603
|
+
const cosTheta = Math.cos(theta);
|
|
4604
|
+
const sinTheta = Math.sin(theta);
|
|
4605
|
+
points.push({
|
|
4606
|
+
x: cx + rx * cosPhi * cosTheta - ry * sinPhi * sinTheta,
|
|
4607
|
+
y: cy + rx * sinPhi * cosTheta + ry * cosPhi * sinTheta,
|
|
4608
|
+
});
|
|
4609
|
+
}
|
|
4610
|
+
return points;
|
|
4611
|
+
}
|
|
4612
|
+
function boundsFromAbsoluteSegments(segments) {
|
|
4613
|
+
if (!segments.length)
|
|
4614
|
+
return null;
|
|
4615
|
+
let minX = Infinity;
|
|
4616
|
+
let minY = Infinity;
|
|
4617
|
+
let maxX = -Infinity;
|
|
4618
|
+
let maxY = -Infinity;
|
|
4619
|
+
const include = (point) => {
|
|
4620
|
+
minX = Math.min(minX, point.x);
|
|
4621
|
+
minY = Math.min(minY, point.y);
|
|
4622
|
+
maxX = Math.max(maxX, point.x);
|
|
4623
|
+
maxY = Math.max(maxY, point.y);
|
|
4624
|
+
};
|
|
4625
|
+
let current = { x: 0, y: 0 };
|
|
4626
|
+
let subpathStart = { x: 0, y: 0 };
|
|
4627
|
+
for (const segment of segments) {
|
|
4628
|
+
switch (segment.command) {
|
|
4629
|
+
case "M": {
|
|
4630
|
+
current = { x: segment.values[0], y: segment.values[1] };
|
|
4631
|
+
subpathStart = { ...current };
|
|
4632
|
+
include(current);
|
|
4633
|
+
break;
|
|
4634
|
+
}
|
|
4635
|
+
case "L": {
|
|
4636
|
+
include(current);
|
|
4637
|
+
current = { x: segment.values[0], y: segment.values[1] };
|
|
4638
|
+
include(current);
|
|
4639
|
+
break;
|
|
4640
|
+
}
|
|
4641
|
+
case "C": {
|
|
4642
|
+
const [x1, y1, x2, y2, x, y] = segment.values;
|
|
4643
|
+
const ts = new Set([0, 1]);
|
|
4644
|
+
cubicExtrema(current.x, x1, x2, x).forEach((value) => ts.add(value));
|
|
4645
|
+
cubicExtrema(current.y, y1, y2, y).forEach((value) => ts.add(value));
|
|
4646
|
+
for (const t of ts) {
|
|
4647
|
+
include({
|
|
4648
|
+
x: cubicAt(current.x, x1, x2, x, t),
|
|
4649
|
+
y: cubicAt(current.y, y1, y2, y, t),
|
|
4650
|
+
});
|
|
4651
|
+
}
|
|
4652
|
+
current = { x, y };
|
|
4653
|
+
break;
|
|
4654
|
+
}
|
|
4655
|
+
case "Q": {
|
|
4656
|
+
const [x1, y1, x, y] = segment.values;
|
|
4657
|
+
const ts = new Set([0, 1]);
|
|
4658
|
+
quadraticExtrema(current.x, x1, x).forEach((value) => ts.add(value));
|
|
4659
|
+
quadraticExtrema(current.y, y1, y).forEach((value) => ts.add(value));
|
|
4660
|
+
for (const t of ts) {
|
|
4661
|
+
include({
|
|
4662
|
+
x: quadraticAt(current.x, x1, x, t),
|
|
4663
|
+
y: quadraticAt(current.y, y1, y, t),
|
|
4664
|
+
});
|
|
4665
|
+
}
|
|
4666
|
+
current = { x, y };
|
|
4667
|
+
break;
|
|
4668
|
+
}
|
|
4669
|
+
case "A": {
|
|
4670
|
+
for (const point of sampleArc(current, segment.values)) {
|
|
4671
|
+
include(point);
|
|
4672
|
+
}
|
|
4673
|
+
current = { x: segment.values[5], y: segment.values[6] };
|
|
4674
|
+
break;
|
|
4675
|
+
}
|
|
4676
|
+
case "Z": {
|
|
4677
|
+
include(current);
|
|
4678
|
+
include(subpathStart);
|
|
4679
|
+
current = { ...subpathStart };
|
|
4680
|
+
break;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
|
|
4685
|
+
return null;
|
|
4686
|
+
}
|
|
4687
|
+
return { minX, minY, maxX, maxY };
|
|
4688
|
+
}
|
|
4689
|
+
function transformX(x, bounds, scaleX) {
|
|
4690
|
+
return (x - bounds.minX) * scaleX;
|
|
4691
|
+
}
|
|
4692
|
+
function transformY(y, bounds, scaleY) {
|
|
4693
|
+
return (y - bounds.minY) * scaleY;
|
|
4694
|
+
}
|
|
4695
|
+
function buildScaledPathData(segments, bounds, width, height) {
|
|
4696
|
+
const sourceWidth = Math.max(bounds.maxX - bounds.minX, EPSILON);
|
|
4697
|
+
const sourceHeight = Math.max(bounds.maxY - bounds.minY, EPSILON);
|
|
4698
|
+
const scaleX = width / sourceWidth;
|
|
4699
|
+
const scaleY = height / sourceHeight;
|
|
4700
|
+
return segments
|
|
4701
|
+
.map((segment) => {
|
|
4702
|
+
switch (segment.command) {
|
|
4703
|
+
case "M":
|
|
4704
|
+
case "L":
|
|
4705
|
+
return [
|
|
4706
|
+
segment.command,
|
|
4707
|
+
formatNumber(transformX(segment.values[0], bounds, scaleX)),
|
|
4708
|
+
formatNumber(transformY(segment.values[1], bounds, scaleY)),
|
|
4709
|
+
].join(" ");
|
|
4710
|
+
case "C":
|
|
4711
|
+
return [
|
|
4712
|
+
"C",
|
|
4713
|
+
formatNumber(transformX(segment.values[0], bounds, scaleX)),
|
|
4714
|
+
formatNumber(transformY(segment.values[1], bounds, scaleY)),
|
|
4715
|
+
formatNumber(transformX(segment.values[2], bounds, scaleX)),
|
|
4716
|
+
formatNumber(transformY(segment.values[3], bounds, scaleY)),
|
|
4717
|
+
formatNumber(transformX(segment.values[4], bounds, scaleX)),
|
|
4718
|
+
formatNumber(transformY(segment.values[5], bounds, scaleY)),
|
|
4719
|
+
].join(" ");
|
|
4720
|
+
case "Q":
|
|
4721
|
+
return [
|
|
4722
|
+
"Q",
|
|
4723
|
+
formatNumber(transformX(segment.values[0], bounds, scaleX)),
|
|
4724
|
+
formatNumber(transformY(segment.values[1], bounds, scaleY)),
|
|
4725
|
+
formatNumber(transformX(segment.values[2], bounds, scaleX)),
|
|
4726
|
+
formatNumber(transformY(segment.values[3], bounds, scaleY)),
|
|
4727
|
+
].join(" ");
|
|
4728
|
+
case "A":
|
|
4729
|
+
return [
|
|
4730
|
+
"A",
|
|
4731
|
+
formatNumber(segment.values[0] * scaleX),
|
|
4732
|
+
formatNumber(segment.values[1] * scaleY),
|
|
4733
|
+
formatNumber(segment.values[2]),
|
|
4734
|
+
formatNumber(segment.values[3]),
|
|
4735
|
+
formatNumber(segment.values[4]),
|
|
4736
|
+
formatNumber(transformX(segment.values[5], bounds, scaleX)),
|
|
4737
|
+
formatNumber(transformY(segment.values[6], bounds, scaleY)),
|
|
4738
|
+
].join(" ");
|
|
4739
|
+
case "Z":
|
|
4740
|
+
return "Z";
|
|
4741
|
+
}
|
|
4742
|
+
})
|
|
4743
|
+
.join(" ");
|
|
4744
|
+
}
|
|
4745
|
+
function intrinsicSizeFromBounds(bounds) {
|
|
4746
|
+
if (!bounds)
|
|
4747
|
+
return { width: 100, height: 100 };
|
|
4748
|
+
return {
|
|
4749
|
+
width: Math.max(1, Math.ceil(bounds.maxX - bounds.minX)),
|
|
4750
|
+
height: Math.max(1, Math.ceil(bounds.maxY - bounds.minY)),
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4753
|
+
function parsePathGeometry(pathData) {
|
|
4754
|
+
const segments = toAbsoluteSegments(parseRawSegments(pathData));
|
|
4755
|
+
return {
|
|
4756
|
+
segments,
|
|
4757
|
+
bounds: boundsFromAbsoluteSegments(segments),
|
|
4758
|
+
};
|
|
4759
|
+
}
|
|
4760
|
+
function getPathIntrinsicSize(pathData) {
|
|
4761
|
+
if (!pathData)
|
|
4762
|
+
return { width: 100, height: 100 };
|
|
4763
|
+
return intrinsicSizeFromBounds(parsePathGeometry(pathData).bounds);
|
|
4764
|
+
}
|
|
4765
|
+
function getRenderablePathData(pathData, width, height) {
|
|
4766
|
+
if (!pathData)
|
|
4767
|
+
return null;
|
|
4768
|
+
const { segments, bounds } = parsePathGeometry(pathData);
|
|
4769
|
+
if (!segments.length || !bounds)
|
|
4770
|
+
return pathData;
|
|
4771
|
+
return buildScaledPathData(segments, bounds, Math.max(1, width), Math.max(1, height));
|
|
4772
|
+
}
|
|
4773
|
+
function getRenderableNodePathData(node) {
|
|
4774
|
+
return getRenderablePathData(node.pathData, node.w, node.h);
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4253
4777
|
const pathShape = {
|
|
4254
4778
|
size(n, labelW) {
|
|
4255
|
-
|
|
4256
|
-
const w = n.width ?? Math.max(
|
|
4779
|
+
const intrinsic = getPathIntrinsicSize(n.pathData);
|
|
4780
|
+
const w = n.width ?? Math.max(intrinsic.width, Math.min(300, labelW + 20));
|
|
4257
4781
|
n.w = w;
|
|
4258
4782
|
if (!n.h) {
|
|
4259
|
-
if (!n.width && labelW + 20 > w) {
|
|
4783
|
+
if (!n.width && !n.height && labelW + 20 > w) {
|
|
4260
4784
|
const fontSize = Number(n.style?.fontSize ?? 14);
|
|
4261
4785
|
const lines = Math.ceil(labelW / (w - 20));
|
|
4262
|
-
n.h = Math.max(
|
|
4786
|
+
n.h = Math.max(intrinsic.height, lines * fontSize * 1.5 + 20);
|
|
4263
4787
|
}
|
|
4264
4788
|
else {
|
|
4265
|
-
n.h = n.height ??
|
|
4789
|
+
n.h = n.height ?? intrinsic.height;
|
|
4266
4790
|
}
|
|
4267
4791
|
}
|
|
4268
4792
|
},
|
|
4269
4793
|
renderSVG(rc, n, _palette, opts) {
|
|
4270
|
-
const d = n
|
|
4794
|
+
const d = getRenderableNodePathData(n);
|
|
4271
4795
|
if (!d) {
|
|
4272
|
-
// No path data — render placeholder box
|
|
4273
4796
|
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
4274
4797
|
}
|
|
4275
4798
|
const el = rc.path(d, opts);
|
|
4276
|
-
// Wrap in a group to translate the user's path to the node position
|
|
4277
4799
|
const g = document.createElementNS(SVG_NS, "g");
|
|
4278
4800
|
g.setAttribute("transform", `translate(${n.x},${n.y})`);
|
|
4279
4801
|
g.appendChild(el);
|
|
4280
4802
|
return [g];
|
|
4281
4803
|
},
|
|
4282
4804
|
renderCanvas(rc, ctx, n, _palette, opts) {
|
|
4283
|
-
const d = n
|
|
4805
|
+
const d = getRenderableNodePathData(n);
|
|
4284
4806
|
if (!d) {
|
|
4285
4807
|
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
4286
4808
|
return;
|
|
@@ -4587,6 +5109,50 @@ function connPoint(n, other) {
|
|
|
4587
5109
|
const t = Math.min(tx, ty);
|
|
4588
5110
|
return [cx + t * dx, cy + t * dy];
|
|
4589
5111
|
}
|
|
5112
|
+
function clampInset(value) {
|
|
5113
|
+
return Math.max(2, value);
|
|
5114
|
+
}
|
|
5115
|
+
function anchoredConnPoint(entity, anchor, otherCX, otherCY) {
|
|
5116
|
+
if (!anchor) {
|
|
5117
|
+
if (entity.shape && otherCX != null && otherCY != null) {
|
|
5118
|
+
return connPoint(entity, { x: otherCX - 1, y: otherCY - 1, w: 2, h: 2});
|
|
5119
|
+
}
|
|
5120
|
+
if (otherCX != null && otherCY != null) {
|
|
5121
|
+
return rectConnPoint(entity.x, entity.y, entity.w, entity.h, otherCX, otherCY);
|
|
5122
|
+
}
|
|
5123
|
+
return [entity.x + entity.w / 2, entity.y + entity.h / 2];
|
|
5124
|
+
}
|
|
5125
|
+
const insetX = clampInset(Math.min(10, entity.w / 2));
|
|
5126
|
+
const insetY = clampInset(Math.min(10, entity.h / 2));
|
|
5127
|
+
const left = entity.x + insetX;
|
|
5128
|
+
const right = entity.x + entity.w - insetX;
|
|
5129
|
+
const top = entity.y + insetY;
|
|
5130
|
+
const bottom = entity.y + entity.h - insetY;
|
|
5131
|
+
const cx = entity.x + entity.w / 2;
|
|
5132
|
+
const cy = entity.y + entity.h / 2;
|
|
5133
|
+
switch (anchor) {
|
|
5134
|
+
case "top":
|
|
5135
|
+
return [cx, top];
|
|
5136
|
+
case "right":
|
|
5137
|
+
return [right, cy];
|
|
5138
|
+
case "bottom":
|
|
5139
|
+
return [cx, bottom];
|
|
5140
|
+
case "left":
|
|
5141
|
+
return [left, cy];
|
|
5142
|
+
case "center":
|
|
5143
|
+
return [cx, cy];
|
|
5144
|
+
case "top-left":
|
|
5145
|
+
return [left, top];
|
|
5146
|
+
case "top-right":
|
|
5147
|
+
return [right, top];
|
|
5148
|
+
case "bottom-left":
|
|
5149
|
+
return [left, bottom];
|
|
5150
|
+
case "bottom-right":
|
|
5151
|
+
return [right, bottom];
|
|
5152
|
+
default:
|
|
5153
|
+
return [cx, cy];
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
4590
5156
|
function rectConnPoint(rx, ry, rw, rh, ox, oy) {
|
|
4591
5157
|
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
4592
5158
|
const dx = ox - cx, dy = oy - cy;
|
|
@@ -4618,17 +5184,6 @@ function routeEdges(sg) {
|
|
|
4618
5184
|
return c;
|
|
4619
5185
|
return null;
|
|
4620
5186
|
}
|
|
4621
|
-
function connPt(src, dstCX, dstCY) {
|
|
4622
|
-
// SceneNode has a .shape field; use the existing connPoint for it
|
|
4623
|
-
if ("shape" in src && src.shape) {
|
|
4624
|
-
return connPoint(src, {
|
|
4625
|
-
x: dstCX - 1,
|
|
4626
|
-
y: dstCY - 1,
|
|
4627
|
-
w: 2,
|
|
4628
|
-
h: 2});
|
|
4629
|
-
}
|
|
4630
|
-
return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
4631
|
-
}
|
|
4632
5187
|
for (const e of sg.edges) {
|
|
4633
5188
|
const src = resolve(e.from);
|
|
4634
5189
|
const dst = resolve(e.to);
|
|
@@ -4638,7 +5193,10 @@ function routeEdges(sg) {
|
|
|
4638
5193
|
}
|
|
4639
5194
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
4640
5195
|
const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
|
|
4641
|
-
e.points = [
|
|
5196
|
+
e.points = [
|
|
5197
|
+
anchoredConnPoint(src, e.fromAnchor, dstCX, dstCY),
|
|
5198
|
+
anchoredConnPoint(dst, e.toAnchor, srcCX, srcCY),
|
|
5199
|
+
];
|
|
4642
5200
|
}
|
|
4643
5201
|
}
|
|
4644
5202
|
function computeBounds(sg, margin) {
|
|
@@ -7779,8 +8337,8 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7779
8337
|
continue;
|
|
7780
8338
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
7781
8339
|
const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
|
|
7782
|
-
const [x1, y1] = getConnPoint(src, dstCX, dstCY);
|
|
7783
|
-
const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
|
|
8340
|
+
const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
|
|
8341
|
+
const [x2, y2] = getConnPoint(dst, srcCX, srcCY, e.toAnchor);
|
|
7784
8342
|
const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
|
|
7785
8343
|
if (e.style?.opacity != null)
|
|
7786
8344
|
eg.setAttribute("opacity", String(e.style.opacity));
|
|
@@ -7856,7 +8414,9 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7856
8414
|
ng.dataset.w = String(n.w);
|
|
7857
8415
|
ng.dataset.h = String(n.h);
|
|
7858
8416
|
if (n.pathData)
|
|
7859
|
-
ng.dataset.pathData = n.pathData;
|
|
8417
|
+
ng.dataset.pathData = getRenderableNodePathData(n) ?? n.pathData;
|
|
8418
|
+
if (n.meta?.animationParent)
|
|
8419
|
+
ng.dataset.animationParent = n.meta.animationParent;
|
|
7860
8420
|
if (n.style?.opacity != null)
|
|
7861
8421
|
ng.setAttribute("opacity", String(n.style.opacity));
|
|
7862
8422
|
// ── Static transform (deg, dx, dy, factor) ──────────
|
|
@@ -8510,8 +9070,8 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
8510
9070
|
continue;
|
|
8511
9071
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
8512
9072
|
const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
|
|
8513
|
-
const [x1, y1] = getConnPoint(src, dstCX, dstCY);
|
|
8514
|
-
const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
|
|
9073
|
+
const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
|
|
9074
|
+
const [x2, y2] = getConnPoint(dst, srcCX, srcCY, e.toAnchor);
|
|
8515
9075
|
if (e.style?.opacity != null)
|
|
8516
9076
|
ctx.globalAlpha = Number(e.style.opacity);
|
|
8517
9077
|
const ecol = String(e.style?.stroke ?? palette.edgeStroke);
|
|
@@ -9368,6 +9928,13 @@ class AnimationController {
|
|
|
9368
9928
|
this.drawTargetNodes.delete(`node-${s.target}`);
|
|
9369
9929
|
}
|
|
9370
9930
|
}
|
|
9931
|
+
this._relatedElementIdsByPrimaryId = this._buildRelatedElementIndex();
|
|
9932
|
+
for (const nodeId of Array.from(this.drawTargetNodes)) {
|
|
9933
|
+
const relatedIds = this._relatedElementIdsByPrimaryId.get(nodeId);
|
|
9934
|
+
if (!relatedIds)
|
|
9935
|
+
continue;
|
|
9936
|
+
relatedIds.forEach((id) => this.drawTargetNodes.add(id));
|
|
9937
|
+
}
|
|
9371
9938
|
this._drawStepIndexByElementId = this._buildDrawStepIndex();
|
|
9372
9939
|
const { parentGroupByElementId, groupDescendantIds } = this._buildGroupVisibilityIndex();
|
|
9373
9940
|
this._parentGroupByElementId = parentGroupByElementId;
|
|
@@ -9398,10 +9965,30 @@ class AnimationController {
|
|
|
9398
9965
|
const el = resolveNonEdgeDrawEl(this.svg, step.target);
|
|
9399
9966
|
if (el && !drawStepIndexByElementId.has(el.id)) {
|
|
9400
9967
|
drawStepIndexByElementId.set(el.id, stepIndex);
|
|
9968
|
+
this._relatedElementIdsByPrimaryId.get(el.id)?.forEach((relatedId) => {
|
|
9969
|
+
if (!drawStepIndexByElementId.has(relatedId)) {
|
|
9970
|
+
drawStepIndexByElementId.set(relatedId, stepIndex);
|
|
9971
|
+
}
|
|
9972
|
+
});
|
|
9401
9973
|
}
|
|
9402
9974
|
});
|
|
9403
9975
|
return drawStepIndexByElementId;
|
|
9404
9976
|
}
|
|
9977
|
+
_buildRelatedElementIndex() {
|
|
9978
|
+
const relatedElementIdsByPrimaryId = new Map();
|
|
9979
|
+
this.svg.querySelectorAll(POSITIONABLE_SELECTOR).forEach((el) => {
|
|
9980
|
+
const animationParent = el.dataset.animationParent;
|
|
9981
|
+
if (!animationParent)
|
|
9982
|
+
return;
|
|
9983
|
+
const primaryEl = resolveNonEdgeDrawEl(this.svg, animationParent);
|
|
9984
|
+
if (!primaryEl || primaryEl.id === el.id)
|
|
9985
|
+
return;
|
|
9986
|
+
const related = relatedElementIdsByPrimaryId.get(primaryEl.id) ?? new Set();
|
|
9987
|
+
related.add(el.id);
|
|
9988
|
+
relatedElementIdsByPrimaryId.set(primaryEl.id, related);
|
|
9989
|
+
});
|
|
9990
|
+
return relatedElementIdsByPrimaryId;
|
|
9991
|
+
}
|
|
9405
9992
|
_buildGroupVisibilityIndex() {
|
|
9406
9993
|
const parentGroupByElementId = new Map();
|
|
9407
9994
|
const directChildIdsByGroup = new Map();
|
|
@@ -9480,10 +10067,18 @@ class AnimationController {
|
|
|
9480
10067
|
const el = resolveEl(this.svg, target);
|
|
9481
10068
|
if (!el)
|
|
9482
10069
|
return [];
|
|
9483
|
-
if (!el.id.startsWith("group-"))
|
|
9484
|
-
|
|
10070
|
+
if (!el.id.startsWith("group-")) {
|
|
10071
|
+
const ids = new Set([el.id]);
|
|
10072
|
+
this._relatedElementIdsByPrimaryId.get(el.id)?.forEach((id) => ids.add(id));
|
|
10073
|
+
return Array.from(ids)
|
|
10074
|
+
.map((id) => getEl(this.svg, id))
|
|
10075
|
+
.filter((candidate) => candidate != null);
|
|
10076
|
+
}
|
|
9485
10077
|
const ids = new Set([el.id]);
|
|
9486
10078
|
this._groupDescendantIds.get(el.id)?.forEach((id) => ids.add(id));
|
|
10079
|
+
Array.from(ids).forEach((id) => {
|
|
10080
|
+
this._relatedElementIdsByPrimaryId.get(id)?.forEach((relatedId) => ids.add(relatedId));
|
|
10081
|
+
});
|
|
9487
10082
|
return Array.from(ids)
|
|
9488
10083
|
.map((id) => getEl(this.svg, id))
|
|
9489
10084
|
.filter((candidate) => candidate != null);
|
|
@@ -9892,9 +10487,11 @@ class AnimationController {
|
|
|
9892
10487
|
// ── highlight ────────────────────────────────────────────
|
|
9893
10488
|
_doHighlight(target) {
|
|
9894
10489
|
this.svg
|
|
9895
|
-
.querySelectorAll(".ng.hl, .tg.hl, .ntg.hl, .cg.hl, .eg.hl")
|
|
10490
|
+
.querySelectorAll(".ng.hl, .gg.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl")
|
|
9896
10491
|
.forEach((e) => e.classList.remove("hl"));
|
|
9897
|
-
|
|
10492
|
+
for (const el of this._resolveCascadeTargets(target)) {
|
|
10493
|
+
el.classList.add("hl");
|
|
10494
|
+
}
|
|
9898
10495
|
}
|
|
9899
10496
|
// ── fade / unfade ─────────────────────────────────────────
|
|
9900
10497
|
_doFade(target, doFade) {
|
|
@@ -9930,8 +10527,8 @@ class AnimationController {
|
|
|
9930
10527
|
}
|
|
9931
10528
|
// ── move ──────────────────────────────────────────────────
|
|
9932
10529
|
_doMove(target, step, silent) {
|
|
9933
|
-
const
|
|
9934
|
-
if (!
|
|
10530
|
+
const targets = this._resolveCascadeTargets(target);
|
|
10531
|
+
if (!targets.length)
|
|
9935
10532
|
return;
|
|
9936
10533
|
const cur = this._transforms.get(target) ?? {
|
|
9937
10534
|
tx: 0,
|
|
@@ -9944,12 +10541,14 @@ class AnimationController {
|
|
|
9944
10541
|
tx: cur.tx + (step.dx ?? 0),
|
|
9945
10542
|
ty: cur.ty + (step.dy ?? 0),
|
|
9946
10543
|
});
|
|
9947
|
-
|
|
10544
|
+
for (const el of targets) {
|
|
10545
|
+
this._writeTransform(el, target, silent, step.duration ?? 420);
|
|
10546
|
+
}
|
|
9948
10547
|
}
|
|
9949
10548
|
// ── scale ─────────────────────────────────────────────────
|
|
9950
10549
|
_doScale(target, step, silent) {
|
|
9951
|
-
const
|
|
9952
|
-
if (!
|
|
10550
|
+
const targets = this._resolveCascadeTargets(target);
|
|
10551
|
+
if (!targets.length)
|
|
9953
10552
|
return;
|
|
9954
10553
|
const cur = this._transforms.get(target) ?? {
|
|
9955
10554
|
tx: 0,
|
|
@@ -9958,12 +10557,14 @@ class AnimationController {
|
|
|
9958
10557
|
rotate: 0,
|
|
9959
10558
|
};
|
|
9960
10559
|
this._transforms.set(target, { ...cur, scale: step.factor ?? 1 });
|
|
9961
|
-
|
|
10560
|
+
for (const el of targets) {
|
|
10561
|
+
this._writeTransform(el, target, silent, step.duration ?? 350);
|
|
10562
|
+
}
|
|
9962
10563
|
}
|
|
9963
10564
|
// ── rotate ────────────────────────────────────────────────
|
|
9964
10565
|
_doRotate(target, step, silent) {
|
|
9965
|
-
const
|
|
9966
|
-
if (!
|
|
10566
|
+
const targets = this._resolveCascadeTargets(target);
|
|
10567
|
+
if (!targets.length)
|
|
9967
10568
|
return;
|
|
9968
10569
|
const cur = this._transforms.get(target) ?? {
|
|
9969
10570
|
tx: 0,
|
|
@@ -9975,7 +10576,9 @@ class AnimationController {
|
|
|
9975
10576
|
...cur,
|
|
9976
10577
|
rotate: cur.rotate + (step.deg ?? 0),
|
|
9977
10578
|
});
|
|
9978
|
-
|
|
10579
|
+
for (const el of targets) {
|
|
10580
|
+
this._writeTransform(el, target, silent, step.duration ?? 400);
|
|
10581
|
+
}
|
|
9979
10582
|
}
|
|
9980
10583
|
_doDraw(step, silent) {
|
|
9981
10584
|
const { target } = step;
|
|
@@ -10119,18 +10722,20 @@ class AnimationController {
|
|
|
10119
10722
|
return;
|
|
10120
10723
|
}
|
|
10121
10724
|
// ── Node draw ──────────────────────────────────────
|
|
10122
|
-
const
|
|
10123
|
-
if (!
|
|
10725
|
+
const nodeEls = this._resolveCascadeTargets(target).filter((el) => el.classList.contains("ng"));
|
|
10726
|
+
if (!nodeEls.length)
|
|
10124
10727
|
return;
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10728
|
+
for (const nodeEl of nodeEls) {
|
|
10729
|
+
showDrawEl(nodeEl);
|
|
10730
|
+
if (silent) {
|
|
10731
|
+
revealNodeInstant(nodeEl);
|
|
10732
|
+
}
|
|
10733
|
+
else {
|
|
10734
|
+
if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
|
|
10735
|
+
prepareNodeForDraw(nodeEl);
|
|
10736
|
+
}
|
|
10737
|
+
animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
|
|
10132
10738
|
}
|
|
10133
|
-
animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
|
|
10134
10739
|
}
|
|
10135
10740
|
}
|
|
10136
10741
|
// ── erase ─────────────────────────────────────────────────
|
|
@@ -10151,45 +10756,44 @@ class AnimationController {
|
|
|
10151
10756
|
}
|
|
10152
10757
|
// ── pulse ─────────────────────────────────────────────────
|
|
10153
10758
|
_doPulse(target, duration = 500) {
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10759
|
+
for (const el of this._resolveCascadeTargets(target)) {
|
|
10760
|
+
el.animate([
|
|
10761
|
+
{ filter: "brightness(1)" },
|
|
10762
|
+
{ filter: "brightness(1.6)" },
|
|
10763
|
+
{ filter: "brightness(1)" },
|
|
10764
|
+
], { duration, iterations: 3 });
|
|
10765
|
+
}
|
|
10159
10766
|
}
|
|
10160
10767
|
// ── color ─────────────────────────────────────────────────
|
|
10161
10768
|
_doColor(target, color) {
|
|
10162
10769
|
if (!color)
|
|
10163
10770
|
return;
|
|
10164
|
-
const el
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
if (attrFill === null && c.tagName === "path")
|
|
10185
|
-
return;
|
|
10186
|
-
c.style.fill = color;
|
|
10187
|
-
hit = true;
|
|
10188
|
-
});
|
|
10189
|
-
if (!hit) {
|
|
10190
|
-
el.querySelectorAll("text").forEach((t) => {
|
|
10191
|
-
t.style.fill = color;
|
|
10771
|
+
for (const el of this._resolveCascadeTargets(target)) {
|
|
10772
|
+
if (parseEdgeTarget(target)) {
|
|
10773
|
+
el.querySelectorAll("path, line, polyline").forEach((p) => {
|
|
10774
|
+
p.style.stroke = color;
|
|
10775
|
+
});
|
|
10776
|
+
el.querySelectorAll("polygon").forEach((p) => {
|
|
10777
|
+
p.style.fill = color;
|
|
10778
|
+
p.style.stroke = color;
|
|
10779
|
+
});
|
|
10780
|
+
continue;
|
|
10781
|
+
}
|
|
10782
|
+
let hit = false;
|
|
10783
|
+
el.querySelectorAll("path, rect, ellipse, polygon").forEach((c) => {
|
|
10784
|
+
const attrFill = c.getAttribute("fill");
|
|
10785
|
+
if (attrFill === "none")
|
|
10786
|
+
return;
|
|
10787
|
+
if (attrFill === null && c.tagName === "path")
|
|
10788
|
+
return;
|
|
10789
|
+
c.style.fill = color;
|
|
10790
|
+
hit = true;
|
|
10192
10791
|
});
|
|
10792
|
+
if (!hit) {
|
|
10793
|
+
el.querySelectorAll("text").forEach((t) => {
|
|
10794
|
+
t.style.fill = color;
|
|
10795
|
+
});
|
|
10796
|
+
}
|
|
10193
10797
|
}
|
|
10194
10798
|
}
|
|
10195
10799
|
// ── narration ───────────────────────────────────────────
|
|
@@ -10783,7 +11387,7 @@ class EventEmitter {
|
|
|
10783
11387
|
}
|
|
10784
11388
|
|
|
10785
11389
|
function render(options) {
|
|
10786
|
-
const { container: rawContainer, dsl, renderer = "svg", injectCSS = true, tts, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
|
|
11390
|
+
const { container: rawContainer, dsl, plugins, renderer = "svg", injectCSS = true, tts, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
|
|
10787
11391
|
if (injectCSS && !document.getElementById("ai-diagram-css")) {
|
|
10788
11392
|
const style = document.createElement("style");
|
|
10789
11393
|
style.id = "ai-diagram-css";
|
|
@@ -10799,7 +11403,7 @@ function render(options) {
|
|
|
10799
11403
|
else {
|
|
10800
11404
|
el = rawContainer;
|
|
10801
11405
|
}
|
|
10802
|
-
const ast = parse(dsl);
|
|
11406
|
+
const ast = parse(dsl, { plugins });
|
|
10803
11407
|
const scene = buildSceneGraph(ast);
|
|
10804
11408
|
layout(scene);
|
|
10805
11409
|
let svg;
|
|
@@ -11143,6 +11747,7 @@ class SketchmarkCanvas {
|
|
|
11143
11747
|
const instance = render({
|
|
11144
11748
|
container: this.diagramWrap,
|
|
11145
11749
|
dsl: this.dsl,
|
|
11750
|
+
plugins: this.options.plugins,
|
|
11146
11751
|
renderer: this.renderer,
|
|
11147
11752
|
svgOptions: { interactive: true, showTitle: true, theme: this.options.svgOptions?.theme ?? this.theme, ...this.options.svgOptions },
|
|
11148
11753
|
canvasOptions: this.options.canvasOptions,
|
|
@@ -12220,6 +12825,7 @@ class SketchmarkEmbed {
|
|
|
12220
12825
|
const instance = render({
|
|
12221
12826
|
container: this.diagramWrap,
|
|
12222
12827
|
dsl: this.dsl,
|
|
12828
|
+
plugins: this.options.plugins,
|
|
12223
12829
|
renderer: "svg",
|
|
12224
12830
|
svgOptions: {
|
|
12225
12831
|
showTitle: true,
|