@mark1russell7/packing 0.1.6 → 0.2.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.
package/build/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
1
2
  // src/vector.ts
2
3
  /** A mutable two-dimensional vector. */
3
4
  var Vector = class Vector {
@@ -599,24 +600,9 @@ function pushCascade(placed, rects, options = {}) {
599
600
  return [...moved];
600
601
  }
601
602
  // src/contract.ts
602
- /** The tolerance for the comparison of two numbers in an engine. */
603
- const EPS = 1e-6;
604
- /** The tolerance for the comparison of two numbers of a file. The files round to 3 decimals. */
605
- const RECORD_EPS = .001;
606
- /** The default tolerances of the hard rules. Each record writes its tolerances in its top-level field `tolerance`. */
607
- const DEFAULT_TOLERANCE = Object.freeze({
608
- overlap: .001,
609
- through: .5
610
- });
611
603
  /** This function gives the margin of an item on each side. A number is the same margin on each side. */
612
604
  function marginOf(item) {
613
- const m = item.margin ?? 0;
614
- if (typeof m === "number") return {
615
- top: m,
616
- right: m,
617
- bottom: m,
618
- left: m
619
- };
605
+ const m = marginSides(item.margin);
620
606
  return {
621
607
  top: m.top,
622
608
  right: m.right,
@@ -624,94 +610,14 @@ function marginOf(item) {
624
610
  left: m.left
625
611
  };
626
612
  }
627
- /** The side of a port after a turn of 90 degrees clockwise. */
628
- const TURNED_SIDE = {
629
- top: "right",
630
- right: "bottom",
631
- bottom: "left",
632
- left: "top"
633
- };
634
- /** This function gives the point `px`, `py` of an item of height `h` after a turn of 90 degrees clockwise. */
635
- function turnPoint(px, py, h) {
636
- return [h - py, px];
637
- }
638
- /** This function turns a port of an item of height `h` by 90 degrees clockwise. The side turns from top to right. */
639
- function turnPort(port, h) {
640
- const turned = {
641
- id: port.id,
642
- side: TURNED_SIDE[port.side]
643
- };
644
- if (port.x !== void 0 && port.y !== void 0) {
645
- const [x, y] = turnPoint(port.x, port.y, h);
646
- turned.x = x;
647
- turned.y = y;
648
- } else if (port.x !== void 0) turned.y = port.x;
649
- else if (port.y !== void 0) turned.x = h - port.y;
650
- return turned;
651
- }
652
- /** This function turns a region relative to an item of height `h` by 90 degrees clockwise. */
653
- function turnRegion(region, h) {
654
- switch (region.kind) {
655
- case "rect": return {
656
- kind: "rect",
657
- x: h - region.y - region.h,
658
- y: region.x,
659
- w: region.h,
660
- h: region.w
661
- };
662
- case "circle": {
663
- const [cx, cy] = turnPoint(region.cx, region.cy, h);
664
- return {
665
- kind: "circle",
666
- cx,
667
- cy,
668
- r: region.r
669
- };
670
- }
671
- case "ellipse": {
672
- const [cx, cy] = turnPoint(region.cx, region.cy, h);
673
- return {
674
- kind: "ellipse",
675
- cx,
676
- cy,
677
- rx: region.ry,
678
- ry: region.rx
679
- };
680
- }
681
- case "polygon": {
682
- const turnRing = (ring) => ring.map(([x, y]) => turnPoint(x, y, h));
683
- const turned = {
684
- kind: "polygon",
685
- points: turnRing(region.points)
686
- };
687
- if (region.holes) turned.holes = region.holes.map(turnRing);
688
- return turned;
689
- }
690
- }
691
- }
692
613
  /**
693
614
  * This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
694
615
  * `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
695
616
  * The ports, the shape and the margin turn with the item. The function does not change its argument.
617
+ * It is `turnBox` of the contract.
696
618
  */
697
619
  function turnItem(item) {
698
- const turned = {
699
- ...item,
700
- w: item.h,
701
- h: item.w
702
- };
703
- if (item.ports) turned.ports = item.ports.map((p) => turnPort(p, item.h));
704
- if (item.shape) turned.shape = turnRegion(item.shape, item.h);
705
- if (item.margin !== void 0 && typeof item.margin !== "number") {
706
- const m = item.margin;
707
- turned.margin = {
708
- top: m.left,
709
- right: m.top,
710
- bottom: m.right,
711
- left: m.bottom
712
- };
713
- }
714
- return turned;
620
+ return turnBox(item);
715
621
  }
716
622
  // src/sha256.ts
717
623
  /** The round constants of SHA-256. */
@@ -888,8 +794,7 @@ function canonicalJson(value) {
888
794
  }
889
795
  /** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
890
796
  function hashInput(file) {
891
- const rest = { ...file };
892
- delete rest.hash;
797
+ const { hash: _hash, ...rest } = file;
893
798
  return sha256Hex(canonicalJson(rest)).slice(0, 16);
894
799
  }
895
800
  /** This function gives the scale of a count: the nearest of 1, 10, 100, 1000 and 10000 on a log scale. The count 0 gives `1`. */
@@ -900,39 +805,63 @@ function countCell(count) {
900
805
  if (count <= 3162) return "1k";
901
806
  return "10k";
902
807
  }
903
- /** This function gives the packing input of a file. The boxes become the items, and the edges go. */
808
+ /**
809
+ * This function gives the packing input of a file. The boxes become the items, and the edges go.
810
+ * A file without a container gets `{ fixed: "none" }`, so the packer picks the size. It is `toPackInput` of the contract.
811
+ */
904
812
  function toPackInput(file) {
905
- const input = {
906
- container: file.container,
907
- items: file.boxes
813
+ return {
814
+ container: file.container ?? { fixed: "none" },
815
+ items: file.boxes,
816
+ ...file.obstacles === void 0 ? {} : { obstacles: file.obstacles },
817
+ ...file.gap === void 0 ? {} : { gap: file.gap }
908
818
  };
909
- if (file.obstacles !== void 0) input.obstacles = file.obstacles;
910
- if (file.gap !== void 0) input.gap = file.gap;
911
- return input;
912
819
  }
913
820
  // src/measures.ts
914
- const spec = (id, family, unit, direction, min, max) => ({
821
+ const spec = (id, family, unit, direction, min, max, guard) => ({
915
822
  id,
916
823
  family,
917
824
  unit,
918
825
  direction,
919
826
  min,
920
- max
827
+ max,
828
+ ...guard
921
829
  });
922
- /** The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. */
830
+ /**
831
+ * The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. This record is the source of the floor
832
+ * and the tolerance of each guard of the packer.
833
+ */
923
834
  const MEASURE_SPECS = Object.freeze({
924
835
  "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity),
925
836
  "pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1),
926
837
  "pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1),
927
838
  "pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity),
928
- "pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity),
929
- "pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1),
839
+ "pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, {
840
+ floor: 0,
841
+ tol: .01
842
+ }),
843
+ "pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, {
844
+ floor: 0,
845
+ tol: 0
846
+ }),
930
847
  "pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity),
931
848
  "pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1),
932
- "pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity),
933
- "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1),
934
- "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity),
935
- "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity),
849
+ "pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, {
850
+ floor: 0,
851
+ tol: 0
852
+ }),
853
+ "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, {
854
+ floor: 0,
855
+ tol: .02
856
+ }),
857
+ "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, {
858
+ floor: 0,
859
+ tol: .005
860
+ }),
861
+ "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, {
862
+ floor: 0,
863
+ tol: .01
864
+ }),
936
865
  "pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1),
