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/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 += esc;
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 tokens = tokenize$1(src).filter((t) => t.type !== "NEWLINE" || t.value === "\n");
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
- if ("shape" in src && src.shape) {
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
- // User should provide width/height; defaults to 100x100
4256
- const w = n.width ?? Math.max(100, Math.min(300, labelW + 20));
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(100, lines * fontSize * 1.5 + 20);
4786
+ n.h = Math.max(intrinsic.height, lines * fontSize * 1.5 + 20);
4263
4787
  }
4264
4788
  else {
4265
- n.h = n.height ?? 100;
4789
+ n.h = n.height ?? intrinsic.height;
4266
4790
  }
4267
4791
  }
4268
4792
  },
4269
4793
  renderSVG(rc, n, _palette, opts) {
4270
- const d = n.pathData;
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.pathData;
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 = [connPt(src, dstCX, dstCY), connPt(dst, srcCX, srcCY)];
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
- return [el];
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
- resolveEl(this.svg, target)?.classList.add("hl");
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 el = resolveEl(this.svg, target);
9934
- if (!el)
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
- this._writeTransform(el, target, silent, step.duration ?? 420);
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 el = resolveEl(this.svg, target);
9952
- if (!el)
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
- this._writeTransform(el, target, silent, step.duration ?? 350);
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 el = resolveEl(this.svg, target);
9966
- if (!el)
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
- this._writeTransform(el, target, silent, step.duration ?? 400);
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 nodeEl = getNodeEl(this.svg, target);
10123
- if (!nodeEl)
10725
+ const nodeEls = this._resolveCascadeTargets(target).filter((el) => el.classList.contains("ng"));
10726
+ if (!nodeEls.length)
10124
10727
  return;
10125
- showDrawEl(nodeEl);
10126
- if (silent) {
10127
- revealNodeInstant(nodeEl);
10128
- }
10129
- else {
10130
- if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
10131
- prepareNodeForDraw(nodeEl);
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
- resolveEl(this.svg, target)?.animate([
10155
- { filter: "brightness(1)" },
10156
- { filter: "brightness(1.6)" },
10157
- { filter: "brightness(1)" },
10158
- ], { duration, iterations: 3 });
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 = resolveEl(this.svg, target);
10165
- if (!el)
10166
- return;
10167
- // edge color stroke
10168
- if (parseEdgeTarget(target)) {
10169
- el.querySelectorAll("path, line, polyline").forEach((p) => {
10170
- p.style.stroke = color;
10171
- });
10172
- el.querySelectorAll("polygon").forEach((p) => {
10173
- p.style.fill = color;
10174
- p.style.stroke = color;
10175
- });
10176
- return;
10177
- }
10178
- // everything else — color fill
10179
- let hit = false;
10180
- el.querySelectorAll("path, rect, ellipse, polygon").forEach((c) => {
10181
- const attrFill = c.getAttribute("fill");
10182
- if (attrFill === "none")
10183
- return;
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,