@pixerate/editor 0.212.0 → 0.213.0

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.
@@ -1,4 +1,16 @@
1
1
  import {
2
+ cloneAnchor,
3
+ inferAnchorType,
4
+ insertAnchor,
5
+ moveAnchorHandle,
6
+ subpathSegment,
7
+ subpathSegmentCount
8
+ } from "./chunk-NDMXB4PR.js";
9
+ import {
10
+ segmentPoint
11
+ } from "./chunk-WEIK3UYB.js";
12
+ import {
13
+ fitCubicWithTangents,
2
14
  fitCubics,
3
15
  simplifyRing
4
16
  } from "./chunk-PWBZH3FK.js";
@@ -1342,6 +1354,402 @@ async function unwoff(view) {
1342
1354
  return o;
1343
1355
  }
1344
1356
 
1357
+ // src/vector/pathEdit.ts
1358
+ var EPS = 1e-9;
1359
+ var unit = (x, y) => {
1360
+ const l = Math.hypot(x, y);
1361
+ return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
1362
+ };
1363
+ function cubicOf(sp, i) {
1364
+ const s = subpathSegment(sp, i);
1365
+ if (s.length === 4) return s;
1366
+ const [a, b] = s;
1367
+ return [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + 2 * (b[0] - a[0]) / 3, a[1] + 2 * (b[1] - a[1]) / 3], b];
1368
+ }
1369
+ var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
1370
+ function startTangent(c) {
1371
+ for (let k = 1; k < 4; k++) {
1372
+ const u = unit(c[k][0] - c[0][0], c[k][1] - c[0][1]);
1373
+ if (u) return u;
1374
+ }
1375
+ return [1, 0];
1376
+ }
1377
+ function endTangent(c) {
1378
+ for (let k = 2; k >= 0; k--) {
1379
+ const u = unit(c[3][0] - c[k][0], c[3][1] - c[k][1]);
1380
+ if (u) return u;
1381
+ }
1382
+ return [1, 0];
1383
+ }
1384
+ function sample(c, n, out, withStart) {
1385
+ for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
1386
+ }
1387
+ var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS;
1388
+ function tidy(a, keepType = true) {
1389
+ const next = cloneAnchor(a);
1390
+ if (onAnchor(next, next.in)) delete next.in;
1391
+ if (onAnchor(next, next.out)) delete next.out;
1392
+ const shown = inferAnchorType(next);
1393
+ const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
1394
+ if (type === "corner") delete next.type;
1395
+ else next.type = type;
1396
+ return next;
1397
+ }
1398
+ function neighbours(sp, i) {
1399
+ const n = sp.anchors.length;
1400
+ const wrap = sp.closed && n > 1;
1401
+ return [i > 0 ? i - 1 : wrap ? n - 1 : -1, i < n - 1 ? i + 1 : wrap ? 0 : -1];
1402
+ }
1403
+ function goldenMin(f, a, b) {
1404
+ const R = 0.618033988749895;
1405
+ let x1 = b - R * (b - a);
1406
+ let x2 = a + R * (b - a);
1407
+ let f1 = f(x1);
1408
+ let f2 = f(x2);
1409
+ for (let i = 0; i < 40; i++) {
1410
+ if (f1.error < f2.error || Number.isNaN(f2.error)) {
1411
+ b = x2;
1412
+ x2 = x1;
1413
+ f2 = f1;
1414
+ x1 = b - R * (b - a);
1415
+ f1 = f(x1);
1416
+ } else {
1417
+ a = x1;
1418
+ x1 = x2;
1419
+ f1 = f2;
1420
+ x2 = a + R * (b - a);
1421
+ f2 = f(x2);
1422
+ }
1423
+ }
1424
+ return f1.error <= f2.error ? f1 : f2;
1425
+ }
1426
+ function fitAcross(pts, N, t0, t1) {
1427
+ const fit = (k) => {
1428
+ const u = [];
1429
+ for (let i = 0; i <= N; i++) u.push(k * i / N);
1430
+ for (let i = 1; i <= N; i++) u.push(k + (1 - k) * i / N);
1431
+ return fitCubicWithTangents(pts, u, t0, t1);
1432
+ };
1433
+ return goldenMin(fit, 0, 1);
1434
+ }
1435
+ function removeAnchorKeepingShape(sp, index) {
1436
+ const anchors = sp.anchors.map(cloneAnchor);
1437
+ const n = anchors.length;
1438
+ if (index < 0 || index >= n) return { anchors, closed: sp.closed };
1439
+ if ((sp.closed || index > 0 && index < n - 1) && n >= 3) {
1440
+ const pi = (index + n - 1) % n;
1441
+ const ni = (index + 1) % n;
1442
+ if (!(isLine(sp, pi) && isLine(sp, index))) {
1443
+ const left = cubicOf(sp, pi);
1444
+ const right = cubicOf(sp, index);
1445
+ const N = 16;
1446
+ const pts = [];
1447
+ sample(left, N, pts, true);
1448
+ sample(right, N, pts, false);
1449
+ const t0 = startTangent(left);
1450
+ const t1 = endTangent(right);
1451
+ const { curve } = fitAcross(pts, N, t0, t1);
1452
+ if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
1453
+ anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
1454
+ anchors[ni] = tidy({ ...anchors[ni], in: { x: curve.p2[0], y: curve.p2[1] } });
1455
+ }
1456
+ }
1457
+ }
1458
+ anchors.splice(index, 1);
1459
+ let closed = sp.closed;
1460
+ if (!closed && anchors.length) {
1461
+ anchors[0] = tidy({ ...anchors[0], in: null });
1462
+ const last = anchors.length - 1;
1463
+ anchors[last] = tidy({ ...anchors[last], out: null });
1464
+ }
1465
+ if (anchors.length < 3) closed = closed && anchors.length === 2 && anchors.some((a) => a.in || a.out);
1466
+ return { anchors, closed };
1467
+ }
1468
+ function removeAnchorsKeepingShape(sp, indices) {
1469
+ const order = [...new Set(indices)].filter((i) => i >= 0 && i < sp.anchors.length).sort((a, b) => b - a);
1470
+ return order.reduce((acc, i) => removeAnchorKeepingShape(acc, i), { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed });
1471
+ }
1472
+ function bendSegment(sp, segment, t, p) {
1473
+ const anchors = sp.anchors.map(cloneAnchor);
1474
+ const n = anchors.length;
1475
+ if (segment < 0 || segment >= subpathSegmentCount(sp) || !Number.isFinite(t) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return { anchors, closed: sp.closed };
1476
+ const tc = Math.min(0.95, Math.max(0.05, t));
1477
+ const ia = segment;
1478
+ const ib = (segment + 1) % n;
1479
+ const a = anchors[ia];
1480
+ const b = anchors[ib];
1481
+ const k = 3 * tc * (1 - tc);
1482
+ const ox = (p.x - (a.x + (b.x - a.x) * tc)) / k;
1483
+ const oy = (p.y - (a.y + (b.y - a.y) * tc)) / k;
1484
+ const c1 = { x: a.x + (b.x - a.x) / 3 + ox, y: a.y + (b.y - a.y) / 3 + oy };
1485
+ const c2 = { x: b.x + (a.x - b.x) / 3 + ox, y: b.y + (a.y - b.y) / 3 + oy };
1486
+ anchors[ia] = moveAnchorHandle(a, "out", c1);
1487
+ anchors[ib] = moveAnchorHandle(b, "in", c2);
1488
+ return { anchors, closed: sp.closed };
1489
+ }
1490
+ function subpathPointAt(sp, segment, t) {
1491
+ const [x, y] = segmentPoint(subpathSegment(sp, segment), t);
1492
+ return { x, y };
1493
+ }
1494
+ function curvatureMoveAnchor(sp, index, q) {
1495
+ const anchors = sp.anchors.map(cloneAnchor);
1496
+ const a = sp.anchors[index];
1497
+ if (!a) return { anchors, closed: sp.closed };
1498
+ const own = [
1499
+ a.in && !onAnchor(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
1500
+ a.out && !onAnchor(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
1501
+ ];
1502
+ const ends = [null, null];
1503
+ neighbours(sp, index).forEach((j, side) => {
1504
+ if (j < 0 || j === index) return;
1505
+ const b = anchors[j];
1506
+ const old = Math.hypot(a.x - b.x, a.y - b.y);
1507
+ const s = old > 1e-9 ? Math.hypot(q.x - b.x, q.y - b.y) / old : 1;
1508
+ const key = side === 0 ? "out" : "in";
1509
+ const h = b[key];
1510
+ if (h) b[key] = { x: b.x + (h.x - b.x) * s, y: b.y + (h.y - b.y) * s };
1511
+ const o = own[side];
1512
+ if (o) own[side] = { x: o.x * s, y: o.y * s };
1513
+ ends[side] = { x: b.x, y: b.y };
1514
+ });
1515
+ const moved = { x: q.x, y: q.y };
1516
+ if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
1517
+ if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
1518
+ if (a.type) moved.type = a.type;
1519
+ if (a.corner) moved.corner = { ...a.corner };
1520
+ if (a.id) moved.id = a.id;
1521
+ const [prev, next] = ends;
1522
+ if (a.type && a.type !== "corner" && prev && next) {
1523
+ const dx = next.x - prev.x;
1524
+ const dy = next.y - prev.y;
1525
+ const l = Math.hypot(dx, dy);
1526
+ if (l > 1e-9) {
1527
+ const len = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
1528
+ let li = len(own[0], prev);
1529
+ let lo = len(own[1], next);
1530
+ if (a.type === "symmetric") li = lo = (li + lo) / 2;
1531
+ moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
1532
+ moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
1533
+ }
1534
+ }
1535
+ anchors[index] = moved;
1536
+ return { anchors, closed: sp.closed };
1537
+ }
1538
+ function cutSubpathAt(sp, at) {
1539
+ const n = sp.anchors.length;
1540
+ const cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
1541
+ if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
1542
+ const first = cuts[0];
1543
+ const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
1544
+ const cutSet = new Set(sp.closed ? [...cuts.map((c) => (c - first + n) % n), n] : cuts);
1545
+ const pieces = [];
1546
+ let cur = [];
1547
+ run.forEach((a, i) => {
1548
+ if (!cutSet.has(i)) {
1549
+ cur.push(cloneAnchor(a));
1550
+ return;
1551
+ }
1552
+ if (cur.length) {
1553
+ cur.push(tidy({ ...a, out: null }, false));
1554
+ pieces.push({ anchors: cur, closed: false });
1555
+ cur = [];
1556
+ }
1557
+ if (i < n) cur.push(tidy({ ...a, in: null }, false));
1558
+ });
1559
+ if (cur.length > 1) pieces.push({ anchors: cur, closed: false });
1560
+ return pieces;
1561
+ }
1562
+ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
1563
+ const n = sp.anchors.length;
1564
+ if (segment < 0 || segment >= subpathSegmentCount(sp)) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
1565
+ if (t <= endTolerance) return cutSubpathAt(sp, [segment]);
1566
+ if (t >= 1 - endTolerance) return cutSubpathAt(sp, [(segment + 1) % n]);
1567
+ const { subpath, index } = insertAnchor(sp, segment, t);
1568
+ return cutSubpathAt(subpath, [index]);
1569
+ }
1570
+ var endOf = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
1571
+ var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
1572
+ function reverse(sp) {
1573
+ return {
1574
+ closed: sp.closed,
1575
+ anchors: sp.anchors.slice().reverse().map((a) => {
1576
+ const next = { x: a.x, y: a.y };
1577
+ if (a.out) next.in = { ...a.out };
1578
+ if (a.in) next.out = { ...a.in };
1579
+ if (a.type) next.type = a.type;
1580
+ if (a.corner) next.corner = { ...a.corner };
1581
+ if (a.id) next.id = a.id;
1582
+ return next;
1583
+ })
1584
+ };
1585
+ }
1586
+ function joinSubpaths(subpaths, tolerance) {
1587
+ const closed = [];
1588
+ let open = [];
1589
+ for (const sp of subpaths) {
1590
+ const copy = { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
1591
+ if (sp.closed || sp.anchors.length < 2) {
1592
+ if (sp.anchors.length) closed.push(copy);
1593
+ } else open.push(copy);
1594
+ }
1595
+ if (open.length === 1) {
1596
+ const sp = open[0];
1597
+ const first = sp.anchors[0];
1598
+ const last = sp.anchors[sp.anchors.length - 1];
1599
+ if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
1600
+ sp.anchors.pop();
1601
+ sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
1602
+ } else {
1603
+ sp.anchors[0] = tidy({ ...first, in: null }, false);
1604
+ sp.anchors[sp.anchors.length - 1] = tidy({ ...sp.anchors[sp.anchors.length - 1], out: null }, false);
1605
+ }
1606
+ sp.closed = true;
1607
+ return [...closed, sp];
1608
+ }
1609
+ while (open.length > 1) {
1610
+ let best = { d: Infinity, i: 0, j: 1, ei: true, ej: false };
1611
+ for (let i = 0; i < open.length; i++) {
1612
+ for (let j = i + 1; j < open.length; j++) {
1613
+ for (const ei of [false, true]) {
1614
+ for (const ej of [false, true]) {
1615
+ const d = dist(endOf(open[i], ei), endOf(open[j], ej));
1616
+ if (d < best.d) best = { d, i, j, ei, ej };
1617
+ }
1618
+ }
1619
+ }
1620
+ }
1621
+ let a = open[best.i];
1622
+ let b = open[best.j];
1623
+ open = open.filter((_, k) => k !== best.j);
1624
+ if (!best.ei) a = reverse(a);
1625
+ if (best.ej) b = reverse(b);
1626
+ const anchors = a.anchors;
1627
+ if (best.d <= tolerance) {
1628
+ const [head, ...rest] = b.anchors;
1629
+ const l = anchors[anchors.length - 1];
1630
+ anchors[anchors.length - 1] = tidy({ ...l, out: head.out ?? null }, false);
1631
+ anchors.push(...rest);
1632
+ } else {
1633
+ const l = anchors[anchors.length - 1];
1634
+ anchors[anchors.length - 1] = tidy({ ...l, out: null }, false);
1635
+ anchors.push(tidy({ ...b.anchors[0], in: null }, false), ...b.anchors.slice(1));
1636
+ }
1637
+ open[best.i > best.j ? best.i - 1 : best.i] = { anchors, closed: false };
1638
+ }
1639
+ return [...closed, ...open];
1640
+ }
1641
+ function smoothSubpath(sp, amount = 1, only) {
1642
+ const t = Math.min(1, Math.max(0, amount));
1643
+ const orig = sp.anchors;
1644
+ const n = orig.length;
1645
+ const anchors = orig.map(cloneAnchor);
1646
+ if (n < 3 || t === 0) return { anchors, closed: sp.closed };
1647
+ for (let i = 0; i < n; i++) {
1648
+ if (!sp.closed && (i === 0 || i === n - 1)) continue;
1649
+ if (only && !only.has(i)) continue;
1650
+ const prev = orig[(i + n - 1) % n];
1651
+ const next = orig[(i + 1) % n];
1652
+ const p = orig[i];
1653
+ const dir = unit(next.x - prev.x, next.y - prev.y);
1654
+ if (!dir) continue;
1655
+ const li = dist(p, prev) / 3;
1656
+ const lo = dist(p, next) / 3;
1657
+ const hin = p.in ?? p;
1658
+ const hout = p.out ?? p;
1659
+ const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
1660
+ anchors[i] = tidy(
1661
+ {
1662
+ x: p.x,
1663
+ y: p.y,
1664
+ in: target(hin, p.x - dir[0] * li, p.y - dir[1] * li),
1665
+ out: target(hout, p.x + dir[0] * lo, p.y + dir[1] * lo)
1666
+ },
1667
+ false
1668
+ );
1669
+ }
1670
+ return { anchors, closed: sp.closed };
1671
+ }
1672
+ function segsOf(sp) {
1673
+ const out = [];
1674
+ for (let i = 0; i < subpathSegmentCount(sp); i++) {
1675
+ const s = subpathSegment(sp, i);
1676
+ out.push(s.length === 2 ? { c: s, line: true } : { c: s, line: false });
1677
+ }
1678
+ return out;
1679
+ }
1680
+ function segsToSubpath(segs, closed, types) {
1681
+ if (!segs.length) return null;
1682
+ const anchors = [];
1683
+ const at = (p) => ({ x: p[0], y: p[1] });
1684
+ anchors.push(at(segs[0].c[0]));
1685
+ for (const s of segs) {
1686
+ const end = s.c[s.c.length - 1];
1687
+ if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
1688
+ const next = at(end);
1689
+ if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
1690
+ anchors.push(next);
1691
+ }
1692
+ if (closed) {
1693
+ const last = anchors.pop();
1694
+ if (last.in) anchors[0].in = last.in;
1695
+ }
1696
+ const typed = anchors.map((a) => {
1697
+ const type = types.get(`${a.x},${a.y}`);
1698
+ return tidy(type ? { ...a, type } : a, !!type);
1699
+ });
1700
+ return { anchors: typed, closed };
1701
+ }
1702
+ var isZeroSeg = (s) => s.c.every((p) => Math.abs(p[0] - s.c[0][0]) < 1e-9 && Math.abs(p[1] - s.c[0][1]) < 1e-9);
1703
+ function mergePair(a, b, tol) {
1704
+ const a0 = a.c[0];
1705
+ const a3 = a.c[a.c.length - 1];
1706
+ const b3 = b.c[b.c.length - 1];
1707
+ if (a.line && b.line) {
1708
+ const cx = b3[0] - a0[0];
1709
+ const cy = b3[1] - a0[1];
1710
+ const len = Math.hypot(cx, cy);
1711
+ if (len < 1e-12) return null;
1712
+ const mx = a3[0] - a0[0];
1713
+ const my = a3[1] - a0[1];
1714
+ const along = (mx * cx + my * cy) / len;
1715
+ return Math.abs((mx * cy - my * cx) / len) <= tol && along >= 0 && along <= len ? { c: [a0, b3], line: true } : null;
1716
+ }
1717
+ const ca = a.line ? [a0, a0, a3, a3] : a.c;
1718
+ const cb = b.line ? [a3, a3, b3, b3] : b.c;
1719
+ const ta = endTangent(ca);
1720
+ const tb = startTangent(cb);
1721
+ if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
1722
+ const pts = [];
1723
+ sample(ca, 16, pts, true);
1724
+ sample(cb, 16, pts, false);
1725
+ const { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));
1726
+ return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
1727
+ }
1728
+ function removeRedundantPoints(sp, tolerance) {
1729
+ const tol = Math.max(tolerance, 1e-9);
1730
+ const types = new Map(sp.anchors.filter((a) => a.type).map((a) => [`${a.x},${a.y}`, a.type]));
1731
+ const segs = segsOf(sp).filter((s) => !isZeroSeg(s));
1732
+ if (!segs.length) return { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
1733
+ const minSegs = sp.closed ? 2 : 1;
1734
+ let changed = true;
1735
+ while (changed && segs.length > minSegs) {
1736
+ changed = false;
1737
+ const n = segs.length;
1738
+ const joints = sp.closed ? n : n - 1;
1739
+ for (let j = 0; j < joints; j++) {
1740
+ const [i0, i1] = sp.closed ? [(j + n - 1) % n, j] : [j, j + 1];
1741
+ const m = mergePair(segs[i0], segs[i1], tol);
1742
+ if (!m) continue;
1743
+ segs[i0] = m;
1744
+ segs.splice(i1, 1);
1745
+ if (sp.closed && i1 === 0) segs.unshift(segs.pop());
1746
+ changed = true;
1747
+ break;
1748
+ }
1749
+ }
1750
+ return segsToSubpath(segs, sp.closed, types) ?? { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
1751
+ }
1752
+
1345
1753
  export {
1346
1754
  VECTOR_TRACE_MAX_PIXELS,
1347
1755
  vectorTraceContours,
@@ -1349,5 +1757,15 @@ export {
1349
1757
  quantizeVectorTraceColors,
1350
1758
  traceVectorImage,
1351
1759
  parseOpenTypeFont,
1352
- loadOpenTypeFont
1760
+ loadOpenTypeFont,
1761
+ removeAnchorKeepingShape,
1762
+ removeAnchorsKeepingShape,
1763
+ bendSegment,
1764
+ subpathPointAt,
1765
+ curvatureMoveAnchor,
1766
+ cutSubpathAt,
1767
+ splitSubpathAt,
1768
+ joinSubpaths,
1769
+ smoothSubpath,
1770
+ removeRedundantPoints
1353
1771
  };