937
866
  "pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity),
938
867
  "pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity)
@@ -1008,13 +937,13 @@ function boxesOf$1(input, output) {
1008
937
  * `RECORD_EPS` are on one line, and the left box comes first. Otherwise the upper box comes first.
1009
938
  */
1010
939
  function readsBefore(a, b) {
1011
- if (Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > .001) return a.x + RECORD_EPS < b.x;
940
+ if (Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > RECORD_EPS) return a.x + RECORD_EPS < b.x;
1012
941
  return a.y < b.y;
1013
942
  }
1014
943
  /** This function sums one measure over the stages of a trace. A trace without the measure gives 0. */
1015
944
  function traceSum(output, id) {
1016
945
  let sum = 0;
1017
- for (const stage of output.trace) sum += stage.measures[id] ?? 0;
946
+ for (const stage of output.trace ?? []) sum += stage.measures?.[id] ?? 0;
1018
947
  return sum;
1019
948
  }
1020
949
  /**
@@ -1100,7 +1029,7 @@ function measureChange(baseInput, baseOutput, changeInput, changeOutput) {
1100
1029
  if (q === void 0) continue;
1101
1030
  const dx = q.x - p.x;
1102
1031
  const dy = q.y - p.y;
1103
- if (Math.abs(dx) > .001 || Math.abs(dy) > .001 || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
1032
+ if (Math.abs(dx) > RECORD_EPS || Math.abs(dy) > RECORD_EPS || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
1104
1033
  distances.push(Math.hypot(dx, dy) / unit);
1105
1034
  }
1106
1035
  if (distances.length === 0) return {
@@ -1383,41 +1312,6 @@ const clock = globalThis.performance;
1383
1312
  function now() {
1384
1313
  return clock ? clock.now() : Date.now();
1385
1314
  }
1386
- /** This function gives the bounding box of a region. */
1387
- function boundsOf(region) {
1388
- switch (region.kind) {
1389
- case "rect": return {
1390
- x: region.x,
1391
- y: region.y,
1392
- w: region.w,
1393
- h: region.h
1394
- };
1395
- case "circle": return {
1396
- x: region.cx - region.r,
1397
- y: region.cy - region.r,
1398
- w: 2 * region.r,
1399
- h: 2 * region.r
1400
- };
1401
- case "ellipse": return {
1402
- x: region.cx - region.rx,
1403
- y: region.cy - region.ry,
1404
- w: 2 * region.rx,
1405
- h: 2 * region.ry
1406
- };
1407
- case "polygon": {
1408
- const xs = region.points.map((p) => p[0]);
1409
- const ys = region.points.map((p) => p[1]);
1410
- const x = Math.min(...xs);
1411
- const y = Math.min(...ys);
1412
- return {
1413
- x,
1414
- y,
1415
- w: Math.max(...xs) - x,
1416
- h: Math.max(...ys) - y
1417
- };
1418
- }
1419
- }
1420
- }
1421
1315
  /** This function gives the rectangle of a container. A rectangle shape gives the origin and the sizes of the fixed sides. */
1422
1316
  function containerBounds(input) {
1423
1317
  const { container } = input;
@@ -1465,7 +1359,7 @@ function buildFrame(input, movable = false) {
1465
1359
  if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
1466
1360
  rect.locked = true;
1467
1361
  rect.position.set(item.x - dx - ox, item.y - dy - oy);
1468
- } else if (rect.size.x > w + 1e-6 || rect.size.y > h + 1e-6) {
1362
+ } else if (rect.size.x > w + EPS || rect.size.y > h + EPS) {
1469
1363
  tooLarge.push(item.id);
1470
1364
  continue;
1471
1365
  }
@@ -1594,59 +1488,55 @@ function transposeRegion(region) {
1594
1488
  };
1595
1489
  case "polygon": {
1596
1490
  const swap = (ring) => ring.map(([x, y]) => [y, x]);
1597
- const turned = {
1491
+ return region.holes === void 0 ? {
1598
1492
  kind: "polygon",
1599
1493
  points: swap(region.points)
1494
+ } : {
1495
+ kind: "polygon",
1496
+ points: swap(region.points),
1497
+ holes: region.holes.map(swap)
1600
1498
  };
1601
- if (region.holes) turned.holes = region.holes.map(swap);
1602
- return turned;
1603
1499
  }
1604
1500
  }
1605
1501
  }
1606
1502
  /** This function gives an item with the axes changed. The ports go, because the adapters of 0.1 do not read them. */
1607
1503
  function transposeItem(item) {
1608
- const { ports: _ports, ...rest } = item;
1609
- const turned = {
1504
+ const { ports: _ports, x, y, shape, margin, ...rest } = item;
1505
+ return {
1610
1506
  ...rest,
1611
1507
  w: item.h,
1612
- h: item.w
1508
+ h: item.w,
1509
+ ...y === void 0 ? {} : { x: y },
1510
+ ...x === void 0 ? {} : { y: x },
1511
+ ...shape === void 0 ? {} : { shape: transposeRegion(shape) },
1512
+ ...margin === void 0 ? {} : { margin: typeof margin === "number" ? margin : {
1513
+ top: margin.left,
1514
+ right: margin.bottom,
1515
+ bottom: margin.right,
1516
+ left: margin.top
1517
+ } }
1613
1518
  };
1614
- delete turned.x;
1615
- delete turned.y;
1616
- if (item.y !== void 0) turned.x = item.y;
1617
- if (item.x !== void 0) turned.y = item.x;
1618
- if (item.shape) turned.shape = transposeRegion(item.shape);
1619
- if (item.margin !== void 0 && typeof item.margin !== "number") {
1620
- const m = item.margin;
1621
- turned.margin = {
1622
- top: m.left,
1623
- right: m.bottom,
1624
- bottom: m.right,
1625
- left: m.top
1626
- };
1627
- }
1628
- return turned;
1629
1519
  }
1630
1520
  /** This function gives an input with the axes changed. A fixed height becomes a fixed width. */
1631
1521
  function transposeInput(input) {
1632
1522
  const c = input.container;
1633
- const container = { fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed };
1634
- if (c.h !== void 0) container.w = c.h;
1635
- if (c.w !== void 0) container.h = c.w;
1636
- if (c.aspect !== void 0) container.aspect = 1 / c.aspect;
1637
- if (c.aspectWeight !== void 0) container.aspectWeight = c.aspectWeight;
1638
- if (c.shape !== void 0) container.shape = transposeRegion(c.shape);
1639
- if (c.inset !== void 0) container.inset = c.inset;
1640
- const turned = {
1641
- container,
1642
- items: input.items.map(transposeItem)
1523
+ return {
1524
+ container: {
1525
+ fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed,
1526
+ ...c.h === void 0 ? {} : { w: c.h },
1527
+ ...c.w === void 0 ? {} : { h: c.w },
1528
+ ...c.aspect === void 0 ? {} : { aspect: 1 / c.aspect },
1529
+ ...c.aspectWeight === void 0 ? {} : { aspectWeight: c.aspectWeight },
1530
+ ...c.shape === void 0 ? {} : { shape: transposeRegion(c.shape) },
1531
+ ...c.inset === void 0 ? {} : { inset: c.inset }
1532
+ },
1533
+ items: input.items.map(transposeItem),
1534
+ ...input.obstacles === void 0 ? {} : { obstacles: input.obstacles.map((o) => ({
1535
+ ...o,
1536
+ region: transposeRegion(o.region)
1537
+ })) },
1538
+ ...input.gap === void 0 ? {} : { gap: input.gap }
1643
1539
  };
1644
- if (input.obstacles) turned.obstacles = input.obstacles.map((o) => ({
1645
- ...o,
1646
- region: transposeRegion(o.region)
1647
- }));
1648
- if (input.gap !== void 0) turned.gap = input.gap;
1649
- return turned;
1650
1540
  }
1651
1541
  /** This function gives an output with the axes changed. */
1652
1542
  function transposeOutput(output) {
@@ -1666,7 +1556,7 @@ function transposeOutput(output) {
1666
1556
  x: m.y,
1667
1557
  y: m.x
1668
1558
  })),
1669
- trace: output.trace
1559
+ ...output.trace === void 0 ? {} : { trace: output.trace }
1670
1560
  };
1671
1561
  }
