@mark1russell7/packing 0.2.4 → 0.2.6

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.4",
4
+ "version": "0.2.6",
5
5
  "description": "Rectangle packing: shelf, guillotine and skyline strategies, push cascades and measures of a layout.",
6
6
  "keywords": [
7
7
  "rectangle-packing",
@@ -49,7 +49,7 @@
49
49
  "node": ">=22.12"
50
50
  },
51
51
  "dependencies": {
52
- "@mark1russell7/layout-contract": "^0.2.4"
52
+ "@mark1russell7/layout-contract": "^0.2.6"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@mark1russell7/cue": "^1.0.2",
package/src/adapters.ts CHANGED
@@ -1,11 +1,12 @@
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";
11
12
  import type { ShelfFit } from "./shelf.ts";
@@ -26,6 +27,28 @@ export interface LayoutStrategyOptions {
26
27
  fit?: ShelfFit;
27
28
  }
28
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
+
29
52
  /** The options of the strategy `push`. */
30
53
  export interface PushStrategyOptions {
31
54
  /** The order of the items that the cascade does not place. */
@@ -61,50 +84,73 @@ function supportsOf(cells: { [D in keyof Supports]: readonly Supports[D][number]
61
84
  };
62
85
  }
63
86
 
64
- /** This function gives the fit of the options of a strategy: only `shelf` reads it. */
65
- function fitOf(id: "rows" | "skyline" | "shelf", options: Partial<LayoutStrategyOptions>): { fit?: ShelfFit } {
66
- return id === "shelf" && options.fit !== undefined ? { fit: options.fit } : {};
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() };
67
98
  }
68
99
 
69
- /** This function gives the move counts of the trace of a strategy: its own move, and the moves of a shelf fit. */
70
- function movesOf(id: "rows" | "skyline" | "shelf", fit: { fit?: ShelfFit }): Record<string, number> {
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
+ }
71
111
  const out: Record<string, number> = { [MOVES[id]]: moveCount(MOVES[id]) };
72
- if (fit.fit !== undefined) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
112
+ if (extras.fit !== undefined) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
73
113
  return out;
74
114
  }
75
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
+
76
121
  /** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
77
- function packFixedWidth(id: "rows" | "skyline" | "shelf", input: PackInput, options: LayoutStrategyOptions): PackOutput {
122
+ function packFixedWidth(id: LayoutId, input: PackInput, options: AnyLayoutOptions, transposed = false): PackOutput {
78
123
  const start = now();
79
- const frame = buildFrame(input);
124
+ const frame = buildFrame(input, false, id === "maxrects" && options.rotate === true);
80
125
  const rects = rectsOf(frame);
81
- const fit = fitOf(id, options);
126
+ const extras = extrasOf(id, options, frame);
82
127
  clearMoves();
83
- if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder, space: spaceOf(input, frame), ...fit });
84
- const output = readFrame(input, frame, []);
85
- return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, fit) } }] };
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) } }] };
86
131
  }
87
132
 
88
133
  /** This function makes the strategy of one function of `pack`. */
