@mark1russell7/packing 0.1.4 → 0.1.6

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
@@ -598,66 +598,6 @@ function pushCascade(placed, rects, options = {}) {
598
598
  }
599
599
  return [...moved];
600
600
  }
601
- // src/metrics.ts
602
- /**
603
- * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
604
- * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
605
- *
606
- * @example
607
- * ```ts
608
- * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
609
- * ```
610
- */
611
- function packingMetrics(rects, options = {}) {
612
- const tolerance = options.rowTolerance ?? 40;
613
- const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0);
614
- if (boxes.length === 0) return {
615
- count: 0,
616
- hull: {
617
- width: 0,
618
- height: 0
619
- },
620
- hullAspect: 0,
621
- rows: [],
622
- raggedness: 0,
623
- utilization: 0,
624
- widestOverMedian: 0
625
- };
626
- const left = Math.min(...boxes.map((r) => r.position.x));
627
- const top = Math.min(...boxes.map((r) => r.position.y));
628
- const right = Math.max(...boxes.map((r) => r.right()));
629
- const bottom = Math.max(...boxes.map((r) => r.bottom()));
630
- const hull = {
631
- width: right - left,
632
- height: bottom - top
633
- };
634
- const rows = [];
635
- for (const r of boxes.toSorted((a, b) => a.position.y - b.position.y)) {
636
- const row = rows.at(-1);
637
- if (row && Math.abs(r.position.y - row.top) <= tolerance) {
638
- row.right = Math.max(row.right, r.right());
639
- row.count += 1;
640
- } else rows.push({
641
- top: r.position.y,
642
- right: r.right(),
643
- count: 1
644
- });
645
- }
646
- const rights = rows.map((row) => row.right);
647
- const widths = boxes.map((r) => r.size.x).toSorted((a, b) => a - b);
648
- const median = widths[Math.floor(widths.length / 2)] ?? 0;
649
- const widest = widths.at(-1) ?? 0;
650
- const area = boxes.reduce((sum, r) => sum + r.area(), 0);
651
- return {
652
- count: boxes.length,
653
- hull,
654
- hullAspect: hull.height > 0 ? hull.width / hull.height : 0,
655
- rows: rows.map((row) => row.count),
656
- raggedness: Math.max(...rights) - Math.min(...rights),
657
- utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
658
- widestOverMedian: median > 0 ? widest / median : 0
659
- };
660
- }
661
601
  // src/contract.ts
662
602
  /** The tolerance for the comparison of two numbers in an engine. */
663
603
  const EPS = 1e-6;
@@ -750,28 +690,563 @@ function turnRegion(region, h) {
750
690
  }
751
691
  }
