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