@mark1russell7/packing 0.1.2 → 0.1.4
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/README.md +32 -0
- package/build/index.d.ts +520 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2095 -2
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters.ts +261 -0
- package/src/check.ts +84 -0
- package/src/contract.ts +203 -0
- package/src/convert.ts +51 -0
- package/src/corpus.ts +65 -0
- package/src/frame.ts +262 -0
- package/src/generators.ts +684 -0
- package/src/index.ts +75 -0
- package/src/layout-input.ts +120 -0
- package/src/pack.ts +2 -1
- package/src/registry.ts +23 -0
- package/src/sha256.ts +93 -0
- package/src/strategy.ts +214 -0
package/build/index.js
CHANGED
|
@@ -370,7 +370,7 @@ function packRow(rects, outer, order) {
|
|
|
370
370
|
if (fillGaps && row.length > 1) {
|
|
371
371
|
const rectInRow = row[row.length - 1];
|
|
372
372
|
const outerLeft = new Rect();
|
|
373
|
-
outerLeft.position.set(rectInRow.position.x, rectInRow.bottom());
|
|
373
|
+
outerLeft.position.set(rectInRow.position.x - outer.position.x, rectInRow.bottom() - outer.position.y);
|
|
374
374
|
outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);
|
|
375
375
|
outerLeft.fixedWidth = true;
|
|
376
376
|
outerLeft.fixedSize = true;
|
|
@@ -658,6 +658,2099 @@ function packingMetrics(rects, options = {}) {
|
|
|
658
658
|
widestOverMedian: median > 0 ? widest / median : 0
|
|
659
659
|
};
|
|
660
660
|
}
|
|
661
|
-
|
|
661
|
+
// src/contract.ts
|
|
662
|
+
/** The tolerance for the comparison of two numbers in an engine. */
|
|
663
|
+
const EPS = 1e-6;
|
|
664
|
+
/** The tolerance for the comparison of two numbers of a file. The files round to 3 decimals. */
|
|
665
|
+
const RECORD_EPS = .001;
|
|
666
|
+
/** The default tolerances of the hard rules. Each record writes its tolerances in its top-level field `tolerance`. */
|
|
667
|
+
const DEFAULT_TOLERANCE = Object.freeze({
|
|
668
|
+
overlap: .001,
|
|
669
|
+
through: .5
|
|
670
|
+
});
|
|
671
|
+
/** This function gives the margin of an item on each side. A number is the same margin on each side. */
|
|
672
|
+
function marginOf(item) {
|
|
673
|
+
const m = item.margin ?? 0;
|
|
674
|
+
if (typeof m === "number") return {
|
|
675
|
+
top: m,
|
|
676
|
+
right: m,
|
|
677
|
+
bottom: m,
|
|
678
|
+
left: m
|
|
679
|
+
};
|
|
680
|
+
return {
|
|
681
|
+
top: m.top,
|
|
682
|
+
right: m.right,
|
|
683
|
+
bottom: m.bottom,
|
|
684
|
+
left: m.left
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/** The side of a port after a turn of 90 degrees clockwise. */
|
|
688
|
+
const TURNED_SIDE = {
|
|
689
|
+
top: "right",
|
|
690
|
+
right: "bottom",
|
|
691
|
+
bottom: "left",
|
|
692
|
+
left: "top"
|
|
693
|
+
};
|
|
694
|
+
/** This function gives the point `px`, `py` of an item of height `h` after a turn of 90 degrees clockwise. */
|
|
695
|
+
function turnPoint(px, py, h) {
|
|
696
|
+
return [h - py, px];
|
|
697
|
+
}
|
|
698
|
+
/** This function turns a port of an item of height `h` by 90 degrees clockwise. The side turns from top to right. */
|
|
699
|
+
function turnPort(port, h) {
|
|
700
|
+
const turned = {
|
|
701
|
+
id: port.id,
|
|
702
|
+
side: TURNED_SIDE[port.side]
|
|
703
|
+
};
|
|
704
|
+
if (port.x !== void 0 && port.y !== void 0) {
|
|
705
|
+
const [x, y] = turnPoint(port.x, port.y, h);
|
|
706
|
+
turned.x = x;
|
|
707
|
+
turned.y = y;
|
|
708
|
+
} else if (port.x !== void 0) turned.y = port.x;
|
|
709
|
+
else if (port.y !== void 0) turned.x = h - port.y;
|
|
710
|
+
return turned;
|
|
711
|
+
}
|
|
712
|
+
/** This function turns a region relative to an item of height `h` by 90 degrees clockwise. */
|
|
713
|
+
function turnRegion(region, h) {
|
|
714
|
+
switch (region.kind) {
|
|
715
|
+
case "rect": return {
|
|
716
|
+
kind: "rect",
|
|
717
|
+
x: h - region.y - region.h,
|
|
718
|
+
y: region.x,
|
|
719
|
+
w: region.h,
|
|
720
|
+
h: region.w
|
|
721
|
+
};
|
|
722
|
+
case "circle": {
|
|
723
|
+
const [cx, cy] = turnPoint(region.cx, region.cy, h);
|
|
724
|
+
return {
|
|
725
|
+
kind: "circle",
|
|
726
|
+
cx,
|
|
727
|
+
cy,
|
|
728
|
+
r: region.r
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
case "ellipse": {
|
|
732
|
+
const [cx, cy] = turnPoint(region.cx, region.cy, h);
|
|
733
|
+
return {
|
|
734
|
+
kind: "ellipse",
|
|
735
|
+
cx,
|
|
736
|
+
cy,
|
|
737
|
+
rx: region.ry,
|
|
738
|
+
ry: region.rx
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
case "polygon": {
|
|
742
|
+
const turnRing = (ring) => ring.map(([x, y]) => turnPoint(x, y, h));
|
|
743
|
+
const turned = {
|
|
744
|
+
kind: "polygon",
|
|
745
|
+
points: turnRing(region.points)
|
|
746
|
+
};
|
|
747
|
+
if (region.holes) turned.holes = region.holes.map(turnRing);
|
|
748
|
+
return turned;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
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.
|
|
756
|
+
*/
|
|
757
|
+
function turnItem(item) {
|
|
758
|
+
const turned = {
|
|
759
|
+
...item,
|
|
760
|
+
w: item.h,
|
|
761
|
+
h: item.w
|
|
762
|
+
};
|
|
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
|
+
}
|
|
776
|
+
// src/strategy.ts
|
|
777
|
+
/**
|
|
778
|
+
* The error of a strategy for an input outside its cells. The benchmark records this result. It is not a failure of the
|
|
779
|
+
* strategy.
|
|
780
|
+
*/
|
|
781
|
+
var UnsupportedCell = class extends Error {
|
|
782
|
+
/** The dimension of the taxonomy. */
|
|
783
|
+
dimension;
|
|
784
|
+
/** The value of the input that the strategy does not support. */
|
|
785
|
+
value;
|
|
786
|
+
constructor(dimension, value) {
|
|
787
|
+
super(`The strategy does not support the value "${value}" of the dimension "${dimension}".`);
|
|
788
|
+
this.name = "UnsupportedCell";
|
|
789
|
+
this.dimension = dimension;
|
|
790
|
+
this.value = value;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
/** This function gives a generator of random numbers in `[0, 1)`: `mulberry32` of `docs/FORMAT.md`. */
|
|
794
|
+
function mulberry32(seed) {
|
|
795
|
+
let s = seed >>> 0;
|
|
796
|
+
return () => {
|
|
797
|
+
s = s + 1831565813 | 0;
|
|
798
|
+
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
799
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
800
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/** This function makes the context of one packing from a seed. */
|
|
804
|
+
function createContext(seed = 0, more = {}) {
|
|
805
|
+
return {
|
|
806
|
+
...more,
|
|
807
|
+
seed,
|
|
808
|
+
rng: mulberry32(seed)
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
/** This function tells if a polygon ring is convex. A ring with fewer than 3 points is convex. */
|
|
812
|
+
function isConvex(points) {
|
|
813
|
+
let sign = 0;
|
|
814
|
+
const n = points.length;
|
|
815
|
+
for (let i = 0; i < n; i++) {
|
|
816
|
+
const [ax, ay] = points[i];
|
|
817
|
+
const [bx, by] = points[(i + 1) % n];
|
|
818
|
+
const [cx, cy] = points[(i + 2) % n];
|
|
819
|
+
const cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx);
|
|
820
|
+
if (cross === 0) continue;
|
|
821
|
+
if (sign === 0) sign = Math.sign(cross);
|
|
822
|
+
else if (Math.sign(cross) !== sign) return false;
|
|
823
|
+
}
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
/** This function gives the value of `containerShape` of one region. */
|
|
827
|
+
function shapeOf$1(region) {
|
|
828
|
+
switch (region.kind) {
|
|
829
|
+
case "rect":
|
|
830
|
+
case "circle":
|
|
831
|
+
case "ellipse": return region.kind;
|
|
832
|
+
case "polygon":
|
|
833
|
+
if (region.holes && region.holes.length > 0) return "holes";
|
|
834
|
+
return isConvex(region.points) ? "convex" : "polygon";
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/** This function gives the value of `itemShape` of one item shape. An ellipse is convex. */
|
|
838
|
+
function itemShapeOf(region) {
|
|
839
|
+
if (region === void 0) return "rect";
|
|
840
|
+
switch (region.kind) {
|
|
841
|
+
case "rect":
|
|
842
|
+
case "circle": return region.kind;
|
|
843
|
+
case "ellipse": return "convex";
|
|
844
|
+
case "polygon": return !region.holes?.length && isConvex(region.points) ? "convex" : "polygon";
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* This function gives the cell of an input: the values of the packer dimensions that the input tells.
|
|
849
|
+
*
|
|
850
|
+
* - `containerSize` comes from `fixed`. The value `none` gives `freeArea` when `aspectWeight` is 0, and `freeAspect` if not.
|
|
851
|
+
* - `containerShape` comes from the shape. A rectangle container with an obstacle of another region gives `holes`.
|
|
852
|
+
* - `obstacleMove` is the hardest value of the obstacles and the locked items: `costly`, then `free`, then `fixed`.
|
|
853
|
+
* - `order` is `soft` when an item has a rank, and `free` if not. The input does not tell a hard order.
|
|
854
|
+
* - `groups` is `nested` when an item has a parent, `flat` when an item has a group, and `none` if not.
|
|
855
|
+
*/
|
|
856
|
+
function cellOf(input) {
|
|
857
|
+
const { container, items } = input;
|
|
858
|
+
const obstacles = input.obstacles ?? [];
|
|
859
|
+
const containerSize = {
|
|
860
|
+
size: "fixed",
|
|
861
|
+
width: "freeHeight",
|
|
862
|
+
height: "freeWidth",
|
|
863
|
+
none: container.aspectWeight === 0 ? "freeArea" : "freeAspect"
|
|
864
|
+
}[container.fixed];
|
|
865
|
+
let containerShape = container.shape ? shapeOf$1(container.shape) : "rect";
|
|
866
|
+
if (containerShape === "rect" && obstacles.some((o) => o.region.kind !== "rect")) containerShape = "holes";
|
|
867
|
+
let obstacleMove = items.some((i) => i.locked === true) ? "fixed" : void 0;
|
|
868
|
+
for (const o of obstacles) {
|
|
869
|
+
const move = o.move === void 0 ? "fixed" : o.move.cost > 0 ? "costly" : "free";
|
|
870
|
+
if (move === "costly" || obstacleMove === void 0 || move === "free" && obstacleMove === "fixed") obstacleMove = move;
|
|
871
|
+
}
|
|
872
|
+
const shapes = new Set(items.map((i) => itemShapeOf(i.shape)));
|
|
873
|
+
const itemShape = shapes.size > 1 ? "mixed" : [...shapes][0] ?? "rect";
|
|
874
|
+
const cell = {
|
|
875
|
+
containerSize,
|
|
876
|
+
containerShape,
|
|
877
|
+
itemShape,
|
|
878
|
+
rotation: items.some((i) => i.turnable === true) ? "quarter" : "none",
|
|
879
|
+
order: items.some((i) => i.rank !== void 0) ? "soft" : "free",
|
|
880
|
+
groups: items.some((i) => i.parent !== void 0) ? "nested" : items.some((i) => i.group !== void 0) ? "flat" : "none"
|
|
881
|
+
};
|
|
882
|
+
if (obstacleMove !== void 0) cell.obstacleMove = obstacleMove;
|
|
883
|
+
return cell;
|
|
884
|
+
}
|
|
885
|
+
/** This function gives the first dimension of a cell that `supports` does not hold, or `undefined` when it holds each one. */
|
|
886
|
+
function unsupportedOf(supports, cell) {
|
|
887
|
+
for (const dimension of Object.keys(supports)) {
|
|
888
|
+
const value = cell[dimension];
|
|
889
|
+
if (value === void 0) continue;
|
|
890
|
+
if (!supports[dimension].includes(value)) return {
|
|
891
|
+
dimension,
|
|
892
|
+
value
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/** This function tells if `supports` holds each value of a cell. A dimension that the cell does not have is held. */
|
|
897
|
+
function supportsCell(supports, cell) {
|
|
898
|
+
return unsupportedOf(supports, cell) === void 0;
|
|
899
|
+
}
|
|
900
|
+
/** This function throws `UnsupportedCell` for the first value of the cell of an input that `supports` does not hold. */
|
|
901
|
+
function assertSupported(supports, input) {
|
|
902
|
+
const miss = unsupportedOf(supports, cellOf(input));
|
|
903
|
+
if (miss) throw new UnsupportedCell(miss.dimension, miss.value);
|
|
904
|
+
}
|
|
905
|
+
// src/frame.ts
|
|
906
|
+
const clock = globalThis.performance;
|
|
907
|
+
/** This function gives the time in milliseconds. */
|
|
908
|
+
function now() {
|
|
909
|
+
return clock ? clock.now() : Date.now();
|
|
910
|
+
}
|
|
911
|
+
/** This function gives the bounding box of a region. */
|
|
912
|
+
function boundsOf(region) {
|
|
913
|
+
switch (region.kind) {
|
|
914
|
+
case "rect": return {
|
|
915
|
+
x: region.x,
|
|
916
|
+
y: region.y,
|
|
917
|
+
w: region.w,
|
|
918
|
+
h: region.h
|
|
919
|
+
};
|
|
920
|
+
case "circle": return {
|
|
921
|
+
x: region.cx - region.r,
|
|
922
|
+
y: region.cy - region.r,
|
|
923
|
+
w: 2 * region.r,
|
|
924
|
+
h: 2 * region.r
|
|
925
|
+
};
|
|
926
|
+
case "ellipse": return {
|
|
927
|
+
x: region.cx - region.rx,
|
|
928
|
+
y: region.cy - region.ry,
|
|
929
|
+
w: 2 * region.rx,
|
|
930
|
+
h: 2 * region.ry
|
|
931
|
+
};
|
|
932
|
+
case "polygon": {
|
|
933
|
+
const xs = region.points.map((p) => p[0]);
|
|
934
|
+
const ys = region.points.map((p) => p[1]);
|
|
935
|
+
const x = Math.min(...xs);
|
|
936
|
+
const y = Math.min(...ys);
|
|
937
|
+
return {
|
|
938
|
+
x,
|
|
939
|
+
y,
|
|
940
|
+
w: Math.max(...xs) - x,
|
|
941
|
+
h: Math.max(...ys) - y
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/** This function gives the rectangle of a container. A rectangle shape gives the origin and the sizes of the fixed sides. */
|
|
947
|
+
function containerBounds(input) {
|
|
948
|
+
const { container } = input;
|
|
949
|
+
const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
|
|
950
|
+
const fixedW = container.fixed === "size" || container.fixed === "width";
|
|
951
|
+
const fixedH = container.fixed === "size" || container.fixed === "height";
|
|
952
|
+
const w = shape?.w ?? container.w;
|
|
953
|
+
const h = shape?.h ?? container.h;
|
|
954
|
+
if (fixedW && w === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a width.`);
|
|
955
|
+
if (fixedH && h === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a height.`);
|
|
956
|
+
return {
|
|
957
|
+
x: shape?.x ?? 0,
|
|
958
|
+
y: shape?.y ?? 0,
|
|
959
|
+
w: fixedW ? w : Infinity,
|
|
960
|
+
h: fixedH ? h : Infinity
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
/** This function gives the items in the order of their rank. The index in the array is the rank of an item without one. */
|
|
964
|
+
function byRank(items) {
|
|
965
|
+
return items.map((item, index) => ({
|
|
966
|
+
item,
|
|
967
|
+
rank: item.rank ?? index
|
|
968
|
+
})).toSorted((a, b) => a.rank - b.rank).map((e) => e.item);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
|
|
972
|
+
* rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`.
|
|
973
|
+
*/
|
|
974
|
+
function buildFrame(input, movable = false) {
|
|
975
|
+
const bounds = containerBounds(input);
|
|
976
|
+
const inset = input.container.inset ?? 0;
|
|
977
|
+
const half = (input.gap ?? 0) / 2;
|
|
978
|
+
const ox = bounds.x + inset - half;
|
|
979
|
+
const oy = bounds.y + inset - half;
|
|
980
|
+
const w = bounds.w - 2 * inset + 2 * half;
|
|
981
|
+
const h = bounds.h - 2 * inset + 2 * half;
|
|
982
|
+
const entries = [];
|
|
983
|
+
const tooLarge = [];
|
|
984
|
+
for (const item of byRank(input.items)) {
|
|
985
|
+
const m = marginOf(item);
|
|
986
|
+
const rect = new Rect();
|
|
987
|
+
rect.size.set(item.w + m.left + m.right + 2 * half, item.h + m.top + m.bottom + 2 * half);
|
|
988
|
+
const dx = m.left + half;
|
|
989
|
+
const dy = m.top + half;
|
|
990
|
+
if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
|
|
991
|
+
rect.locked = true;
|
|
992
|
+
rect.position.set(item.x - dx - ox, item.y - dy - oy);
|
|
993
|
+
} else if (rect.size.x > w + 1e-6 || rect.size.y > h + 1e-6) {
|
|
994
|
+
tooLarge.push(item.id);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
entries.push({
|
|
998
|
+
rect,
|
|
999
|
+
item,
|
|
1000
|
+
dx,
|
|
1001
|
+
dy
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
for (const obstacle of input.obstacles ?? []) {
|
|
1005
|
+
const b = boundsOf(obstacle.region);
|
|
1006
|
+
const rect = new Rect();
|
|
1007
|
+
rect.size.set(b.w + 2 * half, b.h + 2 * half);
|
|
1008
|
+
rect.position.set(b.x - half - ox, b.y - half - oy);
|
|
1009
|
+
rect.locked = !(movable && obstacle.move !== void 0);
|
|
1010
|
+
entries.push({
|
|
1011
|
+
rect,
|
|
1012
|
+
obstacle,
|
|
1013
|
+
dx: half,
|
|
1014
|
+
dy: half
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
ox,
|
|
1019
|
+
oy,
|
|
1020
|
+
w,
|
|
1021
|
+
h,
|
|
1022
|
+
bounds,
|
|
1023
|
+
inset,
|
|
1024
|
+
entries,
|
|
1025
|
+
tooLarge
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
/** This function gives the outer rectangle of 0.1 for a frame. A free height starts at 0 and grows. */
|
|
1029
|
+
function outerOf(frame) {
|
|
1030
|
+
const outer = new Rect();
|
|
1031
|
+
const fixedSize = Number.isFinite(frame.h);
|
|
1032
|
+
outer.size.set(frame.w, fixedSize ? frame.h : 0);
|
|
1033
|
+
outer.fixedWidth = true;
|
|
1034
|
+
outer.fixedSize = fixedSize;
|
|
1035
|
+
return outer;
|
|
1036
|
+
}
|
|
1037
|
+
/** This function gives the rectangles of a frame, in the order of the entries. */
|
|
1038
|
+
function rectsOf(frame) {
|
|
1039
|
+
return frame.entries.map((e) => e.rect);
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
|
|
1043
|
+
* position exactly. Each other item that the packer did not place goes to the overflow.
|
|
1044
|
+
*/
|
|
1045
|
+
function readFrame(input, frame, trace, moved = []) {
|
|
1046
|
+
const placements = [];
|
|
1047
|
+
const overflow = [...frame.tooLarge];
|
|
1048
|
+
for (const e of frame.entries) {
|
|
1049
|
+
const item = e.item;
|
|
1050
|
+
if (item === void 0) continue;
|
|
1051
|
+
if (item.locked === true && item.x !== void 0 && item.y !== void 0) placements.push({
|
|
1052
|
+
id: item.id,
|
|
1053
|
+
x: item.x,
|
|
1054
|
+
y: item.y
|
|
1055
|
+
});
|
|
1056
|
+
else if (e.rect.wasPacked) placements.push({
|
|
1057
|
+
id: item.id,
|
|
1058
|
+
x: e.rect.position.x + e.dx + frame.ox,
|
|
1059
|
+
y: e.rect.position.y + e.dy + frame.oy
|
|
1060
|
+
});
|
|
1061
|
+
else overflow.push(item.id);
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
placements,
|
|
1065
|
+
overflow,
|
|
1066
|
+
size: sizeOf(input, frame, placements, moved),
|
|
1067
|
+
moved,
|
|
1068
|
+
trace
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
|
|
1073
|
+
* far edge of the items with their margins and the inset, or of the obstacles.
|
|
1074
|
+
*/
|
|
1075
|
+
function sizeOf(input, frame, placements, moved) {
|
|
1076
|
+
const items = new Map(input.items.map((i) => [i.id, i]));
|
|
1077
|
+
let right = 0;
|
|
1078
|
+
let bottom = 0;
|
|
1079
|
+
for (const p of placements) {
|
|
1080
|
+
const item = items.get(p.id);
|
|
1081
|
+
const m = marginOf(item);
|
|
1082
|
+
right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
|
|
1083
|
+
bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
|
|
1084
|
+
}
|
|
1085
|
+
const movedById = new Map(moved.map((m) => [m.id, m]));
|
|
1086
|
+
for (const o of input.obstacles ?? []) {
|
|
1087
|
+
const b = boundsOf(o.region);
|
|
1088
|
+
const at = movedById.get(o.id) ?? b;
|
|
1089
|
+
right = Math.max(right, at.x + b.w - frame.bounds.x);
|
|
1090
|
+
bottom = Math.max(bottom, at.y + b.h - frame.bounds.y);
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
|
|
1094
|
+
h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
/** This function gives a region with the axes changed: x for y, and w for h. */
|
|
1098
|
+
function transposeRegion(region) {
|
|
1099
|
+
switch (region.kind) {
|
|
1100
|
+
case "rect": return {
|
|
1101
|
+
kind: "rect",
|
|
1102
|
+
x: region.y,
|
|
1103
|
+
y: region.x,
|
|
1104
|
+
w: region.h,
|
|
1105
|
+
h: region.w
|
|
1106
|
+
};
|
|
1107
|
+
case "circle": return {
|
|
1108
|
+
kind: "circle",
|
|
1109
|
+
cx: region.cy,
|
|
1110
|
+
cy: region.cx,
|
|
1111
|
+
r: region.r
|
|
1112
|
+
};
|
|
1113
|
+
case "ellipse": return {
|
|
1114
|
+
kind: "ellipse",
|
|
1115
|
+
cx: region.cy,
|
|
1116
|
+
cy: region.cx,
|
|
1117
|
+
rx: region.ry,
|
|
1118
|
+
ry: region.rx
|
|
1119
|
+
};
|
|
1120
|
+
case "polygon": {
|
|
1121
|
+
const swap = (ring) => ring.map(([x, y]) => [y, x]);
|
|
1122
|
+
const turned = {
|
|
1123
|
+
kind: "polygon",
|
|
1124
|
+
points: swap(region.points)
|
|
1125
|
+
};
|
|
1126
|
+
if (region.holes) turned.holes = region.holes.map(swap);
|
|
1127
|
+
return turned;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/** This function gives an item with the axes changed. The ports go, because the adapters of 0.1 do not read them. */
|
|
1132
|
+
function transposeItem(item) {
|
|
1133
|
+
const { ports: _ports, ...rest } = item;
|
|
1134
|
+
const turned = {
|
|
1135
|
+
...rest,
|
|
1136
|
+
w: item.h,
|
|
1137
|
+
h: item.w
|
|
1138
|
+
};
|
|
1139
|
+
delete turned.x;
|
|
1140
|
+
delete turned.y;
|
|
1141
|
+
if (item.y !== void 0) turned.x = item.y;
|
|
1142
|
+
if (item.x !== void 0) turned.y = item.x;
|
|
1143
|
+
if (item.shape) turned.shape = transposeRegion(item.shape);
|
|
1144
|
+
if (item.margin !== void 0 && typeof item.margin !== "number") {
|
|
1145
|
+
const m = item.margin;
|
|
1146
|
+
turned.margin = {
|
|
1147
|
+
top: m.left,
|
|
1148
|
+
right: m.bottom,
|
|
1149
|
+
bottom: m.right,
|
|
1150
|
+
left: m.top
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
return turned;
|
|
1154
|
+
}
|
|
1155
|
+
/** This function gives an input with the axes changed. A fixed height becomes a fixed width. */
|
|
1156
|
+
function transposeInput(input) {
|
|
1157
|
+
const c = input.container;
|
|
1158
|
+
const container = { fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed };
|
|
1159
|
+
if (c.h !== void 0) container.w = c.h;
|
|
1160
|
+
if (c.w !== void 0) container.h = c.w;
|
|
1161
|
+
if (c.aspect !== void 0) container.aspect = 1 / c.aspect;
|
|
1162
|
+
if (c.aspectWeight !== void 0) container.aspectWeight = c.aspectWeight;
|
|
1163
|
+
if (c.shape !== void 0) container.shape = transposeRegion(c.shape);
|
|
1164
|
+
if (c.inset !== void 0) container.inset = c.inset;
|
|
1165
|
+
const turned = {
|
|
1166
|
+
container,
|
|
1167
|
+
items: input.items.map(transposeItem)
|
|
1168
|
+
};
|
|
1169
|
+
if (input.obstacles) turned.obstacles = input.obstacles.map((o) => ({
|
|
1170
|
+
...o,
|
|
1171
|
+
region: transposeRegion(o.region)
|
|
1172
|
+
}));
|
|
1173
|
+
if (input.gap !== void 0) turned.gap = input.gap;
|
|
1174
|
+
return turned;
|
|
1175
|
+
}
|
|
1176
|
+
/** This function gives an output with the axes changed. */
|
|
1177
|
+
function transposeOutput(output) {
|
|
1178
|
+
return {
|
|
1179
|
+
placements: output.placements.map((p) => ({
|
|
1180
|
+
...p,
|
|
1181
|
+
x: p.y,
|
|
1182
|
+
y: p.x
|
|
1183
|
+
})),
|
|
1184
|
+
overflow: [...output.overflow],
|
|
1185
|
+
size: {
|
|
1186
|
+
w: output.size.h,
|
|
1187
|
+
h: output.size.w
|
|
1188
|
+
},
|
|
1189
|
+
moved: output.moved.map((m) => ({
|
|
1190
|
+
id: m.id,
|
|
1191
|
+
x: m.y,
|
|
1192
|
+
y: m.x
|
|
1193
|
+
})),
|
|
1194
|
+
trace: output.trace
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* This function packs an input with a fixed height through the turn of the axes. It changes x for y in the input.
|
|
1199
|
+
* Then it packs with a fixed width, and changes the axes of the output back.
|
|
1200
|
+
*/
|
|
1201
|
+
function viaAxes(input, previous, run) {
|
|
1202
|
+
const start = now();
|
|
1203
|
+
const inner = run(transposeInput(input), previous === void 0 ? void 0 : transposeOutput(previous));
|
|
1204
|
+
const output = transposeOutput(inner);
|
|
1205
|
+
output.trace = [{
|
|
1206
|
+
id: "axes",
|
|
1207
|
+
ms: now() - start,
|
|
1208
|
+
measures: {}
|
|
1209
|
+
}, ...inner.trace];
|
|
1210
|
+
return output;
|
|
1211
|
+
}
|
|
1212
|
+
// src/adapters.ts
|
|
1213
|
+
/** The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. */
|
|
1214
|
+
const LAYOUT_CELLS = {
|
|
1215
|
+
containerSize: [
|
|
1216
|
+
"fixed",
|
|
1217
|
+
"freeHeight",
|
|
1218
|
+
"freeWidth",
|
|
1219
|
+
"freeAspect",
|
|
1220
|
+
"freeArea"
|
|
1221
|
+
],
|
|
1222
|
+
containerShape: ["rect"],
|
|
1223
|
+
itemShape: ["rect"],
|
|
1224
|
+
rotation: ["none"],
|
|
1225
|
+
groups: ["none"],
|
|
1226
|
+
mode: ["offline"]
|
|
1227
|
+
};
|
|
1228
|
+
/** This function makes the supports of a strategy, with copies of each list. */
|
|
1229
|
+
function supportsOf(cells) {
|
|
1230
|
+
return {
|
|
1231
|
+
containerSize: [...cells.containerSize],
|
|
1232
|
+
containerShape: [...cells.containerShape],
|
|
1233
|
+
obstacleMove: [...cells.obstacleMove],
|
|
1234
|
+
itemShape: [...cells.itemShape],
|
|
1235
|
+
rotation: [...cells.rotation],
|
|
1236
|
+
order: [...cells.order],
|
|
1237
|
+
groups: [...cells.groups],
|
|
1238
|
+
mode: [...cells.mode]
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
/** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
|
|
1242
|
+
function packFixedWidth(id, input, options) {
|
|
1243
|
+
const start = now();
|
|
1244
|
+
const frame = buildFrame(input);
|
|
1245
|
+
const rects = rectsOf(frame);
|
|
1246
|
+
if (frame.w > 0) pack(rects, outerOf(frame), {
|
|
1247
|
+
strategy: id,
|
|
1248
|
+
sortOrder: options.sortOrder
|
|
1249
|
+
});
|
|
1250
|
+
const output = readFrame(input, frame, []);
|
|
1251
|
+
output.trace.push({
|
|
1252
|
+
id,
|
|
1253
|
+
ms: now() - start,
|
|
1254
|
+
measures: { "pack.overflow": output.overflow.length }
|
|
1255
|
+
});
|
|
1256
|
+
return output;
|
|
1257
|
+
}
|
|
1258
|
+
/** This function makes the strategy of one function of `pack`. */
|
|
1259
|
+
function layoutStrategy(id, family, catalog, cells) {
|
|
1260
|
+
const self = {
|
|
1261
|
+
id,
|
|
1262
|
+
family,
|
|
1263
|
+
catalog,
|
|
1264
|
+
supports: supportsOf({
|
|
1265
|
+
...LAYOUT_CELLS,
|
|
1266
|
+
...cells
|
|
1267
|
+
}),
|
|
1268
|
+
defaults: {
|
|
1269
|
+
sortOrder: "height-desc",
|
|
1270
|
+
samples: 48
|
|
1271
|
+
},
|
|
1272
|
+
deterministic: true,
|
|
1273
|
+
pack(input, options, context) {
|
|
1274
|
+
assertSupported(self.supports, input);
|
|
1275
|
+
const o = {
|
|
1276
|
+
...self.defaults,
|
|
1277
|
+
...options
|
|
1278
|
+
};
|
|
1279
|
+
switch (input.container.fixed) {
|
|
1280
|
+
case "height": return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o));
|
|
1281
|
+
case "none": return logSweep.search(input, self, context, o);
|
|
1282
|
+
default: return packFixedWidth(id, input, o);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
return self;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. It supports no obstacle and
|
|
1290
|
+
* no locked item, because `packRows` does not go around them. Turnable items, groups and shapes throw `UnsupportedCell`.
|
|
1291
|
+
*/
|
|
1292
|
+
const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
|
|
1293
|
+
obstacleMove: [],
|
|
1294
|
+
order: [
|
|
1295
|
+
"free",
|
|
1296
|
+
"soft",
|
|
1297
|
+
"hard"
|
|
1298
|
+
]
|
|
1299
|
+
});
|
|
1300
|
+
/**
|
|
1301
|
+
* The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
|
|
1302
|
+
* locked items and the fixed obstacles. Turnable items, groups and shapes throw `UnsupportedCell`.
|
|
1303
|
+
*/
|
|
1304
|
+
const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
|
|
1305
|
+
obstacleMove: ["fixed"],
|
|
1306
|
+
order: ["free", "soft"]
|
|
1307
|
+
});
|
|
1308
|
+
/**
|
|
1309
|
+
* The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
|
|
1310
|
+
* With the sort order `input`, the rows keep the rank order. Turnable items, groups and shapes throw `UnsupportedCell`.
|
|
1311
|
+
*/
|
|
1312
|
+
const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
|
|
1313
|
+
obstacleMove: ["fixed"],
|
|
1314
|
+
order: [
|
|
1315
|
+
"free",
|
|
1316
|
+
"soft",
|
|
1317
|
+
"hard"
|
|
1318
|
+
]
|
|
1319
|
+
});
|
|
1320
|
+
/**
|
|
1321
|
+
* The size search `log-sweep` uses `packFree` of 0.1.2: widths on a log scale, then the widths near the best width. It
|
|
1322
|
+
* wraps `rows`, `skyline` and `shelf`. The objective is the area of the frame times `1 + aspectWeight * aspectError`.
|
|
1323
|
+
*/
|
|
1324
|
+
const logSweep = {
|
|
1325
|
+
id: "log-sweep",
|
|
1326
|
+
search(input, inner, context, options) {
|
|
1327
|
+
if (input.container.fixed !== "none") return inner.pack(input, options ?? inner.defaults, context);
|
|
1328
|
+
const id = inner.id;
|
|
1329
|
+
if (id !== "rows" && id !== "skyline" && id !== "shelf") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
|
|
1330
|
+
const o = {
|
|
1331
|
+
...inner.defaults,
|
|
1332
|
+
...options
|
|
1333
|
+
};
|
|
1334
|
+
const start = now();
|
|
1335
|
+
const frame = buildFrame(input);
|
|
1336
|
+
const result = packFree(rectsOf(frame), new Rect(), {
|
|
1337
|
+
strategy: id,
|
|
1338
|
+
sortOrder: o.sortOrder,
|
|
1339
|
+
samples: o.samples,
|
|
1340
|
+
aspect: input.container.aspect ?? 1,
|
|
1341
|
+
aspectWeight: input.container.aspectWeight ?? 1
|
|
1342
|
+
});
|
|
1343
|
+
const ms = now() - start;
|
|
1344
|
+
const runs = result.tried.length + 1;
|
|
1345
|
+
const output = readFrame(input, frame, []);
|
|
1346
|
+
output.trace.push({
|
|
1347
|
+
id: "log-sweep",
|
|
1348
|
+
ms,
|
|
1349
|
+
measures: {
|
|
1350
|
+
"pack.search.widths": result.tried.length,
|
|
1351
|
+
"pack.search.ms": ms
|
|
1352
|
+
}
|
|
1353
|
+
}, {
|
|
1354
|
+
id,
|
|
1355
|
+
ms: ms / runs,
|
|
1356
|
+
measures: { "pack.overflow": output.overflow.length }
|
|
1357
|
+
});
|
|
1358
|
+
return output;
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
/** This function tells if the insides of two rectangles overlap by more than `EPS`. */
|
|
1362
|
+
function overlaps(a, b) {
|
|
1363
|
+
return Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x) > 1e-6 && Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y) > 1e-6;
|
|
1364
|
+
}
|
|
1365
|
+
/** This function packs an input with a fixed size or a fixed width with a push cascade. */
|
|
1366
|
+
function pushFixedWidth(input, options, previous) {
|
|
1367
|
+
const start = now();
|
|
1368
|
+
const frame = buildFrame(input, true);
|
|
1369
|
+
const before = new Map(previous?.placements.map((p) => [p.id, p]));
|
|
1370
|
+
const positioned = [];
|
|
1371
|
+
const pending = [];
|
|
1372
|
+
const obstacles = frame.entries.filter((e) => e.obstacle !== void 0);
|
|
1373
|
+
const starts = new Map(obstacles.map((e) => [e, {
|
|
1374
|
+
x: e.rect.position.x,
|
|
1375
|
+
y: e.rect.position.y
|
|
1376
|
+
}]));
|
|
1377
|
+
const lockedItems = frame.entries.filter((e) => e.item !== void 0 && e.rect.locked);
|
|
1378
|
+
for (const e of frame.entries) {
|
|
1379
|
+
const item = e.item;
|
|
1380
|
+
if (item === void 0 || e.rect.locked) continue;
|
|
1381
|
+
const x = item.x ?? before.get(item.id)?.x;
|
|
1382
|
+
const y = item.y ?? before.get(item.id)?.y;
|
|
1383
|
+
if (x === void 0 || y === void 0) {
|
|
1384
|
+
pending.push(e);
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
e.rect.position.set(x - e.dx - frame.ox, y - e.dy - frame.oy);
|
|
1388
|
+
positioned.push(e);
|
|
1389
|
+
}
|
|
1390
|
+
const inPlay = frame.entries.filter((e) => !pending.includes(e)).map((e) => e.rect);
|
|
1391
|
+
const maxShoves = options.maxShovesPerItem * Math.max(1, inPlay.length);
|
|
1392
|
+
for (const e of positioned) {
|
|
1393
|
+
pushCascade(e.rect, inPlay, {
|
|
1394
|
+
gap: 0,
|
|
1395
|
+
maxShoves
|
|
1396
|
+
});
|
|
1397
|
+
e.rect.locked = true;
|
|
1398
|
+
}
|
|
1399
|
+
const { bounds } = frame;
|
|
1400
|
+
const moved = [];
|
|
1401
|
+
for (const e of obstacles) {
|
|
1402
|
+
const startAt = starts.get(e);
|
|
1403
|
+
const x = e.rect.position.x + e.dx + frame.ox;
|
|
1404
|
+
const y = e.rect.position.y + e.dy + frame.oy;
|
|
1405
|
+
const b = boundsOf(e.obstacle.region);
|
|
1406
|
+
if (x < bounds.x - 1e-6 || y < bounds.y - 1e-6 || x + b.w > bounds.x + bounds.w + 1e-6 || y + b.h > bounds.y + bounds.h + 1e-6 || lockedItems.some((l) => overlaps(l.rect, e.rect))) e.rect.position.set(startAt.x, startAt.y);
|
|
1407
|
+
else if (Math.abs(e.rect.position.x - startAt.x) > 1e-6 || Math.abs(e.rect.position.y - startAt.y) > 1e-6) moved.push({
|
|
1408
|
+
id: e.obstacle.id,
|
|
1409
|
+
x,
|
|
1410
|
+
y
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
const pushMs = now() - start;
|
|
1414
|
+
const kept = [...obstacles, ...lockedItems].map((e) => e.rect);
|
|
1415
|
+
for (const e of positioned) {
|
|
1416
|
+
const r = e.rect;
|
|
1417
|
+
if (r.position.x >= -1e-6 && r.position.y >= -1e-6 && r.right() <= frame.w + 1e-6 && r.bottom() <= frame.h + 1e-6 && !kept.some((k) => overlaps(k, r))) kept.push(r);
|
|
1418
|
+
else pending.push(e);
|
|
1419
|
+
}
|
|
1420
|
+
const skylineStart = now();
|
|
1421
|
+
for (const r of kept) {
|
|
1422
|
+
r.locked = true;
|
|
1423
|
+
r.wasPacked = true;
|
|
1424
|
+
}
|
|
1425
|
+
if (pending.length > 0 && frame.w > 0) {
|
|
1426
|
+
for (const e of pending) {
|
|
1427
|
+
e.rect.locked = false;
|
|
1428
|
+
e.rect.wasPacked = false;
|
|
1429
|
+
}
|
|
1430
|
+
packSkyline([...pending.map((e) => e.rect), ...kept], outerOf(frame), { sortOrder: options.sortOrder });
|
|
1431
|
+
}
|
|
1432
|
+
const output = readFrame(input, frame, [], moved);
|
|
1433
|
+
output.trace.push({
|
|
1434
|
+
id: "push",
|
|
1435
|
+
ms: pushMs,
|
|
1436
|
+
measures: { "pack.push.moved": moved.length }
|
|
1437
|
+
}, {
|
|
1438
|
+
id: "skyline",
|
|
1439
|
+
ms: now() - skylineStart,
|
|
1440
|
+
measures: {
|
|
1441
|
+
"pack.push.replaced": pending.length,
|
|
1442
|
+
"pack.overflow": output.overflow.length
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
return output;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* The strategy `push` uses `pushCascade`. Each item starts at its `x` and `y`, or at its place in `context.previous`.
|
|
1449
|
+
* Each item in rank order pushes the items and the movable obstacles that it overlaps. Then `packSkyline` places the
|
|
1450
|
+
* items with no start, and the items that still overlap. The output tells each obstacle that moved.
|
|
1451
|
+
*/
|
|
1452
|
+
const push = {
|
|
1453
|
+
id: "push",
|
|
1454
|
+
family: "incremental",
|
|
1455
|
+
catalog: ["incremental-push"],
|
|
1456
|
+
supports: supportsOf({
|
|
1457
|
+
containerSize: [
|
|
1458
|
+
"fixed",
|
|
1459
|
+
"freeHeight",
|
|
1460
|
+
"freeWidth"
|
|
1461
|
+
],
|
|
1462
|
+
containerShape: ["rect"],
|
|
1463
|
+
obstacleMove: ["fixed", "free"],
|
|
1464
|
+
itemShape: ["rect"],
|
|
1465
|
+
rotation: ["none"],
|
|
1466
|
+
order: ["free", "soft"],
|
|
1467
|
+
groups: ["none"],
|
|
1468
|
+
mode: ["incremental"]
|
|
1469
|
+
}),
|
|
1470
|
+
defaults: {
|
|
1471
|
+
sortOrder: "height-desc",
|
|
1472
|
+
maxShovesPerItem: 8
|
|
1473
|
+
},
|
|
1474
|
+
deterministic: true,
|
|
1475
|
+
pack(input, options, context) {
|
|
1476
|
+
assertSupported(push.supports, input);
|
|
1477
|
+
const o = {
|
|
1478
|
+
...push.defaults,
|
|
1479
|
+
...options
|
|
1480
|
+
};
|
|
1481
|
+
if (input.container.fixed === "height") return viaAxes(input, context.previous, (turned, previous) => pushFixedWidth(turned, o, previous));
|
|
1482
|
+
return pushFixedWidth(input, o, context.previous);
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
// src/convert.ts
|
|
1486
|
+
/**
|
|
1487
|
+
* This function makes a `PackInput` from the `Rect` objects of 0.1. The id of each item is the index of its rectangle.
|
|
1488
|
+
*
|
|
1489
|
+
* - `fixedSize` gives `fixed` set to `size`, `fixedWidth` gives `width`, and an outer rectangle without them gives `none`.
|
|
1490
|
+
* - A locked rectangle gives a locked item at its position.
|
|
1491
|
+
* - An outer rectangle at a position other than 0, 0 gives a rectangle shape at that position.
|
|
1492
|
+
*/
|
|
1493
|
+
function fromRects(rects, outer) {
|
|
1494
|
+
const fixed = outer.fixedSize ? "size" : outer.fixedWidth ? "width" : "none";
|
|
1495
|
+
const container = { fixed };
|
|
1496
|
+
if (fixed !== "none") container.w = outer.size.x;
|
|
1497
|
+
if (fixed === "size") container.h = outer.size.y;
|
|
1498
|
+
if (outer.position.x !== 0 || outer.position.y !== 0) container.shape = {
|
|
1499
|
+
kind: "rect",
|
|
1500
|
+
x: outer.position.x,
|
|
1501
|
+
y: outer.position.y,
|
|
1502
|
+
w: outer.size.x,
|
|
1503
|
+
h: outer.size.y
|
|
1504
|
+
};
|
|
1505
|
+
return {
|
|
1506
|
+
container,
|
|
1507
|
+
items: rects.map((r, i) => {
|
|
1508
|
+
const item = {
|
|
1509
|
+
id: String(i),
|
|
1510
|
+
w: r.size.x,
|
|
1511
|
+
h: r.size.y
|
|
1512
|
+
};
|
|
1513
|
+
if (r.locked) {
|
|
1514
|
+
item.locked = true;
|
|
1515
|
+
item.x = r.position.x;
|
|
1516
|
+
item.y = r.position.y;
|
|
1517
|
+
}
|
|
1518
|
+
return item;
|
|
1519
|
+
})
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* This function writes an output back into the `Rect` objects of `fromRects`. It sets the position and `wasPacked` of
|
|
1524
|
+
* each rectangle, and the free sides of the outer rectangle. It gives the outer rectangle.
|
|
1525
|
+
*/
|
|
1526
|
+
function toRects(output, rects, outer) {
|
|
1527
|
+
const placements = new Map(output.placements.map((p) => [p.id, p]));
|
|
1528
|
+
for (const [i, r] of rects.entries()) {
|
|
1529
|
+
const p = placements.get(String(i));
|
|
1530
|
+
if (p === void 0) {
|
|
1531
|
+
r.wasPacked = r.locked;
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
r.position.set(p.x, p.y);
|
|
1535
|
+
r.wasPacked = true;
|
|
1536
|
+
}
|
|
1537
|
+
if (!outer.fixedSize) {
|
|
1538
|
+
if (!outer.fixedWidth) outer.size.x = output.size.w;
|
|
1539
|
+
outer.size.y = output.size.h;
|
|
1540
|
+
}
|
|
1541
|
+
return outer;
|
|
1542
|
+
}
|
|
1543
|
+
// src/registry.ts
|
|
1544
|
+
/** Each strategy of the package. The benchmark starts each one on each input of the corpus. */
|
|
1545
|
+
const strategies = Object.freeze([
|
|
1546
|
+
rows,
|
|
1547
|
+
skyline,
|
|
1548
|
+
shelf,
|
|
1549
|
+
push
|
|
1550
|
+
]);
|
|
1551
|
+
/** This function gives the strategy with an id. It throws a `RangeError` for an id that the registry does not have. */
|
|
1552
|
+
function strategy(id) {
|
|
1553
|
+
const found = strategies.find((s) => s.id === id);
|
|
1554
|
+
if (found === void 0) throw new RangeError(`The registry has no strategy "${id}".`);
|
|
1555
|
+
return found;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* This function packs an input with the strategy of an id. The options go over the defaults of the strategy. The
|
|
1559
|
+
* context gets `mulberry32` with the seed. The default seed is 0.
|
|
1560
|
+
*/
|
|
1561
|
+
function packWith(id, input, options, seed = 0) {
|
|
1562
|
+
const s = strategy(id);
|
|
1563
|
+
const merged = {
|
|
1564
|
+
...s.defaults,
|
|
1565
|
+
...options
|
|
1566
|
+
};
|
|
1567
|
+
return s.pack(input, merged, createContext(seed));
|
|
1568
|
+
}
|
|
1569
|
+
// src/check.ts
|
|
1570
|
+
/** This function tells if the insides of two boxes overlap by more than a tolerance on each axis. */
|
|
1571
|
+
function overlap(a, b, tolerance) {
|
|
1572
|
+
return Math.min(a.r, b.r) - Math.max(a.l, b.l) > tolerance && Math.min(a.b, b.b) - Math.max(a.t, b.t) > tolerance;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* This function counts the breaks of the 4 hard rules of the packer for an output.
|
|
1576
|
+
*
|
|
1577
|
+
* - `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`.
|
|
1579
|
+
* - `locked-kept`: each locked item that moved by more than `RECORD_EPS`, or that has no position.
|
|
1580
|
+
* - `complete`: each item with no position that is not in the overflow.
|
|
1581
|
+
*
|
|
1582
|
+
* The rules read a shape of the container or of an obstacle as its bounding box. The result has no tolerance in it.
|
|
1583
|
+
*/
|
|
1584
|
+
function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
|
|
1585
|
+
const tol = tolerance.overlap;
|
|
1586
|
+
const placements = /* @__PURE__ */ new Map();
|
|
1587
|
+
for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
|
|
1588
|
+
const overflow = new Set(output.overflow);
|
|
1589
|
+
const breaks = {
|
|
1590
|
+
"no-overlap": 0,
|
|
1591
|
+
"in-container": 0,
|
|
1592
|
+
"locked-kept": 0,
|
|
1593
|
+
complete: 0
|
|
1594
|
+
};
|
|
1595
|
+
const boxes = [];
|
|
1596
|
+
for (const item of input.items) {
|
|
1597
|
+
const p = placements.get(item.id);
|
|
1598
|
+
if (p === void 0) {
|
|
1599
|
+
if (item.locked === true && item.x !== void 0 && item.y !== void 0) breaks["locked-kept"] += 1;
|
|
1600
|
+
if (!overflow.has(item.id)) breaks.complete += 1;
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
|
|
1604
|
+
if (Math.abs(p.x - item.x) > .001 || Math.abs(p.y - item.y) > .001) breaks["locked-kept"] += 1;
|
|
1605
|
+
}
|
|
1606
|
+
const placed = p.turn === 90 ? turnItem(item) : item;
|
|
1607
|
+
const m = marginOf(placed);
|
|
1608
|
+
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
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
const moved = new Map(output.moved.map((m) => [m.id, m]));
|
|
1616
|
+
const obstacles = (input.obstacles ?? []).map((o) => {
|
|
1617
|
+
const b = boundsOf(o.region);
|
|
1618
|
+
const at = moved.get(o.id) ?? b;
|
|
1619
|
+
return {
|
|
1620
|
+
l: at.x,
|
|
1621
|
+
t: at.y,
|
|
1622
|
+
r: at.x + b.w,
|
|
1623
|
+
b: at.y + b.h
|
|
1624
|
+
};
|
|
1625
|
+
});
|
|
1626
|
+
for (const [i, a] of boxes.entries()) {
|
|
1627
|
+
for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) breaks["no-overlap"] += 1;
|
|
1628
|
+
for (const o of obstacles) if (overlap(a, o, tol)) breaks["no-overlap"] += 1;
|
|
1629
|
+
}
|
|
1630
|
+
const { container } = input;
|
|
1631
|
+
const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
|
|
1632
|
+
const x = shape?.x ?? 0;
|
|
1633
|
+
const y = shape?.y ?? 0;
|
|
1634
|
+
const w = shape?.w ?? container.w ?? Infinity;
|
|
1635
|
+
const h = shape?.h ?? container.h ?? Infinity;
|
|
1636
|
+
const fixedW = container.fixed === "size" || container.fixed === "width";
|
|
1637
|
+
const fixedH = container.fixed === "size" || container.fixed === "height";
|
|
1638
|
+
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);
|
|
1641
|
+
if (outX || outY) breaks["in-container"] += 1;
|
|
1642
|
+
}
|
|
1643
|
+
return breaks;
|
|
1644
|
+
}
|
|
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
|
+
// src/generators.ts
|
|
1843
|
+
/** The largest count of boxes that a generator makes. */
|
|
1844
|
+
const MAX_COUNT = 1e5;
|
|
1845
|
+
/** This function gives the random numbers of one step of a seed. */
|
|
1846
|
+
const stepRandom = (seed, step) => mulberry32(fnv1a32(`${seed}:${step}`));
|
|
1847
|
+
/** A whole number from lo to hi, both included. */
|
|
1848
|
+
const int = (r, lo, hi) => lo + Math.floor(r() * (hi - lo + 1));
|
|
1849
|
+
/** 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. */
|
|
1850
|
+
const span = (r, lo, hi) => {
|
|
1851
|
+
const a = Math.floor(lo);
|
|
1852
|
+
return int(r, a, Math.max(a, Math.floor(hi)));
|
|
1853
|
+
};
|
|
1854
|
+
/** A number from lo to hi. */
|
|
1855
|
+
const between = (r, lo, hi) => lo + r() * (hi - lo);
|
|
1856
|
+
/** This function gives an index with a probability in proportion to its weight. */
|
|
1857
|
+
const pick = (r, weights) => {
|
|
1858
|
+
let x = r() * weights.reduce((a, b) => a + b, 0);
|
|
1859
|
+
for (const [i, w] of weights.entries()) {
|
|
1860
|
+
x -= w;
|
|
1861
|
+
if (x < 0) return i;
|
|
1862
|
+
}
|
|
1863
|
+
return weights.length - 1;
|
|
1864
|
+
};
|
|
1865
|
+
/** This function gives a copy of a value with each number rounded by `round3`. A key with the value `undefined` goes. */
|
|
1866
|
+
function roundNumbers(value) {
|
|
1867
|
+
if (typeof value === "number") return round3(value);
|
|
1868
|
+
if (Array.isArray(value)) return value.map(roundNumbers);
|
|
1869
|
+
if (value !== null && typeof value === "object") {
|
|
1870
|
+
const out = {};
|
|
1871
|
+
for (const [k, v] of Object.entries(value)) if (v !== void 0) out[k] = roundNumbers(v);
|
|
1872
|
+
return out;
|
|
1873
|
+
}
|
|
1874
|
+
return value;
|
|
1875
|
+
}
|
|
1876
|
+
/** This function gives the cell of an input with rectangle items, no rotation, no order and no groups. */
|
|
1877
|
+
function cellWith(containerSize, containerShape, obstacles, sizes, count, locked = false) {
|
|
1878
|
+
const cell = {
|
|
1879
|
+
containerSize,
|
|
1880
|
+
containerShape,
|
|
1881
|
+
itemShape: "rect",
|
|
1882
|
+
rotation: "none",
|
|
1883
|
+
order: "free",
|
|
1884
|
+
groups: "none",
|
|
1885
|
+
obstacles,
|
|
1886
|
+
sizes,
|
|
1887
|
+
count: countCell(count),
|
|
1888
|
+
mode: "offline"
|
|
1889
|
+
};
|
|
1890
|
+
if (obstacles !== "none" || locked) cell.obstacleMove = "fixed";
|
|
1891
|
+
return cell;
|
|
1892
|
+
}
|
|
1893
|
+
/** This function makes a generator from its data. It adds the defaults, the checks, the stamp, the rounding and the hash. */
|
|
1894
|
+
function defineGenerator(spec) {
|
|
1895
|
+
const withDefaults = (params = {}) => {
|
|
1896
|
+
const full = { ...spec.defaults };
|
|
1897
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1898
|
+
if (value === void 0) continue;
|
|
1899
|
+
if (!(key in spec.defaults)) throw new RangeError(`The generator "${spec.name}" has no parameter "${key}".`);
|
|
1900
|
+
full[key] = value;
|
|
1901
|
+
}
|
|
1902
|
+
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])}".`);
|
|
1903
|
+
const count = full["count"];
|
|
1904
|
+
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}.`);
|
|
1905
|
+
return full;
|
|
1906
|
+
};
|
|
1907
|
+
return Object.freeze({
|
|
1908
|
+
name: spec.name,
|
|
1909
|
+
version: spec.version,
|
|
1910
|
+
set: spec.set,
|
|
1911
|
+
defaults: Object.freeze({ ...spec.defaults }),
|
|
1912
|
+
values: spec.values,
|
|
1913
|
+
withDefaults,
|
|
1914
|
+
cell: (params) => spec.cell(withDefaults(params)),
|
|
1915
|
+
generate(seed, params, set = spec.set) {
|
|
1916
|
+
if (!Number.isInteger(seed) || seed < 0) throw new RangeError(`The seed is a whole number from 0. It is ${seed}.`);
|
|
1917
|
+
const full = withDefaults(params);
|
|
1918
|
+
const family = spec.family(full);
|
|
1919
|
+
const { container, obstacles, boxes } = spec.build(seed, full);
|
|
1920
|
+
const file = roundNumbers({
|
|
1921
|
+
schema: "layout-input/1",
|
|
1922
|
+
id: `${set}/${family}/s${seed}`,
|
|
1923
|
+
set,
|
|
1924
|
+
family,
|
|
1925
|
+
seed,
|
|
1926
|
+
source: "made-up",
|
|
1927
|
+
generator: {
|
|
1928
|
+
name: spec.name,
|
|
1929
|
+
version: spec.version,
|
|
1930
|
+
params: full,
|
|
1931
|
+
seed
|
|
1932
|
+
},
|
|
1933
|
+
container,
|
|
1934
|
+
obstacles,
|
|
1935
|
+
boxes,
|
|
1936
|
+
edges: []
|
|
1937
|
+
});
|
|
1938
|
+
return {
|
|
1939
|
+
...file,
|
|
1940
|
+
hash: hashInput(file)
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
/** The shared generator `rect-scattered` of `docs/FORMAT.md`, version 1: rectangle obstacles at random places in a fixed rectangle. */
|
|
1946
|
+
const rectScattered = defineGenerator({
|
|
1947
|
+
name: "rect-scattered",
|
|
1948
|
+
version: 1,
|
|
1949
|
+
set: "xcheck-v1",
|
|
1950
|
+
defaults: { count: 40 },
|
|
1951
|
+
values: {},
|
|
1952
|
+
family: () => "rect-scattered",
|
|
1953
|
+
cell: (p) => cellWith("fixed", "rect", "scattered", "uniform", p.count),
|
|
1954
|
+
build(seed, { count }) {
|
|
1955
|
+
let r = stepRandom(seed, "container");
|
|
1956
|
+
const w = int(r, 600, 1e3);
|
|
1957
|
+
const h = int(r, 400, 800);
|
|
1958
|
+
const container = {
|
|
1959
|
+
w,
|
|
1960
|
+
h,
|
|
1961
|
+
fixed: "size"
|
|
1962
|
+
};
|
|
1963
|
+
r = stepRandom(seed, "obstacles");
|
|
1964
|
+
const n = int(r, 3, 8);
|
|
1965
|
+
const obstacles = [];
|
|
1966
|
+
for (let i = 0; i < n; i++) {
|
|
1967
|
+
const ow = int(r, 40, 160);
|
|
1968
|
+
const oh = int(r, 40, 160);
|
|
1969
|
+
const x = int(r, 0, w - ow);
|
|
1970
|
+
const y = int(r, 0, h - oh);
|
|
1971
|
+
obstacles.push({
|
|
1972
|
+
id: `o${i}`,
|
|
1973
|
+
region: {
|
|
1974
|
+
kind: "rect",
|
|
1975
|
+
x,
|
|
1976
|
+
y,
|
|
1977
|
+
w: ow,
|
|
1978
|
+
h: oh
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
r = stepRandom(seed, "boxes");
|
|
1983
|
+
const boxes = [];
|
|
1984
|
+
for (let i = 0; i < count; i++) {
|
|
1985
|
+
const bw = int(r, 20, 120);
|
|
1986
|
+
const bh = int(r, 20, 80);
|
|
1987
|
+
boxes.push({
|
|
1988
|
+
id: `b${i}`,
|
|
1989
|
+
w: bw,
|
|
1990
|
+
h: bh
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
container,
|
|
1995
|
+
obstacles,
|
|
1996
|
+
boxes
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
/** The values of `containerSize` that the generators make. */
|
|
2001
|
+
const CONTAINER_SIZES = [
|
|
2002
|
+
"fixed",
|
|
2003
|
+
"freeHeight",
|
|
2004
|
+
"freeWidth",
|
|
2005
|
+
"freeAspect",
|
|
2006
|
+
"freeArea"
|
|
2007
|
+
];
|
|
2008
|
+
/** The values of `containerShape`. */
|
|
2009
|
+
const CONTAINER_SHAPES = [
|
|
2010
|
+
"rect",
|
|
2011
|
+
"convex",
|
|
2012
|
+
"polygon",
|
|
2013
|
+
"circle",
|
|
2014
|
+
"ellipse",
|
|
2015
|
+
"holes"
|
|
2016
|
+
];
|
|
2017
|
+
/** The values of `obstacles`. */
|
|
2018
|
+
const OBSTACLE_LAYOUTS = [
|
|
2019
|
+
"none",
|
|
2020
|
+
"center",
|
|
2021
|
+
"scattered",
|
|
2022
|
+
"wall",
|
|
2023
|
+
"corridors",
|
|
2024
|
+
"touching",
|
|
2025
|
+
"edge"
|
|
2026
|
+
];
|
|
2027
|
+
/** The values of `sizes`. */
|
|
2028
|
+
const SIZE_DISTRIBUTIONS = [
|
|
2029
|
+
"identical",
|
|
2030
|
+
"nearIdentical",
|
|
2031
|
+
"uniform",
|
|
2032
|
+
"heavyTail",
|
|
2033
|
+
"bimodal",
|
|
2034
|
+
"extremeAspect"
|
|
2035
|
+
];
|
|
2036
|
+
/** This function gives the container of a size value for the nominal rectangle `w` by `h`, with an optional shape. */
|
|
2037
|
+
function containerOf(size, w, h, shape) {
|
|
2038
|
+
const container = {
|
|
2039
|
+
fixed: {
|
|
2040
|
+
w,
|
|
2041
|
+
h,
|
|
2042
|
+
fixed: "size"
|
|
2043
|
+
},
|
|
2044
|
+
freeHeight: {
|
|
2045
|
+
w,
|
|
2046
|
+
fixed: "width"
|
|
2047
|
+
},
|
|
2048
|
+
freeWidth: {
|
|
2049
|
+
h,
|
|
2050
|
+
fixed: "height"
|
|
2051
|
+
},
|
|
2052
|
+
freeAspect: {
|
|
2053
|
+
fixed: "none",
|
|
2054
|
+
aspect: round3(w / h)
|
|
2055
|
+
},
|
|
2056
|
+
freeArea: {
|
|
2057
|
+
fixed: "none",
|
|
2058
|
+
aspectWeight: 0
|
|
2059
|
+
}
|
|
2060
|
+
}[size];
|
|
2061
|
+
if (shape !== void 0) container.shape = shape;
|
|
2062
|
+
return container;
|
|
2063
|
+
}
|
|
2064
|
+
/** This function gives the 4 corners of a rectangle, clockwise from the top left. */
|
|
2065
|
+
const corners = (x, y, w, h) => [
|
|
2066
|
+
[x, y],
|
|
2067
|
+
[x + w, y],
|
|
2068
|
+
[x + w, y + h],
|
|
2069
|
+
[x, y + h]
|
|
2070
|
+
];
|
|
2071
|
+
/**
|
|
2072
|
+
* This function gives the shape of a container in the rectangle `w` by `h`, and its frame. The frame is a rectangle in
|
|
2073
|
+
* the shape. The obstacles and the sizes of the boxes come from the frame.
|
|
2074
|
+
*/
|
|
2075
|
+
function shapeOf(r, shape, w, h) {
|
|
2076
|
+
const m = Math.min(w, h);
|
|
2077
|
+
switch (shape) {
|
|
2078
|
+
case "rect": return { frame: {
|
|
2079
|
+
x: 0,
|
|
2080
|
+
y: 0,
|
|
2081
|
+
w,
|
|
2082
|
+
h
|
|
2083
|
+
} };
|
|
2084
|
+
case "convex": {
|
|
2085
|
+
const [a, b, c, d] = [
|
|
2086
|
+
0,
|
|
2087
|
+
1,
|
|
2088
|
+
2,
|
|
2089
|
+
3
|
|
2090
|
+
].map(() => span(r, m * .1, m * .25));
|
|
2091
|
+
const points = [
|
|
2092
|
+
[a, 0],
|
|
2093
|
+
[w - b, 0],
|
|
2094
|
+
[w, b],
|
|
2095
|
+
[w, h - c],
|
|
2096
|
+
[w - c, h],
|
|
2097
|
+
[d, h],
|
|
2098
|
+
[0, h - d],
|
|
2099
|
+
[0, a]
|
|
2100
|
+
];
|
|
2101
|
+
const top = Math.max(a, b);
|
|
2102
|
+
const bottom = Math.max(c, d);
|
|
2103
|
+
return {
|
|
2104
|
+
region: {
|
|
2105
|
+
kind: "polygon",
|
|
2106
|
+
points
|
|
2107
|
+
},
|
|
2108
|
+
frame: {
|
|
2109
|
+
x: 0,
|
|
2110
|
+
y: top,
|
|
2111
|
+
w,
|
|
2112
|
+
h: h - top - bottom
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
case "polygon": {
|
|
2117
|
+
const nw = span(r, w * .25, w * .45);
|
|
2118
|
+
const nh = span(r, h * .25, h * .45);
|
|
2119
|
+
return {
|
|
2120
|
+
region: {
|
|
2121
|
+
kind: "polygon",
|
|
2122
|
+
points: [
|
|
2123
|
+
[0, 0],
|
|
2124
|
+
[w, 0],
|
|
2125
|
+
[w, h - nh],
|
|
2126
|
+
[w - nw, h - nh],
|
|
2127
|
+
[w - nw, h],
|
|
2128
|
+
[0, h]
|
|
2129
|
+
]
|
|
2130
|
+
},
|
|
2131
|
+
frame: {
|
|
2132
|
+
x: 0,
|
|
2133
|
+
y: 0,
|
|
2134
|
+
w: w - nw,
|
|
2135
|
+
h
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
case "circle": {
|
|
2140
|
+
const cx = Math.floor(w / 2);
|
|
2141
|
+
const cy = Math.floor(h / 2);
|
|
2142
|
+
const radius = Math.min(cx, cy);
|
|
2143
|
+
const side = 2 * Math.floor((radius * Math.SQRT2 - 1) / 2);
|
|
2144
|
+
return {
|
|
2145
|
+
region: {
|
|
2146
|
+
kind: "circle",
|
|
2147
|
+
cx,
|
|
2148
|
+
cy,
|
|
2149
|
+
r: radius
|
|
2150
|
+
},
|
|
2151
|
+
frame: {
|
|
2152
|
+
x: cx - side / 2,
|
|
2153
|
+
y: cy - side / 2,
|
|
2154
|
+
w: side,
|
|
2155
|
+
h: side
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
case "ellipse": {
|
|
2160
|
+
const rx = Math.floor(w / 2);
|
|
2161
|
+
const ry = Math.floor(h / 2);
|
|
2162
|
+
const a = Math.floor(rx / Math.SQRT2);
|
|
2163
|
+
const b = Math.floor(ry / Math.SQRT2);
|
|
2164
|
+
return {
|
|
2165
|
+
region: {
|
|
2166
|
+
kind: "ellipse",
|
|
2167
|
+
cx: rx,
|
|
2168
|
+
cy: ry,
|
|
2169
|
+
rx,
|
|
2170
|
+
ry
|
|
2171
|
+
},
|
|
2172
|
+
frame: {
|
|
2173
|
+
x: rx - a,
|
|
2174
|
+
y: ry - b,
|
|
2175
|
+
w: 2 * a,
|
|
2176
|
+
h: 2 * b
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
case "holes": {
|
|
2181
|
+
const count = int(r, 1, 2);
|
|
2182
|
+
const holes = [];
|
|
2183
|
+
for (let i = 0; i < count; i++) {
|
|
2184
|
+
const hw = span(r, w * .08, w * .15);
|
|
2185
|
+
const hh = span(r, h * .1, h * .25);
|
|
2186
|
+
const x = i === 0 ? span(r, 20, w / 2 - hw - 10) : span(r, w / 2 + 10, w - hw - 20);
|
|
2187
|
+
const y = span(r, 20, h - hh - 20);
|
|
2188
|
+
holes.push(corners(x, y, hw, hh));
|
|
2189
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
region: {
|
|
2192
|
+
kind: "polygon",
|
|
2193
|
+
points: corners(0, 0, w, h),
|
|
2194
|
+
holes
|
|
2195
|
+
},
|
|
2196
|
+
frame: {
|
|
2197
|
+
x: 0,
|
|
2198
|
+
y: 0,
|
|
2199
|
+
w,
|
|
2200
|
+
h
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
/** This function gives the obstacles of a layout in a frame. Each obstacle is a rectangle with whole-number sides. */
|
|
2207
|
+
function obstaclesOf(r, layout, f) {
|
|
2208
|
+
const m = Math.min(f.w, f.h);
|
|
2209
|
+
const rects = [];
|
|
2210
|
+
switch (layout) {
|
|
2211
|
+
case "none": break;
|
|
2212
|
+
case "center": {
|
|
2213
|
+
const w = span(r, f.w * .15, f.w * .3);
|
|
2214
|
+
const h = span(r, f.h * .15, f.h * .3);
|
|
2215
|
+
const jx = Math.floor(f.w * .05);
|
|
2216
|
+
const jy = Math.floor(f.h * .05);
|
|
2217
|
+
rects.push({
|
|
2218
|
+
x: f.x + Math.floor((f.w - w) / 2) + int(r, -jx, jx),
|
|
2219
|
+
y: f.y + Math.floor((f.h - h) / 2) + int(r, -jy, jy),
|
|
2220
|
+
w,
|
|
2221
|
+
h
|
|
2222
|
+
});
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
case "scattered": {
|
|
2226
|
+
const n = int(r, 3, 8);
|
|
2227
|
+
for (let i = 0; i < n; i++) {
|
|
2228
|
+
const w = span(r, m * .05, m * .2);
|
|
2229
|
+
const h = span(r, m * .05, m * .2);
|
|
2230
|
+
rects.push({
|
|
2231
|
+
x: int(r, f.x, f.x + f.w - w),
|
|
2232
|
+
y: int(r, f.y, f.y + f.h - h),
|
|
2233
|
+
w,
|
|
2234
|
+
h
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
break;
|
|
2238
|
+
}
|
|
2239
|
+
case "wall": {
|
|
2240
|
+
const vertical = r() < .5;
|
|
2241
|
+
const across = vertical ? f.w : f.h;
|
|
2242
|
+
const along = vertical ? f.h : f.w;
|
|
2243
|
+
const thick = span(r, Math.max(1, across * .03), Math.max(1, across * .06));
|
|
2244
|
+
const length = span(r, along * .7, along * .85);
|
|
2245
|
+
const at = span(r, across * .3, across * .7 - thick);
|
|
2246
|
+
const from = r() < .5 ? 0 : along - length;
|
|
2247
|
+
rects.push(vertical ? {
|
|
2248
|
+
x: f.x + at,
|
|
2249
|
+
y: f.y + from,
|
|
2250
|
+
w: thick,
|
|
2251
|
+
h: length
|
|
2252
|
+
} : {
|
|
2253
|
+
x: f.x + from,
|
|
2254
|
+
y: f.y + at,
|
|
2255
|
+
w: length,
|
|
2256
|
+
h: thick
|
|
2257
|
+
});
|
|
2258
|
+
break;
|
|
2259
|
+
}
|
|
2260
|
+
case "corridors": {
|
|
2261
|
+
const cols = int(r, 2, 4);
|
|
2262
|
+
const rows = int(r, 2, 3);
|
|
2263
|
+
const path = span(r, m * .08, m * .15);
|
|
2264
|
+
const bw = Math.floor((f.w - (cols + 1) * path) / cols);
|
|
2265
|
+
const bh = Math.floor((f.h - (rows + 1) * path) / rows);
|
|
2266
|
+
for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) rects.push({
|
|
2267
|
+
x: f.x + path + i * (bw + path),
|
|
2268
|
+
y: f.y + path + j * (bh + path),
|
|
2269
|
+
w: bw,
|
|
2270
|
+
h: bh
|
|
2271
|
+
});
|
|
2272
|
+
break;
|
|
2273
|
+
}
|
|
2274
|
+
case "touching": {
|
|
2275
|
+
const n = int(r, 2, 4);
|
|
2276
|
+
let w = span(r, m * .08, m * .15);
|
|
2277
|
+
let h = span(r, m * .08, m * .15);
|
|
2278
|
+
let x = span(r, f.x + f.w * .15, f.x + f.w * .35);
|
|
2279
|
+
let y = span(r, f.y + f.h * .15, f.y + f.h * .35);
|
|
2280
|
+
rects.push({
|
|
2281
|
+
x,
|
|
2282
|
+
y,
|
|
2283
|
+
w,
|
|
2284
|
+
h
|
|
2285
|
+
});
|
|
2286
|
+
for (let i = 1; i < n; i++) {
|
|
2287
|
+
const right = r() < .5;
|
|
2288
|
+
const nw = span(r, m * .08, m * .15);
|
|
2289
|
+
const nh = span(r, m * .08, m * .15);
|
|
2290
|
+
if (right) {
|
|
2291
|
+
y = int(r, y, y + Math.floor(h / 2));
|
|
2292
|
+
x += w;
|
|
2293
|
+
} else {
|
|
2294
|
+
x = int(r, x, x + Math.floor(w / 2));
|
|
2295
|
+
y += h;
|
|
2296
|
+
}
|
|
2297
|
+
w = nw;
|
|
2298
|
+
h = nh;
|
|
2299
|
+
rects.push({
|
|
2300
|
+
x,
|
|
2301
|
+
y,
|
|
2302
|
+
w,
|
|
2303
|
+
h
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
break;
|
|
2307
|
+
}
|
|
2308
|
+
case "edge": {
|
|
2309
|
+
const n = int(r, 2, 5);
|
|
2310
|
+
for (let i = 0; i < n; i++) {
|
|
2311
|
+
const side = int(r, 0, 3);
|
|
2312
|
+
const w = span(r, m * .06, m * .15);
|
|
2313
|
+
const h = span(r, m * .06, m * .15);
|
|
2314
|
+
const x = side === 1 ? f.x + f.w - w : side === 3 ? f.x : int(r, f.x, f.x + f.w - w);
|
|
2315
|
+
const y = side === 0 ? f.y : side === 2 ? f.y + f.h - h : int(r, f.y, f.y + f.h - h);
|
|
2316
|
+
rects.push({
|
|
2317
|
+
x,
|
|
2318
|
+
y,
|
|
2319
|
+
w,
|
|
2320
|
+
h
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
break;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
return rects.map((rect, i) => ({
|
|
2327
|
+
id: `o${i}`,
|
|
2328
|
+
region: {
|
|
2329
|
+
kind: "rect",
|
|
2330
|
+
...rect
|
|
2331
|
+
}
|
|
2332
|
+
}));
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* This function gives the boxes of a size distribution for a frame. The mean box has an area of 40 percent of the
|
|
2336
|
+
* frame divided by the count. No box is wider or higher than the frame.
|
|
2337
|
+
*/
|
|
2338
|
+
function boxesOf(r, sizes, count, f) {
|
|
2339
|
+
const unit = Math.sqrt(f.w * f.h * .4 / Math.max(count, 1));
|
|
2340
|
+
const fit = (x, max) => Math.min(Math.max(round3(x), .01), max);
|
|
2341
|
+
const boxes = new Array(count);
|
|
2342
|
+
const put = (i, w, h) => {
|
|
2343
|
+
boxes[i] = {
|
|
2344
|
+
id: `b${i}`,
|
|
2345
|
+
w: fit(w, f.w),
|
|
2346
|
+
h: fit(h, f.h)
|
|
2347
|
+
};
|
|
2348
|
+
};
|
|
2349
|
+
switch (sizes) {
|
|
2350
|
+
case "identical":
|
|
2351
|
+
case "nearIdentical": {
|
|
2352
|
+
const aspect = Math.sqrt(between(r, .6, 1.6));
|
|
2353
|
+
const bw = unit * aspect;
|
|
2354
|
+
const bh = unit / aspect;
|
|
2355
|
+
for (let i = 0; i < count; i++) if (sizes === "identical") put(i, bw, bh);
|
|
2356
|
+
else put(i, bw * (1 + between(r, -.03, .03)), bh * (1 + between(r, -.03, .03)));
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
case "uniform":
|
|
2360
|
+
for (let i = 0; i < count; i++) put(i, unit * between(r, .5, 1.5), unit * between(r, .5, 1.5));
|
|
2361
|
+
break;
|
|
2362
|
+
case "heavyTail": {
|
|
2363
|
+
const cap = .6 * Math.min(f.w, f.h);
|
|
2364
|
+
for (let i = 0; i < count; i++) {
|
|
2365
|
+
const s = Math.min(.6 * unit * (1 - r()) ** (-1 / 1.6), cap);
|
|
2366
|
+
put(i, s * between(r, .75, 1.33), s * between(r, .75, 1.33));
|
|
2367
|
+
}
|
|
2368
|
+
break;
|
|
2369
|
+
}
|
|
2370
|
+
case "bimodal":
|
|
2371
|
+
for (let i = 0; i < count; i++) {
|
|
2372
|
+
const s = r() < .25 ? 1.8 * unit : .6 * unit;
|
|
2373
|
+
put(i, s * between(r, .85, 1.15), s * between(r, .85, 1.15));
|
|
2374
|
+
}
|
|
2375
|
+
break;
|
|
2376
|
+
case "extremeAspect": for (let i = 0; i < count; i++) {
|
|
2377
|
+
const long = unit * between(r, 2.5, 4);
|
|
2378
|
+
const short = unit * between(r, .15, .35);
|
|
2379
|
+
if (r() < .5) put(i, long, short);
|
|
2380
|
+
else put(i, short, long);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
return boxes;
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* The generator `rect-grid`, version 1: rectangle boxes for each value of `containerSize`, `containerShape`, `obstacles`
|
|
2387
|
+
* and `sizes`, with each count. The container is a nominal rectangle of 600 to 1000 by 400 to 800.
|
|
2388
|
+
*/
|
|
2389
|
+
const rectGrid = defineGenerator({
|
|
2390
|
+
name: "rect-grid",
|
|
2391
|
+
version: 1,
|
|
2392
|
+
set: "corpus-v1",
|
|
2393
|
+
defaults: {
|
|
2394
|
+
containerSize: "fixed",
|
|
2395
|
+
containerShape: "rect",
|
|
2396
|
+
obstacles: "none",
|
|
2397
|
+
sizes: "uniform",
|
|
2398
|
+
count: 100
|
|
2399
|
+
},
|
|
2400
|
+
values: {
|
|
2401
|
+
containerSize: CONTAINER_SIZES,
|
|
2402
|
+
containerShape: CONTAINER_SHAPES,
|
|
2403
|
+
obstacles: OBSTACLE_LAYOUTS,
|
|
2404
|
+
sizes: SIZE_DISTRIBUTIONS
|
|
2405
|
+
},
|
|
2406
|
+
family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.n${p.count}`,
|
|
2407
|
+
cell: (p) => cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count),
|
|
2408
|
+
build(seed, p) {
|
|
2409
|
+
let r = stepRandom(seed, "container");
|
|
2410
|
+
const w = int(r, 600, 1e3);
|
|
2411
|
+
const h = int(r, 400, 800);
|
|
2412
|
+
const { region, frame } = shapeOf(r, p.containerShape, w, h);
|
|
2413
|
+
const container = containerOf(p.containerSize, w, h, region);
|
|
2414
|
+
r = stepRandom(seed, "obstacles");
|
|
2415
|
+
const obstacles = obstaclesOf(r, p.obstacles, frame);
|
|
2416
|
+
r = stepRandom(seed, "boxes");
|
|
2417
|
+
return {
|
|
2418
|
+
container,
|
|
2419
|
+
obstacles,
|
|
2420
|
+
boxes: boxesOf(r, p.sizes, p.count, frame)
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
/** 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. */
|
|
2425
|
+
function uiGenerator(name, sizes, count, boxes) {
|
|
2426
|
+
return defineGenerator({
|
|
2427
|
+
name,
|
|
2428
|
+
version: 1,
|
|
2429
|
+
set: "corpus-v1",
|
|
2430
|
+
defaults: {
|
|
2431
|
+
containerSize: "freeHeight",
|
|
2432
|
+
count
|
|
2433
|
+
},
|
|
2434
|
+
values: { containerSize: CONTAINER_SIZES },
|
|
2435
|
+
family: (p) => `${name}.${p.containerSize}.n${p.count}`,
|
|
2436
|
+
cell: (p) => cellWith(p.containerSize, "rect", "none", sizes, p.count),
|
|
2437
|
+
build(seed, p) {
|
|
2438
|
+
const r = stepRandom(seed, "container");
|
|
2439
|
+
const w = int(r, 800, 1400);
|
|
2440
|
+
const h = int(r, 600, 1e3);
|
|
2441
|
+
return {
|
|
2442
|
+
container: containerOf(p.containerSize, w, h),
|
|
2443
|
+
obstacles: [],
|
|
2444
|
+
boxes: boxes(stepRandom(seed, "boxes"), p.count)
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
/** The aspects of the thumbnails of a gallery. */
|
|
2450
|
+
const THUMBNAIL_ASPECTS = [
|
|
2451
|
+
1,
|
|
2452
|
+
4 / 3,
|
|
2453
|
+
3 / 2,
|
|
2454
|
+
16 / 9
|
|
2455
|
+
];
|
|
2456
|
+
/** 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. */
|
|
2457
|
+
const gallery = uiGenerator("gallery", "identical", 100, (r, count) => {
|
|
2458
|
+
const aspect = THUMBNAIL_ASPECTS[int(r, 0, THUMBNAIL_ASPECTS.length - 1)];
|
|
2459
|
+
const w = int(r, 120, 240);
|
|
2460
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
2461
|
+
id: `b${i}`,
|
|
2462
|
+
w,
|
|
2463
|
+
h: round3(w / aspect)
|
|
2464
|
+
}));
|
|
2465
|
+
});
|
|
2466
|
+
/** The spans of the dashboard tiles in grid units, and their weights. */
|
|
2467
|
+
const TILE_SPANS = [
|
|
2468
|
+
[1, 1],
|
|
2469
|
+
[2, 1],
|
|
2470
|
+
[1, 2],
|
|
2471
|
+
[2, 2],
|
|
2472
|
+
[3, 2],
|
|
2473
|
+
[4, 2]
|
|
2474
|
+
];
|
|
2475
|
+
const TILE_WEIGHTS = [
|
|
2476
|
+
4,
|
|
2477
|
+
3,
|
|
2478
|
+
2,
|
|
2479
|
+
3,
|
|
2480
|
+
1,
|
|
2481
|
+
1
|
|
2482
|
+
];
|
|
2483
|
+
/** The generator `dashboard`, version 1: tiles in 6 standard sizes, from 1 by 1 to 4 by 2 grid units of 60 to 100. */
|
|
2484
|
+
const dashboard = uiGenerator("dashboard", "bimodal", 10, (r, count) => {
|
|
2485
|
+
const unit = int(r, 60, 100);
|
|
2486
|
+
return Array.from({ length: count }, (_, i) => {
|
|
2487
|
+
const [cw, ch] = TILE_SPANS[pick(r, TILE_WEIGHTS)];
|
|
2488
|
+
return {
|
|
2489
|
+
id: `b${i}`,
|
|
2490
|
+
w: cw * unit,
|
|
2491
|
+
h: ch * unit
|
|
2492
|
+
};
|
|
2493
|
+
});
|
|
2494
|
+
});
|
|
2495
|
+
/** The generator `cards`, version 1: cards with one width from 200 to 320, and a height from 120 to 420 each. */
|
|
2496
|
+
const cards = uiGenerator("cards", "uniform", 100, (r, count) => {
|
|
2497
|
+
const w = int(r, 200, 320);
|
|
2498
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
2499
|
+
id: `b${i}`,
|
|
2500
|
+
w,
|
|
2501
|
+
h: int(r, 120, 420)
|
|
2502
|
+
}));
|
|
2503
|
+
});
|
|
2504
|
+
/** The font sizes of a word cloud, and their weights: many small words and few large words. */
|
|
2505
|
+
const FONT_SIZES = [
|
|
2506
|
+
12,
|
|
2507
|
+
14,
|
|
2508
|
+
16,
|
|
2509
|
+
20,
|
|
2510
|
+
28,
|
|
2511
|
+
40
|
|
2512
|
+
];
|
|
2513
|
+
const FONT_WEIGHTS = [
|
|
2514
|
+
30,
|
|
2515
|
+
25,
|
|
2516
|
+
20,
|
|
2517
|
+
12,
|
|
2518
|
+
8,
|
|
2519
|
+
5
|
|
2520
|
+
];
|
|
2521
|
+
/** The generator `word-cloud`, version 1: boxes of text. The height comes from 6 font sizes, the width from 3 to 12 letters. */
|
|
2522
|
+
const wordCloud = uiGenerator("word-cloud", "heavyTail", 100, (r, count) => Array.from({ length: count }, (_, i) => {
|
|
2523
|
+
const font = FONT_SIZES[pick(r, FONT_WEIGHTS)];
|
|
2524
|
+
const letters = int(r, 3, 12);
|
|
2525
|
+
return {
|
|
2526
|
+
id: `b${i}`,
|
|
2527
|
+
w: round3(letters * font * .6),
|
|
2528
|
+
h: round3(font * 1.25)
|
|
2529
|
+
};
|
|
2530
|
+
}));
|
|
2531
|
+
/** The values of `case`. */
|
|
2532
|
+
const EDGE_CASES = [
|
|
2533
|
+
"empty",
|
|
2534
|
+
"one",
|
|
2535
|
+
"oversize",
|
|
2536
|
+
"exactTiling",
|
|
2537
|
+
"touchObstacles"
|
|
2538
|
+
];
|
|
2539
|
+
/** This function gives the largest divisor of `n` that is not more than the square root of `n`. */
|
|
2540
|
+
const rowsOf = (n) => {
|
|
2541
|
+
for (let d = Math.floor(Math.sqrt(n)); d > 1; d--) if (n % d === 0) return d;
|
|
2542
|
+
return 1;
|
|
2543
|
+
};
|
|
2544
|
+
/**
|
|
2545
|
+
* The generator `edge-cases`, version 1. Each case has a fixed rectangle container.
|
|
2546
|
+
*
|
|
2547
|
+
* - `empty`: no box.
|
|
2548
|
+
* - `one`: one box.
|
|
2549
|
+
* - `oversize`: `count` boxes. One box is wider than the container.
|
|
2550
|
+
* - `exactTiling`: `count` identical boxes that fill the container with no free space.
|
|
2551
|
+
* - `touchObstacles`: a locked box on the top of an obstacle, and boxes as wide as the path between 2 obstacles.
|
|
2552
|
+
*/
|
|
2553
|
+
const edgeCases = defineGenerator({
|
|
2554
|
+
name: "edge-cases",
|
|
2555
|
+
version: 1,
|
|
2556
|
+
set: "corpus-v1",
|
|
2557
|
+
defaults: {
|
|
2558
|
+
case: "oversize",
|
|
2559
|
+
count: 12
|
|
2560
|
+
},
|
|
2561
|
+
values: { case: EDGE_CASES },
|
|
2562
|
+
family: (p) => p.case === "empty" || p.case === "one" ? `edge-cases.${p.case}` : `edge-cases.${p.case}.n${p.count}`,
|
|
2563
|
+
cell: (p) => {
|
|
2564
|
+
switch (p.case) {
|
|
2565
|
+
case "empty": return cellWith("fixed", "rect", "none", "identical", 0);
|
|
2566
|
+
case "one": return cellWith("fixed", "rect", "none", "identical", 1);
|
|
2567
|
+
case "oversize": return cellWith("fixed", "rect", "none", "heavyTail", p.count);
|
|
2568
|
+
case "exactTiling": return cellWith("fixed", "rect", "none", "identical", p.count);
|
|
2569
|
+
case "touchObstacles": return cellWith("fixed", "rect", "corridors", "uniform", p.count, true);
|
|
2570
|
+
}
|
|
2571
|
+
},
|
|
2572
|
+
build(seed, p) {
|
|
2573
|
+
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.`);
|
|
2574
|
+
let r = stepRandom(seed, "container");
|
|
2575
|
+
let w;
|
|
2576
|
+
let h;
|
|
2577
|
+
let tile = {
|
|
2578
|
+
w: 0,
|
|
2579
|
+
h: 0,
|
|
2580
|
+
cols: 0,
|
|
2581
|
+
rows: 0
|
|
2582
|
+
};
|
|
2583
|
+
if (p.case === "exactTiling") {
|
|
2584
|
+
const rows = rowsOf(p.count);
|
|
2585
|
+
tile = {
|
|
2586
|
+
w: int(r, 40, 120),
|
|
2587
|
+
h: int(r, 30, 90),
|
|
2588
|
+
cols: p.count / rows,
|
|
2589
|
+
rows
|
|
2590
|
+
};
|
|
2591
|
+
w = tile.cols * tile.w;
|
|
2592
|
+
h = tile.rows * tile.h;
|
|
2593
|
+
} else {
|
|
2594
|
+
w = int(r, 600, 1e3);
|
|
2595
|
+
h = int(r, 400, 800);
|
|
2596
|
+
}
|
|
2597
|
+
const container = {
|
|
2598
|
+
w,
|
|
2599
|
+
h,
|
|
2600
|
+
fixed: "size"
|
|
2601
|
+
};
|
|
2602
|
+
r = stepRandom(seed, "obstacles");
|
|
2603
|
+
const obstacles = [];
|
|
2604
|
+
let path = 0;
|
|
2605
|
+
let first = {
|
|
2606
|
+
x: 0,
|
|
2607
|
+
y: 0
|
|
2608
|
+
};
|
|
2609
|
+
if (p.case === "touchObstacles") {
|
|
2610
|
+
path = int(r, 40, 100);
|
|
2611
|
+
const ow = int(r, 60, 150);
|
|
2612
|
+
const oh = int(r, 100, 200);
|
|
2613
|
+
first = {
|
|
2614
|
+
x: span(r, w * .1, w * .25),
|
|
2615
|
+
y: span(r, h * .2, h * .4)
|
|
2616
|
+
};
|
|
2617
|
+
obstacles.push({
|
|
2618
|
+
id: "o0",
|
|
2619
|
+
region: {
|
|
2620
|
+
kind: "rect",
|
|
2621
|
+
x: first.x,
|
|
2622
|
+
y: first.y,
|
|
2623
|
+
w: ow,
|
|
2624
|
+
h: oh
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
obstacles.push({
|
|
2628
|
+
id: "o1",
|
|
2629
|
+
region: {
|
|
2630
|
+
kind: "rect",
|
|
2631
|
+
x: first.x + ow + path,
|
|
2632
|
+
y: first.y,
|
|
2633
|
+
w: int(r, 60, 150),
|
|
2634
|
+
h: oh
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
r = stepRandom(seed, "boxes");
|
|
2639
|
+
const boxes = [];
|
|
2640
|
+
switch (p.case) {
|
|
2641
|
+
case "empty": break;
|
|
2642
|
+
case "one":
|
|
2643
|
+
boxes.push({
|
|
2644
|
+
id: "b0",
|
|
2645
|
+
w: int(r, 40, 200),
|
|
2646
|
+
h: int(r, 30, 150)
|
|
2647
|
+
});
|
|
2648
|
+
break;
|
|
2649
|
+
case "oversize": {
|
|
2650
|
+
const big = int(r, 0, p.count - 1);
|
|
2651
|
+
for (let i = 0; i < p.count; i++) if (i === big) boxes.push({
|
|
2652
|
+
id: `b${i}`,
|
|
2653
|
+
w: w + int(r, 10, 200),
|
|
2654
|
+
h: int(r, 40, 100)
|
|
2655
|
+
});
|
|
2656
|
+
else boxes.push({
|
|
2657
|
+
id: `b${i}`,
|
|
2658
|
+
w: int(r, 20, 120),
|
|
2659
|
+
h: int(r, 20, 80)
|
|
2660
|
+
});
|
|
2661
|
+
break;
|
|
2662
|
+
}
|
|
2663
|
+
case "exactTiling":
|
|
2664
|
+
for (let i = 0; i < p.count; i++) boxes.push({
|
|
2665
|
+
id: `b${i}`,
|
|
2666
|
+
w: tile.w,
|
|
2667
|
+
h: tile.h
|
|
2668
|
+
});
|
|
2669
|
+
break;
|
|
2670
|
+
case "touchObstacles": {
|
|
2671
|
+
const lw = int(r, 30, 80);
|
|
2672
|
+
const lh = int(r, 20, 60);
|
|
2673
|
+
boxes.push({
|
|
2674
|
+
id: "b0",
|
|
2675
|
+
w: lw,
|
|
2676
|
+
h: lh,
|
|
2677
|
+
x: first.x,
|
|
2678
|
+
y: first.y - lh,
|
|
2679
|
+
locked: true
|
|
2680
|
+
});
|
|
2681
|
+
for (let i = 1; i < p.count; i++) boxes.push({
|
|
2682
|
+
id: `b${i}`,
|
|
2683
|
+
w: path,
|
|
2684
|
+
h: int(r, 20, 80)
|
|
2685
|
+
});
|
|
2686
|
+
break;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return {
|
|
2690
|
+
container,
|
|
2691
|
+
obstacles,
|
|
2692
|
+
boxes
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
/** Each generator of the package. */
|
|
2697
|
+
const generators = Object.freeze([
|
|
2698
|
+
rectScattered,
|
|
2699
|
+
rectGrid,
|
|
2700
|
+
gallery,
|
|
2701
|
+
dashboard,
|
|
2702
|
+
cards,
|
|
2703
|
+
wordCloud,
|
|
2704
|
+
edgeCases
|
|
2705
|
+
]);
|
|
2706
|
+
/** This function gives the generator with a name. It throws a `RangeError` for a name that the package does not have. */
|
|
2707
|
+
function generator(name) {
|
|
2708
|
+
const found = generators.find((g) => g.name === name);
|
|
2709
|
+
if (found === void 0) throw new RangeError(`The package has no generator "${name}".`);
|
|
2710
|
+
return found;
|
|
2711
|
+
}
|
|
2712
|
+
// src/corpus.ts
|
|
2713
|
+
/**
|
|
2714
|
+
* This function generates a corpus from specs. Each input gets the name of the corpus as its set. The function throws a
|
|
2715
|
+
* `RangeError` for an unknown generator, a different version or 2 inputs with the same id.
|
|
2716
|
+
*/
|
|
2717
|
+
function corpus(name, specs) {
|
|
2718
|
+
const manifest = {
|
|
2719
|
+
schema: "layout-corpus/1",
|
|
2720
|
+
corpus: name,
|
|
2721
|
+
specs: [],
|
|
2722
|
+
inputs: []
|
|
2723
|
+
};
|
|
2724
|
+
const inputs = [];
|
|
2725
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2726
|
+
for (const spec of specs) {
|
|
2727
|
+
const gen = generator(spec.generator);
|
|
2728
|
+
if (spec.version !== void 0 && spec.version !== gen.version) throw new RangeError(`The generator "${gen.name}" has the version ${gen.version}, not ${spec.version}.`);
|
|
2729
|
+
const params = gen.withDefaults(spec.params);
|
|
2730
|
+
const cell = gen.cell(params);
|
|
2731
|
+
manifest.specs.push({
|
|
2732
|
+
generator: gen.name,
|
|
2733
|
+
version: gen.version,
|
|
2734
|
+
params,
|
|
2735
|
+
seeds: [...spec.seeds]
|
|
2736
|
+
});
|
|
2737
|
+
for (const seed of spec.seeds) {
|
|
2738
|
+
const file = gen.generate(seed, params, name);
|
|
2739
|
+
if (ids.has(file.id)) throw new RangeError(`The corpus has 2 inputs with the id "${file.id}".`);
|
|
2740
|
+
ids.add(file.id);
|
|
2741
|
+
inputs.push(file);
|
|
2742
|
+
manifest.inputs.push({
|
|
2743
|
+
id: file.id,
|
|
2744
|
+
hash: file.hash,
|
|
2745
|
+
cell
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
return {
|
|
2750
|
+
manifest,
|
|
2751
|
+
inputs
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
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 };
|
|
662
2755
|
|
|
663
2756
|
//# sourceMappingURL=index.js.map
|