@mark1russell7/packing 0.2.11 → 0.2.12

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
@@ -3630,6 +3641,346 @@ function toRects(output, rects, outer) {
3630
3641
  }
3631
3642
  return outer;
3632
3643
  }
3644
+ // src/check.ts
3645
+ /** This function tells if the insides of two boxes overlap by more than a tolerance on each axis. */
3646
+ function overlap(a, b, tolerance) {
3647
+ 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;
3648
+ }
3649
+ /** This function gives the box made smaller by a tolerance on each side, as a rectangle. */
3650
+ function inner(box, tolerance) {
3651
+ return {
3652
+ x: box.l + tolerance,
3653
+ y: box.t + tolerance,
3654
+ w: box.r - box.l - 2 * tolerance,
3655
+ h: box.b - box.t - 2 * tolerance
3656
+ };
3657
+ }
3658
+ /**
3659
+ * This function counts the breaks of the 4 hard rules of the packer for an output.
3660
+ *
3661
+ * - `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`.
3662
+ * - `in-container`: each item with its margin that goes out of a fixed side by more than `tolerance.overlap`. The inset of
3663
+ * the container makes each fixed side smaller (decision 3 of `docs/PACKER.md`).
3664
+ * - `locked-kept`: each locked item that moved by more than `RECORD_EPS`, or that has no placement.
3665
+ * - `complete`: each item with no placement that is not in the overflow. A locked item breaks `locked-kept` in its place.
3666
+ *
3667
+ * A locked item with no placement stays at its input position for `no-overlap` and `in-container`. A locked item with no
3668
+ * `x` or no `y` in the input is a bad input. The rules skip it, as `packHard` of the contract does. `checkInput` finds it.
3669
+ *
3670
+ * The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
3671
+ * The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
3672
+ * shape is relative to the position of the item. The result has no tolerance in it.
3673
+ *
3674
+ * With `options.exactContainer`, a shaped container with the size `fixed` also uses its true outline for `in-container`.
3675
+ * A box then counts when `rectInRegion` of the contract tells that it is not in the shape, as in `packHard`.
3676
+ * For that test, the box grows by the inset and becomes smaller by `tolerance.overlap` on each side. The default is `false`.
3677
+ */
3678
+ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE, options = {}) {
3679
+ const tol = tolerance.overlap;
3680
+ const placements = /* @__PURE__ */ new Map();
3681
+ for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
3682
+ const overflow = new Set(output.overflow);
3683
+ let lockedKept = 0;
3684
+ let complete = 0;
3685
+ const boxes = [];
3686
+ for (const item of input.items) {
3687
+ if (item.locked === true && (item.x === void 0 || item.y === void 0)) continue;
3688
+ const lockedAt = item.locked === true ? {
3689
+ x: item.x,
3690
+ y: item.y
3691
+ } : void 0;
3692
+ let p = placements.get(item.id);
3693
+ if (p === void 0) {
3694
+ if (lockedAt === void 0) {
3695
+ if (!overflow.has(item.id)) complete += 1;
3696
+ continue;
3697
+ }
3698
+ lockedKept += 1;
3699
+ p = lockedAt;
3700
+ } else if (lockedAt !== void 0) {
3701
+ if (Math.abs(p.x - lockedAt.x) > RECORD_EPS || Math.abs(p.y - lockedAt.y) > RECORD_EPS) lockedKept += 1;
3702
+ }
3703
+ const placed = p.turn === 90 ? turnItem(item) : item;
3704
+ const m = marginOf(placed);
3705
+ const area = placed.shape === void 0 ? {
3706
+ x: 0,
3707
+ y: 0,
3708
+ w: placed.w,
3709
+ h: placed.h
3710
+ } : boundsOf(placed.shape);
3711
+ const l = p.x + area.x;
3712
+ const t = p.y + area.y;
3713
+ boxes.push({
3714
+ l: l - m.left,
3715
+ t: t - m.top,
3716
+ r: l + area.w + m.right,
3717
+ b: t + area.h + m.bottom
3718
+ });
3719
+ }
3720
+ const moved = new Map(output.moved.map((m) => [m.id, m]));
3721
+ const obstacles = (input.obstacles ?? []).map((o) => {
3722
+ const at = moved.get(o.id);
3723
+ if (at === void 0) return o.region;
3724
+ const b = boundsOf(o.region);
3725
+ return translate(o.region, at.x - b.x, at.y - b.y);
3726
+ });
3727
+ let overlaps = 0;
3728
+ for (const [i, a] of boxes.entries()) {
3729
+ for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) overlaps += 1;
3730
+ if (obstacles.length === 0) continue;
3731
+ const rect = inner(a, tol);
3732
+ for (const region of obstacles) if (regionHitsRect(region, rect)) overlaps += 1;
3733
+ }
3734
+ const { container } = input;
3735
+ const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
3736
+ const x = shape?.x ?? 0;
3737
+ const y = shape?.y ?? 0;
3738
+ const w = shape?.w ?? container.w ?? Infinity;
3739
+ const h = shape?.h ?? container.h ?? Infinity;
3740
+ const inset = container.inset ?? 0;
3741
+ const fixedW = container.fixed === "size" || container.fixed === "width";
3742
+ const fixedH = container.fixed === "size" || container.fixed === "height";
3743
+ const exact = options.exactContainer === true && container.fixed === "size" ? container.shape : void 0;
3744
+ let outside = 0;
3745
+ for (const box of boxes) {
3746
+ const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
3747
+ const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
3748
+ if (outX || outY) outside += 1;
3749
+ else if (exact !== void 0 && !rectInRegion(exact, inner({
3750
+ l: box.l - inset,
3751
+ t: box.t - inset,
3752
+ r: box.r + inset,
3753
+ b: box.b + inset
3754
+ }, tol))) outside += 1;
3755
+ }
3756
+ return {
3757
+ "no-overlap": overlaps,
3758
+ "in-container": outside,
3759
+ "locked-kept": lockedKept,
3760
+ complete
3761
+ };
3762
+ }
3763
+ // src/select-key.ts
3764
+ /** This function gives the count of the hard breaks of an output: the sum of `checkHard` with `exactContainer`. */
3765
+ function hardBreaks(input, output) {
3766
+ return Object.values(checkHard(input, output, void 0, { exactContainer: true })).reduce((sum, n) => sum + n, 0);
3767
+ }
3768
+ /** This function gives the level of a measure id with its spec of `MEASURE_SPECS`. It throws a `RangeError` for an unknown id. */
3769
+ function measureLevel(id) {
3770
+ const spec = MEASURE_SPECS[id];
3771
+ if (spec === void 0 || id === "hard") throw new RangeError(`A level of a key is a measure of MEASURE_SPECS, not "${id}".`);
3772
+ return {
3773
+ id,
3774
+ spec
3775
+ };
3776
+ }
3777
+ /**
3778
+ * This function makes the key of a pick: `hardBreaks`, then the measures of `levels` in order, then `outputHash`.
3779
+ *
3780
+ * - The levels read the measures after `roundMeasures`, as a record keeps them. A replay of the records gives the same pick.
3781
+ * - The measures of one output are computed one time.
3782
+ * - `view` gives the input of the measures. The default is the input. The hard breaks always read the input.
3783
+ */
3784
+ function measureKey(levels, view = (input) => input) {
3785
+ const cache = /* @__PURE__ */ new WeakMap();
3786
+ const measured = (input, output) => {
3787
+ let m = cache.get(output);
3788
+ if (m === void 0) {
3789
+ m = roundMeasures(measure(view(input), output));
3790
+ cache.set(output, m);
3791
+ }
3792
+ return m;
3793
+ };
3794
+ return {
3795
+ hard: hardBreaks,
3796
+ levels: levels.map((level) => ({
3797
+ id: level.id,
3798
+ spec: level.spec,
3799
+ value: (input, output) => measured(input, output)[level.id]
3800
+ })),
3801
+ hash: outputHash
3802
+ };
3803
+ }
3804
+ // src/aspect-fit.ts
3805
+ /** 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. */
3806
+ const ASPECT_FIT_STAGE = "aspect-fit";
3807
+ /**
3808
+ * The factors of the widths of `aspectFit`. Each width is a factor times the square root of the area of the first output
3809
+ * times the target aspect. The steps are approximately 5% of the width, thus approximately 0.1 of the aspect error.
3810
+ */
3811
+ const ASPECT_FIT_FACTORS = Object.freeze([
3812
+ .86,
3813
+ .9,
3814
+ .95,
3815
+ 1,
3816
+ 1.05,
3817
+ 1.1,
3818
+ 1.16
3819
+ ]);
3820
+ /**
3821
+ * The levels of the pick of `aspectFit`, in order. Level 0 is `hard`.
3822
+ *
3823
+ * - `hard`: the count of hard breaks of `checkHard` with `exactContainer`. Lower is better, with the minimum effect 0.
3824
+ * - `pack.overflow.excessArea`: the overflow area above its bound. Lower is better.
3825
+ * - `pack.aspectError`: the distance from the target aspect. Lower is better, with the minimum effect 0.01.
3826
+ * - `pack.area`: the area of the hull. Lower is better, with the minimum effect of 1% of the area.
3827
+ */
3828
+ const ASPECT_FIT_LEVELS = Object.freeze([
3829
+ {
3830
+ id: "hard",
3831
+ spec: {
3832
+ direction: "lower",
3833
+ minEffect: 0
3834
+ }
3835
+ },
3836
+ {
3837
+ id: "pack.overflow.excessArea",
3838
+ spec: MEASURE_SPECS["pack.overflow.excessArea"]
3839
+ },
3840
+ {
3841
+ id: "pack.aspectError",
3842
+ spec: MEASURE_SPECS["pack.aspectError"]
3843
+ },
3844
+ {
3845
+ id: "pack.area",
3846
+ spec: MEASURE_SPECS["pack.area"]
3847
+ }
3848
+ ]);
3849
+ /** 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. */
3850
+ function stripOf(shape, w) {
3851
+ const b = boundsOf$1(shape);
3852
+ if (shape.kind === "rect") return {
3853
+ kind: "rect",
3854
+ x: b.x,
3855
+ y: b.y,
3856
+ w,
3857
+ h: b.h
3858
+ };
3859
+ const points = [
3860
+ [b.x, b.y],
3861
+ [b.x + w, b.y],
3862
+ [b.x + w, b.y + b.h],
3863
+ [b.x, b.y + b.h]
3864
+ ];
3865
+ return shape.kind === "polygon" && shape.holes !== void 0 ? {
3866
+ kind: "polygon",
3867
+ points,
3868
+ holes: shape.holes
3869
+ } : {
3870
+ kind: "polygon",
3871
+ points
3872
+ };
3873
+ }
3874
+ /** This function gives an input with the fixed width `w` and a free height. A shape becomes a strip of the width `w`. */
3875
+ function atWidth(input, w) {
3876
+ const c = input.container;
3877
+ return {
3878
+ ...input,
3879
+ container: {
3880
+ ...c,
3881
+ fixed: "width",
3882
+ w,
3883
+ ...c.shape === void 0 ? {} : { shape: stripOf(c.shape, w) }
3884
+ }
3885
+ };
3886
+ }
3887
+ /** This function gives the widths of `aspectFit` for an input and the first output. Each width is narrower than a fixed width. */
3888
+ function widthsOf(input, first, target) {
3889
+ const area = measure(input, first)["pack.area"];
3890
+ if (!(area > 0)) return [];
3891
+ const inset = input.container.inset ?? 0;
3892
+ const fixed = input.container.fixed === "width" ? containerBounds(input).w : Infinity;
3893
+ let widest = 0;
3894
+ for (const item of input.items) {
3895
+ const m = marginOf(item);
3896
+ widest = Math.max(widest, item.w + m.left + m.right);
3897
+ }
3898
+ const ideal = Math.sqrt(area * target);
3899
+ const out = [];
3900
+ for (const f of ASPECT_FIT_FACTORS) {
3901
+ const w = Math.max(Math.round(f * ideal + 2 * inset), Math.ceil(widest + 2 * inset));
3902
+ if (w < fixed && !out.includes(w)) out.push(w);
3903
+ }
3904
+ return out;
3905
+ }
3906
+ /** This function packs an input with a free height or a free size near the target aspect. */
3907
+ function fit(input, target, inner, options, context) {
3908
+ const fresh = () => createContext(context.seed, {
3909
+ ...context.budget === void 0 ? {} : { budget: context.budget },
3910
+ ...context.previous === void 0 ? {} : { previous: context.previous }
3911
+ });
3912
+ const first = inner.pack(input, options, fresh());
3913
+ const widths = widthsOf(input, first, target);
3914
+ if (widths.length === 0) return first;
3915
+ const origin = containerBounds(input);
3916
+ const inset = input.container.inset ?? 0;
3917
+ const free = input.container.fixed === "none";
3918
+ const sized = (out) => {
3919
+ const w = free ? extentOf(input, out.placements, out.moved, origin, inset, turnItem).right : origin.w;
3920
+ return {
3921
+ ...out,
3922
+ size: {
3923
+ w,
3924
+ h: out.size.h
3925
+ }
3926
+ };
3927
+ };
3928
+ const members = [{
3929
+ id: inner.id,
3930
+ run: () => first
3931
+ }, ...widths.map((w) => ({
3932
+ id: `${inner.id}@${w}`,
3933
+ run: (x) => sized(inner.pack(atWidth(x, w), options, fresh()))
3934
+ }))];
3935
+ const view = (x) => x.container.aspect === target ? x : {
3936
+ ...x,
3937
+ container: {
3938
+ ...x.container,
3939
+ aspect: target
3940
+ }
3941
+ };
3942
+ const got = portfolio(ASPECT_FIT_STAGE, members, measureKey(ASPECT_FIT_LEVELS.slice(1), view)).pick(input);
3943
+ const stage = {
3944
+ id: ASPECT_FIT_STAGE,
3945
+ measures: { "move.aspectPick": pickMoves(got.trace).pick }
3946
+ };
3947
+ return {
3948
+ ...got.output,
3949
+ trace: [...got.output.trace ?? [], stage]
3950
+ };
3951
+ }
3952
+ /**
3953
+ * This function wraps a strategy. For a free-size container, it packs at some widths near the target aspect and keeps the best output.
3954
+ *
3955
+ * - The target is `targetAspect`, else `aspect` of the container, else 1.
3956
+ * - A container with the size `fixed` goes to the wrapped strategy without a change.
3957
+ * - The first member is the output of the wrapped strategy. Each other member packs at one fixed width of `ASPECT_FIT_FACTORS`.
3958
+ * - The key is `ASPECT_FIT_LEVELS`, then `outputHash`. The aspect error reads the target. `selectBest` of the contract picks the winner.
3959
+ * - A fixed height goes through the turn of the axes, with the target `1 / target`.
3960
+ * - The function throws a `RangeError` for a target that is not a number above 0.
3961
+ */
3962
+ function aspectFit(inner, options = {}) {
3963
+ const given = options.targetAspect;
3964
+ 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)}.`);
3965
+ return {
3966
+ id: `aspect-fit(${inner.id})`,
3967
+ family: inner.family,
3968
+ catalog: [],
3969
+ supports: inner.supports,
3970
+ defaults: inner.defaults,
3971
+ deterministic: inner.deterministic,
3972
+ pack(input, o, context) {
3973
+ const c = input.container;
3974
+ if (c.fixed === "size") return inner.pack(input, o, context);
3975
+ const target = given ?? c.aspect ?? 1;
3976
+ if (c.fixed === "height") return viaAxes(input, context.previous, (turned, previous) => fit(turned, 1 / target, inner, o, {
3977
+ ...context,
3978
+ ...previous === void 0 ? {} : { previous }
3979
+ }));
3980
+ return fit(input, target, inner, o, context);
3981
+ }
3982
+ };
3983
+ }
3633
3984
  // src/portfolio-sets.ts
3634
3985
  /** 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
3986
  const PORTFOLIO_SETS = {
@@ -5851,125 +6202,6 @@ function portfolioDefaultMembers(input, k, table = PORTFOLIO_SETS) {
5851
6202
  const count = portfolioSetOf(input, table).sets.defaultMembers ?? 3;
5852
6203
  return k === void 0 ? count : Math.min(count, k);
5853
6204
  }
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
6205
  // src/portfolio.ts
5974
6206
  /** The id of the trace stage of `packPortfolio`. Its measure `move.pick` is 1 when the pick is not member 0. */
5975
6207
  const PORTFOLIO_STAGE = "portfolio";
@@ -5997,10 +6229,6 @@ const PORTFOLIO_LEVELS = Object.freeze([
5997
6229
  spec: MEASURE_SPECS["pack.fill"]
5998
6230
  }
5999
6231
  ]);
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
6232
  /**
6005
6233
  * This function makes an opt-in portfolio of strategies of the registry with `portfolio` of the contract.
6006
6234
  * Its `pick(input)` packs the input with each member and gives the output of the winner, its id and the trace.
@@ -6008,10 +6236,11 @@ function hardBreaks(input, output) {
6008
6236
  * - A member is a strategy id, or a `PortfolioEntry` with options. The members run in the given order.
6009
6237
  * - `members` is a list, or a function of the input that gives the list. The strategy `portfolio` uses the function form.
6010
6238
  * - The key is the hard breaks, then `pack.overflow.excessArea`, then `pack.fill`. `PORTFOLIO_LEVELS` gives the specs.
6239
+ * The option `key` gives other measures after the hard breaks.
6011
6240
  * - The key reads the measures after `roundMeasures`, as a record keeps them. A replay of the records gives the same pick.
6012
6241
  * - The hash of the key is `outputHash`. `selectBest` of the contract picks the winner.
6013
6242
  * - 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.
6243
+ * - 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
6244
  * With the function form, `pick` throws this error for the list of an input.
6016
6245
  */
6017
6246
  function packPortfolio(members, options = {}) {
@@ -6028,24 +6257,7 @@ function packPortfolio(members, options = {}) {
6028
6257
  };
6029
6258
  });
6030
6259
  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
- };
6260
+ const key = measureKey(options.key === void 0 ? PORTFOLIO_LEVELS.slice(1) : options.key.map(measureLevel));
6049
6261
  const limits = {
6050
6262
  ...options.budgetMs !== void 0 ? { budgetMs: options.budgetMs } : {},
6051
6263
  ...options.maxMembers !== void 0 ? { maxMembers: options.maxMembers } : {},
@@ -6099,7 +6311,8 @@ function unionSupports() {
6099
6311
  * The default output thus does not depend on the clock.
6100
6312
  * - 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
6313
  * - 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.
6314
+ * - With `members`, each input gets these members, and `maxMembers` has no default. The id of the portfolio stays `portfolio`.
6315
+ * - The strategy throws a `RangeError` for a `k` other than 2 or 3, and for `k` with `members`.
6103
6316
  */
6104
6317
  const portfolioStrategy = {
6105
6318
  id: "portfolio",
@@ -6109,15 +6322,17 @@ const portfolioStrategy = {
6109
6322
  defaults: {},
6110
6323
  deterministic: true,
6111
6324
  pack(input, options, context) {
6112
- const { k, maxMembers, budgetMs } = options;
6325
+ const { k, maxMembers, budgetMs, members, key } = options;
6113
6326
  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)}.`);