1672
1562
  /**
@@ -1676,13 +1566,14 @@ function transposeOutput(output) {
1676
1566
  function viaAxes(input, previous, run) {
1677
1567
  const start = now();
1678
1568
  const inner = run(transposeInput(input), previous === void 0 ? void 0 : transposeOutput(previous));
1679
- const output = transposeOutput(inner);
1680
- output.trace = [{
1681
- id: "axes",
1682
- ms: now() - start,
1683
- measures: {}
1684
- }, ...inner.trace];
1685
- return output;
1569
+ return {
1570
+ ...transposeOutput(inner),
1571
+ trace: [{
1572
+ id: "axes",
1573
+ ms: now() - start,
1574
+ measures: {}
1575
+ }, ...inner.trace ?? []]
1576
+ };
1686
1577
  }
1687
1578
  // src/adapters.ts
1688
1579
  /** The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. */
@@ -1723,12 +1614,14 @@ function packFixedWidth(id, input, options) {
1723
1614
  sortOrder: options.sortOrder
1724
1615
  });
1725
1616
  const output = readFrame(input, frame, []);
1726
- output.trace.push({
1727
- id,
1728
- ms: now() - start,
1729
- measures: { "pack.overflow": output.overflow.length }
1730
- });
1731
- return output;
1617
+ return {
1618
+ ...output,
1619
+ trace: [{
1620
+ id,
1621
+ ms: now() - start,
1622
+ measures: { "pack.overflow": output.overflow.length }
1623
+ }]
1624
+ };
1732
1625
  }
