sketchmark 1.2.0 → 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 CHANGED
@@ -351,7 +351,7 @@ Every node has the form:
351
351
  | Image | `image` | URL-loaded image | label width |
352
352
  | Icon | `icon` | Iconify icon | 48×48 + label |
353
353
  | Line | `line` | Horizontal rule | label width |
354
- | Path | `path` | Custom SVG path data | user-specified |
354
+ | Path | `path` | Custom SVG path data scaled into `width`/`height` | user-specified |
355
355
  | Note | `note` | Sticky-note shape | line count × line height |
356
356
 
357
357
  ```
@@ -367,9 +367,11 @@ text myTxt label="Some prose" width=300
367
367
  image myImg label="Logo" url="https://example.com/logo.png" width=120 height=60
368
368
  icon myIcon label="Settings" name="mdi:cog"
369
369
  line myLine label="Section" width=200
370
- path myPath value="M 0 0 L 50 50 L 100 0 Z" width=100 height=60
371
- note myNote label="Remember this!"
372
- ```
370
+ path myPath value="M 0 0 L 50 50 L 100 0 Z" width=100 height=60
371
+ note myNote label="Remember this!"
372
+ ```
373
+
374
+ For `path`, write `value` in local coordinates near `0,0`. The renderer normalizes the path bounds into the node's `width` and `height`, and then uses `x`/`y` only for placement.
373
375
 
374
376
  ---
375
377
 
@@ -1051,7 +1053,7 @@ Nodes can also opt into authored absolute `x`/`y` positioning when their parent
1051
1053
  | image (URL) | ✅ | Cross-origin images |
1052
1054
  | icon (Iconify) | ✅ | Uses Iconify API |
1053
1055
  | line | ✅ | Horizontal rule with label |
1054
- | path (SVG path data) | ✅ | Raw SVG `d` attribute |
1056
+ | path (SVG path data) | ✅ | Local SVG `d` attribute scaled into `width`/`height` |
1055
1057
  | note | ✅ | Sticky-note shape |
1056
1058
  | Multiline label (`\n`) | ✅ | Use `\n` in label strings |
1057
1059
  | Per-node font override | ✅ | |
package/dist/index.cjs CHANGED
@@ -4299,37 +4299,510 @@ const lineShape = {
4299
4299
  },
4300
4300
  };
4301
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
+
4302
4777
  const pathShape = {
4303
4778
  size(n, labelW) {
4304
- // User should provide width/height; defaults to 100x100
4305
- 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));
4306
4781
  n.w = w;
4307
4782
  if (!n.h) {
4308
- if (!n.width && labelW + 20 > w) {
4783
+ if (!n.width && !n.height && labelW + 20 > w) {
4309
4784
  const fontSize = Number(n.style?.fontSize ?? 14);
4310
4785
  const lines = Math.ceil(labelW / (w - 20));
4311
- n.h = Math.max(100, lines * fontSize * 1.5 + 20);
4786
+ n.h = Math.max(intrinsic.height, lines * fontSize * 1.5 + 20);
4312
4787
  }
4313
4788
  else {
4314
- n.h = n.height ?? 100;
4789
+ n.h = n.height ?? intrinsic.height;
4315
4790
  }
4316
4791
  }
4317
4792
  },
4318
4793
  renderSVG(rc, n, _palette, opts) {
4319
- const d = n.pathData;
4794
+ const d = getRenderableNodePathData(n);
4320
4795
  if (!d) {
4321
- // No path data — render placeholder box
4322
4796
  return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
4323
4797
  }
4324
4798
  const el = rc.path(d, opts);
4325
- // Wrap in a group to translate the user's path to the node position
4326
4799
  const g = document.createElementNS(SVG_NS, "g");
4327
4800
  g.setAttribute("transform", `translate(${n.x},${n.y})`);
4328
4801
  g.appendChild(el);
4329
4802
  return [g];
4330
4803
  },
4331
4804
  renderCanvas(rc, ctx, n, _palette, opts) {
4332
- const d = n.pathData;
4805
+ const d = getRenderableNodePathData(n);
4333
4806
  if (!d) {
4334
4807
  rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
4335
4808
  return;
@@ -7941,7 +8414,7 @@ function renderToSVG(sg, container, options = {}) {
7941
8414
  ng.dataset.w = String(n.w);
7942
8415
  ng.dataset.h = String(n.h);
7943
8416
  if (n.pathData)
7944
- ng.dataset.pathData = n.pathData;
8417
+ ng.dataset.pathData = getRenderableNodePathData(n) ?? n.pathData;
7945
8418
  if (n.meta?.animationParent)
7946
8419
  ng.dataset.animationParent = n.meta.animationParent;
7947
8420
  if (n.style?.opacity != null)