@mark1russell7/packing 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/packing",
4
- "version": "0.2.3",
4
+ "version": "0.2.5",
5
5
  "description": "Rectangle packing: shelf, guillotine and skyline strategies, push cascades and measures of a layout.",
6
6
  "keywords": [
7
7
  "rectangle-packing",
package/src/adapters.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  // The adapters of 0.1 (docs/PACKER.md): each function of 0.1 runs behind the Packer interface as a strategy.
2
2
 
3
3
  import { EPS, type PackInput, type PackOutput } from "./contract.ts";
4
- import { boundsOf, buildFrame, now, outerOf, readFrame, rectsOf, viaAxes, type Entry } from "./frame.ts";
4
+ import { boundsOf, buildFrame, frameTurn, now, outerOf, readFrame, rectsOf, viaAxes, type Entry, type Frame, type FrameTurns } from "./frame.ts";
5
5
  import { packFree } from "./free.ts";
6
+ import type { MaxRectsRule } from "./maxrects.ts";
6
7
  import { clearMoves, moveCount, MOVES } from "./moves.ts";
7
8
  import type { SortOrder } from "./order.ts";
8
- import { pack } from "./pack.ts";
9
+ import { pack, type PackLayoutOptions } from "./pack.ts";
9
10
  import { pushCascade } from "./push.ts";
10
11
  import { Rect } from "./rect.ts";
12
+ import type { ShelfFit } from "./shelf.ts";
11
13
  import { packSkyline } from "./skyline.ts";
12
14
  import { spaceOf } from "./space.ts";
13
15
  import { assertSupported, cellOf, UnsupportedCell, type PackContext, type SizeSearch, type Strategy, type Supports } from "./strategy.ts";
@@ -18,8 +20,35 @@ export interface LayoutStrategyOptions {
18
20
  sortOrder: SortOrder;
19
21
  /** The number of widths that the first pass of the size search `log-sweep` tries. */
20
22
  samples: number;
23
+ /**
24
+ * The rule of `shelf` to choose a shelf: `next`, `first` or `best`. Only `shelf` reads it. Without it, `shelf` packs
25
+ * as in 0.2.3. With it, the trace also counts `move.firstFit` and `move.bestFit`.
26
+ */
27
+ fit?: ShelfFit;
21
28
  }
22
29
 
30
+ /** The options of the strategy `maxrects`. The strategy does not read `fit`. */
31
+ export interface MaxRectsStrategyOptions extends LayoutStrategyOptions {
32
+ /** The rule to choose a free rectangle: `bssf` (the default), `blsf`, `baf`, `bl` or `cp`. `MAXRECTS_RULES` tells each rule. */
33
+ rule: MaxRectsRule;
34
+ /** The value `true` lets each turnable item that is not locked turn by 90 degrees. The default is `false`. */
35
+ rotate: boolean;
36
+ /**
37
+ * The largest height of a band of the approximation of a container that is not a plain rectangle. `MaxRectsOptions`
38
+ * tells the default.
39
+ */
40
+ bandHeight?: number;
41
+ }
42
+
43
+ /** The id of a strategy of `pack`. */
44
+ type LayoutId = "rows" | "skyline" | "shelf" | "maxrects";
45
+
46
+ /** The options of a strategy of `pack`: the options of `maxrects` are optional for the other strategies. */
47
+ type AnyLayoutOptions = LayoutStrategyOptions & Partial<Pick<MaxRectsStrategyOptions, "rule" | "rotate" | "bandHeight">>;
48
+
49
+ /** The options of `pack` that a strategy adds to the sort order and the space. */
50
+ type PackExtras = Pick<PackLayoutOptions, "fit" | "rule" | "bandHeight" | "turnable" | "turned">;
51
+
23
52
  /** The options of the strategy `push`. */
24
53
  export interface PushStrategyOptions {
25
54
  /** The order of the items that the cascade does not place. */
@@ -55,37 +84,73 @@ function supportsOf(cells: { [D in keyof Supports]: readonly Supports[D][number]
55
84
  };
56
85
  }
57
86
 
87
+ /**
88
+ * This function gives the options of `pack` that a strategy adds: the fit of `shelf`, or the rule, the bands and the turns
89
+ * of `maxrects`. A turnable item turns only with `rotate`, and a locked item does not turn.
90
+ */
91
+ function extrasOf(id: LayoutId, options: AnyLayoutOptions, frame: Frame): PackExtras {
92
+ if (id === "shelf") return options.fit !== undefined ? { fit: options.fit } : {};
93
+ if (id !== "maxrects") return {};
94
+ const out: PackExtras = { rule: options.rule ?? "bssf", ...(options.bandHeight === undefined ? {} : { bandHeight: options.bandHeight }) };
95
+ if (options.rotate !== true) return out;
96
+ const turnable = new Set(frame.entries.filter((e) => e.item?.turnable === true && !e.rect.locked).map((e) => e.rect));
97
+ return { ...out, turnable, turned: new Set() };
98
+ }
99
+
100
+ /**
101
+ * This function gives the move counts of the trace of a strategy: its own move, and the moves of a shelf fit. The trace of
102
+ * `maxrects` has the move of its rule and `move.split`. The rule `bl` has only `move.split`.
103
+ */
104
+ function movesOf(id: LayoutId, extras: PackExtras): Record<string, number> {
105
+ if (id === "maxrects") {
106
+ const rule = extras.rule ?? "bssf";
107
+ const out: Record<string, number> = rule === "bl" ? {} : { [MOVES[rule]]: moveCount(MOVES[rule]) };
108
+ out[MOVES.split] = moveCount(MOVES.split);
109
+ return out;
110
+ }
111
+ const out: Record<string, number> = { [MOVES[id]]: moveCount(MOVES[id]) };
112
+ if (extras.fit !== undefined) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
113
+ return out;
114
+ }
115
+
116
+ /** This function gives the turns of a packing for `readFrame`, or `undefined` when no rectangle can turn. */
117
+ function turnsOf(extras: PackExtras, transposed: boolean): FrameTurns | undefined {
118
+ return extras.turned === undefined ? undefined : { rects: extras.turned, turn: frameTurn(transposed) };
119
+ }
120
+
58
121
  /** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
59
- function packFixedWidth(id: "rows" | "skyline" | "shelf", input: PackInput, options: LayoutStrategyOptions): PackOutput {
122
+ function packFixedWidth(id: LayoutId, input: PackInput, options: AnyLayoutOptions, transposed = false): PackOutput {
60
123
  const start = now();
61
- const frame = buildFrame(input);
124
+ const frame = buildFrame(input, false, id === "maxrects" && options.rotate === true);
62
125
  const rects = rectsOf(frame);
126
+ const extras = extrasOf(id, options, frame);
63
127
  clearMoves();
64
- if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder, space: spaceOf(input, frame) });
65
- const output = readFrame(input, frame, []);
66
- return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length, [MOVES[id]]: moveCount(MOVES[id]) } }] };
128
+ if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder, space: spaceOf(input, frame), ...extras });
129
+ const output = readFrame(input, frame, [], [], turnsOf(extras, transposed));
130
+ return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, extras) } }] };
67
131
  }
68
132
 
69
133
  /** This function makes the strategy of one function of `pack`. */
70
- function layoutStrategy(
71
- id: "rows" | "skyline" | "shelf",
134
+ function layoutStrategy<O extends AnyLayoutOptions>(
135
+ id: LayoutId,
72
136
  family: string,
73
137
  catalog: string[],
74
- cells: Pick<Supports, "obstacleMove" | "order">,
75
- ): Strategy<LayoutStrategyOptions> {
76
- const self: Strategy<LayoutStrategyOptions> = {
138
+ cells: Pick<Supports, "obstacleMove" | "order"> & Partial<Pick<Supports, "rotation">>,
139
+ defaults: O,
140
+ ): Strategy<O> {
141
+ const self: Strategy<O> = {
77
142
  id,
78
143
  family,
79
144
  catalog,
80
145
  supports: supportsOf({ ...LAYOUT_CELLS, ...cells }),
81
- defaults: { sortOrder: "height-desc", samples: 48 },
146
+ defaults: { ...defaults },
82
147
  deterministic: true,
83
148
  pack(input, options, context) {
84
149
  assertSupported(self.supports, input);
85
150
  const o = { ...self.defaults, ...options };
86
151
  switch (input.container.fixed) {
87
152
  case "height":
88
- return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o));
153
+ return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o, true));
89
154
  case "none":
90
155
  return logSweep.search(input, self, context, o);
91
156
  default:
@@ -96,6 +161,9 @@ function layoutStrategy(
96
161
  return self;
97
162
  }
98
163
 
164
+ /** The defaults of `rows`, `skyline` and `shelf`. */
165
+ const LAYOUT_DEFAULTS: LayoutStrategyOptions = { sortOrder: "height-desc", samples: 48 };
166
+
99
167
  /**
100
168
  * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
101
169
  * container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
@@ -104,7 +172,7 @@ function layoutStrategy(
104
172
  export const rows: Strategy<LayoutStrategyOptions> = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
105
173
  obstacleMove: ["fixed"],
106
174
  order: ["free", "soft", "hard"],
107
- });
175
+ }, LAYOUT_DEFAULTS);
108
176
 
109
177
  /**
110
178
  * The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
@@ -113,30 +181,48 @@ export const rows: Strategy<LayoutStrategyOptions> = layoutStrategy("rows", "she
113
181
  export const skyline: Strategy<LayoutStrategyOptions> = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
114
182
  obstacleMove: ["fixed"],
115
183
  order: ["free", "soft"],
116
- });
184
+ }, LAYOUT_DEFAULTS);
117
185
 
118
186
  /**
119
187
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
120
188
  * With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
189
+ * The option `fit` selects a shelf rule of roadmap item k21: `next`, `first` or `best`. The default has no `fit`.
121
190
  */
122
191
  export const shelf: Strategy<LayoutStrategyOptions> = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
123
192
  obstacleMove: ["fixed"],
124
193
  order: ["free", "soft", "hard"],
125
- });
194
+ }, LAYOUT_DEFAULTS);
195
+
196
+ /**
197
+ * The strategy `maxrects` uses `packMaxRects`: the MaxRects packer of roadmap item k24. It keeps the list of the maximal
198
+ * free rectangles, and the option `rule` chooses the free rectangle for each item. The default rule is `bssf`.
199
+ *
200
+ * - The free rectangles go around the locked items and the fixed obstacles. A plain rectangle container gives exact free
201
+ * rectangles. Another shape gives an inner approximation with horizontal bands.
202
+ * - With `rotate`, each turnable item that is not locked can turn by 90 degrees. Groups and item shapes throw `UnsupportedCell`.
203
+ */
204
+ export const maxrects: Strategy<MaxRectsStrategyOptions> = layoutStrategy(
205
+ "maxrects",
206
+ "maxrects",
207
+ ["maxrects-bssf", "maxrects-blsf", "maxrects-baf", "maxrects-bl", "maxrects-cp", "maxrects-locked"],
208
+ { obstacleMove: ["fixed"], order: ["free", "soft"], rotation: ["none", "quarter"] },
209
+ { ...LAYOUT_DEFAULTS, rule: "bssf", rotate: false },
210
+ );
126
211
 
