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