89
- function layoutStrategy(
90
- id: "rows" | "skyline" | "shelf",
134
+ function layoutStrategy<O extends AnyLayoutOptions>(
135
+ id: LayoutId,
91
136
  family: string,
92
137
  catalog: string[],
93
- cells: Pick<Supports, "obstacleMove" | "order">,
94
- ): Strategy<LayoutStrategyOptions> {
95
- 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> = {
96
142
  id,
97
143
  family,
98
144
  catalog,
99
145
  supports: supportsOf({ ...LAYOUT_CELLS, ...cells }),
100
- defaults: { sortOrder: "height-desc", samples: 48 },
146
+ defaults: { ...defaults },
101
147
  deterministic: true,
102
148
  pack(input, options, context) {
103
149
  assertSupported(self.supports, input);
104
150
  const o = { ...self.defaults, ...options };
105
151
  switch (input.container.fixed) {
106
152
  case "height":
107
- return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o));
153
+ return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o, true));
108
154
  case "none":
109
155
  return logSweep.search(input, self, context, o);
110
156
  default:
@@ -115,6 +161,9 @@ function layoutStrategy(
115
161
  return self;
116
162
  }
117
163
 
164
+ /** The defaults of `rows`, `skyline` and `shelf`. */
165
+ const LAYOUT_DEFAULTS: LayoutStrategyOptions = { sortOrder: "height-desc", samples: 48 };
166
+
118
167
  /**
119
168
  * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
120
169
  * container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
@@ -123,7 +172,7 @@ function layoutStrategy(
123
172
  export const rows: Strategy<LayoutStrategyOptions> = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
124
173
  obstacleMove: ["fixed"],
125
174
  order: ["free", "soft", "hard"],
126
- });
175
+ }, LAYOUT_DEFAULTS);
127
176
 
128
177
  /**
129
178
  * The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
@@ -132,7 +181,7 @@ export const rows: Strategy<LayoutStrategyOptions> = layoutStrategy("rows", "she
132
181
  export const skyline: Strategy<LayoutStrategyOptions> = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
133
182
  obstacleMove: ["fixed"],
134
183
  order: ["free", "soft"],
135
- });
184
+ }, LAYOUT_DEFAULTS);
136
185
 
137
186
  /**
138
187
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
@@ -142,22 +191,38 @@ export const skyline: Strategy<LayoutStrategyOptions> = layoutStrategy("skyline"
142
191
  export const shelf: Strategy<LayoutStrategyOptions> = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
143
192
  obstacleMove: ["fixed"],
144
193
  order: ["free", "soft", "hard"],
145
- });
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
+ );
146
211
 
147
212
  /**
148
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
149
- * 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`.
150
215
  */
151
216
  export const logSweep: SizeSearch = {
152
217
  id: "log-sweep",
153
218
  search(input: PackInput, inner: Strategy, context: PackContext, options?: unknown): PackOutput {
154
219
  if (input.container.fixed !== "none") return inner.pack(input, options ?? inner.defaults, context);
155
220
  const id = inner.id;
156
- if (id !== "rows" && id !== "skyline" && id !== "shelf") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
157
- 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) };
158
223
  const start = now();
159
- const frame = buildFrame(input);
160
- const fit = fitOf(id, o);
224
+ const frame = buildFrame(input, false, id === "maxrects" && o.rotate === true);
225
+ const extras = extrasOf(id, o, frame);
161
226
  const result = packFree(rectsOf(frame), new Rect<number>(), {
162
227
  strategy: id,
163
228
  sortOrder: o.sortOrder,
@@ -165,16 +230,16 @@ export const logSweep: SizeSearch = {
165
230
  samples: o.samples,
166
231
  aspect: input.container.aspect ?? 1,
167
232
  aspectWeight: input.container.aspectWeight ?? 1,
168
- ...fit,
233
+ ...extras,
169
234
  });
170
235
  const ms = now() - start;
171
236
  const runs = result.tried.length + 1;
172
- const output = readFrame(input, frame, []);
237
+ const output = readFrame(input, frame, [], [], turnsOf(extras, false));
173
238
  return {
174
239
  ...output,
175
240
  trace: [
176
241
  { id: "log-sweep", ms, measures: { "pack.search.widths": result.tried.length, "pack.search.ms": ms } },
177
- { id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, fit) } },
242
+ { id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, extras) } },
178
243
  ],
179
244
  };
180
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
@@ -4,6 +4,7 @@ export { pack, packRows, type PackOptions, type PackLayoutOptions } from "./pack
4
4
  export { packSkyline, lowestWindow, type SkylineOptions } from "./skyline.ts";
5
5
  export { freeSpace, type FreeSpace, type FreeSpan, type SpaceRect } from "./space.ts";
6
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,15 +67,28 @@ 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";
83
+ export { hardBreaks, packPortfolio, PORTFOLIO_LEVELS, PORTFOLIO_STAGE, type PackPortfolioOptions, type PortfolioEntry } from "./portfolio.ts";
84
+ export type { Portfolio, PortfolioPick, PortfolioTrace, MemberTrace, SelectLevel } from "@mark1russell7/layout-contract";
72
85
  export { checkHard, type ContainerTest, type PackHard } from "./check.ts";
73
86
  export {
74
87
  canonicalJson,
75
88
  countCell,
76
89
  fnv1a32,
77
90
  hashInput,
91
+ outputHash,
78
92
  round3,
79
93
  toPackInput,
80
94
  type CorpusCell,
@@ -1,7 +1,7 @@
1
1
  // The input file `layout-input/1` of `docs/FORMAT.md`: its types, its numbers, its canonical form and its hash.
2
2
 
3
3
  import type { LayoutContainer, LayoutEdge, LayoutGenerator, LayoutInput } from "@mark1russell7/layout-contract";
4
- import type { PackInput } from "./contract.ts";
4
+ import type { PackInput, PackOutput } from "./contract.ts";
5
5
  import { sha256Hex, utf8 } from "./sha256.ts";
6
6
  import type { Cell } from "./strategy.ts";
7
7
 
@@ -90,6 +90,16 @@ export function hashInput(file: LayoutInput): string {
90
90
  return sha256Hex(canonicalJson(rest)).slice(0, 16);
91
91
  }
92
92
 
93
+ /**
94
+ * This function gives the `outputHash` of an output. It is the first 16 hex characters of the SHA-256 of the canonical form of
95
+ * its placements, sorted by id. The canonical form rounds each number with `round3`. Thus the order of the list does not count.
96
+ * The records of the benchmark and the key of `packPortfolio` use it. It is not `outputHash` of the contract, which uses a grid.
97
+ */
98
+ export function outputHash(output: Pick<PackOutput, "placements">): string {
99
+ const sorted = output.placements.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
100
+ return sha256Hex(canonicalJson(sorted)).slice(0, 16);
101
+ }
102
+
93
103
  /** This function gives the scale of a count: the nearest of 1, 10, 100, 1000 and 10000 on a log scale. The count 0 gives `1`. */
94
104
  export function countCell(count: number): CountScale {
95
105
  if (count <= 3) return "1";