127
212
  /**
128
213
  * The size search `log-sweep` uses `packFree` of 0.1.2: widths on a log scale, then the widths near the best width. It
129
- * wraps `rows`, `skyline` and `shelf`. The objective is the area of the frame times `1 + aspectWeight * aspectError`.
214
+ * wraps `rows`, `skyline`, `shelf` and `maxrects`. The objective is the area of the frame times `1 + aspectWeight * aspectError`.
130
215
  */
131
216
  export const logSweep: SizeSearch = {
132
217
  id: "log-sweep",
133
218
  search(input: PackInput, inner: Strategy, context: PackContext, options?: unknown): PackOutput {
134
219
  if (input.container.fixed !== "none") return inner.pack(input, options ?? inner.defaults, context);
135
220
  const id = inner.id;
136
- if (id !== "rows" && id !== "skyline" && id !== "shelf") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
137
- const o = { ...(inner.defaults as LayoutStrategyOptions), ...(options as Partial<LayoutStrategyOptions> | undefined) };
221
+ if (id !== "rows" && id !== "skyline" && id !== "shelf" && id !== "maxrects") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
222
+ const o = { ...(inner.defaults as AnyLayoutOptions), ...(options as Partial<AnyLayoutOptions> | undefined) };
138
223
  const start = now();
139
- const frame = buildFrame(input);
224
+ const frame = buildFrame(input, false, id === "maxrects" && o.rotate === true);
225
+ const extras = extrasOf(id, o, frame);
140
226
  const result = packFree(rectsOf(frame), new Rect<number>(), {
141
227
  strategy: id,
142
228
  sortOrder: o.sortOrder,
@@ -144,15 +230,16 @@ export const logSweep: SizeSearch = {
144
230
  samples: o.samples,
145
231
  aspect: input.container.aspect ?? 1,
146
232
  aspectWeight: input.container.aspectWeight ?? 1,
233
+ ...extras,
147
234
  });
148
235
  const ms = now() - start;
149
236
  const runs = result.tried.length + 1;
150
- const output = readFrame(input, frame, []);
237
+ const output = readFrame(input, frame, [], [], turnsOf(extras, false));
151
238
  return {
152
239
  ...output,
153
240
  trace: [
154
241
  { id: "log-sweep", ms, measures: { "pack.search.widths": result.tried.length, "pack.search.ms": ms } },
155
- { id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length, [MOVES[id]]: moveCount(MOVES[id]) } },
242
+ { id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, extras) } },
156
243
  ],
157
244
  };
