@mark1russell7/packing 0.2.11 → 0.2.13

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
@@ -369,27 +369,38 @@ function readFrame(input, frame, trace, moved = [], turns) {
369
369
  };
370
370
  }
371
371
  /**
372
- * This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
373
- * far edge of the items with their margins and the inset, or of the obstacles.
372
+ * This function gives the far edges of a packing from the corner of the container. It reads the items with their margins,
373
+ * the inset and the obstacles. With `turn`, a placement with `turn: 90` reads the turned item.
374
374
  */
375
- function sizeOf(input, frame, placements, moved, turns) {
375
+ function extentOf(input, placements, moved, origin = containerBounds(input), inset = input.container.inset ?? 0, turn) {
376
376
  const items = new Map(input.items.map((i) => [i.id, i]));
377
377
  let right = 0;
378
378
  let bottom = 0;
379
379
  for (const p of placements) {
380
380
  const given = items.get(p.id);
381
- const item = p.turn === 90 && turns !== void 0 ? turns.turn(given) : given;
381
+ const item = p.turn === 90 && turn !== void 0 ? turn(given) : given;
382
382
  const m = marginOf(item);
383
- right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
384
- bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
383
+ right = Math.max(right, p.x + item.w + m.right + inset - origin.x);
384
+ bottom = Math.max(bottom, p.y + item.h + m.bottom + inset - origin.y);
385
385
  }
386
386
  const movedById = new Map(moved.map((m) => [m.id, m]));
387
387
  for (const o of input.obstacles ?? []) {
388
388
  const b = boundsOf(o.region);
389
389
  const at = movedById.get(o.id) ?? b;
390
- right = Math.max(right, at.x + b.w - frame.bounds.x);
391
- bottom = Math.max(bottom, at.y + b.h - frame.bounds.y);
390
+ right = Math.max(right, at.x + b.w - origin.x);
391
+ bottom = Math.max(bottom, at.y + b.h - origin.y);
392
392
  }
393
+ return {
394
+ right,
395
+ bottom
396
+ };
397
+ }
398
+ /**
399
+ * This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
400
+ * far edge of the items with their margins and the inset, or of the obstacles.
401
+ */
402
+ function sizeOf(input, frame, placements, moved, turns) {
403
+ const { right, bottom } = extentOf(input, placements, moved, frame.bounds, frame.inset, turns?.turn);
393
404
  return {
394
405
  w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
395
406
  h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom
@@ -867,23 +878,44 @@ function inside(a, b) {
867
878
  */
868
879
  function cut(free, b, minW, minH) {
869
880
  const kept = [];
870
- const parts = [];
871
- const sides = [];
881
+ const near = [
882
+ [],
883
+ [],
884
+ [],
885
+ []
886
+ ];
887
+ const stay = [
888
+ [],
889
+ [],
890
+ [],
891
+ []
892
+ ];
893
+ let made = 0;
872
894
  let hits = 0;
873
895
  const add = (side, x0, y0, x1, y1) => {
874
- if (x1 - x0 >= minW && y1 - y0 >= minH) {
875
- parts.push({
876
- x0,
877
- y0,
878
- x1,
879
- y1
880
- });
881
- sides.push(side);
896
+ if (x1 - x0 < minW || y1 - y0 < minH) return;
897
+ const own = stay[side];
898
+ for (let k = 0; k < own.length;) {
899
+ const qx0 = own[k];
900
+ const qy0 = own[k + 1];
901
+ const qx1 = own[k + 2];
902
+ const qy1 = own[k + 3];
903
+ if (x0 >= qx0 && y0 >= qy0 && x1 <= qx1 && y1 <= qy1) return;
904
+ if (qx0 >= x0 && qy0 >= y0 && qx1 <= x1 && qy1 <= y1) {
905
+ const last = own.length - 5;
906
+ if (k !== last) for (let n = 0; n < 5; n++) own[k + n] = own[last + n];
907
+ own.length = last;
908
+ } else k += 5;
882
909
  }
910
+ own.push(x0, y0, x1, y1, made++);
883
911
  };
884
912
  for (const f of free) {
885
913
  if (b.x0 >= f.x1 || b.x1 <= f.x0 || b.y0 >= f.y1 || b.y1 <= f.y0) {
886
914
  kept.push(f);
915
+ if (f.x1 === b.x0) near[0].push(f);
916
+ if (f.x0 === b.x1) near[1].push(f);
917
+ if (f.y1 === b.y0) near[2].push(f);
918
+ if (f.y0 === b.y1) near[3].push(f);
887
919
  continue;
888
920
  }
889
921
  hits++;
@@ -892,67 +924,34 @@ function cut(free, b, minW, minH) {
892
924
  if (b.y0 > f.y0) add(2, f.x0, f.y0, f.x1, b.y0);
893
925
  if (b.y1 < f.y1) add(3, f.x0, b.y1, f.x1, f.y1);
894
926
  }
895
- if (parts.length === 0) return {
927
+ if (made === 0) return {
896
928
  free: kept,
897
929
  hits
898
930
  };
899
- const near = [
900
- [],
901
- [],
902
- [],
903
- []
904
- ];
905
- for (const k of kept) {
906
- if (k.x1 === b.x0) near[0].push(k);
907
- if (k.x0 === b.x1) near[1].push(k);
908
- if (k.y1 === b.y0) near[2].push(k);
909
- if (k.y0 === b.y1) near[3].push(k);
910
- }
911
- const order = parts.map((_, i) => i).sort((i, j) => byExtent(parts[i], parts[j]) || i - j);
912
- const gone = new Uint8Array(parts.length);
913
- const stay = [
914
- [],
915
- [],
916
- [],
917
- []
918
- ];
919
- for (const i of order) {
920
- const p = parts[i];
921
- const own = stay[sides[i]];
922
- let drop = false;
923
- for (const q of own) if (inside(p, q)) {
924
- drop = true;
925
- break;
926
- }
927
- if (drop) gone[i] = 1;
928
- else own.push(p);
929
- }
930
- for (let i = 0; i < parts.length; i++) {
931
- if (gone[i] === 1) continue;
932
- const p = parts[i];
933
- let drop = false;
934
- for (const k of near[sides[i]]) if (inside(p, k)) {
935
- drop = true;
936
- break;
931
+ const out = new Array(made).fill(void 0);
932
+ for (let side = 0; side < 4; side++) {
933
+ const own = stay[side];
934
+ for (let k = 0; k < own.length; k += 5) {
935
+ const p = {
936
+ x0: own[k],
937
+ y0: own[k + 1],
938
+ x1: own[k + 2],
939
+ y1: own[k + 3]
940
+ };
941
+ let drop = false;
942
+ for (const q of near[side]) if (inside(p, q)) {
943
+ drop = true;
944
+ break;
945
+ }
946
+ if (!drop) out[own[k + 4]] = p;
937
947
  }
938
- if (!drop) kept.push(p);
939
948
  }
949
+ for (const p of out) if (p !== void 0) kept.push(p);
940
950
  return {
941
951
  free: kept,
942
952
  hits
943
953
  };
944
954
  }
945
- /**
946
- * This function orders two boxes. The smaller `x0` comes first, then the smaller `y0`, then the larger `x1`, then the
947
- * larger `y1`. Thus a box comes before each other box in it.
948
- */
949
- function byExtent(a, b) {
950
- if (a.x0 !== b.x0) return a.x0 < b.x0 ? -1 : 1;
951
- if (a.y0 !== b.y0) return a.y0 < b.y0 ? -1 : 1;
952
- if (a.x1 !== b.x1) return a.x1 > b.x1 ? -1 : 1;
953
- if (a.y1 !== b.y1) return a.y1 > b.y1 ? -1 : 1;
954
- return 0;
955
- }
956
955
  /** This function adds a range to the edges at a coordinate. */
957
956
  function addEdge(edges, at, a, b) {
958
957
  const list = edges.get(at);
@@ -3630,6 +3629,346 @@ function toRects(output, rects, outer) {
3630
3629
  }
3631
3630
  return outer;
3632
3631
  }
3632
+ // src/check.ts
3633
+ /** This function tells if the insides of two boxes overlap by more than a tolerance on each axis. */
3634
+ function overlap(a, b, tolerance) {
3635
+ 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;
3636
+ }
3637
+ /** This function gives the box made smaller by a tolerance on each side, as a rectangle. */
3638
+ function inner(box, tolerance) {
3639
+ return {
3640
+ x: box.l + tolerance,
3641
+ y: box.t + tolerance,
3642
+ w: box.r - box.l - 2 * tolerance,
3643
+ h: box.b - box.t - 2 * tolerance
3644
+ };
3645
+ }
3646
+ /**
3647
+ * This function counts the breaks of the 4 hard rules of the packer for an output.
3648
+ *
3649
+ * - `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`.
3650
+ * - `in-container`: each item with its margin that goes out of a fixed side by more than `tolerance.overlap`. The inset of
3651
+ * the container makes each fixed side smaller (decision 3 of `docs/PACKER.md`).
3652
+ * - `locked-kept`: each locked item that moved by more than `RECORD_EPS`, or that has no placement.
3653
+ * - `complete`: each item with no placement that is not in the overflow. A locked item breaks `locked-kept` in its place.
3654
+ *
3655
+ * A locked item with no placement stays at its input position for `no-overlap` and `in-container`. A locked item with no
3656
+ * `x` or no `y` in the input is a bad input. The rules skip it, as `packHard` of the contract does. `checkInput` finds it.
3657
+ *
3658
+ * The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
3659
+ * The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
3660
+ * shape is relative to the position of the item. The result has no tolerance in it.
3661
+ *
3662
+ * With `options.exactContainer`, a shaped container with the size `fixed` also uses its true outline for `in-container`.
3663
+ * A box then counts when `rectInRegion` of the contract tells that it is not in the shape, as in `packHard`.
3664
+ * For that test, the box grows by the inset and becomes smaller by `tolerance.overlap` on each side. The default is `false`.
3665
+ */
3666
+ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE, options = {}) {
3667
+ const tol = tolerance.overlap;
3668
+ const placements = /* @__PURE__ */ new Map();
3669
+ for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
3670
+ const overflow = new Set(output.overflow);
3671
+ let lockedKept = 0;
3672
+ let complete = 0;
3673
+ const boxes = [];
3674
+ for (const item of input.items) {
3675
+ if (item.locked === true && (item.x === void 0 || item.y === void 0)) continue;
3676
+ const lockedAt = item.locked === true ? {
3677
+ x: item.x,
3678
+ y: item.y
3679
+ } : void 0;
3680
+ let p = placements.get(item.id);
3681
+ if (p === void 0) {
3682
+ if (lockedAt === void 0) {
3683
+ if (!overflow.has(item.id)) complete += 1;
3684
+ continue;
3685
+ }
3686
+ lockedKept += 1;
3687
+ p = lockedAt;
3688
+ } else if (lockedAt !== void 0) {
3689
+ if (Math.abs(p.x - lockedAt.x) > RECORD_EPS || Math.abs(p.y - lockedAt.y) > RECORD_EPS) lockedKept += 1;
3690
+ }
3691
+ const placed = p.turn === 90 ? turnItem(item) : item;
3692
+ const m = marginOf(placed);
3693
+ const area = placed.shape === void 0 ? {
3694
+ x: 0,
3695
+ y: 0,
3696
+ w: placed.w,
3697
+ h: placed.h
3698
+ } : boundsOf(placed.shape);
3699
+ const l = p.x + area.x;
3700
+ const t = p.y + area.y;
3701
+ boxes.push({
3702
+ l: l - m.left,
3703
+ t: t - m.top,
3704
+ r: l + area.w + m.right,
3705
+ b: t + area.h + m.bottom
3706
+ });
3707
+ }
3708
+ const moved = new Map(output.moved.map((m) => [m.id, m]));
3709
+ const obstacles = (input.obstacles ?? []).map((o) => {
3710
+ const at = moved.get(o.id);
3711
+ if (at === void 0) return o.region;
3712
+ const b = boundsOf(o.region);
3713
+ return translate(o.region, at.x - b.x, at.y - b.y);
3714
+ });
3715
+ let overlaps = 0;
3716
+ for (const [i, a] of boxes.entries()) {
3717
+ for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) overlaps += 1;
3718
+ if (obstacles.length === 0) continue;
3719
+ const rect = inner(a, tol);
3720
+ for (const region of obstacles) if (regionHitsRect(region, rect)) overlaps += 1;
3721
+ }
3722
+ const { container } = input;
3723
+ const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
3724
+ const x = shape?.x ?? 0;
3725
+ const y = shape?.y ?? 0;
3726
+ const w = shape?.w ?? container.w ?? Infinity;
3727
+ const h = shape?.h ?? container.h ?? Infinity;
3728
+ const inset = container.inset ?? 0;
3729
+ const fixedW = container.fixed === "size" || container.fixed === "width";
3730
+ const fixedH = container.fixed === "size" || container.fixed === "height";
3731
+ const exact = options.exactContainer === true && container.fixed === "size" ? container.shape : void 0;
3732
+ let outside = 0;
3733
+ for (const box of boxes) {
3734
+ const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
3735
+ const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
3736
+ if (outX || outY) outside += 1;
3737
+ else if (exact !== void 0 && !rectInRegion(exact, inner({
3738
+ l: box.l - inset,
3739
+ t: box.t - inset,
3740
+ r: box.r + inset,
3741
+ b: box.b + inset
3742
+ }, tol))) outside += 1;
3743
+ }
3744
+ return {
3745
+ "no-overlap": overlaps,
3746
+ "in-container": outside,
3747
+ "locked-kept": lockedKept,
3748
+ complete
3749
+ };
3750
+ }
3751
+ // src/select-key.ts
3752
+ /** This function gives the count of the hard breaks of an output: the sum of `checkHard` with `exactContainer`. */
3753
+ function hardBreaks(input, output) {
3754
+ return Object.values(checkHard(input, output, void 0, { exactContainer: true })).reduce((sum, n) => sum + n, 0);
3755
+ }
3756
+ /** This function gives the level of a measure id with its spec of `MEASURE_SPECS`. It throws a `RangeError` for an unknown id. */
3757
+ function measureLevel(id) {
3758
+ const spec = MEASURE_SPECS[id];
3759
+ if (spec === void 0 || id === "hard") throw new RangeError(`A level of a key is a measure of MEASURE_SPECS, not "${id}".`);
3760
+ return {
3761
+ id,
3762
+ spec
3763
+ };
3764
+ }
3765
+ /**
3766
+ * This function makes the key of a pick: `hardBreaks`, then the measures of `levels` in order, then `outputHash`.
3767
+ *
3768
+ * - The levels read the measures after `roundMeasures`, as a record keeps them. A replay of the records gives the same pick.
3769
+ * - The measures of one output are computed one time.
3770
+ * - `view` gives the input of the measures. The default is the input. The hard breaks always read the input.
3771
+ */
3772
+ function measureKey(levels, view = (input) => input) {
3773
+ const cache = /* @__PURE__ */ new WeakMap();
3774
+ const measured = (input, output) => {
3775
+ let m = cache.get(output);
3776
+ if (m === void 0) {
3777
+ m = roundMeasures(measure(view(input), output));
3778
+ cache.set(output, m);
3779
+ }
3780
+ return m;
3781
+ };
3782
+ return {
3783
+ hard: hardBreaks,
3784
+ levels: levels.map((level) => ({
3785
+ id: level.id,
3786
+ spec: level.spec,
3787
+ value: (input, output) => measured(input, output)[level.id]
3788
+ })),
3789
+ hash: outputHash
3790
+ };
3791
+ }
3792
+ // src/aspect-fit.ts
3793
+ /** The id of the trace stage of `aspectFit`. Its measure `move.aspectPick` is 1 when the pick is not the output of the wrapped strategy. */
3794
+ const ASPECT_FIT_STAGE = "aspect-fit";
3795
+ /**
3796
+ * The factors of the widths of `aspectFit`. Each width is a factor times the square root of the area of the first output
3797
+ * times the target aspect. The steps are approximately 5% of the width, thus approximately 0.1 of the aspect error.
3798
+ */
3799
+ const ASPECT_FIT_FACTORS = Object.freeze([
3800
+ .86,
3801
+ .9,
3802
+ .95,
3803
+ 1,
3804
+ 1.05,
3805
+ 1.1,
3806
+ 1.16
3807
+ ]);
3808
+ /**
3809
+ * The levels of the pick of `aspectFit`, in order. Level 0 is `hard`.
3810
+ *
3811
+ * - `hard`: the count of hard breaks of `checkHard` with `exactContainer`. Lower is better, with the minimum effect 0.
3812
+ * - `pack.overflow.excessArea`: the overflow area above its bound. Lower is better.
3813
+ * - `pack.aspectError`: the distance from the target aspect. Lower is better, with the minimum effect 0.01.
3814
+ * - `pack.area`: the area of the hull. Lower is better, with the minimum effect of 1% of the area.
3815
+ */
3816
+ const ASPECT_FIT_LEVELS = Object.freeze([
3817
+ {
3818
+ id: "hard",
3819
+ spec: {
3820
+ direction: "lower",
3821
+ minEffect: 0
3822
+ }
3823
+ },
3824
+ {
3825
+ id: "pack.overflow.excessArea",
3826
+ spec: MEASURE_SPECS["pack.overflow.excessArea"]
3827
+ },
3828
+ {
3829
+ id: "pack.aspectError",
3830
+ spec: MEASURE_SPECS["pack.aspectError"]
3831
+ },
3832
+ {
3833
+ id: "pack.area",
3834
+ spec: MEASURE_SPECS["pack.area"]
3835
+ }
3836
+ ]);
3837
+ /** This function gives a shape with the origin of `shape` and the width `w`. A shape that is not a rectangle keeps the holes of its polygon. */
3838
+ function stripOf(shape, w) {
3839
+ const b = boundsOf$1(shape);
3840
+ if (shape.kind === "rect") return {
3841
+ kind: "rect",
3842
+ x: b.x,
3843
+ y: b.y,
3844
+ w,
3845
+ h: b.h
3846
+ };
3847
+ const points = [
3848
+ [b.x, b.y],
3849
+ [b.x + w, b.y],
3850
+ [b.x + w, b.y + b.h],
3851
+ [b.x, b.y + b.h]
3852
+ ];
3853
+ return shape.kind === "polygon" && shape.holes !== void 0 ? {
3854
+ kind: "polygon",
3855
+ points,
3856
+ holes: shape.holes
3857
+ } : {
3858
+ kind: "polygon",
3859
+ points
3860
+ };
3861
+ }
3862
+ /** This function gives an input with the fixed width `w` and a free height. A shape becomes a strip of the width `w`. */
3863
+ function atWidth(input, w) {
3864
+ const c = input.container;
3865
+ return {
3866
+ ...input,
3867
+ container: {
3868
+ ...c,
3869
+ fixed: "width",
3870
+ w,
3871
+ ...c.shape === void 0 ? {} : { shape: stripOf(c.shape, w) }
3872
+ }
3873
+ };
3874
+ }
3875
+ /** This function gives the widths of `aspectFit` for an input and the first output. Each width is narrower than a fixed width. */
3876
+ function widthsOf(input, first, target) {
3877
+ const area = measure(input, first)["pack.area"];
3878
+ if (!(area > 0)) return [];
3879
+ const inset = input.container.inset ?? 0;
3880
+ const fixed = input.container.fixed === "width" ? containerBounds(input).w : Infinity;
3881
+ let widest = 0;
3882
+ for (const item of input.items) {
3883
+ const m = marginOf(item);
3884
+ widest = Math.max(widest, item.w + m.left + m.right);
3885
+ }
3886
+ const ideal = Math.sqrt(area * target);
3887
+ const out = [];
3888
+ for (const f of ASPECT_FIT_FACTORS) {
3889
+ const w = Math.max(Math.round(f * ideal + 2 * inset), Math.ceil(widest + 2 * inset));
3890
+ if (w < fixed && !out.includes(w)) out.push(w);
3891
+ }
3892
+ return out;
3893
+ }
3894
+ /** This function packs an input with a free height or a free size near the target aspect. */
3895
+ function fit(input, target, inner, options, context) {
3896
+ const fresh = () => createContext(context.seed, {
3897
+ ...context.budget === void 0 ? {} : { budget: context.budget },
3898
+ ...context.previous === void 0 ? {} : { previous: context.previous }
3899
+ });
3900
+ const first = inner.pack(input, options, fresh());
3901
+ const widths = widthsOf(input, first, target);
3902
+ if (widths.length === 0) return first;
3903
+ const origin = containerBounds(input);
3904
+ const inset = input.container.inset ?? 0;
3905
+ const free = input.container.fixed === "none";
3906
+ const sized = (out) => {
3907
+ const w = free ? extentOf(input, out.placements, out.moved, origin, inset, turnItem).right : origin.w;
3908
+ return {
3909
+ ...out,
3910
+ size: {
3911
+ w,
3912
+ h: out.size.h
3913
+ }
3914
+ };
3915
+ };
3916
+ const members = [{
3917
+ id: inner.id,
3918
+ run: () => first
3919
+ }, ...widths.map((w) => ({
3920
+ id: `${inner.id}@${w}`,
3921
+ run: (x) => sized(inner.pack(atWidth(x, w), options, fresh()))
3922
+ }))];
3923
+ const view = (x) => x.container.aspect === target ? x : {
3924
+ ...x,
3925
+ container: {
3926
+ ...x.container,
3927
+ aspect: target
3928
+ }
3929
+ };
3930
+ const got = portfolio(ASPECT_FIT_STAGE, members, measureKey(ASPECT_FIT_LEVELS.slice(1), view)).pick(input);
3931
+ const stage = {
3932
+ id: ASPECT_FIT_STAGE,
3933
+ measures: { "move.aspectPick": pickMoves(got.trace).pick }
3934
+ };
3935
+ return {
3936
+ ...got.output,
3937
+ trace: [...got.output.trace ?? [], stage]
3938
+ };
3939
+ }
3940
+ /**
3941
+ * This function wraps a strategy. For a free-size container, it packs at some widths near the target aspect and keeps the best output.
3942
+ *
3943
+ * - The target is `targetAspect`, else `aspect` of the container, else 1.
3944
+ * - A container with the size `fixed` goes to the wrapped strategy without a change.
3945
+ * - The first member is the output of the wrapped strategy. Each other member packs at one fixed width of `ASPECT_FIT_FACTORS`.
3946
+ * - The key is `ASPECT_FIT_LEVELS`, then `outputHash`. The aspect error reads the target. `selectBest` of the contract picks the winner.
3947
+ * - A fixed height goes through the turn of the axes, with the target `1 / target`.
3948
+ * - The function throws a `RangeError` for a target that is not a number above 0.
3949
+ */
3950
+ function aspectFit(inner, options = {}) {
3951
+ const given = options.targetAspect;
3952
+ if (given !== void 0 && !(Number.isFinite(given) && given > 0)) throw new RangeError(`The target aspect of aspectFit is a number above 0, not ${String(given)}.`);
3953
+ return {
3954
+ id: `aspect-fit(${inner.id})`,
3955
+ family: inner.family,
3956
+ catalog: [],
3957
+ supports: inner.supports,
3958
+ defaults: inner.defaults,
3959
+ deterministic: inner.deterministic,
3960
+ pack(input, o, context) {
3961
+ const c = input.container;
3962
+ if (c.fixed === "size") return inner.pack(input, o, context);
3963
+ const target = given ?? c.aspect ?? 1;
3964
+ if (c.fixed === "height") return viaAxes(input, context.previous, (turned, previous) => fit(turned, 1 / target, inner, o, {
3965
+ ...context,
3966
+ ...previous === void 0 ? {} : { previous }
3967
+ }));
3968
+ return fit(input, target, inner, o, context);
3969
+ }
3970
+ };
3971
+ }
3633
3972
  // src/portfolio-sets.ts
3634
3973
  /** The table of the per-cell portfolio: the sets of 1, 2 and 3 members of each cell, with their training scores. The run 40652567756 gives it. */
3635
3974
  const PORTFOLIO_SETS = {
@@ -5851,125 +6190,6 @@ function portfolioDefaultMembers(input, k, table = PORTFOLIO_SETS) {
5851
6190
  const count = portfolioSetOf(input, table).sets.defaultMembers ?? 3;
5852
6191
  return k === void 0 ? count : Math.min(count, k);
5853
6192
  }
5854
- // src/check.ts
5855
- /** This function tells if the insides of two boxes overlap by more than a tolerance on each axis. */
5856
- function overlap(a, b, tolerance) {
5857
- 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;
5858
- }
5859
- /** This function gives the box made smaller by a tolerance on each side, as a rectangle. */
5860
- function inner(box, tolerance) {
5861
- return {
5862
- x: box.l + tolerance,
5863
- y: box.t + tolerance,
5864
- w: box.r - box.l - 2 * tolerance,
5865
- h: box.b - box.t - 2 * tolerance
5866
- };
5867
- }
5868
- /**
5869
- * This function counts the breaks of the 4 hard rules of the packer for an output.
5870
- *
5871
- * - `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`.
5872
- * - `in-container`: each item with its margin that goes out of a fixed side by more than `tolerance.overlap`. The inset of
5873
- * the container makes each fixed side smaller (decision 3 of `docs/PACKER.md`).
5874
- * - `locked-kept`: each locked item that moved by more than `RECORD_EPS`, or that has no placement.
5875
- * - `complete`: each item with no placement that is not in the overflow. A locked item breaks `locked-kept` in its place.
5876
- *
5877
- * A locked item with no placement stays at its input position for `no-overlap` and `in-container`. A locked item with no
5878
- * `x` or no `y` in the input is a bad input. The rules skip it, as `packHard` of the contract does. `checkInput` finds it.
5879
- *
5880
- * The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
5881
- * The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
5882
- * shape is relative to the position of the item. The result has no tolerance in it.
5883
- *
5884
- * With `options.exactContainer`, a shaped container with the size `fixed` also uses its true outline for `in-container`.
5885
- * A box then counts when `rectInRegion` of the contract tells that it is not in the shape, as in `packHard`.
5886
- * For that test, the box grows by the inset and becomes smaller by `tolerance.overlap` on each side. The default is `false`.
5887
- */
5888
- function checkHard(input, output, tolerance = DEFAULT_TOLERANCE, options = {}) {
5889
- const tol = tolerance.overlap;
5890
- const placements = /* @__PURE__ */ new Map();
5891
- for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
5892
- const overflow = new Set(output.overflow);
5893
- let lockedKept = 0;
5894
- let complete = 0;
5895
- const boxes = [];
5896
- for (const item of input.items) {
5897
- if (item.locked === true && (item.x === void 0 || item.y === void 0)) continue;
5898
- const lockedAt = item.locked === true ? {
5899
- x: item.x,
5900
- y: item.y
5901
- } : void 0;
5902
- let p = placements.get(item.id);
5903
- if (p === void 0) {
5904
- if (lockedAt === void 0) {
5905
- if (!overflow.has(item.id)) complete += 1;
5906
- continue;
5907
- }
5908
- lockedKept += 1;
5909
- p = lockedAt;
5910
- } else if (lockedAt !== void 0) {
5911
- if (Math.abs(p.x - lockedAt.x) > RECORD_EPS || Math.abs(p.y - lockedAt.y) > RECORD_EPS) lockedKept += 1;
5912
- }
5913
- const placed = p.turn === 90 ? turnItem(item) : item;
5914
- const m = marginOf(placed);
5915
- const area = placed.shape === void 0 ? {
5916
- x: 0,
5917
- y: 0,
5918
- w: placed.w,
5919
- h: placed.h
5920
- } : boundsOf(placed.shape);
5921
- const l = p.x + area.x;
5922
- const t = p.y + area.y;
5923
- boxes.push({
5924
- l: l - m.left,
5925
- t: t - m.top,
5926
- r: l + area.w + m.right,
5927
- b: t + area.h + m.bottom
5928
- });
5929
- }
5930
- const moved = new Map(output.moved.map((m) => [m.id, m]));
5931
- const obstacles = (input.obstacles ?? []).map((o) => {
5932
- const at = moved.get(o.id);
5933
- if (at === void 0) return o.region;
5934
- const b = boundsOf(o.region);
5935
- return translate(o.region, at.x - b.x, at.y - b.y);
5936
- });
5937
- let overlaps = 0;
5938
- for (const [i, a] of boxes.entries()) {
5939
- for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) overlaps += 1;
5940
- if (obstacles.length === 0) continue;
5941
- const rect = inner(a, tol);
5942
- for (const region of obstacles) if (regionHitsRect(region, rect)) overlaps += 1;
5943
- }
5944
- const { container } = input;
5945
- const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
5946
- const x = shape?.x ?? 0;
5947
- const y = shape?.y ?? 0;
5948
- const w = shape?.w ?? container.w ?? Infinity;
5949
- const h = shape?.h ?? container.h ?? Infinity;
5950
- const inset = container.inset ?? 0;
5951
- const fixedW = container.fixed === "size" || container.fixed === "width";
5952
- const fixedH = container.fixed === "size" || container.fixed === "height";
5953
- const exact = options.exactContainer === true && container.fixed === "size" ? container.shape : void 0;
5954
- let outside = 0;
5955
- for (const box of boxes) {
5956
- const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
5957
- const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
5958
- if (outX || outY) outside += 1;
5959
- else if (exact !== void 0 && !rectInRegion(exact, inner({
5960
- l: box.l - inset,
5961
- t: box.t - inset,
5962
- r: box.r + inset,
5963
- b: box.b + inset
5964
- }, tol))) outside += 1;
5965
- }
5966
- return {
5967
- "no-overlap": overlaps,
5968
- "in-container": outside,
5969
- "locked-kept": lockedKept,
5970
- complete
5971
- };
5972
- }
5973
6193
  // src/portfolio.ts
5974
6194
  /** The id of the trace stage of `packPortfolio`. Its measure `move.pick` is 1 when the pick is not member 0. */
5975
6195
  const PORTFOLIO_STAGE = "portfolio";
@@ -5997,10 +6217,6 @@ const PORTFOLIO_LEVELS = Object.freeze([
5997
6217
  spec: MEASURE_SPECS["pack.fill"]
5998
6218
  }
5999
6219
  ]);
6000
- /** This function gives the count of the hard breaks of an output: the sum of `checkHard` with `exactContainer`. */
6001
- function hardBreaks(input, output) {
6002
- return Object.values(checkHard(input, output, void 0, { exactContainer: true })).reduce((sum, n) => sum + n, 0);
6003
- }
6004
6220
  /**
6005
6221
  * This function makes an opt-in portfolio of strategies of the registry with `portfolio` of the contract.
6006
6222
  * Its `pick(input)` packs the input with each member and gives the output of the winner, its id and the trace.
@@ -6008,10 +6224,11 @@ function hardBreaks(input, output) {
6008
6224
  * - A member is a strategy id, or a `PortfolioEntry` with options. The members run in the given order.
6009
6225
  * - `members` is a list, or a function of the input that gives the list. The strategy `portfolio` uses the function form.
6010
6226
  * - The key is the hard breaks, then `pack.overflow.excessArea`, then `pack.fill`. `PORTFOLIO_LEVELS` gives the specs.
6227
+ * The option `key` gives other measures after the hard breaks.
6011
6228
  * - The key reads the measures after `roundMeasures`, as a record keeps them. A replay of the records gives the same pick.
6012
6229
  * - The hash of the key is `outputHash`. `selectBest` of the contract picks the winner.
6013
6230
  * - The output of the winner gets the trace stage `portfolio` with `move.pick` of `pickMoves`.
6014
- * - The function throws a `RangeError` for an unknown strategy, an empty list and two members with the same id.
6231
+ * - The function throws a `RangeError` for an unknown strategy, an empty list, two members with the same id and an unknown measure of `key`.
6015
6232
  * With the function form, `pick` throws this error for the list of an input.
6016
6233
  */
6017
6234
  function packPortfolio(members, options = {}) {
@@ -6028,24 +6245,7 @@ function packPortfolio(members, options = {}) {
6028
6245
  };
6029
6246
  });
6030
6247
  const list = typeof members === "function" ? (input) => toMembers(members(input)) : toMembers(members);
6031
- const cache = /* @__PURE__ */ new WeakMap();
6032
- const measured = (input, output) => {
6033
- let m = cache.get(output);
6034
- if (m === void 0) {
6035
- m = roundMeasures(measure(input, output));
6036
- cache.set(output, m);
6037
- }
6038
- return m;
6039
- };
6040
- const key = {
6041
- hard: hardBreaks,
6042
- levels: PORTFOLIO_LEVELS.slice(1).map((level) => ({
6043
- id: level.id,
6044
- spec: level.spec,
6045
- value: (input, output) => measured(input, output)[level.id]
6046
- })),
6047
- hash: outputHash
6048
- };
6248
+ const key = measureKey(options.key === void 0 ? PORTFOLIO_LEVELS.slice(1) : options.key.map(measureLevel));
6049
6249
  const limits = {
6050
6250
  ...options.budgetMs !== void 0 ? { budgetMs: options.budgetMs } : {},
6051
6251
  ...options.maxMembers !== void 0 ? { maxMembers: options.maxMembers } : {},
@@ -6099,7 +6299,8 @@ function unionSupports() {
6099
6299
  * The default output thus does not depend on the clock.
6100
6300
  * - A member that does not support the cell of an input throws, and it does not win. When each member throws, the strategy throws the first error.
6101
6301
  * - The option `budgetMs` goes to `portfolio` of the contract. The first member always runs.
6102
- * - The strategy throws a `RangeError` for a `k` other than 2 or 3.
6302
+ * - With `members`, each input gets these members, and `maxMembers` has no default. The id of the portfolio stays `portfolio`.
6303
+ * - The strategy throws a `RangeError` for a `k` other than 2 or 3, and for `k` with `members`.
6103
6304
  */
6104
6305
  const portfolioStrategy = {
6105
6306
  id: "portfolio",
@@ -6109,15 +6310,17 @@ const portfolioStrategy = {
6109
6310
  defaults: {},
6110
6311
  deterministic: true,
6111
6312
  pack(input, options, context) {
6112
- const { k, maxMembers, budgetMs } = options;
6313
+ const { k, maxMembers, budgetMs, members, key } = options;
6113
6314
  if (k !== void 0 && k !== 2 && k !== 3) throw new RangeError(`The option k of the strategy portfolio is 2 or 3, not ${String(k)}.`);
6315
+ if (k !== void 0 && members !== void 0) throw new RangeError("The strategy portfolio takes the option k or the option members, not the two.");
6114
6316
  const size = k ?? 3;
6115
- const count = maxMembers ?? (k === void 0 ? portfolioDefaultMembers(input) : void 0);
6317
+ const count = maxMembers ?? (k === void 0 && members === void 0 ? portfolioDefaultMembers(input) : void 0);
6116
6318
  const limits = {
6117
6319
  ...count === void 0 ? {} : { maxMembers: count },
6118
- ...budgetMs === void 0 ? {} : { budgetMs }
6320
+ ...budgetMs === void 0 ? {} : { budgetMs },
6321
+ ...key === void 0 ? {} : { key }
6119
6322
  };
6120
- return packPortfolio((x) => portfolioMembers(x, size), {
6323
+ return packPortfolio(members ?? ((x) => portfolioMembers(x, size)), {
6121
6324
  id: k === void 0 ? "portfolio" : `portfolio-k${k}`,
6122
6325
  seed: context.seed,
6123
6326
  ...limits
@@ -6133,11 +6336,42 @@ const strategies = Object.freeze([
6133
6336
  push,
6134
6337
  maxrects
6135
6338
  ]);
6339
+ /** This function gives the union of the supports of some strategies. */
6340
+ function unionOf(list) {
6341
+ const out = {};
6342
+ for (const s of list) for (const [dim, values] of Object.entries(s.supports)) {
6343
+ const own = out[dim] ??= [];
6344
+ for (const v of values) if (!own.includes(v)) own.push(v);
6345
+ }
6346
+ return out;
6347
+ }
6348
+ /**
6349
+ * The strategy `aspect-fit`: `aspectFit` of roadmap item k62 around the strategy of the option `strategy`. Its supports are
6350
+ * the union of the supports of the single strategies. A cell that the wrapped strategy does not support throws its error.
6351
+ * The strategy throws a `RangeError` without the option `strategy`.
6352
+ */
6353
+ const aspectFitStrategy = {
6354
+ id: "aspect-fit",
6355
+ family: "aspect-fit",
6356
+ catalog: [],
6357
+ supports: unionOf(strategies),
6358
+ defaults: {},
6359
+ deterministic: true,
6360
+ pack(input, options, context) {
6361
+ if (options.strategy === void 0) throw new RangeError("The strategy aspect-fit needs the option strategy.");
6362
+ const inner = strategy(options.strategy);
6363
+ const merged = {
6364
+ ...inner.defaults,
6365
+ ...options.options
6366
+ };
6367
+ return aspectFit(inner, options.targetAspect === void 0 ? {} : { targetAspect: options.targetAspect }).pack(input, merged, context);
6368
+ }
6369
+ };
6136
6370
  /**
6137
6371
  * The composite strategies of the registry: each one packs an input with other strategies. They are not in `strategies`.
6138
- * Since 0.2.7, the list has `portfolio` of roadmap item k20.
6372
+ * Since 0.2.7, the list has `portfolio` of roadmap item k20. Roadmap item k62 adds `aspect-fit`.
6139
6373
  */
6140
- const composites = Object.freeze([portfolioStrategy]);
6374
+ const composites = Object.freeze([portfolioStrategy, aspectFitStrategy]);
6141
6375
  /** This function gives the strategy with an id, also a composite strategy. It throws a `RangeError` for an id that the registry does not have. */
6142
6376
  function strategy(id) {
6143
6377
  const found = strategies.find((s) => s.id === id) ?? composites.find((s) => s.id === id);
@@ -6724,19 +6958,67 @@ const rectGrid = defineGenerator({
6724
6958
  },
6725
6959
  family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.n${p.count}`,
6726
6960
  cell: (p) => cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count),
6961
+ build: gridOf
6962
+ });
6963
+ /** This function builds an input of `rect-grid`: the container, then the obstacles, then the boxes. */
6964
+ function gridOf(seed, p) {
6965
+ let r = stepRandom(seed, "container");
6966
+ const w = int(r, 600, 1e3);
6967
+ const h = int(r, 400, 800);
6968
+ const { region, frame } = shapeOf(r, p.containerShape, w, h);
6969
+ const container = containerOf(p.containerSize, w, h, region);
6970
+ r = stepRandom(seed, "obstacles");
6971
+ const obstacles = obstaclesOf(r, p.obstacles, frame);
6972
+ r = stepRandom(seed, "boxes");
6973
+ return {
6974
+ container,
6975
+ obstacles,
6976
+ boxes: boxesOf(r, p.sizes, p.count, frame)
6977
+ };
6978
+ }
6979
+ /**
6980
+ * The generator `rect-grid`, version 2: the inputs of version 1 with the parameter `order`. With `soft`, each box gets a
6981
+ * rank, a random permutation of the box indexes. With `free`, the boxes have no rank.
6982
+ */
6983
+ const rectGridV2 = defineGenerator({
6984
+ name: "rect-grid",
6985
+ version: 2,
6986
+ set: "corpus-v1",
6987
+ defaults: {
6988
+ containerSize: "fixed",
6989
+ containerShape: "rect",
6990
+ obstacles: "none",
6991
+ sizes: "uniform",
6992
+ order: "free",
6993
+ count: 100
6994
+ },
6995
+ values: {
6996
+ containerSize: CONTAINER_SIZES,
6997
+ containerShape: CONTAINER_SHAPES,
6998
+ obstacles: OBSTACLE_LAYOUTS,
6999
+ sizes: SIZE_DISTRIBUTIONS,
7000
+ order: ["free", "soft"]
7001
+ },
7002
+ family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.${p.order}.n${p.count}`,
7003
+ cell: (p) => ({
7004
+ ...cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count),
7005
+ order: p.order
7006
+ }),
6727
7007
  build(seed, p) {
6728
- let r = stepRandom(seed, "container");
6729
- const w = int(r, 600, 1e3);
6730
- const h = int(r, 400, 800);
6731
- const { region, frame } = shapeOf(r, p.containerShape, w, h);
6732
- const container = containerOf(p.containerSize, w, h, region);
6733
- r = stepRandom(seed, "obstacles");
6734
- const obstacles = obstaclesOf(r, p.obstacles, frame);
6735
- r = stepRandom(seed, "boxes");
7008
+ const built = gridOf(seed, p);
7009
+ if (p.order === "free") return built;
7010
+ const r = stepRandom(seed, "order");
7011
+ const ranks = built.boxes.map((_, i) => i);
7012
+ for (let i = ranks.length - 1; i > 0; i--) {
7013
+ const j = Math.floor(r() * (i + 1));
7014
+ [ranks[i], ranks[j]] = [ranks[j], ranks[i]];
7015
+ }
6736
7016
  return {
6737
- container,
6738
- obstacles,
6739
- boxes: boxesOf(r, p.sizes, p.count, frame)
7017
+ ...built,
7018
+ boxes: built.boxes.map((b, i) => ({
7019
+ ...b,
7020
+ rank: ranks[i]
7021
+ }))
6740
7022
  };
6741
7023
  }
6742
7024
  });
@@ -7016,16 +7298,23 @@ const edgeCases = defineGenerator({
7016
7298
  const generators = Object.freeze([
7017
7299
  rectScattered,
7018
7300
  rectGrid,
7301
+ rectGridV2,
7019
7302
  gallery,
7020
7303
  dashboard,
7021
7304
  cards,
7022
7305
  wordCloud,
7023
7306
  edgeCases
7024
7307
  ]);
7025
- /** This function gives the generator with a name. It throws a `RangeError` for a name that the package does not have. */
7026
- function generator(name) {
7027
- const found = generators.find((g) => g.name === name);
7028
- if (found === void 0) throw new RangeError(`The package has no generator "${name}".`);
7308
+ /**
7309
+ * This function gives the generator with a name and a version. Without a version, it gives the first generator of the name
7310
+ * in `generators`: version 1 of `rect-grid`. It throws a `RangeError` for a name or a version that the package does not have.
7311
+ */
7312
+ function generator(name, version) {
7313
+ const named = generators.filter((g) => g.name === name);
7314
+ if (named.length === 0) throw new RangeError(`The package has no generator "${name}".`);
7315
+ if (version === void 0) return named[0];
7316
+ const found = named.find((g) => g.version === version);
7317
+ if (found === void 0) throw new RangeError(`The generator "${name}" has the versions ${named.map((g) => g.version).join(", ")}, not ${version}.`);
7029
7318
  return found;
7030
7319
  }
7031
7320
  // src/corpus.ts
@@ -7043,8 +7332,7 @@ function corpus(name, specs) {
7043
7332
  const inputs = [];
7044
7333
  const ids = /* @__PURE__ */ new Set();
7045
7334
  for (const spec of specs) {
7046
- const gen = generator(spec.generator);
7047
- if (spec.version !== void 0 && spec.version !== gen.version) throw new RangeError(`The generator "${gen.name}" has the version ${gen.version}, not ${spec.version}.`);
7335
+ const gen = generator(spec.generator, spec.version);
7048
7336
  const params = gen.withDefaults(spec.params);
7049
7337
  const cell = gen.cell(params);
7050
7338
  manifest.specs.push({
@@ -7070,6 +7358,6 @@ function corpus(name, specs) {
7070
7358
  inputs
7071
7359
  };
7072
7360
  }
7073
- export { DEFAULT_TOLERANCE, EPS, MAXRECTS_BANDS, MAXRECTS_RULES, MEASURE_SPECS, PORTFOLIO_FRAME_MS, PORTFOLIO_LEVELS, PORTFOLIO_SETS, PORTFOLIO_STAGE, RECORD_EPS, Rect, SHELF_FITS, SORT_ORDERS, SPREAD_THRESHOLDS, THIN_INPUTS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellKey, cellOf, checkHard, checkInput, compareRects, composites, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, freeSpace, fromRects, gallery, generator, generators, hardBreaks, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, maxrects, measure, measureChange, medianRatio, mulberry32, outputHash, pack, packFree, packMaxRects, packPortfolio, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, portfolioCell, portfolioDefaultMembers, portfolioMembers, portfolioSetOf, portfolioStrategy, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, spreadBand, spreadOf, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
7361
+ export { ASPECT_FIT_FACTORS, ASPECT_FIT_LEVELS, ASPECT_FIT_STAGE, DEFAULT_TOLERANCE, EPS, MAXRECTS_BANDS, MAXRECTS_RULES, MEASURE_SPECS, PORTFOLIO_FRAME_MS, PORTFOLIO_LEVELS, PORTFOLIO_SETS, PORTFOLIO_STAGE, RECORD_EPS, Rect, SHELF_FITS, SORT_ORDERS, SPREAD_THRESHOLDS, THIN_INPUTS, UnsupportedCell, Vector, aspectError, aspectFit, aspectFitStrategy, assertSupported, canonicalJson, cards, cellKey, cellOf, checkHard, checkInput, compareRects, composites, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, freeSpace, fromRects, gallery, generator, generators, hardBreaks, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, maxrects, measure, measureChange, medianRatio, mulberry32, outputHash, pack, packFree, packMaxRects, packPortfolio, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, portfolioCell, portfolioDefaultMembers, portfolioMembers, portfolioSetOf, portfolioStrategy, push, pushCascade, quantile, rectGrid, rectGridV2, rectScattered, round3, roundMeasures, rows, shelf, skyline, spreadBand, spreadOf, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
7074
7362
 
7075
7363
  //# sourceMappingURL=index.js.map