@mark1russell7/packing 0.1.3 → 0.1.5

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;
@@ -773,6 +713,541 @@ function turnItem(item) {
773
713
  }
774
714
  return turned;
775
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
+ * ```
1209
+ */
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
1249
+ };
1250
+ }
776
1251
  // src/strategy.ts
777
1252
  /**
778
1253
  * The error of a strategy for an input outside its cells. The benchmark records this result. It is not a failure of the
@@ -824,7 +1299,7 @@ function isConvex(points) {
824
1299
  return true;
825
1300
  }
826
1301
  /** This function gives the value of `containerShape` of one region. */
827
- function shapeOf(region) {
1302
+ function shapeOf$1(region) {
828
1303
  switch (region.kind) {
829
1304
  case "rect":
830
1305
  case "circle":
@@ -862,7 +1337,7 @@ function cellOf(input) {
862
1337
  height: "freeWidth",
863
1338
  none: container.aspectWeight === 0 ? "freeArea" : "freeAspect"
864
1339
  }[container.fixed];
865
- let containerShape = container.shape ? shapeOf(container.shape) : "rect";
1340
+ let containerShape = container.shape ? shapeOf$1(container.shape) : "rect";
866
1341
  if (containerShape === "rect" && obstacles.some((o) => o.region.kind !== "rect")) containerShape = "holes";
867
1342
  let obstacleMove = items.some((i) => i.locked === true) ? "fixed" : void 0;
868
1343
  for (const o of obstacles) {
@@ -1642,6 +2117,918 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
1642
2117
  }
1643
2118
  return breaks;
1644
2119
  }
1645
- export { DEFAULT_TOLERANCE, EPS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, cellOf, checkHard, compareRects, createContext, fromRects, logSweep, lowestWindow, marginOf, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, push, pushCascade, rows, shelf, skyline, strategies, strategy, supportsCell, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf };
2120
+ // src/generators.ts
2121
+ /** The largest count of boxes that a generator makes. */
2122
+ const MAX_COUNT = 1e5;
2123
+ /** This function gives the random numbers of one step of a seed. */
2124
+ const stepRandom = (seed, step) => mulberry32(fnv1a32(`${seed}:${step}`));
2125
+ /** A whole number from lo to hi, both included. */
2126
+ const int = (r, lo, hi) => lo + Math.floor(r() * (hi - lo + 1));
2127
+ /** A whole number from the floor of lo to the floor of hi, both included. The upper end is not less than the lower end at any time. */
2128
+ const span = (r, lo, hi) => {
2129
+ const a = Math.floor(lo);
2130
+ return int(r, a, Math.max(a, Math.floor(hi)));
2131
+ };
2132
+ /** A number from lo to hi. */
2133
+ const between = (r, lo, hi) => lo + r() * (hi - lo);
2134
+ /** This function gives an index with a probability in proportion to its weight. */
2135
+ const pick = (r, weights) => {
2136
+ let x = r() * weights.reduce((a, b) => a + b, 0);
2137
+ for (const [i, w] of weights.entries()) {
2138
+ x -= w;
2139
+ if (x < 0) return i;
2140
+ }
2141
+ return weights.length - 1;
2142
+ };
2143
+ /** This function gives a copy of a value with each number rounded by `round3`. A key with the value `undefined` goes. */
2144
+ function roundNumbers(value) {
2145
+ if (typeof value === "number") return round3(value);
2146
+ if (Array.isArray(value)) return value.map(roundNumbers);
2147
+ if (value !== null && typeof value === "object") {
2148
+ const out = {};
2149
+ for (const [k, v] of Object.entries(value)) if (v !== void 0) out[k] = roundNumbers(v);
2150
+ return out;
2151
+ }
2152
+ return value;
2153
+ }
2154
+ /** This function gives the cell of an input with rectangle items, no rotation, no order and no groups. */
2155
+ function cellWith(containerSize, containerShape, obstacles, sizes, count, locked = false) {
2156
+ const cell = {
2157
+ containerSize,
2158
+ containerShape,
2159
+ itemShape: "rect",
2160
+ rotation: "none",
2161
+ order: "free",
2162
+ groups: "none",
2163
+ obstacles,
2164
+ sizes,
2165
+ count: countCell(count),
2166
+ mode: "offline"
2167
+ };
2168
+ if (obstacles !== "none" || locked) cell.obstacleMove = "fixed";
2169
+ return cell;
2170
+ }
2171
+ /** This function makes a generator from its data. It adds the defaults, the checks, the stamp, the rounding and the hash. */
2172
+ function defineGenerator(spec) {
2173
+ const withDefaults = (params = {}) => {
2174
+ const full = { ...spec.defaults };
2175
+ for (const [key, value] of Object.entries(params)) {
2176
+ if (value === void 0) continue;
2177
+ if (!(key in spec.defaults)) throw new RangeError(`The generator "${spec.name}" has no parameter "${key}".`);
2178
+ full[key] = value;
2179
+ }
2180
+ for (const [key, allowed] of Object.entries(spec.values)) if (allowed !== void 0 && !allowed.includes(full[key])) throw new RangeError(`The parameter "${key}" of the generator "${spec.name}" has no value "${String(full[key])}".`);
2181
+ const count = full["count"];
2182
+ if (count !== void 0 && (typeof count !== "number" || !Number.isInteger(count) || count < 0 || count > MAX_COUNT)) throw new RangeError(`The parameter "count" of the generator "${spec.name}" is a whole number from 0 to ${MAX_COUNT}.`);
2183
+ return full;
2184
+ };
2185
+ return Object.freeze({
2186
+ name: spec.name,
2187
+ version: spec.version,
2188
+ set: spec.set,
2189
+ defaults: Object.freeze({ ...spec.defaults }),
2190
+ values: spec.values,
2191
+ withDefaults,
2192
+ cell: (params) => spec.cell(withDefaults(params)),
2193
+ generate(seed, params, set = spec.set) {
2194
+ if (!Number.isInteger(seed) || seed < 0) throw new RangeError(`The seed is a whole number from 0. It is ${seed}.`);
2195
+ const full = withDefaults(params);
2196
+ const family = spec.family(full);
2197
+ const { container, obstacles, boxes } = spec.build(seed, full);
2198
+ const file = roundNumbers({
2199
+ schema: "layout-input/1",
2200
+ id: `${set}/${family}/s${seed}`,
2201
+ set,
2202
+ family,
2203
+ seed,
2204
+ source: "made-up",
2205
+ generator: {
2206
+ name: spec.name,
2207
+ version: spec.version,
2208
+ params: full,
2209
+ seed
2210
+ },
2211
+ container,
2212
+ obstacles,
2213
+ boxes,
2214
+ edges: []
2215
+ });
2216
+ return {
2217
+ ...file,
2218
+ hash: hashInput(file)
2219
+ };
2220
+ }
2221
+ });
2222
+ }
2223
+ /** The shared generator `rect-scattered` of `docs/FORMAT.md`, version 1: rectangle obstacles at random places in a fixed rectangle. */
2224
+ const rectScattered = defineGenerator({
2225
+ name: "rect-scattered",
2226
+ version: 1,
2227
+ set: "xcheck-v1",
2228
+ defaults: { count: 40 },
2229
+ values: {},
2230
+ family: () => "rect-scattered",
2231
+ cell: (p) => cellWith("fixed", "rect", "scattered", "uniform", p.count),
2232
+ build(seed, { count }) {
2233
+ let r = stepRandom(seed, "container");
2234
+ const w = int(r, 600, 1e3);
2235
+ const h = int(r, 400, 800);
2236
+ const container = {
2237
+ w,
2238
+ h,
2239
+ fixed: "size"
2240
+ };
2241
+ r = stepRandom(seed, "obstacles");
2242
+ const n = int(r, 3, 8);
2243
+ const obstacles = [];
2244
+ for (let i = 0; i < n; i++) {
2245
+ const ow = int(r, 40, 160);
2246
+ const oh = int(r, 40, 160);
2247
+ const x = int(r, 0, w - ow);
2248
+ const y = int(r, 0, h - oh);
2249
+ obstacles.push({
2250
+ id: `o${i}`,
2251
+ region: {
2252
+ kind: "rect",
2253
+ x,
2254
+ y,
2255
+ w: ow,
2256
+ h: oh
2257
+ }
2258
+ });
2259
+ }
2260
+ r = stepRandom(seed, "boxes");
2261
+ const boxes = [];
2262
+ for (let i = 0; i < count; i++) {
2263
+ const bw = int(r, 20, 120);
2264
+ const bh = int(r, 20, 80);
2265
+ boxes.push({
2266
+ id: `b${i}`,
2267
+ w: bw,
2268
+ h: bh
2269
+ });
2270
+ }
2271
+ return {
2272
+ container,
2273
+ obstacles,
2274
+ boxes
2275
+ };
2276
+ }
2277
+ });
2278
+ /** The values of `containerSize` that the generators make. */
2279
+ const CONTAINER_SIZES = [
2280
+ "fixed",
2281
+ "freeHeight",
2282
+ "freeWidth",
2283
+ "freeAspect",
2284
+ "freeArea"
2285
+ ];
2286
+ /** The values of `containerShape`. */
2287
+ const CONTAINER_SHAPES = [
2288
+ "rect",
2289
+ "convex",
2290
+ "polygon",
2291
+ "circle",
2292
+ "ellipse",
2293
+ "holes"
2294
+ ];
2295
+ /** The values of `obstacles`. */
2296
+ const OBSTACLE_LAYOUTS = [
2297
+ "none",
2298
+ "center",
2299
+ "scattered",
2300
+ "wall",
2301
+ "corridors",
2302
+ "touching",
2303
+ "edge"
2304
+ ];
2305
+ /** The values of `sizes`. */
2306
+ const SIZE_DISTRIBUTIONS = [
2307
+ "identical",
2308
+ "nearIdentical",
2309
+ "uniform",
2310
+ "heavyTail",
2311
+ "bimodal",
2312
+ "extremeAspect"
2313
+ ];
2314
+ /** This function gives the container of a size value for the nominal rectangle `w` by `h`, with an optional shape. */
2315
+ function containerOf(size, w, h, shape) {
2316
+ const container = {
2317
+ fixed: {
2318
+ w,
2319
+ h,
2320
+ fixed: "size"
2321
+ },
2322
+ freeHeight: {
2323
+ w,
2324
+ fixed: "width"
2325
+ },
2326
+ freeWidth: {
2327
+ h,
2328
+ fixed: "height"
2329
+ },
2330
+ freeAspect: {
2331
+ fixed: "none",
2332
+ aspect: round3(w / h)
2333
+ },
2334
+ freeArea: {
2335
+ fixed: "none",
2336
+ aspectWeight: 0
2337
+ }
2338
+ }[size];
2339
+ if (shape !== void 0) container.shape = shape;
2340
+ return container;
2341
+ }
2342
+ /** This function gives the 4 corners of a rectangle, clockwise from the top left. */
2343
+ const corners = (x, y, w, h) => [
2344
+ [x, y],
2345
+ [x + w, y],
2346
+ [x + w, y + h],
2347
+ [x, y + h]
2348
+ ];
2349
+ /**
2350
+ * This function gives the shape of a container in the rectangle `w` by `h`, and its frame. The frame is a rectangle in
2351
+ * the shape. The obstacles and the sizes of the boxes come from the frame.
2352
+ */
2353
+ function shapeOf(r, shape, w, h) {
2354
+ const m = Math.min(w, h);
2355
+ switch (shape) {
2356
+ case "rect": return { frame: {
2357
+ x: 0,
2358
+ y: 0,
2359
+ w,
2360
+ h
2361
+ } };
2362
+ case "convex": {
2363
+ const [a, b, c, d] = [
2364
+ 0,
2365
+ 1,
2366
+ 2,
2367
+ 3
2368
+ ].map(() => span(r, m * .1, m * .25));
2369
+ const points = [
2370
+ [a, 0],
2371
+ [w - b, 0],
2372
+ [w, b],
2373
+ [w, h - c],
2374
+ [w - c, h],
2375
+ [d, h],
2376
+ [0, h - d],
2377
+ [0, a]
2378
+ ];
2379
+ const top = Math.max(a, b);
2380
+ const bottom = Math.max(c, d);
2381
+ return {
2382
+ region: {
2383
+ kind: "polygon",
2384
+ points
2385
+ },
2386
+ frame: {
2387
+ x: 0,
2388
+ y: top,
2389
+ w,
2390
+ h: h - top - bottom
2391
+ }
2392
+ };
2393
+ }
2394
+ case "polygon": {
2395
+ const nw = span(r, w * .25, w * .45);
2396
+ const nh = span(r, h * .25, h * .45);
2397
+ return {
2398
+ region: {
2399
+ kind: "polygon",
2400
+ points: [
2401
+ [0, 0],
2402
+ [w, 0],
2403
+ [w, h - nh],
2404
+ [w - nw, h - nh],
2405
+ [w - nw, h],
2406
+ [0, h]
2407
+ ]
2408
+ },
2409
+ frame: {
2410
+ x: 0,
2411
+ y: 0,
2412
+ w: w - nw,
2413
+ h
2414
+ }
2415
+ };
2416
+ }
2417
+ case "circle": {
2418
+ const cx = Math.floor(w / 2);
2419
+ const cy = Math.floor(h / 2);
2420
+ const radius = Math.min(cx, cy);
2421
+ const side = 2 * Math.floor((radius * Math.SQRT2 - 1) / 2);
2422
+ return {
2423
+ region: {
2424
+ kind: "circle",
2425
+ cx,
2426
+ cy,
2427
+ r: radius
2428
+ },
2429
+ frame: {
2430
+ x: cx - side / 2,
2431
+ y: cy - side / 2,
2432
+ w: side,
2433
+ h: side
2434
+ }
2435
+ };
2436
+ }
2437
+ case "ellipse": {
2438
+ const rx = Math.floor(w / 2);
2439
+ const ry = Math.floor(h / 2);
2440
+ const a = Math.floor(rx / Math.SQRT2);
2441
+ const b = Math.floor(ry / Math.SQRT2);
2442
+ return {
2443
+ region: {
2444
+ kind: "ellipse",
2445
+ cx: rx,
2446
+ cy: ry,
2447
+ rx,
2448
+ ry
2449
+ },
2450
+ frame: {
2451
+ x: rx - a,
2452
+ y: ry - b,
2453
+ w: 2 * a,
2454
+ h: 2 * b
2455
+ }
2456
+ };
2457
+ }
2458
+ case "holes": {
2459
+ const count = int(r, 1, 2);
2460
+ const holes = [];
2461
+ for (let i = 0; i < count; i++) {
2462
+ const hw = span(r, w * .08, w * .15);
2463
+ const hh = span(r, h * .1, h * .25);
2464
+ const x = i === 0 ? span(r, 20, w / 2 - hw - 10) : span(r, w / 2 + 10, w - hw - 20);
2465
+ const y = span(r, 20, h - hh - 20);
2466
+ holes.push(corners(x, y, hw, hh));
2467
+ }
2468
+ return {
2469
+ region: {
2470
+ kind: "polygon",
2471
+ points: corners(0, 0, w, h),
2472
+ holes
2473
+ },
2474
+ frame: {
2475
+ x: 0,
2476
+ y: 0,
2477
+ w,
2478
+ h
2479
+ }
2480
+ };
2481
+ }
2482
+ }
2483
+ }
2484
+ /** This function gives the obstacles of a layout in a frame. Each obstacle is a rectangle with whole-number sides. */
2485
+ function obstaclesOf(r, layout, f) {
2486
+ const m = Math.min(f.w, f.h);
2487
+ const rects = [];
2488
+ switch (layout) {
2489
+ case "none": break;
2490
+ case "center": {
2491
+ const w = span(r, f.w * .15, f.w * .3);
2492
+ const h = span(r, f.h * .15, f.h * .3);
2493
+ const jx = Math.floor(f.w * .05);
2494
+ const jy = Math.floor(f.h * .05);
2495
+ rects.push({
2496
+ x: f.x + Math.floor((f.w - w) / 2) + int(r, -jx, jx),
2497
+ y: f.y + Math.floor((f.h - h) / 2) + int(r, -jy, jy),
2498
+ w,
2499
+ h
2500
+ });
2501
+ break;
2502
+ }
2503
+ case "scattered": {
2504
+ const n = int(r, 3, 8);
2505
+ for (let i = 0; i < n; i++) {
2506
+ const w = span(r, m * .05, m * .2);
2507
+ const h = span(r, m * .05, m * .2);
2508
+ rects.push({
2509
+ x: int(r, f.x, f.x + f.w - w),
2510
+ y: int(r, f.y, f.y + f.h - h),
2511
+ w,
2512
+ h
2513
+ });
2514
+ }
2515
+ break;
2516
+ }
2517
+ case "wall": {
2518
+ const vertical = r() < .5;
2519
+ const across = vertical ? f.w : f.h;
2520
+ const along = vertical ? f.h : f.w;
2521
+ const thick = span(r, Math.max(1, across * .03), Math.max(1, across * .06));
2522
+ const length = span(r, along * .7, along * .85);
2523
+ const at = span(r, across * .3, across * .7 - thick);
2524
+ const from = r() < .5 ? 0 : along - length;
2525
+ rects.push(vertical ? {
2526
+ x: f.x + at,
2527
+ y: f.y + from,
2528
+ w: thick,
2529
+ h: length
2530
+ } : {
2531
+ x: f.x + from,
2532
+ y: f.y + at,
2533
+ w: length,
2534
+ h: thick
2535
+ });
2536
+ break;
2537
+ }
2538
+ case "corridors": {
2539
+ const cols = int(r, 2, 4);
2540
+ const rows = int(r, 2, 3);
2541
+ const path = span(r, m * .08, m * .15);
2542
+ const bw = Math.floor((f.w - (cols + 1) * path) / cols);
2543
+ const bh = Math.floor((f.h - (rows + 1) * path) / rows);
2544
+ for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) rects.push({
2545
+ x: f.x + path + i * (bw + path),
2546
+ y: f.y + path + j * (bh + path),
2547
+ w: bw,
2548
+ h: bh
2549
+ });
2550
+ break;
2551
+ }
2552
+ case "touching": {
2553
+ const n = int(r, 2, 4);
2554
+ let w = span(r, m * .08, m * .15);
2555
+ let h = span(r, m * .08, m * .15);
2556
+ let x = span(r, f.x + f.w * .15, f.x + f.w * .35);
2557
+ let y = span(r, f.y + f.h * .15, f.y + f.h * .35);
2558
+ rects.push({
2559
+ x,
2560
+ y,
2561
+ w,
2562
+ h
2563
+ });
2564
+ for (let i = 1; i < n; i++) {
2565
+ const right = r() < .5;
2566
+ const nw = span(r, m * .08, m * .15);
2567
+ const nh = span(r, m * .08, m * .15);
2568
+ if (right) {
2569
+ y = int(r, y, y + Math.floor(h / 2));
2570
+ x += w;
2571
+ } else {
2572
+ x = int(r, x, x + Math.floor(w / 2));
2573
+ y += h;
2574
+ }
2575
+ w = nw;
2576
+ h = nh;
2577
+ rects.push({
2578
+ x,
2579
+ y,
2580
+ w,
2581
+ h
2582
+ });
2583
+ }
2584
+ break;
2585
+ }
2586
+ case "edge": {
2587
+ const n = int(r, 2, 5);
2588
+ for (let i = 0; i < n; i++) {
2589
+ const side = int(r, 0, 3);
2590
+ const w = span(r, m * .06, m * .15);
2591
+ const h = span(r, m * .06, m * .15);
2592
+ const x = side === 1 ? f.x + f.w - w : side === 3 ? f.x : int(r, f.x, f.x + f.w - w);
2593
+ const y = side === 0 ? f.y : side === 2 ? f.y + f.h - h : int(r, f.y, f.y + f.h - h);
2594
+ rects.push({
2595
+ x,
2596
+ y,
2597
+ w,
2598
+ h
2599
+ });
2600
+ }
2601
+ break;
2602
+ }
2603
+ }
2604
+ return rects.map((rect, i) => ({
2605
+ id: `o${i}`,
2606
+ region: {
2607
+ kind: "rect",
2608
+ ...rect
2609
+ }
2610
+ }));
2611
+ }
2612
+ /**
2613
+ * This function gives the boxes of a size distribution for a frame. The mean box has an area of 40 percent of the
2614
+ * frame divided by the count. No box is wider or higher than the frame.
2615
+ */
2616
+ function boxesOf(r, sizes, count, f) {
2617
+ const unit = Math.sqrt(f.w * f.h * .4 / Math.max(count, 1));
2618
+ const fit = (x, max) => Math.min(Math.max(round3(x), .01), max);
2619
+ const boxes = new Array(count);
2620
+ const put = (i, w, h) => {
2621
+ boxes[i] = {
2622
+ id: `b${i}`,
2623
+ w: fit(w, f.w),
2624
+ h: fit(h, f.h)
2625
+ };
2626
+ };
2627
+ switch (sizes) {
2628
+ case "identical":
2629
+ case "nearIdentical": {
2630
+ const aspect = Math.sqrt(between(r, .6, 1.6));
2631
+ const bw = unit * aspect;
2632
+ const bh = unit / aspect;
2633
+ for (let i = 0; i < count; i++) if (sizes === "identical") put(i, bw, bh);
2634
+ else put(i, bw * (1 + between(r, -.03, .03)), bh * (1 + between(r, -.03, .03)));
2635
+ break;
2636
+ }
2637
+ case "uniform":
2638
+ for (let i = 0; i < count; i++) put(i, unit * between(r, .5, 1.5), unit * between(r, .5, 1.5));
2639
+ break;
2640
+ case "heavyTail": {
2641
+ const cap = .6 * Math.min(f.w, f.h);
2642
+ for (let i = 0; i < count; i++) {
2643
+ const s = Math.min(.6 * unit * (1 - r()) ** (-1 / 1.6), cap);
2644
+ put(i, s * between(r, .75, 1.33), s * between(r, .75, 1.33));
2645
+ }
2646
+ break;
2647
+ }
2648
+ case "bimodal":
2649
+ for (let i = 0; i < count; i++) {
2650
+ const s = r() < .25 ? 1.8 * unit : .6 * unit;
2651
+ put(i, s * between(r, .85, 1.15), s * between(r, .85, 1.15));
2652
+ }
2653
+ break;
2654
+ case "extremeAspect": for (let i = 0; i < count; i++) {
2655
+ const long = unit * between(r, 2.5, 4);
2656
+ const short = unit * between(r, .15, .35);
2657
+ if (r() < .5) put(i, long, short);
2658
+ else put(i, short, long);
2659
+ }
2660
+ }
2661
+ return boxes;
2662
+ }
2663
+ /**
2664
+ * The generator `rect-grid`, version 1: rectangle boxes for each value of `containerSize`, `containerShape`, `obstacles`
2665
+ * and `sizes`, with each count. The container is a nominal rectangle of 600 to 1000 by 400 to 800.
2666
+ */
2667
+ const rectGrid = defineGenerator({
2668
+ name: "rect-grid",
2669
+ version: 1,
2670
+ set: "corpus-v1",
2671
+ defaults: {
2672
+ containerSize: "fixed",
2673
+ containerShape: "rect",
2674
+ obstacles: "none",
2675
+ sizes: "uniform",
2676
+ count: 100
2677
+ },
2678
+ values: {
2679
+ containerSize: CONTAINER_SIZES,
2680
+ containerShape: CONTAINER_SHAPES,
2681
+ obstacles: OBSTACLE_LAYOUTS,
2682
+ sizes: SIZE_DISTRIBUTIONS
2683
+ },
2684
+ family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.n${p.count}`,
2685
+ cell: (p) => cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count),
2686
+ build(seed, p) {
2687
+ let r = stepRandom(seed, "container");
2688
+ const w = int(r, 600, 1e3);
2689
+ const h = int(r, 400, 800);
2690
+ const { region, frame } = shapeOf(r, p.containerShape, w, h);
2691
+ const container = containerOf(p.containerSize, w, h, region);
2692
+ r = stepRandom(seed, "obstacles");
2693
+ const obstacles = obstaclesOf(r, p.obstacles, frame);
2694
+ r = stepRandom(seed, "boxes");
2695
+ return {
2696
+ container,
2697
+ obstacles,
2698
+ boxes: boxesOf(r, p.sizes, p.count, frame)
2699
+ };
2700
+ }
2701
+ });
2702
+ /** This function makes the generator of a family of a user interface. The container is 800 to 1400 by 600 to 1000, with no obstacle. */
2703
+ function uiGenerator(name, sizes, count, boxes) {
2704
+ return defineGenerator({
2705
+ name,
2706
+ version: 1,
2707
+ set: "corpus-v1",
2708
+ defaults: {
2709
+ containerSize: "freeHeight",
2710
+ count
2711
+ },
2712
+ values: { containerSize: CONTAINER_SIZES },
2713
+ family: (p) => `${name}.${p.containerSize}.n${p.count}`,
2714
+ cell: (p) => cellWith(p.containerSize, "rect", "none", sizes, p.count),
2715
+ build(seed, p) {
2716
+ const r = stepRandom(seed, "container");
2717
+ const w = int(r, 800, 1400);
2718
+ const h = int(r, 600, 1e3);
2719
+ return {
2720
+ container: containerOf(p.containerSize, w, h),
2721
+ obstacles: [],
2722
+ boxes: boxes(stepRandom(seed, "boxes"), p.count)
2723
+ };
2724
+ }
2725
+ });
2726
+ }
2727
+ /** The aspects of the thumbnails of a gallery. */
2728
+ const THUMBNAIL_ASPECTS = [
2729
+ 1,
2730
+ 4 / 3,
2731
+ 3 / 2,
2732
+ 16 / 9
2733
+ ];
2734
+ /** The generator `gallery`, version 1: thumbnails with one size and one aspect (1, 4:3, 3:2 or 16:9). The default container has a fixed width. */
2735
+ const gallery = uiGenerator("gallery", "identical", 100, (r, count) => {
2736
+ const aspect = THUMBNAIL_ASPECTS[int(r, 0, THUMBNAIL_ASPECTS.length - 1)];
2737
+ const w = int(r, 120, 240);
2738
+ return Array.from({ length: count }, (_, i) => ({
2739
+ id: `b${i}`,
2740
+ w,
2741
+ h: round3(w / aspect)
2742
+ }));
2743
+ });
2744
+ /** The spans of the dashboard tiles in grid units, and their weights. */
2745
+ const TILE_SPANS = [
2746
+ [1, 1],
2747
+ [2, 1],
2748
+ [1, 2],
2749
+ [2, 2],
2750
+ [3, 2],
2751
+ [4, 2]
2752
+ ];
2753
+ const TILE_WEIGHTS = [
2754
+ 4,
2755
+ 3,
2756
+ 2,
2757
+ 3,
2758
+ 1,
2759
+ 1
2760
+ ];
2761
+ /** The generator `dashboard`, version 1: tiles in 6 standard sizes, from 1 by 1 to 4 by 2 grid units of 60 to 100. */
2762
+ const dashboard = uiGenerator("dashboard", "bimodal", 10, (r, count) => {
2763
+ const unit = int(r, 60, 100);
2764
+ return Array.from({ length: count }, (_, i) => {
2765
+ const [cw, ch] = TILE_SPANS[pick(r, TILE_WEIGHTS)];
2766
+ return {
2767
+ id: `b${i}`,
2768
+ w: cw * unit,
2769
+ h: ch * unit
2770
+ };
2771
+ });
2772
+ });
2773
+ /** The generator `cards`, version 1: cards with one width from 200 to 320, and a height from 120 to 420 each. */
2774
+ const cards = uiGenerator("cards", "uniform", 100, (r, count) => {
2775
+ const w = int(r, 200, 320);
2776
+ return Array.from({ length: count }, (_, i) => ({
2777
+ id: `b${i}`,
2778
+ w,
2779
+ h: int(r, 120, 420)
2780
+ }));
2781
+ });
2782
+ /** The font sizes of a word cloud, and their weights: many small words and few large words. */
2783
+ const FONT_SIZES = [
2784
+ 12,
2785
+ 14,
2786
+ 16,
2787
+ 20,
2788
+ 28,
2789
+ 40
2790
+ ];
2791
+ const FONT_WEIGHTS = [
2792
+ 30,
2793
+ 25,
2794
+ 20,
2795
+ 12,
2796
+ 8,
2797
+ 5
2798
+ ];
2799
+ /** The generator `word-cloud`, version 1: boxes of text. The height comes from 6 font sizes, the width from 3 to 12 letters. */
2800
+ const wordCloud = uiGenerator("word-cloud", "heavyTail", 100, (r, count) => Array.from({ length: count }, (_, i) => {
2801
+ const font = FONT_SIZES[pick(r, FONT_WEIGHTS)];
2802
+ const letters = int(r, 3, 12);
2803
+ return {
2804
+ id: `b${i}`,
2805
+ w: round3(letters * font * .6),
2806
+ h: round3(font * 1.25)
2807
+ };
2808
+ }));
2809
+ /** The values of `case`. */
2810
+ const EDGE_CASES = [
2811
+ "empty",
2812
+ "one",
2813
+ "oversize",
2814
+ "exactTiling",
2815
+ "touchObstacles"
2816
+ ];
2817
+ /** This function gives the largest divisor of `n` that is not more than the square root of `n`. */
2818
+ const rowsOf = (n) => {
2819
+ for (let d = Math.floor(Math.sqrt(n)); d > 1; d--) if (n % d === 0) return d;
2820
+ return 1;
2821
+ };
2822
+ /**
2823
+ * The generator `edge-cases`, version 1. Each case has a fixed rectangle container.
2824
+ *
2825
+ * - `empty`: no box.
2826
+ * - `one`: one box.
2827
+ * - `oversize`: `count` boxes. One box is wider than the container.
2828
+ * - `exactTiling`: `count` identical boxes that fill the container with no free space.
2829
+ * - `touchObstacles`: a locked box on the top of an obstacle, and boxes as wide as the path between 2 obstacles.
2830
+ */
2831
+ const edgeCases = defineGenerator({
2832
+ name: "edge-cases",
2833
+ version: 1,
2834
+ set: "corpus-v1",
2835
+ defaults: {
2836
+ case: "oversize",
2837
+ count: 12
2838
+ },
2839
+ values: { case: EDGE_CASES },
2840
+ family: (p) => p.case === "empty" || p.case === "one" ? `edge-cases.${p.case}` : `edge-cases.${p.case}.n${p.count}`,
2841
+ cell: (p) => {
2842
+ switch (p.case) {
2843
+ case "empty": return cellWith("fixed", "rect", "none", "identical", 0);
2844
+ case "one": return cellWith("fixed", "rect", "none", "identical", 1);
2845
+ case "oversize": return cellWith("fixed", "rect", "none", "heavyTail", p.count);
2846
+ case "exactTiling": return cellWith("fixed", "rect", "none", "identical", p.count);
2847
+ case "touchObstacles": return cellWith("fixed", "rect", "corridors", "uniform", p.count, true);
2848
+ }
2849
+ },
2850
+ build(seed, p) {
2851
+ if ((p.case === "oversize" || p.case === "exactTiling" || p.case === "touchObstacles") && p.count < 1) throw new RangeError(`The case "${p.case}" of "edge-cases" needs a count of 1 or more.`);
2852
+ let r = stepRandom(seed, "container");
2853
+ let w;
2854
+ let h;
2855
+ let tile = {
2856
+ w: 0,
2857
+ h: 0,
2858
+ cols: 0,
2859
+ rows: 0
2860
+ };
2861
+ if (p.case === "exactTiling") {
2862
+ const rows = rowsOf(p.count);
2863
+ tile = {
2864
+ w: int(r, 40, 120),
2865
+ h: int(r, 30, 90),
2866
+ cols: p.count / rows,
2867
+ rows
2868
+ };
2869
+ w = tile.cols * tile.w;
2870
+ h = tile.rows * tile.h;
2871
+ } else {
2872
+ w = int(r, 600, 1e3);
2873
+ h = int(r, 400, 800);
2874
+ }
2875
+ const container = {
2876
+ w,
2877
+ h,
2878
+ fixed: "size"
2879
+ };
2880
+ r = stepRandom(seed, "obstacles");
2881
+ const obstacles = [];
2882
+ let path = 0;
2883
+ let first = {
2884
+ x: 0,
2885
+ y: 0
2886
+ };
2887
+ if (p.case === "touchObstacles") {
2888
+ path = int(r, 40, 100);
2889
+ const ow = int(r, 60, 150);
2890
+ const oh = int(r, 100, 200);
2891
+ first = {
2892
+ x: span(r, w * .1, w * .25),
2893
+ y: span(r, h * .2, h * .4)
2894
+ };
2895
+ obstacles.push({
2896
+ id: "o0",
2897
+ region: {
2898
+ kind: "rect",
2899
+ x: first.x,
2900
+ y: first.y,
2901
+ w: ow,
2902
+ h: oh
2903
+ }
2904
+ });
2905
+ obstacles.push({
2906
+ id: "o1",
2907
+ region: {
2908
+ kind: "rect",
2909
+ x: first.x + ow + path,
2910
+ y: first.y,
2911
+ w: int(r, 60, 150),
2912
+ h: oh
2913
+ }
2914
+ });
2915
+ }
2916
+ r = stepRandom(seed, "boxes");
2917
+ const boxes = [];
2918
+ switch (p.case) {
2919
+ case "empty": break;
2920
+ case "one":
2921
+ boxes.push({
2922
+ id: "b0",
2923
+ w: int(r, 40, 200),
2924
+ h: int(r, 30, 150)
2925
+ });
2926
+ break;
2927
+ case "oversize": {
2928
+ const big = int(r, 0, p.count - 1);
2929
+ for (let i = 0; i < p.count; i++) if (i === big) boxes.push({
2930
+ id: `b${i}`,
2931
+ w: w + int(r, 10, 200),
2932
+ h: int(r, 40, 100)
2933
+ });
2934
+ else boxes.push({
2935
+ id: `b${i}`,
2936
+ w: int(r, 20, 120),
2937
+ h: int(r, 20, 80)
2938
+ });
2939
+ break;
2940
+ }
2941
+ case "exactTiling":
2942
+ for (let i = 0; i < p.count; i++) boxes.push({
2943
+ id: `b${i}`,
2944
+ w: tile.w,
2945
+ h: tile.h
2946
+ });
2947
+ break;
2948
+ case "touchObstacles": {
2949
+ const lw = int(r, 30, 80);
2950
+ const lh = int(r, 20, 60);
2951
+ boxes.push({
2952
+ id: "b0",
2953
+ w: lw,
2954
+ h: lh,
2955
+ x: first.x,
2956
+ y: first.y - lh,
2957
+ locked: true
2958
+ });
2959
+ for (let i = 1; i < p.count; i++) boxes.push({
2960
+ id: `b${i}`,
2961
+ w: path,
2962
+ h: int(r, 20, 80)
2963
+ });
2964
+ break;
2965
+ }
2966
+ }
2967
+ return {
2968
+ container,
2969
+ obstacles,
2970
+ boxes
2971
+ };
2972
+ }
2973
+ });
2974
+ /** Each generator of the package. */
2975
+ const generators = Object.freeze([
2976
+ rectScattered,
2977
+ rectGrid,
2978
+ gallery,
2979
+ dashboard,
2980
+ cards,
2981
+ wordCloud,
2982
+ edgeCases
2983
+ ]);
2984
+ /** This function gives the generator with a name. It throws a `RangeError` for a name that the package does not have. */
2985
+ function generator(name) {
2986
+ const found = generators.find((g) => g.name === name);
2987
+ if (found === void 0) throw new RangeError(`The package has no generator "${name}".`);
2988
+ return found;
2989
+ }
2990
+ // src/corpus.ts
2991
+ /**
2992
+ * This function generates a corpus from specs. Each input gets the name of the corpus as its set. The function throws a
2993
+ * `RangeError` for an unknown generator, a different version or 2 inputs with the same id.
2994
+ */
2995
+ function corpus(name, specs) {
2996
+ const manifest = {
2997
+ schema: "layout-corpus/1",
2998
+ corpus: name,
2999
+ specs: [],
3000
+ inputs: []
3001
+ };
3002
+ const inputs = [];
3003
+ const ids = /* @__PURE__ */ new Set();
3004
+ for (const spec of specs) {
3005
+ const gen = generator(spec.generator);
3006
+ if (spec.version !== void 0 && spec.version !== gen.version) throw new RangeError(`The generator "${gen.name}" has the version ${gen.version}, not ${spec.version}.`);
3007
+ const params = gen.withDefaults(spec.params);
3008
+ const cell = gen.cell(params);
3009
+ manifest.specs.push({
3010
+ generator: gen.name,
3011
+ version: gen.version,
3012
+ params,
3013
+ seeds: [...spec.seeds]
3014
+ });
3015
+ for (const seed of spec.seeds) {
3016
+ const file = gen.generate(seed, params, name);
3017
+ if (ids.has(file.id)) throw new RangeError(`The corpus has 2 inputs with the id "${file.id}".`);
3018
+ ids.add(file.id);
3019
+ inputs.push(file);
3020
+ manifest.inputs.push({
3021
+ id: file.id,
3022
+ hash: file.hash,
3023
+ cell
3024
+ });
3025
+ }
3026
+ }
3027
+ return {
3028
+ manifest,
3029
+ inputs
3030
+ };
3031
+ }
3032
+ 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 };
1646
3033
 
1647
3034
  //# sourceMappingURL=index.js.map