158
245
  },
package/src/frame.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // are `mA + gap + mB` apart. The area of the frame is the container less the inset, grown by half the gap on each side.
4
4
 
5
5
  import { boundsOf } from "@mark1russell7/layout-contract";
6
- import { EPS, marginOf, type Item, type Obstacle, type PackInput, type PackOutput, type Placement, type Region, type StageTrace } from "./contract.ts";
6
+ import { EPS, marginOf, turnItem, type Item, type Obstacle, type PackInput, type PackOutput, type Placement, type Region, type StageTrace } from "./contract.ts";
7
7
  import { Rect } from "./rect.ts";
8
8
 
9
9
  const clock = (globalThis as { performance?: { now(): number } }).performance;
@@ -73,9 +73,10 @@ export function byRank(items: readonly Item[]): Item[] {
73
73
 
74
74
  /**
75
75
  * This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
76
- * rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`.
76
+ * rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`. With `turnable`, a turnable item
77
+ * is too large only when it is too large in the two directions.
77
78
  */
78
- export function buildFrame(input: PackInput, movable = false): Frame {
79
+ export function buildFrame(input: PackInput, movable = false, turnable = false): Frame {
79
80
  const bounds = containerBounds(input);
80
81
  const inset = input.container.inset ?? 0;
81
82
  const half = (input.gap ?? 0) / 2;
@@ -95,7 +96,10 @@ export function buildFrame(input: PackInput, movable = false): Frame {
95
96
  if (item.locked === true && item.x !== undefined && item.y !== undefined) {
96
97
  rect.locked = true;
97
98
  rect.position.set(item.x - dx - ox, item.y - dy - oy);
98
- } else if (rect.size.x > w + EPS || rect.size.y > h + EPS) {
99
+ } else if (
100
+ (rect.size.x > w + EPS || rect.size.y > h + EPS) &&
101
+ !(turnable && item.turnable === true && rect.size.y <= w + EPS && rect.size.x <= h + EPS)
102
+ ) {
99
103
  tooLarge.push(item.id);
100
104
  continue;
101
105
  }
@@ -128,33 +132,56 @@ export function rectsOf(frame: Frame): Rect<number>[] {
128
132
  return frame.entries.map((e) => e.rect);
129
133
  }
130
134
 
135
+ /** The turns of a packing: the rectangles that turned, and the turn of an item in the axes of the frame. */
136
+ export interface FrameTurns {
137
+ /** The rectangles of the frame that the packer turned. */
138
+ rects: ReadonlySet<unknown>;
139
+ /** This function gives an item after a turn of 90 degrees clockwise in the axes of the input, in the axes of the frame. */
140
+ turn: (item: Item) => Item;
141
+ }
142
+
143
+ /**
144
+ * This function gives the turn of an item for a frame. A frame of `viaAxes` has the axes changed. The turn of the input
145
+ * goes through that change, thus the item turns the other way in the frame.
146
+ */
147
+ export function frameTurn(transposed: boolean): (item: Item) => Item {
148
+ return transposed ? (item) => transposeItem(turnItem(transposeItem(item))) : turnItem;
149
+ }
150
+
131
151
  /**
132
152
  * This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
133
- * position exactly. Each other item that the packer did not place goes to the overflow.
153
+ * position exactly. Each other item that the packer did not place goes to the overflow. With `turns`, each rectangle
154
+ * that turned gives a placement with `turn: 90`.
134
155
  */
135
- export function readFrame(input: PackInput, frame: Frame, trace: readonly StageTrace[], moved: PackOutput["moved"] = []): PackOutput {
156
+ export function readFrame(input: PackInput, frame: Frame, trace: readonly StageTrace[], moved: PackOutput["moved"] = [], turns?: FrameTurns): PackOutput {
136
157
  const placements: Placement[] = [];
137
158
  const overflow: string[] = [...frame.tooLarge];
138
159
  for (const e of frame.entries) {
139
160
  const item = e.item;
140
161
  if (item === undefined) continue;
141
162
  if (item.locked === true && item.x !== undefined && item.y !== undefined) placements.push({ id: item.id, x: item.x, y: item.y });
142
- else if (e.rect.wasPacked) placements.push({ id: item.id, x: e.rect.position.x + e.dx + frame.ox, y: e.rect.position.y + e.dy + frame.oy });
163
+ else if (e.rect.wasPacked && turns?.rects.has(e.rect) === true) {
164
+ // The half gap is the offset less the margin. The margin of the turned item gives the new offset.
165
+ const half = e.dx - marginOf(item).left;
166
+ const m = marginOf(turns.turn(item));
167
+ placements.push({ id: item.id, x: e.rect.position.x + m.left + half + frame.ox, y: e.rect.position.y + m.top + half + frame.oy, turn: 90 });
168
+ } else if (e.rect.wasPacked) placements.push({ id: item.id, x: e.rect.position.x + e.dx + frame.ox, y: e.rect.position.y + e.dy + frame.oy });
143
169
  else overflow.push(item.id);
144
170
  }
145
- return { placements, overflow, size: sizeOf(input, frame, placements, moved), moved, trace };
171
+ return { placements, overflow, size: sizeOf(input, frame, placements, moved, turns), moved, trace };
146
172
  }
147
173
 
148
174
  /**
149
175
  * This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
150
176
  * far edge of the items with their margins and the inset, or of the obstacles.
151
177
  */
152
- function sizeOf(input: PackInput, frame: Frame, placements: Placement[], moved: PackOutput["moved"]): { w: number; h: number } {
178
+ function sizeOf(input: PackInput, frame: Frame, placements: Placement[], moved: PackOutput["moved"], turns?: FrameTurns): { w: number; h: number } {
153
179
  const items = new Map(input.items.map((i) => [i.id, i]));
154
180
  let right = 0;
155
181
  let bottom = 0;
156
182
  for (const p of placements) {
157
- const item = items.get(p.id)!;
183
+ const given = items.get(p.id)!;
184
+ const item = p.turn === 90 && turns !== undefined ? turns.turn(given) : given;
158
185
  const m = marginOf(item);
159
186
  right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
160
187
  bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
package/src/index.ts CHANGED
@@ -3,7 +3,8 @@ export { Rect } from "./rect.ts";
3
3
  export { pack, packRows, type PackOptions, type PackLayoutOptions } from "./pack.ts";
4
4
  export { packSkyline, lowestWindow, type SkylineOptions } from "./skyline.ts";
5
5
  export { freeSpace, type FreeSpace, type FreeSpan, type SpaceRect } from "./space.ts";
6
- export { packShelf, type ShelfOptions } from "./shelf.ts";
6
+ export { packShelf, SHELF_FITS, type ShelfFit, type ShelfOptions } from "./shelf.ts";
7
+ export { packMaxRects, MAXRECTS_BANDS, MAXRECTS_RULES, type MaxRectsOptions, type MaxRectsRule } from "./maxrects.ts";
7
8
  export { packFree, aspectError, type FreeWidthOptions, type FreeWidthResult, type FreeWidthTry } from "./free.ts";
8
9
  export { compareRects, SORT_ORDERS, type SortOrder } from "./order.ts";
9
10
  export { pushCascade, type PushOptions } from "./push.ts";
@@ -66,7 +67,17 @@ export {
66
67
  type Strategy,
67
68
  type Supports,
68
69
  } from "./strategy.ts";
69
- export { logSweep, push, rows, shelf, skyline, type LayoutStrategyOptions, type PushStrategyOptions } from "./adapters.ts";
70
+ export {
71
+ logSweep,
72
+ maxrects,
73
+ push,
74
+ rows,
75
+ shelf,
76
+ skyline,
77
+ type LayoutStrategyOptions,
78
+ type MaxRectsStrategyOptions,
79
+ type PushStrategyOptions,
80
+ } from "./adapters.ts";
70
81
  export { fromRects, toRects } from "./convert.ts";
71
82
  export { packWith, strategies, strategy } from "./registry.ts";
72
83
  export { checkHard, type ContainerTest, type PackHard } from "./check.ts";