752
692
  /**
753
- * This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
754
- * `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
755
- * The ports, the shape and the margin turn with the item. The function does not change its argument.
693
+ * This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
694
+ * `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
695
+ * The ports, the shape and the margin turn with the item. The function does not change its argument.
696
+ */
697
+ 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;
715
+ }
716
+ // src/sha256.ts
717
+ /** The round constants of SHA-256. */
718
+ const K = new Uint32Array([
719
+ 1116352408,
720
+ 1899447441,
721
+ 3049323471,
722
+ 3921009573,
723
+ 961987163,
724
+ 1508970993,
725
+ 2453635748,
726
+ 2870763221,
727
+ 3624381080,
728
+ 310598401,
729
+ 607225278,
730
+ 1426881987,
731
+ 1925078388,
732
+ 2162078206,
733
+ 2614888103,
734
+ 3248222580,
735
+ 3835390401,
736
+ 4022224774,
737
+ 264347078,
738
+ 604807628,
739
+ 770255983,
740
+ 1249150122,
741
+ 1555081692,
742
+ 1996064986,
743
+ 2554220882,
744
+ 2821834349,
745
+ 2952996808,
746
+ 3210313671,
747
+ 3336571891,
748
+ 3584528711,
749
+ 113926993,
750
+ 338241895,
751
+ 666307205,
752
+ 773529912,
753
+ 1294757372,
754
+ 1396182291,
755
+ 1695183700,
756
+ 1986661051,
757
+ 2177026350,
758
+ 2456956037,
759
+ 2730485921,
760
+ 2820302411,
761
+ 3259730800,
762
+ 3345764771,
763
+ 3516065817,
764
+ 3600352804,
765
+ 4094571909,
766
+ 275423344,
767
+ 430227734,
768
+ 506948616,
769
+ 659060556,
770
+ 883997877,
771
+ 958139571,
772
+ 1322822218,
773
+ 1537002063,
774
+ 1747873779,
775
+ 1955562222,
776
+ 2024104815,
777
+ 2227730452,
778
+ 2361852424,
779
+ 2428436474,
780
+ 2756734187,
781
+ 3204031479,
782
+ 3329325298
783
+ ]);
784
+ /** The first hash value of SHA-256. */
785
+ const H0 = [
786
+ 1779033703,
787
+ 3144134277,
788
+ 1013904242,
789
+ 2773480762,
790
+ 1359893119,
791
+ 2600822924,
792
+ 528734635,
793
+ 1541459225
794
+ ];
795
+ /** This function turns a 32-bit word to the right by `n` bits. */
796
+ const rotr = (x, n) => x >>> n | x << 32 - n;
797
+ /**
798
+ * This function gives the UTF-8 bytes of a string. A lone surrogate becomes U+FFFD, as with `TextEncoder`. The
799
+ * package has no `TextEncoder` type, because its code operates on each runtime with no global types.
800
+ */
801
+ function utf8(text) {
802
+ const out = [];
803
+ for (let i = 0; i < text.length; i++) {
804
+ let c = text.charCodeAt(i);
805
+ if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
806
+ const next = text.charCodeAt(i + 1);
807
+ if (next >= 56320 && next <= 57343) {
808
+ c = 65536 + (c - 55296 << 10) + (next - 56320);
809
+ i++;
810
+ }
811
+ }
812
+ if (c >= 55296 && c <= 57343) c = 65533;
813
+ if (c < 128) out.push(c);
814
+ else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
815
+ else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
816
+ else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
817
+ }
818
+ return Uint8Array.from(out);
819
+ }
820
+ /** This function gives the SHA-256 of the UTF-8 bytes of a string, as 64 lowercase hex characters. */
821
+ function sha256Hex(text) {
822
+ const bytes = utf8(text);
823
+ const length = bytes.length;
824
+ const padded = new Uint8Array(((length + 9 + 63) / 64 | 0) * 64);
825
+ padded.set(bytes);
826
+ padded[length] = 128;
827
+ const view = new DataView(padded.buffer);
828
+ const bits = length * 8;
829
+ view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
830
+ view.setUint32(padded.length - 4, bits >>> 0);
831
+ const hash = [...H0];
832
+ const w = /* @__PURE__ */ new Uint32Array(64);
833
+ for (let block = 0; block < padded.length; block += 64) {
834
+ for (let t = 0; t < 16; t++) w[t] = view.getUint32(block + t * 4);
835
+ for (let t = 16; t < 64; t++) {
836
+ const a = w[t - 15];
837
+ const b = w[t - 2];
838
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
839
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
840
+ w[t] = w[t - 16] + s0 + w[t - 7] + s1 | 0;
841
+ }
842
+ let [a, b, c, d, e, f, g, h] = hash;
843
+ for (let t = 0; t < 64; t++) {
844
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
845
+ const ch = e & f ^ ~e & g;
846
+ const t1 = h + s1 + ch + K[t] + w[t] | 0;
847
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
848
+ h = g;
849
+ g = f;
850
+ f = e;
851
+ e = d + t1 | 0;
852
+ d = c;
853
+ c = b;
854
+ b = a;
855
+ a = t1 + t2 | 0;
856
+ }
857
+ hash[0] = hash[0] + a | 0;
858
+ hash[1] = hash[1] + b | 0;
859
+ hash[2] = hash[2] + c | 0;
860
+ hash[3] = hash[3] + d | 0;
861
+ hash[4] = hash[4] + e | 0;
862
+ hash[5] = hash[5] + f | 0;
863
+ hash[6] = hash[6] + g | 0;
864
+ hash[7] = hash[7] + h | 0;
865
+ }
866
+ return hash.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
867
+ }
868
+ // src/layout-input.ts
869
+ /** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
870
+ function fnv1a32(text) {
871
+ let h = 2166136261;
872
+ for (const b of utf8(text)) h = Math.imul(h ^ b, 16777619) >>> 0;
873
+ return h;
874
+ }
875
+ /** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
876
+ function round3(x) {
877
+ const v = Math.round(x * 1e3) / 1e3;
878
+ return Object.is(v, -0) ? 0 : v;
879
+ }
880
+ /**
881
+ * This function gives the canonical form of a value: sorted keys, no whitespace and the numbers of `JSON.stringify`
882
+ * after `round3`. A key with the value `undefined` is not in the form.
883
+ */
884
+ function canonicalJson(value) {
885
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
886
+ if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
887
+ return JSON.stringify(typeof value === "number" ? round3(value) : value);
888
+ }
889
+ /** 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
+ function hashInput(file) {
891
+ const rest = { ...file };
892
+ delete rest.hash;
893
+ return sha256Hex(canonicalJson(rest)).slice(0, 16);
894
+ }
895
+ /** 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`. */
896
+ function countCell(count) {
897
+ if (count <= 3) return "1";
898
+ if (count <= 31) return "10";
899
+ if (count <= 316) return "100";
900
+ if (count <= 3162) return "1k";
901
+ return "10k";
902
+ }
903
+ /** This function gives the packing input of a file. The boxes become the items, and the edges go. */
904
+ function toPackInput(file) {
905
+ const input = {
906
+ container: file.container,
907
+ items: file.boxes
908
+ };
909
+ if (file.obstacles !== void 0) input.obstacles = file.obstacles;
910
+ if (file.gap !== void 0) input.gap = file.gap;
911
+ return input;
912
+ }
913
+ // src/measures.ts
914
+ const spec = (id, family, unit, direction, min, max) => ({
915
+ id,
916
+ family,
917
+ unit,
918
+ direction,
919
+ min,
920
+ max
921
+ });
922
+ /** The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. */
923
+ const MEASURE_SPECS = Object.freeze({
924
+ "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity),
925
+ "pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1),
926
+ "pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1),
927
+ "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),
930
+ "pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity),
931
+ "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),
936
+ "pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1),
937
+ "pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity),
938
+ "pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity)
939
+ });
940
+ /** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
941
+ function hullOf(boxes) {
942
+ if (boxes.length === 0) return {
943
+ x: 0,
944
+ y: 0,
945
+ w: 0,
946
+ h: 0
947
+ };
948
+ let left = Infinity;
949
+ let top = Infinity;
950
+ let right = -Infinity;
951
+ let bottom = -Infinity;
952
+ for (const b of boxes) {
953
+ left = Math.min(left, b.x);
954
+ top = Math.min(top, b.y);
955
+ right = Math.max(right, b.x + b.w);
956
+ bottom = Math.max(bottom, b.y + b.h);
957
+ }
958
+ return {
959
+ x: left,
960
+ y: top,
961
+ w: right - left,
962
+ h: bottom - top
963
+ };
964
+ }
965
+ /**
966
+ * This function puts the boxes in rows. It sorts the boxes by their top, and keeps the order of the list for equal tops.
967
+ * A box joins the last row when its top is within `tolerance` of the top of the first box of that row.
968
+ */
969
+ function rowsOf$1(boxes, tolerance) {
970
+ const rows = [];
971
+ for (const b of boxes.toSorted((p, q) => p.y - q.y)) {
972
+ const row = rows.at(-1);
973
+ if (row && Math.abs(b.y - row.top) <= tolerance) {
974
+ row.right = Math.max(row.right, b.x + b.w);
975
+ row.count += 1;
976
+ } else rows.push({
977
+ top: b.y,
978
+ right: b.x + b.w,
979
+ count: 1
980
+ });
981
+ }
982
+ return rows;
983
+ }
984
+ /**
985
+ * This function gives the placed boxes of an output, in the order of the items of the input. A box has the size of its
986
+ * item after the turn. The list has no item without a placement, and no item with a width or a height of 0.
987
+ */
988
+ function boxesOf$1(input, output) {
989
+ const placements = /* @__PURE__ */ new Map();
990
+ for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
991
+ const boxes = [];
992
+ for (const [index, item] of input.items.entries()) {
993
+ const p = placements.get(item.id);
994
+ if (p === void 0 || !(item.w > 0 && item.h > 0)) continue;
995
+ const turned = p.turn === 90;
996
+ boxes.push({
997
+ x: p.x,
998
+ y: p.y,
999
+ w: turned ? item.h : item.w,
1000
+ h: turned ? item.w : item.h,
1001
+ rank: item.rank ?? index
1002
+ });
1003
+ }
1004
+ return boxes;
1005
+ }
1006
+ /**
1007
+ * This function tells if box `a` comes before box `b` in the reading order. Two boxes whose heights overlap by more than
1008
+ * `RECORD_EPS` are on one line, and the left box comes first. Otherwise the upper box comes first.
1009
+ */
1010
+ 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;
1012
+ return a.y < b.y;
1013
+ }
1014
+ /** This function sums one measure over the stages of a trace. A trace without the measure gives 0. */
1015
+ function traceSum(output, id) {
1016
+ let sum = 0;
1017
+ for (const stage of output.trace) sum += stage.measures[id] ?? 0;
1018
+ return sum;
1019
+ }
1020
+ /**
1021
+ * This function gives the measures of one layout, by the ids of `docs/MEASURES.md`. The values are not rounded. A
1022
+ * record rounds them with `roundMeasures`.
1023
+ *
1024
+ * - The boxes are the placed items with a width and a height above 0. The hull is the smallest rectangle around them.
1025
+ * - An empty layout gives the area 0, the aspect 1 and the aspect error 0. Its fill is 0 and its whitespace is 1.
1026
+ * - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
1027
+ *
1028
+ * @example
1029
+ * ```ts
1030
+ * const m = measure(input, packWith("shelf", input));
1031
+ * m["pack.fill"]; // the area of the boxes divided by the area of the hull
1032
+ * ```
1033
+ */
1034
+ function measure(input, output, options = {}) {
1035
+ const boxes = boxesOf$1(input, output);
1036
+ const hull = hullOf(boxes);
1037
+ const hullArea = hull.w * hull.h;
1038
+ const boxArea = boxes.reduce((sum, b) => sum + b.w * b.h, 0);
1039
+ const fill = hullArea > 0 ? boxArea / hullArea : 0;
1040
+ const empty = boxes.length === 0;
1041
+ const target = input.container.aspect ?? 1;
1042
+ let pairs = 0;
1043
+ let inversions = 0;
1044
+ for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
1045
+ const a = boxes[i];
1046
+ const b = boxes[j];
1047
+ if (a.rank === b.rank) continue;
1048
+ pairs += 1;
1049
+ const first = a.rank < b.rank ? a : b;
1050
+ if (readsBefore(first === a ? b : a, first)) inversions += 1;
1051
+ }
1052
+ const byRank = boxes.toSorted((a, b) => a.rank - b.rank);
1053
+ let breaks = 0;
1054
+ for (let k = 1; k < byRank.length; k++) {
1055
+ const prev = byRank[k - 1];
1056
+ const next = byRank[k];
1057
+ if (prev.rank !== next.rank && readsBefore(next, prev)) breaks += 1;
1058
+ }
1059
+ const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
1060
+ const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
1061
+ return {
1062
+ "pack.area": hullArea,
1063
+ "pack.fill": fill,
1064
+ "pack.whitespace": 1 - fill,
1065
+ "pack.aspect": empty ? 1 : Math.max(hull.w / hull.h, hull.h / hull.w),
1066
+ "pack.aspectError": empty ? 0 : aspectError(hull.w, hull.h, target),
1067
+ "pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
1068
+ "pack.order.breaks": breaks,
1069
+ "pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
1070
+ "pack.overflow": new Set(output.overflow).size,
1071
+ "pack.search.widths": traceSum(output, "pack.search.widths"),
1072
+ "pack.search.ms": traceSum(output, "pack.search.ms")
1073
+ };
1074
+ }
1075
+ /**
1076
+ * This function gives the stability measures of a change pair: the run on the base input and the run on the changed
1077
+ * input. The items match by id. The values are not rounded.
1078
+ *
1079
+ * - An item stays when both inputs have it and both outputs place it. The added and the removed items do not count.
1080
+ * - A staying item moved when its `x` or its `y` changed by more than `RECORD_EPS`, or when its turn changed.
1081
+ * - The distance of an item is the length of the move of its top-left corner. The unit is the width `W`.
1082
+ * - `W` is the larger width of the 2 output sizes. A `W` of 0 becomes 1, so the unit is then the unit of the input.
1083
+ * - No staying item gives 0 for each measure.
1084
+ */
1085
+ function measureChange(baseInput, baseOutput, changeInput, changeOutput) {
1086
+ const placed = (input, output) => {
1087
+ const ids = new Set(input.items.map((i) => i.id));
1088
+ const map = /* @__PURE__ */ new Map();
1089
+ for (const p of output.placements) if (ids.has(p.id) && !map.has(p.id)) map.set(p.id, p);
1090
+ return map;
1091
+ };
1092
+ const before = placed(baseInput, baseOutput);
1093
+ const after = placed(changeInput, changeOutput);
1094
+ const width = Math.max(baseOutput.size.w, changeOutput.size.w);
1095
+ const unit = width > 0 ? width : 1;
1096
+ const distances = [];
1097
+ let moved = 0;
1098
+ for (const [id, p] of before) {
1099
+ const q = after.get(id);
1100
+ if (q === void 0) continue;
1101
+ const dx = q.x - p.x;
1102
+ const dy = q.y - p.y;
1103
+ if (Math.abs(dx) > .001 || Math.abs(dy) > .001 || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
1104
+ distances.push(Math.hypot(dx, dy) / unit);
1105
+ }
1106
+ if (distances.length === 0) return {
1107
+ "pack.stability.moved": 0,
1108
+ "pack.stability.mean": 0,
1109
+ "pack.stability.q95": 0
1110
+ };
1111
+ return {
1112
+ "pack.stability.moved": moved / distances.length,
1113
+ "pack.stability.mean": distances.reduce((sum, d) => sum + d, 0) / distances.length,
1114
+ "pack.stability.q95": quantile(distances, .95)
1115
+ };
1116
+ }
1117
+ /**
1118
+ * This function gives the measure `pack.deterministic` of two runs with the same input, options and seed. It gives
1119
+ * `true` when the runs have the same placements, overflow, size and moved obstacles.
1120
+ *
1121
+ * - The placements match by id, so their order in the list does not count. The numbers must be equal.
1122
+ * - The trace is not part of the rule, because its times change each time.
1123
+ */
1124
+ function isDeterministic(first, second) {
1125
+ const key = (list) => new Map(list.map((e) => [e.id, e]));
1126
+ const samePlacements = (a, b) => {
1127
+ if (a.length !== b.length) return false;
1128
+ const other = key(b);
1129
+ return a.every((p) => {
1130
+ const q = other.get(p.id);
1131
+ return q !== void 0 && q.x === p.x && q.y === p.y && (q.turn ?? 0) === (p.turn ?? 0);
1132
+ });
1133
+ };
1134
+ const sameMoved = (a, b) => {
1135
+ if (a.length !== b.length) return false;
1136
+ const other = key(b);
1137
+ return a.every((m) => {
1138
+ const n = other.get(m.id);
1139
+ return n !== void 0 && n.x === m.x && n.y === m.y;
1140
+ });
1141
+ };
1142
+ const overflowA = new Set(first.overflow);
1143
+ const overflowB = new Set(second.overflow);
1144
+ return first.size.w === second.size.w && first.size.h === second.size.h && overflowA.size === overflowB.size && [...overflowA].every((id) => overflowB.has(id)) && samePlacements(first.placements, second.placements) && sameMoved(first.moved, second.moved);
1145
+ }
1146
+ /**
1147
+ * This function rounds each number of a record of measures with `round3`, as `docs/FORMAT.md` tells. A half goes up,
1148
+ * and `-0` becomes 0. The function keeps the other values, and it does not change its argument.
1149
+ */
1150
+ function roundMeasures(measures) {
1151
+ const out = {};
1152
+ for (const [id, value] of Object.entries(measures)) out[id] = typeof value === "number" ? round3(value) : value;
1153
+ return out;
1154
+ }
1155
+ /**
1156
+ * This function gives the paired ratio of a measure on one input: the value of the candidate against the value of the
1157
+ * current default. A ratio above 1 is worse, in the direction of the measure.
1158
+ *
1159
+ * - For `lower`, the ratio is `candidate / current`. For `higher`, it is `current / candidate`.
1160
+ * - When both values are 0, the ratio is 1. When only the divisor is 0, the ratio is `Infinity`.
1161
+ * - The values are 0 or more. A negative value or `NaN` throws a `RangeError`.
1162
+ */
1163
+ function pairedRatio(candidate, current, direction) {
1164
+ if (!(candidate >= 0 && current >= 0)) throw new RangeError("A paired ratio needs two values of 0 or more.");
1165
+ const top = direction === "lower" ? candidate : current;
1166
+ const bottom = direction === "lower" ? current : candidate;
1167
+ if (bottom === 0) return top === 0 ? 1 : Infinity;
1168
+ return top / bottom;
1169
+ }
1170
+ /**
1171
+ * This function gives the quantile `q` of a list of numbers, with linear interpolation between the sorted values. The
1172
+ * position in the sorted list is `(n - 1) * q`. An empty list or a `q` outside 0 to 1 throws a `RangeError`.
1173
+ */
1174
+ function quantile(values, q) {
1175
+ if (values.length === 0) throw new RangeError("A quantile needs one value or more.");
1176
+ if (!(q >= 0 && q <= 1)) throw new RangeError("A quantile needs a q from 0 to 1.");
1177
+ const sorted = values.toSorted((a, b) => a - b);
1178
+ const at = (sorted.length - 1) * q;
1179
+ const low = Math.floor(at);
1180
+ const part = at - low;
1181
+ const a = sorted[low];
1182
+ if (part === 0) return a;
1183
+ const b = sorted[low + 1];
1184
+ if (a === b) return a;
1185
+ return a + part * (b - a);
1186
+ }
1187
+ /** This function gives the quantile `q` of a list, and 0 for an empty list. */
1188
+ function quantileOrZero(values, q) {
1189
+ return values.length === 0 ? 0 : quantile(values, q);
1190
+ }
1191
+ /** This function gives the median of the paired ratios of a corpus: the quantile 0.5. It is not a ratio of two medians. */
1192
+ function medianRatio(ratios) {
1193
+ return quantile(ratios, .5);
1194
+ }
1195
+ /** This function gives the share of the paired ratios above a limit. An empty list throws a `RangeError`. */
1196
+ function tailShare(ratios, limit) {
1197
+ if (ratios.length === 0) throw new RangeError("A tail share needs one ratio or more.");
1198
+ return ratios.filter((r) => r > limit).length / ratios.length;
1199
+ }
1200
+ // src/metrics.ts
1201
+ /**
1202
+ * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
1203
+ * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
1204
+ *
1205
+ * @example
1206
+ * ```ts
1207
+ * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
1208
+ * ```
756
1209
  */
757
- function turnItem(item) {
758
- const turned = {
759
- ...item,
760
- w: item.h,
761
- h: item.w
1210
+ function packingMetrics(rects, options = {}) {
1211
+ const tolerance = options.rowTolerance ?? 40;
1212
+ const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0).map((r) => ({
1213
+ x: r.position.x,
1214
+ y: r.position.y,
1215
+ w: r.size.x,
1216
+ h: r.size.y
1217
+ }));
1218
+ if (boxes.length === 0) return {
1219
+ count: 0,
1220
+ hull: {
1221
+ width: 0,
1222
+ height: 0
1223
+ },
1224
+ hullAspect: 0,
1225
+ rows: [],
1226
+ raggedness: 0,
1227
+ utilization: 0,
1228
+ widestOverMedian: 0
1229
+ };
1230
+ const box = hullOf(boxes);
1231
+ const hull = {
1232
+ width: box.w,
1233
+ height: box.h
1234
+ };
1235
+ const rows = rowsOf$1(boxes, tolerance);
1236
+ const rights = rows.map((row) => row.right);
1237
+ const widths = boxes.map((r) => r.w).toSorted((a, b) => a - b);
1238
+ const median = widths[Math.floor(widths.length / 2)] ?? 0;
1239
+ const widest = widths.at(-1) ?? 0;
1240
+ const area = boxes.reduce((sum, r) => sum + r.w * r.h, 0);
1241
+ return {
1242
+ count: boxes.length,
1243
+ hull,
1244
+ hullAspect: hull.height > 0 ? hull.width / hull.height : 0,
1245
+ rows: rows.map((row) => row.count),
1246
+ raggedness: Math.max(...rights) - Math.min(...rights),
1247
+ utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
1248
+ widestOverMedian: median > 0 ? widest / median : 0
762
1249
  };
763
- if (item.ports) turned.ports = item.ports.map((p) => turnPort(p, item.h));
764
- if (item.shape) turned.shape = turnRegion(item.shape, item.h);
765
- if (item.margin !== void 0 && typeof item.margin !== "number") {
766
- const m = item.margin;
767
- turned.margin = {
768
- top: m.left,
769
- right: m.top,
770
- bottom: m.right,
771
- left: m.bottom
772
- };
773
- }
774
- return turned;
775
1250
  }
776
1251
  // src/strategy.ts
777
1252
  /**
@@ -1575,11 +2050,13 @@ function overlap(a, b, tolerance) {
1575
2050
  * This function counts the breaks of the 4 hard rules of the packer for an output.
1576
2051
  *
1577
2052
  * - `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`.
1578
- * - `in-container`: each item with its margin that goes out of a fixed side by more than `tolerance.overlap`.
2053
+ * - `in-container`: each item with its margin that goes out of a fixed side by more than `tolerance.overlap`. The inset of
2054
+ * the container makes each fixed side smaller (decision 3 of `docs/PACKER.md`).
1579
2055
  * - `locked-kept`: each locked item that moved by more than `RECORD_EPS`, or that has no position.
1580
2056
  * - `complete`: each item with no position that is not in the overflow.
1581
2057
  *
1582
- * The rules read a shape of the container or of an obstacle as its bounding box. The result has no tolerance in it.
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.
1583
2060
  */
1584
2061
  function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
1585
2062
  const tol = tolerance.overlap;
@@ -1605,11 +2082,19 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
1605
2082
  }
1606
2083
  const placed = p.turn === 90 ? turnItem(item) : item;
1607
2084
  const m = marginOf(placed);
2085
+ const area = placed.shape === void 0 ? {
2086
+ x: 0,
2087
+ y: 0,
2088
+ w: placed.w,
2089
+ h: placed.h
2090
+ } : boundsOf(placed.shape);
2091
+ const l = p.x + area.x;
2092
+ const t = p.y + area.y;
1608
2093
  boxes.push({
1609
- l: p.x - m.left,
1610
- t: p.y - m.top,
1611
- r: p.x + placed.w + m.right,
1612
- b: p.y + placed.h + m.bottom
2094
+ l: l - m.left,
2095
+ t: t - m.top,
2096
+ r: l + area.w + m.right,
2097
+ b: t + area.h + m.bottom
1613
2098
  });
1614
2099
  }