1733
1626
  /** This function makes the strategy of one function of `pack`. */
1734
1627
  function layoutStrategy(id, family, catalog, cells) {
@@ -1818,24 +1711,26 @@ const logSweep = {
1818
1711
  const ms = now() - start;
1819
1712
  const runs = result.tried.length + 1;
1820
1713
  const output = readFrame(input, frame, []);
1821
- output.trace.push({
1822
- id: "log-sweep",
1823
- ms,
1824
- measures: {
1825
- "pack.search.widths": result.tried.length,
1826
- "pack.search.ms": ms
1827
- }
1828
- }, {
1829
- id,
1830
- ms: ms / runs,
1831
- measures: { "pack.overflow": output.overflow.length }
1832
- });
1833
- return output;
1714
+ return {
1715
+ ...output,
1716
+ trace: [{
1717
+ id: "log-sweep",
1718
+ ms,
1719
+ measures: {
1720
+ "pack.search.widths": result.tried.length,
1721
+ "pack.search.ms": ms
1722
+ }
1723
+ }, {
1724
+ id,
1725
+ ms: ms / runs,
1726
+ measures: { "pack.overflow": output.overflow.length }
1727
+ }]
1728
+ };
1834
1729
  }
1835
1730
  };
1836
1731
  /** This function tells if the insides of two rectangles overlap by more than `EPS`. */