6327
+ 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
6328
  const size = k ?? 3;
6115
- const count = maxMembers ?? (k === void 0 ? portfolioDefaultMembers(input) : void 0);
6329
+ const count = maxMembers ?? (k === void 0 && members === void 0 ? portfolioDefaultMembers(input) : void 0);
6116
6330
  const limits = {
6117
6331
  ...count === void 0 ? {} : { maxMembers: count },
6118
- ...budgetMs === void 0 ? {} : { budgetMs }
6332
+ ...budgetMs === void 0 ? {} : { budgetMs },
6333
+ ...key === void 0 ? {} : { key }
6119
6334
  };
6120
- return packPortfolio((x) => portfolioMembers(x, size), {
6335
+ return packPortfolio(members ?? ((x) => portfolioMembers(x, size)), {
6121
6336
  id: k === void 0 ? "portfolio" : `portfolio-k${k}`,
6122
6337
  seed: context.seed,
6123
6338
  ...limits
@@ -6133,11 +6348,42 @@ const strategies = Object.freeze([
6133
6348
  push,
6134
6349
  maxrects
6135
6350
  ]);
6351
+ /** This function gives the union of the supports of some strategies. */
6352
+ function unionOf(list) {
6353
+ const out = {};
6354
+ for (const s of list) for (const [dim, values] of Object.entries(s.supports)) {
6355
+ const own = out[dim] ??= [];
6356
+ for (const v of values) if (!own.includes(v)) own.push(v);
6357
+ }
6358
+ return out;
6359
+ }
6360
+ /**
6361
+ * The strategy `aspect-fit`: `aspectFit` of roadmap item k62 around the strategy of the option `strategy`. Its supports are
6362
+ * the union of the supports of the single strategies. A cell that the wrapped strategy does not support throws its error.
6363
+ * The strategy throws a `RangeError` without the option `strategy`.
6364
+ */
6365
+ const aspectFitStrategy = {
6366
+ id: "aspect-fit",
6367
+ family: "aspect-fit",
6368
+ catalog: [],
6369
+ supports: unionOf(strategies),
6370
+ defaults: {},
6371
+ deterministic: true,
6372
+ pack(input, options, context) {
6373
+ if (options.strategy === void 0) throw new RangeError("The strategy aspect-fit needs the option strategy.");
6374
+ const inner = strategy(options.strategy);
6375
+ const merged = {
6376
+ ...inner.defaults,
6377
+ ...options.options
6378
+ };
6379
+ return aspectFit(inner, options.targetAspect === void 0 ? {} : { targetAspect: options.targetAspect }).pack(input, merged, context);
6380
+ }
6381
+ };
6136
6382
  /**
6137
6383
  * 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.
6384
+ * Since 0.2.7, the list has `portfolio` of roadmap item k20. Roadmap item k62 adds `aspect-fit`.
6139
6385
  */
6140
- const composites = Object.freeze([portfolioStrategy]);
6386
+ const composites = Object.freeze([portfolioStrategy, aspectFitStrategy]);
6141
6387
  /** 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
6388
  function strategy(id) {
6143
6389
  const found = strategies.find((s) => s.id === id) ?? composites.find((s) => s.id === id);
@@ -6724,19 +6970,67 @@ const rectGrid = defineGenerator({
6724
6970
  },
6725
6971
  family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.n${p.count}`,
6726
6972
  cell: (p) => cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count),
6973
+ build: gridOf
6974
+ });
6975
+ /** This function builds an input of `rect-grid`: the container, then the obstacles, then the boxes. */
6976
+ function gridOf(seed, p) {
6977
+ let r = stepRandom(seed, "container");
6978
+ const w = int(r, 600, 1e3);
6979
+ const h = int(r, 400, 800);
6980
+ const { region, frame } = shapeOf(r, p.containerShape, w, h);
6981
+ const container = containerOf(p.containerSize, w, h, region);
6982
+ r = stepRandom(seed, "obstacles");
6983
+ const obstacles = obstaclesOf(r, p.obstacles, frame);
6984
+ r = stepRandom(seed, "boxes");
6985
+ return {
6986
+ container,
6987
+ obstacles,
6988
+ boxes: boxesOf(r, p.sizes, p.count, frame)
6989
+ };
6990
+ }
6991
+ /**
6992
+ * The generator `rect-grid`, version 2: the inputs of version 1 with the parameter `order`. With `soft`, each box gets a
6993
+ * rank, a random permutation of the box indexes. With `free`, the boxes have no rank.
6994
+ */
6995
+ const rectGridV2 = defineGenerator({
6996
+ name: "rect-grid",
6997
+ version: 2,
6998
+ set: "corpus-v1",
6999
+ defaults: {
7000
+ containerSize: "fixed",
7001
+ containerShape: "rect",
7002
+ obstacles: "none",
7003
+ sizes: "uniform",
7004
+ order: "free",
7005
+ count: 100
7006
+ },
7007
+ values: {
7008
+ containerSize: CONTAINER_SIZES,
7009
+ containerShape: CONTAINER_SHAPES,
7010
+ obstacles: OBSTACLE_LAYOUTS,
7011
+ sizes: SIZE_DISTRIBUTIONS,
7012
+ order: ["free", "soft"]
7013
+ },
7014
+ family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.${p.order}.n${p.count}`,
7015
+ cell: (p) => ({
7016
+ ...cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count),
7017
+ order: p.order
7018
+ }),
6727
7019
  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");
7020
+ const built = gridOf(seed, p);
7021
+ if (p.order === "free") return built;
7022
+ const r = stepRandom(seed, "order");
7023
+ const ranks = built.boxes.map((_, i) => i);
7024
+ for (let i = ranks.length - 1; i > 0; i--) {
7025
+ const j = Math.floor(r() * (i + 1));
7026
+ [ranks[i], ranks[j]] = [ranks[j], ranks[i]];
7027
+ }
6736
7028
  return {
6737
- container,
6738
- obstacles,
6739
- boxes: boxesOf(r, p.sizes, p.count, frame)
7029
+ ...built,
7030
+ boxes: built.boxes.map((b, i) => ({
7031
+ ...b,
7032
+ rank: ranks[i]
7033
+ }))
6740
7034
  };
6741
7035
  }
6742
7036
  });
@@ -7016,16 +7310,23 @@ const edgeCases = defineGenerator({
7016
7310
  const generators = Object.freeze([
7017
7311
  rectScattered,
7018
7312
  rectGrid,
7313
+ rectGridV2,
7019
7314
  gallery,
7020
7315
  dashboard,
7021
7316
  cards,
7022
7317
  wordCloud,
7023
7318
  edgeCases
7024
7319
  ]);
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}".`);
7320
+ /**
7321
+ * This function gives the generator with a name and a version. Without a version, it gives the first generator of the name
7322
+ * in `generators`: version 1 of `rect-grid`. It throws a `RangeError` for a name or a version that the package does not have.
7323
+ */
7324
+ function generator(name, version) {
7325
+ const named = generators.filter((g) => g.name === name);
7326
+ if (named.length === 0) throw new RangeError(`The package has no generator "${name}".`);
7327
+ if (version === void 0) return named[0];
7328
+ const found = named.find((g) => g.version === version);
7329
+ if (found === void 0) throw new RangeError(`The generator "${name}" has the versions ${named.map((g) => g.version).join(", ")}, not ${version}.`);
7029
7330
  return found;
7030
7331
  }
7031
7332
  // src/corpus.ts
@@ -7043,8 +7344,7 @@ function corpus(name, specs) {
7043
7344
  const inputs = [];
7044
7345
  const ids = /* @__PURE__ */ new Set();
7045
7346
  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}.`);
7347
+ const gen = generator(spec.generator, spec.version);
7048
7348
  const params = gen.withDefaults(spec.params);
7049
7349
  const cell = gen.cell(params);
7050
7350
  manifest.specs.push({
@@ -7070,6 +7370,6 @@ function corpus(name, specs) {
7070
7370
  inputs
7071
7371
  };
7072
7372
  }
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 };
7373
+ 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
7374
 
7075
7375
  //# sourceMappingURL=index.js.map