1615
2100
  const moved = new Map(output.moved.map((m) => [m.id, m]));
@@ -1633,212 +2118,16 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
1633
2118
  const y = shape?.y ?? 0;
1634
2119
  const w = shape?.w ?? container.w ?? Infinity;
1635
2120
  const h = shape?.h ?? container.h ?? Infinity;
2121
+ const inset = container.inset ?? 0;
1636
2122
  const fixedW = container.fixed === "size" || container.fixed === "width";
1637
2123
  const fixedH = container.fixed === "size" || container.fixed === "height";
1638
2124
  for (const box of boxes) {
1639
- const outX = fixedW && (box.l < x - tol || box.r > x + w + tol);
1640
- const outY = fixedH && (box.t < y - tol || box.b > y + h + tol);
2125
+ const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
2126
+ const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
1641
2127
  if (outX || outY) breaks["in-container"] += 1;
1642
2128
  }
1643
2129
  return breaks;
1644
2130
  }
1645
- // src/sha256.ts
1646
- /** The round constants of SHA-256. */
1647
- const K = new Uint32Array([
1648
- 1116352408,
1649
- 1899447441,
1650
- 3049323471,
1651
- 3921009573,
1652
- 961987163,
1653
- 1508970993,
1654
- 2453635748,
1655
- 2870763221,
1656
- 3624381080,
1657
- 310598401,
1658
- 607225278,
1659
- 1426881987,
1660
- 1925078388,
1661
- 2162078206,
1662
- 2614888103,
1663
- 3248222580,
1664
- 3835390401,
1665
- 4022224774,
1666
- 264347078,
1667
- 604807628,
1668
- 770255983,
1669
- 1249150122,
1670
- 1555081692,
1671
- 1996064986,
1672
- 2554220882,
1673
- 2821834349,
1674
- 2952996808,
1675
- 3210313671,
1676
- 3336571891,
1677
- 3584528711,
1678
- 113926993,
1679
- 338241895,
1680
- 666307205,
1681
- 773529912,
1682
- 1294757372,
1683
- 1396182291,
1684
- 1695183700,
1685
- 1986661051,
1686
- 2177026350,
1687
- 2456956037,
1688
- 2730485921,
1689
- 2820302411,
1690
- 3259730800,
1691
- 3345764771,
1692
- 3516065817,
1693
- 3600352804,
1694
- 4094571909,
1695
- 275423344,
1696
- 430227734,
1697
- 506948616,
1698
- 659060556,
1699
- 883997877,
1700
- 958139571,
1701
- 1322822218,
1702
- 1537002063,
1703
- 1747873779,
1704
- 1955562222,
1705
- 2024104815,
1706
- 2227730452,
1707
- 2361852424,
1708
- 2428436474,
1709
- 2756734187,
1710
- 3204031479,
1711
- 3329325298
1712
- ]);
1713
- /** The first hash value of SHA-256. */
1714
- const H0 = [
1715
- 1779033703,
1716
- 3144134277,
1717
- 1013904242,
1718
- 2773480762,
1719
- 1359893119,
1720
- 2600822924,
1721
- 528734635,
1722
- 1541459225
1723
- ];
1724
- /** This function turns a 32-bit word to the right by `n` bits. */
1725
- const rotr = (x, n) => x >>> n | x << 32 - n;
1726
- /**
1727
- * This function gives the UTF-8 bytes of a string. A lone surrogate becomes U+FFFD, as with `TextEncoder`. The
1728
- * package has no `TextEncoder` type, because its code operates on each runtime with no global types.
1729
- */
1730
- function utf8(text) {
1731
- const out = [];
1732
- for (let i = 0; i < text.length; i++) {
1733
- let c = text.charCodeAt(i);
1734
- if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
1735
- const next = text.charCodeAt(i + 1);
1736
- if (next >= 56320 && next <= 57343) {
1737
- c = 65536 + (c - 55296 << 10) + (next - 56320);
1738
- i++;
1739
- }
1740
- }
1741
- if (c >= 55296 && c <= 57343) c = 65533;
1742
- if (c < 128) out.push(c);
1743
- else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
1744
- else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
1745
- else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
1746
- }
1747
- return Uint8Array.from(out);
1748
- }
1749
- /** This function gives the SHA-256 of the UTF-8 bytes of a string, as 64 lowercase hex characters. */
1750
- function sha256Hex(text) {
1751
- const bytes = utf8(text);
1752
- const length = bytes.length;
1753
- const padded = new Uint8Array(((length + 9 + 63) / 64 | 0) * 64);
1754
- padded.set(bytes);
1755
- padded[length] = 128;
1756
- const view = new DataView(padded.buffer);
1757
- const bits = length * 8;
1758
- view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
1759
- view.setUint32(padded.length - 4, bits >>> 0);
1760
- const hash = [...H0];
1761
- const w = /* @__PURE__ */ new Uint32Array(64);
1762
- for (let block = 0; block < padded.length; block += 64) {
1763
- for (let t = 0; t < 16; t++) w[t] = view.getUint32(block + t * 4);
1764
- for (let t = 16; t < 64; t++) {
1765
- const a = w[t - 15];
1766
- const b = w[t - 2];
1767
- const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
1768
- const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
1769
- w[t] = w[t - 16] + s0 + w[t - 7] + s1 | 0;
1770
- }
1771
- let [a, b, c, d, e, f, g, h] = hash;
1772
- for (let t = 0; t < 64; t++) {
1773
- const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1774
- const ch = e & f ^ ~e & g;
1775
- const t1 = h + s1 + ch + K[t] + w[t] | 0;
1776
- const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
1777
- h = g;
1778
- g = f;
1779
- f = e;
1780
- e = d + t1 | 0;
1781
- d = c;
1782
- c = b;
1783
- b = a;
1784
- a = t1 + t2 | 0;
1785
- }
1786
- hash[0] = hash[0] + a | 0;
1787
- hash[1] = hash[1] + b | 0;
1788
- hash[2] = hash[2] + c | 0;
1789
- hash[3] = hash[3] + d | 0;
1790
- hash[4] = hash[4] + e | 0;
1791
- hash[5] = hash[5] + f | 0;
1792
- hash[6] = hash[6] + g | 0;
1793
- hash[7] = hash[7] + h | 0;
1794
- }
1795
- return hash.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
1796
- }
1797
- // src/layout-input.ts
1798
- /** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
1799
- function fnv1a32(text) {
1800
- let h = 2166136261;
1801
- for (const b of utf8(text)) h = Math.imul(h ^ b, 16777619) >>> 0;
1802
- return h;
1803
- }
1804
- /** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
1805
- function round3(x) {
1806
- const v = Math.round(x * 1e3) / 1e3;
1807
- return Object.is(v, -0) ? 0 : v;
1808
- }
1809
- /**
1810
- * This function gives the canonical form of a value: sorted keys, no whitespace and the numbers of `JSON.stringify`
1811
- * after `round3`. A key with the value `undefined` is not in the form.
1812
- */
1813
- function canonicalJson(value) {
1814
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
1815
- if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
1816
- return JSON.stringify(typeof value === "number" ? round3(value) : value);
1817
- }
1818
- /** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
1819
- function hashInput(file) {
1820
- const rest = { ...file };
1821
- delete rest.hash;
1822
- return sha256Hex(canonicalJson(rest)).slice(0, 16);
1823
- }
1824
- /** 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`. */
1825
- function countCell(count) {
1826
- if (count <= 3) return "1";
1827
- if (count <= 31) return "10";
1828
- if (count <= 316) return "100";
1829
- if (count <= 3162) return "1k";
1830
- return "10k";
1831
- }
1832
- /** This function gives the packing input of a file. The boxes become the items, and the edges go. */
1833
- function toPackInput(file) {
1834
- const input = {
1835
- container: file.container,
1836
- items: file.boxes
1837
- };
1838
- if (file.obstacles !== void 0) input.obstacles = file.obstacles;
1839
- if (file.gap !== void 0) input.gap = file.gap;
1840
- return input;
1841
- }
1842
2131
  // src/generators.ts
1843
2132
  /** The largest count of boxes that a generator makes. */
1844
2133
  const MAX_COUNT = 1e5;
@@ -2751,6 +3040,6 @@ function corpus(name, specs) {
2751
3040
  inputs
2752
3041
  };
2753
3042
  }
2754
- export { DEFAULT_TOLERANCE, EPS, 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, logSweep, lowestWindow, marginOf, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, push, pushCascade, rectGrid, rectScattered, round3, rows, shelf, skyline, strategies, strategy, supportsCell, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
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 };
2755
3044
 
2756
3045
  //# sourceMappingURL=index.js.map