1837
1732
  function overlaps(a, b) {
1838
- return Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x) > 1e-6 && Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y) > 1e-6;
1733
+ return Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x) > EPS && Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y) > EPS;
1839
1734
  }
1840
1735
  /** This function packs an input with a fixed size or a fixed width with a push cascade. */
1841
1736
  function pushFixedWidth(input, options, previous) {
@@ -1877,9 +1772,9 @@ function pushFixedWidth(input, options, previous) {
1877
1772
  const startAt = starts.get(e);
1878
1773
  const x = e.rect.position.x + e.dx + frame.ox;
1879
1774
  const y = e.rect.position.y + e.dy + frame.oy;
1880
- const b = boundsOf(e.obstacle.region);
1881
- if (x < bounds.x - 1e-6 || y < bounds.y - 1e-6 || x + b.w > bounds.x + bounds.w + 1e-6 || y + b.h > bounds.y + bounds.h + 1e-6 || lockedItems.some((l) => overlaps(l.rect, e.rect))) e.rect.position.set(startAt.x, startAt.y);
1882
- else if (Math.abs(e.rect.position.x - startAt.x) > 1e-6 || Math.abs(e.rect.position.y - startAt.y) > 1e-6) moved.push({
1775
+ const b = boundsOf$1(e.obstacle.region);
1776
+ if (x < bounds.x - EPS || y < bounds.y - EPS || x + b.w > bounds.x + bounds.w + EPS || y + b.h > bounds.y + bounds.h + EPS || lockedItems.some((l) => overlaps(l.rect, e.rect))) e.rect.position.set(startAt.x, startAt.y);
1777
+ else if (Math.abs(e.rect.position.x - startAt.x) > EPS || Math.abs(e.rect.position.y - startAt.y) > EPS) moved.push({
1883
1778
  id: e.obstacle.id,
1884
1779
  x,
1885
1780
  y
@@ -1889,7 +1784,7 @@ function pushFixedWidth(input, options, previous) {
1889
1784
  const kept = [...obstacles, ...lockedItems].map((e) => e.rect);
1890
1785
  for (const e of positioned) {
1891
1786
  const r = e.rect;
1892
- if (r.position.x >= -1e-6 && r.position.y >= -1e-6 && r.right() <= frame.w + 1e-6 && r.bottom() <= frame.h + 1e-6 && !kept.some((k) => overlaps(k, r))) kept.push(r);
1787
+ if (r.position.x >= -EPS && r.position.y >= -EPS && r.right() <= frame.w + EPS && r.bottom() <= frame.h + EPS && !kept.some((k) => overlaps(k, r))) kept.push(r);
1893
1788
  else pending.push(e);
1894
1789
  }
1895
1790
  const skylineStart = now();
@@ -1905,19 +1800,21 @@ function pushFixedWidth(input, options, previous) {
1905
1800
  packSkyline([...pending.map((e) => e.rect), ...kept], outerOf(frame), { sortOrder: options.sortOrder });
1906
1801
  }
1907
1802
  const output = readFrame(input, frame, [], moved);
1908
- output.trace.push({
1909
- id: "push",
1910
- ms: pushMs,
1911
- measures: { "pack.push.moved": moved.length }
1912
- }, {
1913
- id: "skyline",
1914
- ms: now() - skylineStart,
1915
- measures: {
1916
- "pack.push.replaced": pending.length,
1917
- "pack.overflow": output.overflow.length
1918
- }
1919
- });
1920
- return output;
1803
+ return {
1804
+ ...output,
1805
+ trace: [{
1806
+ id: "push",
1807
+ ms: pushMs,
1808
+ measures: { "pack.push.moved": moved.length }
1809
+ }, {
1810
+ id: "skyline",
1811
+ ms: now() - skylineStart,
1812
+ measures: {
1813
+ "pack.push.replaced": pending.length,
1814
+ "pack.overflow": output.overflow.length
1815
+ }
1816
+ }]
1817
+ };
1921
1818
  }
1922
1819
  /**
1923
1820
  * The strategy `push` uses `pushCascade`. Each item starts at its `x` and `y`, or at its place in `context.previous`.
@@ -1967,31 +1864,29 @@ const push = {
1967
1864
  */
1968
1865
  function fromRects(rects, outer) {
1969
1866
  const fixed = outer.fixedSize ? "size" : outer.fixedWidth ? "width" : "none";
1970
- const container = { fixed };
1971
- if (fixed !== "none") container.w = outer.size.x;
1972
- if (fixed === "size") container.h = outer.size.y;
1973
- if (outer.position.x !== 0 || outer.position.y !== 0) container.shape = {
1974
- kind: "rect",
1975
- x: outer.position.x,
1976
- y: outer.position.y,
1977
- w: outer.size.x,
1978
- h: outer.size.y
1979
- };
1980
1867
  return {
1981
- container,
1982
- items: rects.map((r, i) => {
1983
- const item = {
1984
- id: String(i),
1985
- w: r.size.x,
1986
- h: r.size.y
1987
- };
1988
- if (r.locked) {
1989
- item.locked = true;
1990
- item.x = r.position.x;
1991
- item.y = r.position.y;
1992
- }
1993
- return item;
1994
- })
1868
+ container: {
1869
+ fixed,
1870
+ ...fixed === "none" ? {} : { w: outer.size.x },
1871
+ ...fixed === "size" ? { h: outer.size.y } : {},
1872
+ ...outer.position.x !== 0 || outer.position.y !== 0 ? { shape: {
1873
+ kind: "rect",
1874
+ x: outer.position.x,
1875
+ y: outer.position.y,
1876
+ w: outer.size.x,
1877
+ h: outer.size.y
1878
+ } } : {}
1879
+ },
1880
+ items: rects.map((r, i) => ({
1881
+ id: String(i),
1882
+ w: r.size.x,
1883
+ h: r.size.y,
1884
+ ...r.locked ? {
1885
+ locked: true,
1886
+ x: r.position.x,
1887
+ y: r.position.y
1888
+ } : {}
1889
+ }))
1995
1890
  };
1996
1891
  }
1997
1892
  /**
@@ -2046,39 +1941,55 @@ function packWith(id, input, options, seed = 0) {
2046
1941
  function overlap(a, b, tolerance) {
2047
1942
  return Math.min(a.r, b.r) - Math.max(a.l, b.l) > tolerance && Math.min(a.b, b.b) - Math.max(a.t, b.t) > tolerance;
2048
1943
  }
1944
+ /** This function gives the box made smaller by a tolerance on each side, as a rectangle. */
1945
+ function inner(box, tolerance) {
1946
+ return {
1947
+ x: box.l + tolerance,
1948
+ y: box.t + tolerance,
1949
+ w: box.r - box.l - 2 * tolerance,
1950
+ h: box.b - box.t - 2 * tolerance
1951
+ };
1952
+ }
2049
1953
  /**
2050
1954
  * This function counts the breaks of the 4 hard rules of the packer for an output.
2051
1955
  *
2052
1956
  * - `no-overlap`: each pair of items with their margins, and each pair of an item and an obstacle, that overlap by more than `tolerance.overlap`.
2053
1957
  * - `in-container`: each item with its margin that goes out of a fixed side by more than `tolerance.overlap`. The inset of
2054
1958
  * the container makes each fixed side smaller (decision 3 of `docs/PACKER.md`).
2055
- * - `locked-kept`: each locked item that moved by more than `RECORD_EPS`, or that has no position.
2056
- * - `complete`: each item with no position that is not in the overflow.
1959
+ * - `locked-kept`: each locked item that moved by more than `RECORD_EPS`, or that has no placement.
1960
+ * - `complete`: each item with no placement that is not in the overflow. A locked item breaks `locked-kept` in its place.
1961
+ *
1962
+ * A locked item with no placement stays at its input position for `no-overlap` and `in-container`. A locked item with no
1963
+ * `x` or no `y` in the input is a bad input. The rules skip it, as `packHard` of the contract does. `checkInput` finds it.
2057
1964
  *
2058
- * The rules read a shape of the container, of an obstacle or of an item as its bounding box (decision 5). The bounding box
2059
- * of an item shape is relative to the position of the item. The result has no tolerance in it.
1965
+ * The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
1966
+ * The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
1967
+ * shape is relative to the position of the item. The result has no tolerance in it.
2060
1968
  */
2061
1969
  function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2062
1970
  const tol = tolerance.overlap;
2063
1971
  const placements = /* @__PURE__ */ new Map();
2064
1972
  for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
2065
1973
  const overflow = new Set(output.overflow);
2066
- const breaks = {
2067
- "no-overlap": 0,
2068
- "in-container": 0,
2069
- "locked-kept": 0,
2070
- complete: 0
2071
- };
1974
+ let lockedKept = 0;
1975
+ let complete = 0;
2072
1976
  const boxes = [];
2073
1977
  for (const item of input.items) {
2074
- const p = placements.get(item.id);
1978
+ if (item.locked === true && (item.x === void 0 || item.y === void 0)) continue;
1979
+ const lockedAt = item.locked === true ? {
1980
+ x: item.x,
1981
+ y: item.y
1982
+ } : void 0;
1983
+ let p = placements.get(item.id);
2075
1984
  if (p === void 0) {
2076
- if (item.locked === true && item.x !== void 0 && item.y !== void 0) breaks["locked-kept"] += 1;
2077
- if (!overflow.has(item.id)) breaks.complete += 1;
2078
- continue;
2079
- }
2080
- if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
2081
- if (Math.abs(p.x - item.x) > .001 || Math.abs(p.y - item.y) > .001) breaks["locked-kept"] += 1;
1985
+ if (lockedAt === void 0) {
1986
+ if (!overflow.has(item.id)) complete += 1;
1987
+ continue;
1988
+ }
1989
+ lockedKept += 1;
1990
+ p = lockedAt;
1991
+ } else if (lockedAt !== void 0) {
1992
+ if (Math.abs(p.x - lockedAt.x) > RECORD_EPS || Math.abs(p.y - lockedAt.y) > RECORD_EPS) lockedKept += 1;
2082
1993
  }
2083
1994
  const placed = p.turn === 90 ? turnItem(item) : item;
2084
1995
  const m = marginOf(placed);
@@ -2099,18 +2010,17 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2099
2010
  }
2100
2011
  const moved = new Map(output.moved.map((m) => [m.id, m]));
2101
2012
  const obstacles = (input.obstacles ?? []).map((o) => {
2013
+ const at = moved.get(o.id);
2014
+ if (at === void 0) return o.region;
2102
2015
  const b = boundsOf(o.region);
2103
- const at = moved.get(o.id) ?? b;
2104
- return {
2105
- l: at.x,
2106
- t: at.y,
2107
- r: at.x + b.w,
2108
- b: at.y + b.h
2109
- };
2016
+ return translate(o.region, at.x - b.x, at.y - b.y);
2110
2017
  });
2018
+ let overlaps = 0;
2111
2019
  for (const [i, a] of boxes.entries()) {
2112
- for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) breaks["no-overlap"] += 1;
2113
- for (const o of obstacles) if (overlap(a, o, tol)) breaks["no-overlap"] += 1;
2020
+ for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) overlaps += 1;
2021
+ if (obstacles.length === 0) continue;
2022
+ const rect = inner(a, tol);
2023
+ for (const region of obstacles) if (regionHitsRect(region, rect)) overlaps += 1;
2114
2024
  }
2115
2025
  const { container } = input;
2116
2026
  const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
@@ -2121,12 +2031,18 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2121
2031
  const inset = container.inset ?? 0;
2122
2032
  const fixedW = container.fixed === "size" || container.fixed === "width";
2123
2033
  const fixedH = container.fixed === "size" || container.fixed === "height";
2034
+ let outside = 0;
2124
2035
  for (const box of boxes) {
2125
2036
  const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
2126
2037
  const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
2127
- if (outX || outY) breaks["in-container"] += 1;
2038
+ if (outX || outY) outside += 1;
2128
2039
  }
2129
- return breaks;
2040
+ return {
2041
+ "no-overlap": overlaps,
2042
+ "in-container": outside,
2043
+ "locked-kept": lockedKept,
2044
+ complete
2045
+ };
2130
2046
  }
2131
2047
  // src/generators.ts
2132
2048
  /** The largest count of boxes that a generator makes. */
@@ -2347,8 +2263,10 @@ function containerOf(size, w, h, shape) {
2347
2263
  aspectWeight: 0
2348
2264
  }
2349
2265
  }[size];
2350
- if (shape !== void 0) container.shape = shape;
2351
- return container;
2266
+ return shape === void 0 ? container : {
2267
+ ...container,
2268
+ shape
2269
+ };
2352
2270
  }
2353
2271
  /** This function gives the 4 corners of a rectangle, clockwise from the top left. */
2354
2272
  const corners = (x, y, w, h) => [
@@ -3040,6 +2958,6 @@ function corpus(name, specs) {
3040
2958
  inputs
3041
2959
  };
3042
2960
  }
3043
- export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, compareRects, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, fromRects, gallery, generator, generators, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, measure, measureChange, medianRatio, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
2961
+ export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, checkInput, compareRects, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, fromRects, gallery, generator, generators, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, measure, measureChange, medianRatio, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
3044
2962
 
3045
2963
  //# sourceMappingURL=index.js.map