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/dist/index.js CHANGED
@@ -4297,37 +4297,510 @@ const lineShape = {
4297
4297
  },
4298
4298
  };
4299
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
+
4300
4775
  const pathShape = {
4301
4776
  size(n, labelW) {
4302
- // User should provide width/height; defaults to 100x100
4303
- 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));
4304
4779
  n.w = w;
4305
4780
  if (!n.h) {
4306
- if (!n.width && labelW + 20 > w) {
4781
+ if (!n.width && !n.height && labelW + 20 > w) {
4307
4782
  const fontSize = Number(n.style?.fontSize ?? 14);
4308
4783
  const lines = Math.ceil(labelW / (w - 20));
4309
- n.h = Math.max(100, lines * fontSize * 1.5 + 20);
4784
+ n.h = Math.max(intrinsic.height, lines * fontSize * 1.5 + 20);
4310
4785
  }
4311
4786
  else {
4312
- n.h = n.height ?? 100;
4787
+ n.h = n.height ?? intrinsic.height;
4313
4788
  }
4314
4789
  }
4315
4790
  },
4316
4791
  renderSVG(rc, n, _palette, opts) {
4317
- const d = n.pathData;
4792
+ const d = getRenderableNodePathData(n);
4318
4793
  if (!d) {
4319
- // No path data — render placeholder box
4320
4794
  return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
4321
4795
  }
4322
4796
  const el = rc.path(d, opts);
4323
- // Wrap in a group to translate the user's path to the node position
4324
4797
  const g = document.createElementNS(SVG_NS, "g");
4325
4798
  g.setAttribute("transform", `translate(${n.x},${n.y})`);
4326
4799
  g.appendChild(el);
4327
4800
  return [g];
4328
4801
  },
4329
4802
  renderCanvas(rc, ctx, n, _palette, opts) {
4330
- const d = n.pathData;
4803
+ const d = getRenderableNodePathData(n);
4331
4804
  if (!d) {
4332
4805
  rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
4333
4806
  return;
@@ -7939,7 +8412,7 @@ function renderToSVG(sg, container, options = {}) {
7939
8412
  ng.dataset.w = String(n.w);
7940
8413
  ng.dataset.h = String(n.h);
7941
8414
  if (n.pathData)
7942
- ng.dataset.pathData = n.pathData;
8415
+ ng.dataset.pathData = getRenderableNodePathData(n) ?? n.pathData;
7943
8416
  if (n.meta?.animationParent)
7944
8417
  ng.dataset.animationParent = n.meta.animationParent;
7945
8418
  if (n.style?.opacity != null)