@mark1russell7/packing 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/index.d.ts +141 -233
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1906 -1496
- package/build/index.js.map +1 -1
- package/package.json +5 -2
- package/src/adapters.ts +30 -21
- package/src/check.ts +34 -27
- package/src/contract.ts +30 -192
- package/src/convert.ts +11 -15
- package/src/corpus.ts +3 -3
- package/src/frame.ts +37 -50
- package/src/generators.ts +11 -12
- package/src/index.ts +9 -4
- package/src/layout-input.ts +34 -43
- package/src/measures.ts +12 -15
- package/src/pack.ts +12 -1
- package/src/shelf.ts +107 -0
- package/src/skyline.ts +125 -2
- package/src/space.ts +379 -0
- package/src/strategy.ts +3 -3
package/build/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
|
|
1
2
|
// src/vector.ts
|
|
2
3
|
/** A mutable two-dimensional vector. */
|
|
3
4
|
var Vector = class Vector {
|
|
@@ -155,1559 +156,1940 @@ function compareRects(order) {
|
|
|
155
156
|
case "input": return () => 0;
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
|
-
// src/
|
|
159
|
+
// src/contract.ts
|
|
160
|
+
/** This function gives the margin of an item on each side. A number is the same margin on each side. */
|
|
161
|
+
function marginOf(item) {
|
|
162
|
+
const m = marginSides(item.margin);
|
|
163
|
+
return {
|
|
164
|
+
top: m.top,
|
|
165
|
+
right: m.right,
|
|
166
|
+
bottom: m.bottom,
|
|
167
|
+
left: m.left
|
|
168
|
+
};
|
|
169
|
+
}
|
|
159
170
|
/**
|
|
160
|
-
* This function gives
|
|
161
|
-
* `
|
|
171
|
+
* This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
|
|
172
|
+
* `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
|
|
173
|
+
* The ports, the shape and the margin turn with the item. The function does not change its argument.
|
|
174
|
+
* It is `turnBox` of the contract.
|
|
162
175
|
*/
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
let moved = true;
|
|
166
|
-
while (moved) {
|
|
167
|
-
moved = false;
|
|
168
|
-
for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.left < x + w && x < o.right) {
|
|
169
|
-
x = o.right;
|
|
170
|
-
moved = true;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return x;
|
|
176
|
+
function turnItem(item) {
|
|
177
|
+
return turnBox(item);
|
|
174
178
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return
|
|
179
|
+
// src/frame.ts
|
|
180
|
+
const clock = globalThis.performance;
|
|
181
|
+
/** This function gives the time in milliseconds. */
|
|
182
|
+
function now() {
|
|
183
|
+
return clock ? clock.now() : Date.now();
|
|
184
|
+
}
|
|
185
|
+
/** This function gives the rectangle of a container. A rectangle shape gives the origin and the sizes of the fixed sides. */
|
|
186
|
+
function containerBounds(input) {
|
|
187
|
+
const { container } = input;
|
|
188
|
+
const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
|
|
189
|
+
const fixedW = container.fixed === "size" || container.fixed === "width";
|
|
190
|
+
const fixedH = container.fixed === "size" || container.fixed === "height";
|
|
191
|
+
const w = shape?.w ?? container.w;
|
|
192
|
+
const h = shape?.h ?? container.h;
|
|
193
|
+
if (fixedW && w === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a width.`);
|
|
194
|
+
if (fixedH && h === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a height.`);
|
|
195
|
+
return {
|
|
196
|
+
x: shape?.x ?? 0,
|
|
197
|
+
y: shape?.y ?? 0,
|
|
198
|
+
w: fixedW ? w : Infinity,
|
|
199
|
+
h: fixedH ? h : Infinity
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** This function gives the items in the order of their rank. The index in the array is the rank of an item without one. */
|
|
203
|
+
function byRank(items) {
|
|
204
|
+
return items.map((item, index) => ({
|
|
205
|
+
item,
|
|
206
|
+
rank: item.rank ?? index
|
|
207
|
+
})).toSorted((a, b) => a.rank - b.rank).map((e) => e.item);
|
|
180
208
|
}
|
|
181
209
|
/**
|
|
182
|
-
* This function
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
|
|
186
|
-
* - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
|
|
187
|
-
* - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
|
|
188
|
-
* a new row starts under the current row.
|
|
189
|
-
* - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
|
|
190
|
-
* - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
|
|
191
|
-
* rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
210
|
+
* This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
|
|
211
|
+
* rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`.
|
|
192
212
|
*/
|
|
193
|
-
function
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
function buildFrame(input, movable = false) {
|
|
214
|
+
const bounds = containerBounds(input);
|
|
215
|
+
const inset = input.container.inset ?? 0;
|
|
216
|
+
const half = (input.gap ?? 0) / 2;
|
|
217
|
+
const ox = bounds.x + inset - half;
|
|
218
|
+
const oy = bounds.y + inset - half;
|
|
219
|
+
const w = bounds.w - 2 * inset + 2 * half;
|
|
220
|
+
const h = bounds.h - 2 * inset + 2 * half;
|
|
221
|
+
const entries = [];
|
|
222
|
+
const tooLarge = [];
|
|
223
|
+
for (const item of byRank(input.items)) {
|
|
224
|
+
const m = marginOf(item);
|
|
225
|
+
const rect = new Rect();
|
|
226
|
+
rect.size.set(item.w + m.left + m.right + 2 * half, item.h + m.top + m.bottom + 2 * half);
|
|
227
|
+
const dx = m.left + half;
|
|
228
|
+
const dy = m.top + half;
|
|
229
|
+
if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
|
|
230
|
+
rect.locked = true;
|
|
231
|
+
rect.position.set(item.x - dx - ox, item.y - dy - oy);
|
|
232
|
+
} else if (rect.size.x > w + EPS || rect.size.y > h + EPS) {
|
|
233
|
+
tooLarge.push(item.id);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
entries.push({
|
|
237
|
+
rect,
|
|
238
|
+
item,
|
|
239
|
+
dx,
|
|
240
|
+
dy
|
|
207
241
|
});
|
|
208
|
-
contentBottom = Math.max(contentBottom, top + rect.size.y);
|
|
209
242
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
rowTop = rowBottom;
|
|
223
|
-
cursor = 0;
|
|
224
|
-
rowEmpty = true;
|
|
225
|
-
}
|
|
226
|
-
let x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
|
|
227
|
-
while (!overflows && x + w > width) {
|
|
228
|
-
rowTop = rowEmpty ? nextTop(obstacles, rowTop, rowTop + h) : rowBottom;
|
|
229
|
-
rowBottom = rowTop;
|
|
230
|
-
cursor = 0;
|
|
231
|
-
rowEmpty = true;
|
|
232
|
-
x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
|
|
233
|
-
}
|
|
234
|
-
while (overflows && x !== 0) {
|
|
235
|
-
rowTop = nextTop(obstacles, rowTop, rowTop + h);
|
|
236
|
-
rowBottom = rowTop;
|
|
237
|
-
x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
|
|
238
|
-
}
|
|
239
|
-
if (outer.fixedSize && rowTop + h > outer.size.y) continue;
|
|
240
|
-
rect.position.set(x + outer.position.x, rowTop + outer.position.y);
|
|
241
|
-
rect.wasPacked = true;
|
|
242
|
-
rowEmpty = false;
|
|
243
|
-
cursor = x + w;
|
|
244
|
-
rowBottom = Math.max(rowBottom, rowTop + h);
|
|
245
|
-
contentBottom = Math.max(contentBottom, rowTop + h);
|
|
246
|
-
if (overflows) {
|
|
247
|
-
rowTop = rowBottom;
|
|
248
|
-
cursor = 0;
|
|
249
|
-
rowEmpty = true;
|
|
250
|
-
}
|
|
243
|
+
for (const obstacle of input.obstacles ?? []) {
|
|
244
|
+
const b = boundsOf(obstacle.region);
|
|
245
|
+
const rect = new Rect();
|
|
246
|
+
rect.size.set(b.w + 2 * half, b.h + 2 * half);
|
|
247
|
+
rect.position.set(b.x - half - ox, b.y - half - oy);
|
|
248
|
+
rect.locked = !(movable && obstacle.move !== void 0);
|
|
249
|
+
entries.push({
|
|
250
|
+
rect,
|
|
251
|
+
obstacle,
|
|
252
|
+
dx: half,
|
|
253
|
+
dy: half
|
|
254
|
+
});
|
|
251
255
|
}
|
|
252
|
-
|
|
256
|
+
return {
|
|
257
|
+
ox,
|
|
258
|
+
oy,
|
|
259
|
+
w,
|
|
260
|
+
h,
|
|
261
|
+
bounds,
|
|
262
|
+
inset,
|
|
263
|
+
entries,
|
|
264
|
+
tooLarge
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/** This function gives the outer rectangle of 0.1 for a frame. A free height starts at 0 and grows. */
|
|
268
|
+
function outerOf(frame) {
|
|
269
|
+
const outer = new Rect();
|
|
270
|
+
const fixedSize = Number.isFinite(frame.h);
|
|
271
|
+
outer.size.set(frame.w, fixedSize ? frame.h : 0);
|
|
272
|
+
outer.fixedWidth = true;
|
|
273
|
+
outer.fixedSize = fixedSize;
|
|
253
274
|
return outer;
|
|
254
275
|
}
|
|
255
|
-
|
|
276
|
+
/** This function gives the rectangles of a frame, in the order of the entries. */
|
|
277
|
+
function rectsOf(frame) {
|
|
278
|
+
return frame.entries.map((e) => e.rect);
|
|
279
|
+
}
|
|
256
280
|
/**
|
|
257
|
-
* This function
|
|
258
|
-
*
|
|
259
|
-
* monotonic deque makes the search O(width) for one rectangle.
|
|
281
|
+
* This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
|
|
282
|
+
* position exactly. Each other item that the packer did not place goes to the overflow.
|
|
260
283
|
*/
|
|
261
|
-
function
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
function readFrame(input, frame, trace, moved = []) {
|
|
285
|
+
const placements = [];
|
|
286
|
+
const overflow = [...frame.tooLarge];
|
|
287
|
+
for (const e of frame.entries) {
|
|
288
|
+
const item = e.item;
|
|
289
|
+
if (item === void 0) continue;
|
|
290
|
+
if (item.locked === true && item.x !== void 0 && item.y !== void 0) placements.push({
|
|
291
|
+
id: item.id,
|
|
292
|
+
x: item.x,
|
|
293
|
+
y: item.y
|
|
294
|
+
});
|
|
295
|
+
else if (e.rect.wasPacked) placements.push({
|
|
296
|
+
id: item.id,
|
|
297
|
+
x: e.rect.position.x + e.dx + frame.ox,
|
|
298
|
+
y: e.rect.position.y + e.dy + frame.oy
|
|
299
|
+
});
|
|
300
|
+
else overflow.push(item.id);
|
|
278
301
|
}
|
|
279
302
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
303
|
+
placements,
|
|
304
|
+
overflow,
|
|
305
|
+
size: sizeOf(input, frame, placements, moved),
|
|
306
|
+
moved,
|
|
307
|
+
trace
|
|
282
308
|
};
|
|
283
309
|
}
|
|
284
310
|
/**
|
|
285
|
-
* This function
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other
|
|
289
|
-
* rectangles go under it. The space above it stays empty, because a skyline has no holes.
|
|
290
|
-
* - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer
|
|
291
|
-
* rectangle has goes under all other rectangles, at the left edge.
|
|
292
|
-
* - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that
|
|
293
|
-
* does not fit is not packed: its `wasPacked` stays `false`.
|
|
294
|
-
* - The default sort order is `height-desc`. The order of the array of the caller stays.
|
|
311
|
+
* This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
|
|
312
|
+
* far edge of the items with their margins and the inset, or of the obstacles.
|
|
295
313
|
*/
|
|
296
|
-
function
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
for (const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const bottom = rect.position.y - outer.position.y + rect.size.y;
|
|
306
|
-
for (let i = x0; i < x1; i++) skyline[i] = Math.max(skyline[i], bottom);
|
|
307
|
-
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, bottom);
|
|
314
|
+
function sizeOf(input, frame, placements, moved) {
|
|
315
|
+
const items = new Map(input.items.map((i) => [i.id, i]));
|
|
316
|
+
let right = 0;
|
|
317
|
+
let bottom = 0;
|
|
318
|
+
for (const p of placements) {
|
|
319
|
+
const item = items.get(p.id);
|
|
320
|
+
const m = marginOf(item);
|
|
321
|
+
right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
|
|
322
|
+
bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
|
|
308
323
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
const movedById = new Map(moved.map((m) => [m.id, m]));
|
|
325
|
+
for (const o of input.obstacles ?? []) {
|
|
326
|
+
const b = boundsOf(o.region);
|
|
327
|
+
const at = movedById.get(o.id) ?? b;
|
|
328
|
+
right = Math.max(right, at.x + b.w - frame.bounds.x);
|
|
329
|
+
bottom = Math.max(bottom, at.y + b.h - frame.bounds.y);
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
|
|
333
|
+
h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/** This function gives a region with the axes changed: x for y, and w for h. */
|
|
337
|
+
function transposeRegion(region) {
|
|
338
|
+
switch (region.kind) {
|
|
339
|
+
case "rect": return {
|
|
340
|
+
kind: "rect",
|
|
341
|
+
x: region.y,
|
|
342
|
+
y: region.x,
|
|
343
|
+
w: region.h,
|
|
344
|
+
h: region.w
|
|
345
|
+
};
|
|
346
|
+
case "circle": return {
|
|
347
|
+
kind: "circle",
|
|
348
|
+
cx: region.cy,
|
|
349
|
+
cy: region.cx,
|
|
350
|
+
r: region.r
|
|
351
|
+
};
|
|
352
|
+
case "ellipse": return {
|
|
353
|
+
kind: "ellipse",
|
|
354
|
+
cx: region.cy,
|
|
355
|
+
cy: region.cx,
|
|
356
|
+
rx: region.ry,
|
|
357
|
+
ry: region.rx
|
|
358
|
+
};
|
|
359
|
+
case "polygon": {
|
|
360
|
+
const swap = (ring) => ring.map(([x, y]) => [y, x]);
|
|
361
|
+
return region.holes === void 0 ? {
|
|
362
|
+
kind: "polygon",
|
|
363
|
+
points: swap(region.points)
|
|
364
|
+
} : {
|
|
365
|
+
kind: "polygon",
|
|
366
|
+
points: swap(region.points),
|
|
367
|
+
holes: region.holes.map(swap)
|
|
368
|
+
};
|
|
324
369
|
}
|
|
325
|
-
rect.position.set(x + outer.position.x, y + outer.position.y);
|
|
326
|
-
rect.wasPacked = true;
|
|
327
|
-
const top = y + rh;
|
|
328
|
-
for (let i = x; i < Math.min(width, x + rw); i++) skyline[i] = top;
|
|
329
|
-
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
|
|
330
370
|
}
|
|
331
|
-
return outer;
|
|
332
371
|
}
|
|
333
|
-
|
|
334
|
-
function
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
372
|
+
/** This function gives an item with the axes changed. The ports go, because the adapters of 0.1 do not read them. */
|
|
373
|
+
function transposeItem(item) {
|
|
374
|
+
const { ports: _ports, x, y, shape, margin, ...rest } = item;
|
|
375
|
+
return {
|
|
376
|
+
...rest,
|
|
377
|
+
w: item.h,
|
|
378
|
+
h: item.w,
|
|
379
|
+
...y === void 0 ? {} : { x: y },
|
|
380
|
+
...x === void 0 ? {} : { y: x },
|
|
381
|
+
...shape === void 0 ? {} : { shape: transposeRegion(shape) },
|
|
382
|
+
...margin === void 0 ? {} : { margin: typeof margin === "number" ? margin : {
|
|
383
|
+
top: margin.left,
|
|
384
|
+
right: margin.bottom,
|
|
385
|
+
bottom: margin.right,
|
|
386
|
+
left: margin.top
|
|
387
|
+
} }
|
|
388
|
+
};
|
|
338
389
|
}
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
390
|
+
/** This function gives an input with the axes changed. A fixed height becomes a fixed width. */
|
|
391
|
+
function transposeInput(input) {
|
|
392
|
+
const c = input.container;
|
|
393
|
+
return {
|
|
394
|
+
container: {
|
|
395
|
+
fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed,
|
|
396
|
+
...c.h === void 0 ? {} : { w: c.h },
|
|
397
|
+
...c.w === void 0 ? {} : { h: c.w },
|
|
398
|
+
...c.aspect === void 0 ? {} : { aspect: 1 / c.aspect },
|
|
399
|
+
...c.aspectWeight === void 0 ? {} : { aspectWeight: c.aspectWeight },
|
|
400
|
+
...c.shape === void 0 ? {} : { shape: transposeRegion(c.shape) },
|
|
401
|
+
...c.inset === void 0 ? {} : { inset: c.inset }
|
|
402
|
+
},
|
|
403
|
+
items: input.items.map(transposeItem),
|
|
404
|
+
...input.obstacles === void 0 ? {} : { obstacles: input.obstacles.map((o) => ({
|
|
405
|
+
...o,
|
|
406
|
+
region: transposeRegion(o.region)
|
|
407
|
+
})) },
|
|
408
|
+
...input.gap === void 0 ? {} : { gap: input.gap }
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/** This function gives an output with the axes changed. */
|
|
412
|
+
function transposeOutput(output) {
|
|
413
|
+
return {
|
|
414
|
+
placements: output.placements.map((p) => ({
|
|
415
|
+
...p,
|
|
416
|
+
x: p.y,
|
|
417
|
+
y: p.x
|
|
418
|
+
})),
|
|
419
|
+
overflow: [...output.overflow],
|
|
420
|
+
size: {
|
|
421
|
+
w: output.size.h,
|
|
422
|
+
h: output.size.w
|
|
423
|
+
},
|
|
424
|
+
moved: output.moved.map((m) => ({
|
|
425
|
+
id: m.id,
|
|
426
|
+
x: m.y,
|
|
427
|
+
y: m.x
|
|
428
|
+
})),
|
|
429
|
+
...output.trace === void 0 ? {} : { trace: output.trace }
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* This function packs an input with a fixed height through the turn of the axes. It changes x for y in the input.
|
|
434
|
+
* Then it packs with a fixed width, and changes the axes of the output back.
|
|
435
|
+
*/
|
|
436
|
+
function viaAxes(input, previous, run) {
|
|
437
|
+
const start = now();
|
|
438
|
+
const inner = run(transposeInput(input), previous === void 0 ? void 0 : transposeOutput(previous));
|
|
439
|
+
return {
|
|
440
|
+
...transposeOutput(inner),
|
|
441
|
+
trace: [{
|
|
442
|
+
id: "axes",
|
|
443
|
+
ms: now() - start,
|
|
444
|
+
measures: {}
|
|
445
|
+
}, ...inner.trace ?? []]
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// src/space.ts
|
|
449
|
+
/** This function removes the closed interval from `a` to `b` from a list of spans. A span of no length goes. */
|
|
450
|
+
function subtract(spans, a, b) {
|
|
451
|
+
const out = [];
|
|
452
|
+
for (const [s, e] of spans) {
|
|
453
|
+
if (b <= s || a >= e) {
|
|
454
|
+
out.push([s, e]);
|
|
455
|
+
continue;
|
|
357
456
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
457
|
+
if (a > s) out.push([s, a]);
|
|
458
|
+
if (e > b) out.push([b, e]);
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
/** This function gives the x of a segment at `y`. The ends give their exact x. */
|
|
463
|
+
function xAt(ax, ay, bx, by, y) {
|
|
464
|
+
if (y === ay) return ax;
|
|
465
|
+
if (y === by) return bx;
|
|
466
|
+
return ax + (y - ay) * (bx - ax) / (by - ay);
|
|
467
|
+
}
|
|
468
|
+
/** This function gives the x-range of the part of a segment in the open strip from `y0` to `y1`, or `undefined`. */
|
|
469
|
+
function edgeRange(ax, ay, bx, by, y0, y1) {
|
|
470
|
+
const lo = Math.min(ay, by);
|
|
471
|
+
const hi = Math.max(ay, by);
|
|
472
|
+
if (hi <= y0 || lo >= y1) return void 0;
|
|
473
|
+
if (ay === by) return [Math.min(ax, bx), Math.max(ax, bx)];
|
|
474
|
+
const p = xAt(ax, ay, bx, by, Math.max(y0, lo));
|
|
475
|
+
const q = xAt(ax, ay, bx, by, Math.min(y1, hi));
|
|
476
|
+
return [Math.min(p, q), Math.max(p, q)];
|
|
477
|
+
}
|
|
478
|
+
/** This function gives the x-range of a region in the open strip from `y0` to `y1`, or `undefined` when they have no common point. */
|
|
479
|
+
function regionRange(region, y0, y1) {
|
|
480
|
+
switch (region.kind) {
|
|
481
|
+
case "rect": return region.y < y1 && y0 < region.y + region.h ? [region.x, region.x + region.w] : void 0;
|
|
482
|
+
case "circle":
|
|
483
|
+
case "ellipse": {
|
|
484
|
+
const rx = region.kind === "circle" ? region.r : region.rx;
|
|
485
|
+
const ry = region.kind === "circle" ? region.r : region.ry;
|
|
486
|
+
if (y1 <= region.cy - ry || y0 >= region.cy + ry) return void 0;
|
|
487
|
+
const dy = y0 <= region.cy && region.cy <= y1 ? 0 : Math.min(Math.abs(y0 - region.cy), Math.abs(y1 - region.cy));
|
|
488
|
+
const c = rx * Math.sqrt(Math.max(0, 1 - (dy / ry) ** 2));
|
|
489
|
+
return [region.cx - c, region.cx + c];
|
|
364
490
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
491
|
+
case "polygon": {
|
|
492
|
+
let lo = Infinity;
|
|
493
|
+
let hi = -Infinity;
|
|
494
|
+
const ring = region.points;
|
|
495
|
+
for (let i = 0; i < ring.length; i++) {
|
|
496
|
+
const a = ring[i];
|
|
497
|
+
const b = ring[(i + 1) % ring.length];
|
|
498
|
+
const r = edgeRange(a[0], a[1], b[0], b[1], y0, y1);
|
|
499
|
+
if (r === void 0) continue;
|
|
500
|
+
lo = Math.min(lo, r[0]);
|
|
501
|
+
hi = Math.max(hi, r[1]);
|
|
502
|
+
}
|
|
503
|
+
return lo <= hi ? [lo, hi] : void 0;
|
|
378
504
|
}
|
|
379
505
|
}
|
|
380
|
-
return true;
|
|
381
506
|
}
|
|
382
|
-
/**
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
507
|
+
/** This function gives the spans of the outline of a layer for the strip from `y0` to `y1`, before the holes and the blockers. */
|
|
508
|
+
function outline(layer, y0, y1) {
|
|
509
|
+
const { d } = layer;
|
|
510
|
+
switch (layer.kind) {
|
|
511
|
+
case "plain": return [[0, layer.w]];
|
|
512
|
+
case "box": return y0 < 0 || y1 > layer.h ? [] : [[0, layer.w]];
|
|
513
|
+
case "round": {
|
|
514
|
+
const r = layer.region;
|
|
515
|
+
if (r.kind !== "circle" && r.kind !== "ellipse") return [];
|
|
516
|
+
const rx = r.kind === "circle" ? r.r : r.rx;
|
|
517
|
+
const ry = r.kind === "circle" ? r.r : r.ry;
|
|
518
|
+
const top = y0 - d;
|
|
519
|
+
const bottom = y1 + d;
|
|
520
|
+
if (top < r.cy - ry || bottom > r.cy + ry) return [];
|
|
521
|
+
const dy = Math.max(Math.abs(top - r.cy), Math.abs(bottom - r.cy));
|
|
522
|
+
const c = rx * Math.sqrt(Math.max(0, 1 - (dy / ry) ** 2)) - EPS;
|
|
523
|
+
const a = r.cx - c + d;
|
|
524
|
+
const b = r.cx + c - d;
|
|
525
|
+
return b > a ? [[a, b]] : [];
|
|
526
|
+
}
|
|
527
|
+
case "rings": {
|
|
528
|
+
const top = y0 - d;
|
|
529
|
+
const bottom = y1 + d;
|
|
530
|
+
if (!(bottom > top)) return [];
|
|
531
|
+
const blocked = [];
|
|
532
|
+
for (const ring of layer.rings) for (let i = 0; i < ring.length; i++) {
|
|
533
|
+
const a = ring[i];
|
|
534
|
+
const b = ring[(i + 1) % ring.length];
|
|
535
|
+
const r = edgeRange(a[0], a[1], b[0], b[1], top, bottom);
|
|
536
|
+
if (r !== void 0) blocked.push(r);
|
|
537
|
+
}
|
|
538
|
+
if (blocked.length === 0) return [];
|
|
539
|
+
blocked.sort((p, q) => p[0] - q[0] || p[1] - q[1]);
|
|
540
|
+
const merged = [];
|
|
541
|
+
for (const r of blocked) {
|
|
542
|
+
const last = merged.at(-1);
|
|
543
|
+
if (last !== void 0 && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);
|
|
544
|
+
else merged.push([r[0], r[1]]);
|
|
545
|
+
}
|
|
546
|
+
const spans = [];
|
|
547
|
+
for (let i = 0; i + 1 < merged.length; i++) {
|
|
548
|
+
const a = merged[i][1];
|
|
549
|
+
const b = merged[i + 1][0];
|
|
550
|
+
if (!(b > a)) continue;
|
|
551
|
+
if (!regionHitsRect(layer.region, {
|
|
552
|
+
x: a,
|
|
553
|
+
y: top,
|
|
554
|
+
w: b - a,
|
|
555
|
+
h: bottom - top
|
|
556
|
+
})) continue;
|
|
557
|
+
if (b - d > a + d) spans.push([a + d, b - d]);
|
|
558
|
+
}
|
|
559
|
+
return spans;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
392
562
|
}
|
|
393
|
-
/**
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
* - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
|
|
400
|
-
* - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
401
|
-
* - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
|
|
402
|
-
*/
|
|
403
|
-
function pack(rects, outer = new Rect(), options = {}) {
|
|
404
|
-
let totalArea = 0;
|
|
405
|
-
let widest = 0;
|
|
406
|
-
let minX = rects.length > 0 ? Infinity : 0;
|
|
407
|
-
let minY = rects.length > 0 ? Infinity : 0;
|
|
408
|
-
for (const r of rects) {
|
|
409
|
-
totalArea += r.area();
|
|
410
|
-
widest = Math.max(widest, r.size.x);
|
|
411
|
-
minX = Math.min(minX, r.size.x);
|
|
412
|
-
minY = Math.min(minY, r.size.y);
|
|
563
|
+
/** This function gives the free spans of a layer for the strip from `y0` to `y1`. */
|
|
564
|
+
function spansOf(layer, y0, y1) {
|
|
565
|
+
let spans = outline(layer, y0, y1);
|
|
566
|
+
for (const hole of layer.holes) {
|
|
567
|
+
if (spans.length === 0) return spans;
|
|
568
|
+
if (Math.min(y1, hole.c1) - Math.max(y0, hole.c0) > 0) spans = subtract(spans, hole.a0, hole.a1);
|
|
413
569
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
else packRows(rects, outer, options);
|
|
420
|
-
if (!outer.fixedSize) {
|
|
421
|
-
const contentRight = Rect.maxRight(rects);
|
|
422
|
-
if (!outer.size.x) outer.size.x = contentRight;
|
|
423
|
-
else if (!outer.fixedWidth) outer.size.x = Math.min(contentRight, outer.size.x);
|
|
424
|
-
outer.size.y = Rect.maxBottom(rects);
|
|
570
|
+
for (const b of layer.blockers) {
|
|
571
|
+
if (spans.length === 0) return spans;
|
|
572
|
+
if (b.c1 + b.g <= y0 || b.c0 - b.g >= y1) continue;
|
|
573
|
+
const r = regionRange(b.region, y0 - b.g, y1 + b.g);
|
|
574
|
+
if (r !== void 0) spans = subtract(spans, r[0] - b.g - EPS, r[1] + b.g + EPS);
|
|
425
575
|
}
|
|
426
|
-
return
|
|
576
|
+
return spans;
|
|
427
577
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
578
|
+
/** This function gives the first x from `x` where a box of width `w` fits in one of the spans, or `Infinity`. */
|
|
579
|
+
function firstFit(spans, x, w) {
|
|
580
|
+
for (const s of spans) {
|
|
581
|
+
const at = Math.max(x, s.start);
|
|
582
|
+
if (at + w <= s.end) return at;
|
|
583
|
+
}
|
|
584
|
+
return Infinity;
|
|
585
|
+
}
|
|
586
|
+
/** This function gives the next top edge of a layer after `y0` for a strip of the height `y1 - y0`, as `FreeSpace.nextY` tells. */
|
|
587
|
+
function nextOf(layer, y0, y1, w) {
|
|
588
|
+
const height = y1 - y0;
|
|
589
|
+
const { d } = layer;
|
|
590
|
+
let next = Infinity;
|
|
591
|
+
const take = (y) => {
|
|
592
|
+
if (y > y0 + EPS && y < next) next = y;
|
|
593
|
+
};
|
|
594
|
+
for (const hole of layer.holes) if (hole.c0 < y1 && y0 < hole.c1) take(hole.c1);
|
|
595
|
+
for (const b of layer.blockers) if (b.c0 - b.g < y1 && y0 < b.c1 + b.g) take(b.c1 + b.g);
|
|
596
|
+
switch (layer.kind) {
|
|
597
|
+
case "plain": break;
|
|
598
|
+
case "box":
|
|
599
|
+
if (y1 > layer.h) return Infinity;
|
|
600
|
+
break;
|
|
601
|
+
case "rings":
|
|
602
|
+
for (const ring of layer.rings) for (const [, vy] of ring) {
|
|
603
|
+
take(vy + d);
|
|
604
|
+
take(vy - height - d);
|
|
605
|
+
}
|
|
606
|
+
break;
|
|
607
|
+
case "round": {
|
|
608
|
+
const r = layer.region;
|
|
609
|
+
if (r.kind !== "circle" && r.kind !== "ellipse") break;
|
|
610
|
+
const rx = r.kind === "circle" ? r.r : r.rx;
|
|
611
|
+
const ry = r.kind === "circle" ? r.r : r.ry;
|
|
612
|
+
const need = w / 2 + d + EPS;
|
|
613
|
+
if (need >= rx) return Infinity;
|
|
614
|
+
const reach = ry * Math.sqrt(1 - (need / rx) ** 2);
|
|
615
|
+
if (y1 + d > r.cy + reach) return Infinity;
|
|
616
|
+
take(r.cy - reach + d);
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return next;
|
|
621
|
+
}
|
|
622
|
+
/** This function gives the layer with the axes changed: the spans go along y, and the query goes along x. */
|
|
623
|
+
function transposeLayer(layer) {
|
|
624
|
+
return {
|
|
625
|
+
kind: layer.kind,
|
|
626
|
+
w: layer.h,
|
|
627
|
+
h: layer.w,
|
|
628
|
+
d: layer.d,
|
|
629
|
+
...layer.region === void 0 ? {} : { region: transposeRegion(layer.region) },
|
|
630
|
+
rings: layer.rings.map((ring) => ring.map(([x, y]) => [y, x])),
|
|
631
|
+
holes: layer.holes.map((h) => ({
|
|
632
|
+
a0: h.c0,
|
|
633
|
+
a1: h.c1,
|
|
634
|
+
c0: h.a0,
|
|
635
|
+
c1: h.a1
|
|
636
|
+
})),
|
|
637
|
+
blockers: layer.blockers.map((b) => {
|
|
638
|
+
const region = transposeRegion(b.region);
|
|
639
|
+
const box = boundsOf(region);
|
|
640
|
+
return {
|
|
641
|
+
region,
|
|
642
|
+
g: b.g,
|
|
643
|
+
c0: box.y,
|
|
644
|
+
c1: box.y + box.h
|
|
645
|
+
};
|
|
646
|
+
})
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/** This function makes a blocker of a region in the frame. */
|
|
650
|
+
function blockerOf(region, g) {
|
|
651
|
+
const box = boundsOf(region);
|
|
652
|
+
return {
|
|
653
|
+
region,
|
|
654
|
+
g,
|
|
655
|
+
c0: box.y,
|
|
656
|
+
c1: box.y + box.h
|
|
657
|
+
};
|
|
433
658
|
}
|
|
434
659
|
/**
|
|
435
|
-
* This function
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
|
|
439
|
-
* - The second pass tries the widths between the two neighbours of the best width of the first pass.
|
|
440
|
-
* - The search is deterministic: the same input gives the same widths and the same result.
|
|
441
|
-
* - A `locked` rectangle keeps its position. The width is not less than its right edge.
|
|
442
|
-
* - Each strategy of `pack` works with this function, because it only changes the width.
|
|
660
|
+
* This function gives the free space of an input and its frame. The adapters use it with the frame that they pack.
|
|
661
|
+
* `freeSpace` makes the frame itself.
|
|
443
662
|
*/
|
|
444
|
-
function
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
let
|
|
453
|
-
for (const
|
|
454
|
-
if (
|
|
455
|
-
|
|
663
|
+
function spaceOf(input, frame) {
|
|
664
|
+
const { container } = input;
|
|
665
|
+
const half = (input.gap ?? 0) / 2;
|
|
666
|
+
const d = frame.inset - half;
|
|
667
|
+
const shape = container.shape === void 0 ? void 0 : translate(container.shape, -frame.ox, -frame.oy);
|
|
668
|
+
const fixedSize = Number.isFinite(frame.w) && Number.isFinite(frame.h);
|
|
669
|
+
const holes = [];
|
|
670
|
+
const blockers = [];
|
|
671
|
+
let rectObstacles = true;
|
|
672
|
+
for (const e of frame.entries) {
|
|
673
|
+
if (!e.rect.locked) continue;
|
|
674
|
+
if (e.obstacle !== void 0 && e.obstacle.region.kind !== "rect") {
|
|
675
|
+
rectObstacles = false;
|
|
676
|
+
blockers.push(blockerOf(translate(e.obstacle.region, -frame.ox, -frame.oy), half));
|
|
456
677
|
continue;
|
|
457
678
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const tried = [];
|
|
466
|
-
const run = (width) => {
|
|
467
|
-
Rect.resetPacked(rects);
|
|
468
|
-
const box = new Rect();
|
|
469
|
-
box.position.setTo(outer.position);
|
|
470
|
-
box.size.set(width, 0);
|
|
471
|
-
box.fixedWidth = true;
|
|
472
|
-
pack(rects, box, options);
|
|
473
|
-
const placed = Rect.packed(rects);
|
|
474
|
-
const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
|
|
475
|
-
const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
|
|
476
|
-
const area = contentWidth * contentHeight;
|
|
477
|
-
const t = {
|
|
478
|
-
width,
|
|
479
|
-
contentWidth,
|
|
480
|
-
contentHeight,
|
|
481
|
-
objective: area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0
|
|
482
|
-
};
|
|
483
|
-
tried.push(t);
|
|
484
|
-
return t;
|
|
485
|
-
};
|
|
486
|
-
const widths = [];
|
|
487
|
-
if (maxWidth > 0) {
|
|
488
|
-
const ratio = maxWidth / minWidth;
|
|
489
|
-
for (let i = 0; i < samples; i++) {
|
|
490
|
-
const w = minWidth * ratio ** (i / (samples - 1));
|
|
491
|
-
const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
|
|
492
|
-
if (widths.at(-1) !== rounded) widths.push(rounded);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
let best = void 0;
|
|
496
|
-
let bestIndex = 0;
|
|
497
|
-
for (const [i, w] of widths.entries()) {
|
|
498
|
-
const t = run(w);
|
|
499
|
-
if (best === void 0 || t.objective < best.objective) {
|
|
500
|
-
best = t;
|
|
501
|
-
bestIndex = i;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
if (widths.length > 2) {
|
|
505
|
-
const low = widths[Math.max(0, bestIndex - 1)];
|
|
506
|
-
const high = widths[Math.min(widths.length - 1, bestIndex + 1)];
|
|
507
|
-
const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
|
|
508
|
-
for (let i = 1; i <= steps; i++) {
|
|
509
|
-
const w = Math.round(low + (high - low) * i / (steps + 1));
|
|
510
|
-
if (widths.includes(w)) continue;
|
|
511
|
-
const t = run(w);
|
|
512
|
-
if (best === void 0 || t.objective < best.objective) best = t;
|
|
513
|
-
}
|
|
679
|
+
const r = e.rect;
|
|
680
|
+
holes.push({
|
|
681
|
+
a0: r.position.x,
|
|
682
|
+
a1: r.right(),
|
|
683
|
+
c0: r.position.y,
|
|
684
|
+
c1: r.bottom()
|
|
685
|
+
});
|
|
514
686
|
}
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
fill: area > 0 ? itemArea / area : 0,
|
|
525
|
-
aspect: h > 0 ? w / h : 0,
|
|
526
|
-
aspectError: aspectError(w, h, target),
|
|
527
|
-
objective: chosen?.objective ?? 0,
|
|
528
|
-
tried
|
|
687
|
+
const plain = (shape === void 0 || shape.kind === "rect") && rectObstacles;
|
|
688
|
+
let layer = {
|
|
689
|
+
kind: plain ? "plain" : "box",
|
|
690
|
+
w: frame.w,
|
|
691
|
+
h: frame.h,
|
|
692
|
+
d,
|
|
693
|
+
rings: [],
|
|
694
|
+
holes,
|
|
695
|
+
blockers
|
|
529
696
|
};
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
697
|
+
if (shape !== void 0 && shape.kind !== "rect") {
|
|
698
|
+
if (fixedSize && shape.kind === "polygon") layer = {
|
|
699
|
+
...layer,
|
|
700
|
+
kind: "rings",
|
|
701
|
+
region: shape,
|
|
702
|
+
rings: [shape.points, ...shape.holes ?? []]
|
|
703
|
+
};
|
|
704
|
+
else if (fixedSize) layer = {
|
|
705
|
+
...layer,
|
|
706
|
+
kind: "round",
|
|
707
|
+
region: shape
|
|
708
|
+
};
|
|
709
|
+
else if (shape.kind === "polygon") for (const ring of shape.holes ?? []) blockers.push(blockerOf({
|
|
710
|
+
kind: "polygon",
|
|
711
|
+
points: ring
|
|
712
|
+
}, d));
|
|
713
|
+
}
|
|
714
|
+
const across = transposeLayer(layer);
|
|
715
|
+
const band = (y0, y1) => spansOf(layer, y0, y1).map(([start, end]) => ({
|
|
716
|
+
start,
|
|
717
|
+
end
|
|
718
|
+
}));
|
|
534
719
|
return {
|
|
535
|
-
|
|
536
|
-
|
|
720
|
+
w: frame.w,
|
|
721
|
+
h: frame.h,
|
|
722
|
+
plain,
|
|
723
|
+
band,
|
|
724
|
+
column(x0, x1, from = 0) {
|
|
725
|
+
for (const [start, end] of spansOf(across, x0, x1)) {
|
|
726
|
+
const top = Math.max(start, from);
|
|
727
|
+
if (end > top) return {
|
|
728
|
+
start: top,
|
|
729
|
+
end
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
fits(rect) {
|
|
734
|
+
return firstFit(band(rect.y, rect.y + rect.h), rect.x, rect.w) === rect.x;
|
|
735
|
+
},
|
|
736
|
+
fitX(x, w, y0, y1) {
|
|
737
|
+
return firstFit(band(y0, y1), x, w);
|
|
738
|
+
},
|
|
739
|
+
nextY(y0, y1, w) {
|
|
740
|
+
return nextOf(layer, y0, y1, w);
|
|
741
|
+
}
|
|
537
742
|
};
|
|
538
743
|
}
|
|
539
744
|
/**
|
|
540
|
-
* This function gives the
|
|
541
|
-
*
|
|
745
|
+
* This function gives the free space of an input, in the coordinates of the frame of the adapters. The origin of the
|
|
746
|
+
* frame is the corner of the container, moved in by the inset and out by half the gap. `FreeSpace` tells the methods.
|
|
542
747
|
*/
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
if (overlap.x < overlap.y) return {
|
|
546
|
-
axis: "x",
|
|
547
|
-
sign: r.position.x + r.size.x / 2 < pusher.position.x + pusher.size.x / 2 && pusher.position.x - r.size.x - gap >= 0 ? -1 : 1
|
|
548
|
-
};
|
|
549
|
-
return {
|
|
550
|
-
axis: "y",
|
|
551
|
-
sign: r.position.y + r.size.y / 2 < pusher.position.y + pusher.size.y / 2 && pusher.position.y - r.size.y - gap >= 0 ? -1 : 1
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
/** This function puts a rectangle past the pusher, along a direction. The rectangle does not go past 0. */
|
|
555
|
-
function shove(pusher, r, direction, gap) {
|
|
556
|
-
if (direction.axis === "x") {
|
|
557
|
-
const x = direction.sign > 0 ? pusher.right() + gap : pusher.position.x - r.size.x - gap;
|
|
558
|
-
r.position.set(Math.max(0, x), r.position.y);
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
const y = direction.sign > 0 ? pusher.bottom() + gap : pusher.position.y - r.size.y - gap;
|
|
562
|
-
r.position.set(r.position.x, Math.max(0, y));
|
|
748
|
+
function freeSpace(input) {
|
|
749
|
+
return spaceOf(input, buildFrame(input));
|
|
563
750
|
}
|
|
751
|
+
// src/shelf.ts
|
|
564
752
|
/**
|
|
565
|
-
* This function
|
|
566
|
-
*
|
|
567
|
-
* shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.
|
|
568
|
-
* The function moves the rectangles in place and gives the rectangles that moved.
|
|
569
|
-
*
|
|
570
|
-
* @example
|
|
571
|
-
* ```ts
|
|
572
|
-
* const moved = pushCascade(dropped, cards, { gap: 8 });
|
|
573
|
-
* ```
|
|
753
|
+
* This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from
|
|
754
|
+
* `top` to `bottom`.
|
|
574
755
|
*/
|
|
575
|
-
function
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (!step) break;
|
|
584
|
-
const { pusher, direction } = step;
|
|
585
|
-
for (const r of rects) {
|
|
586
|
-
if (r === pusher || r === placed || r.locked) continue;
|
|
587
|
-
const overlap = overlapOf(pusher, r);
|
|
588
|
-
if (overlap.x <= 0 || overlap.y <= 0) continue;
|
|
589
|
-
const next = direction ?? firstDirection(pusher, r, gap);
|
|
590
|
-
shove(pusher, r, next, gap);
|
|
591
|
-
moved.add(r);
|
|
592
|
-
queue.push({
|
|
593
|
-
pusher: r,
|
|
594
|
-
direction: next
|
|
595
|
-
});
|
|
596
|
-
if (++shoves >= maxShoves) break;
|
|
756
|
+
function firstFreeX(obstacles, start, w, top, bottom) {
|
|
757
|
+
let x = start;
|
|
758
|
+
let moved = true;
|
|
759
|
+
while (moved) {
|
|
760
|
+
moved = false;
|
|
761
|
+
for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.left < x + w && x < o.right) {
|
|
762
|
+
x = o.right;
|
|
763
|
+
moved = true;
|
|
597
764
|
}
|
|
598
765
|
}
|
|
599
|
-
return
|
|
600
|
-
}
|
|
601
|
-
// src/contract.ts
|
|
602
|
-
/** The tolerance for the comparison of two numbers in an engine. */
|
|
603
|
-
const EPS = 1e-6;
|
|
604
|
-
/** The tolerance for the comparison of two numbers of a file. The files round to 3 decimals. */
|
|
605
|
-
const RECORD_EPS = .001;
|
|
606
|
-
/** The default tolerances of the hard rules. Each record writes its tolerances in its top-level field `tolerance`. */
|
|
607
|
-
const DEFAULT_TOLERANCE = Object.freeze({
|
|
608
|
-
overlap: .001,
|
|
609
|
-
through: .5
|
|
610
|
-
});
|
|
611
|
-
/** This function gives the margin of an item on each side. A number is the same margin on each side. */
|
|
612
|
-
function marginOf(item) {
|
|
613
|
-
const m = item.margin ?? 0;
|
|
614
|
-
if (typeof m === "number") return {
|
|
615
|
-
top: m,
|
|
616
|
-
right: m,
|
|
617
|
-
bottom: m,
|
|
618
|
-
left: m
|
|
619
|
-
};
|
|
620
|
-
return {
|
|
621
|
-
top: m.top,
|
|
622
|
-
right: m.right,
|
|
623
|
-
bottom: m.bottom,
|
|
624
|
-
left: m.left
|
|
625
|
-
};
|
|
766
|
+
return x;
|
|
626
767
|
}
|
|
627
|
-
/**
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
left: "top"
|
|
633
|
-
};
|
|
634
|
-
/** This function gives the point `px`, `py` of an item of height `h` after a turn of 90 degrees clockwise. */
|
|
635
|
-
function turnPoint(px, py, h) {
|
|
636
|
-
return [h - py, px];
|
|
637
|
-
}
|
|
638
|
-
/** This function turns a port of an item of height `h` by 90 degrees clockwise. The side turns from top to right. */
|
|
639
|
-
function turnPort(port, h) {
|
|
640
|
-
const turned = {
|
|
641
|
-
id: port.id,
|
|
642
|
-
side: TURNED_SIDE[port.side]
|
|
643
|
-
};
|
|
644
|
-
if (port.x !== void 0 && port.y !== void 0) {
|
|
645
|
-
const [x, y] = turnPoint(port.x, port.y, h);
|
|
646
|
-
turned.x = x;
|
|
647
|
-
turned.y = y;
|
|
648
|
-
} else if (port.x !== void 0) turned.y = port.x;
|
|
649
|
-
else if (port.y !== void 0) turned.x = h - port.y;
|
|
650
|
-
return turned;
|
|
651
|
-
}
|
|
652
|
-
/** This function turns a region relative to an item of height `h` by 90 degrees clockwise. */
|
|
653
|
-
function turnRegion(region, h) {
|
|
654
|
-
switch (region.kind) {
|
|
655
|
-
case "rect": return {
|
|
656
|
-
kind: "rect",
|
|
657
|
-
x: h - region.y - region.h,
|
|
658
|
-
y: region.x,
|
|
659
|
-
w: region.h,
|
|
660
|
-
h: region.w
|
|
661
|
-
};
|
|
662
|
-
case "circle": {
|
|
663
|
-
const [cx, cy] = turnPoint(region.cx, region.cy, h);
|
|
664
|
-
return {
|
|
665
|
-
kind: "circle",
|
|
666
|
-
cx,
|
|
667
|
-
cy,
|
|
668
|
-
r: region.r
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
case "ellipse": {
|
|
672
|
-
const [cx, cy] = turnPoint(region.cx, region.cy, h);
|
|
673
|
-
return {
|
|
674
|
-
kind: "ellipse",
|
|
675
|
-
cx,
|
|
676
|
-
cy,
|
|
677
|
-
rx: region.ry,
|
|
678
|
-
ry: region.rx
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
case "polygon": {
|
|
682
|
-
const turnRing = (ring) => ring.map(([x, y]) => turnPoint(x, y, h));
|
|
683
|
-
const turned = {
|
|
684
|
-
kind: "polygon",
|
|
685
|
-
points: turnRing(region.points)
|
|
686
|
-
};
|
|
687
|
-
if (region.holes) turned.holes = region.holes.map(turnRing);
|
|
688
|
-
return turned;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
768
|
+
/** This function gives the lowest bottom edge of the obstacles in the band from `top` to `bottom`, under `top`. */
|
|
769
|
+
function nextTop(obstacles, top, bottom) {
|
|
770
|
+
let next = Infinity;
|
|
771
|
+
for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.bottom > top) next = Math.min(next, o.bottom);
|
|
772
|
+
return next;
|
|
691
773
|
}
|
|
692
774
|
/**
|
|
693
|
-
* This function
|
|
694
|
-
*
|
|
695
|
-
*
|
|
775
|
+
* This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill
|
|
776
|
+
* again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.
|
|
777
|
+
*
|
|
778
|
+
* - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
|
|
779
|
+
* - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
|
|
780
|
+
* - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
|
|
781
|
+
* a new row starts under the current row.
|
|
782
|
+
* - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
|
|
783
|
+
* - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
|
|
784
|
+
* rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
785
|
+
* - With a space that is not plain, `packRowsInSpace` packs the rows into the outline of the space and around its holes.
|
|
696
786
|
*/
|
|
697
|
-
function
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
};
|
|
787
|
+
function packShelf(rects, outer, options = {}) {
|
|
788
|
+
if (options.space !== void 0 && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
|
|
789
|
+
const width = outer.size.x;
|
|
790
|
+
const obstacles = [];
|
|
791
|
+
let contentBottom = 0;
|
|
792
|
+
for (const rect of rects) {
|
|
793
|
+
if (!rect.locked) continue;
|
|
794
|
+
rect.wasPacked = true;
|
|
795
|
+
const left = rect.position.x - outer.position.x;
|
|
796
|
+
const top = rect.position.y - outer.position.y;
|
|
797
|
+
obstacles.push({
|
|
798
|
+
left,
|
|
799
|
+
top,
|
|
800
|
+
right: left + rect.size.x,
|
|
801
|
+
bottom: top + rect.size.y
|
|
802
|
+
});
|
|
803
|
+
contentBottom = Math.max(contentBottom, top + rect.size.y);
|
|
713
804
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
2730485921,
|
|
760
|
-
2820302411,
|
|
761
|
-
3259730800,
|
|
762
|
-
3345764771,
|
|
763
|
-
3516065817,
|
|
764
|
-
3600352804,
|
|
765
|
-
4094571909,
|
|
766
|
-
275423344,
|
|
767
|
-
430227734,
|
|
768
|
-
506948616,
|
|
769
|
-
659060556,
|
|
770
|
-
883997877,
|
|
771
|
-
958139571,
|
|
772
|
-
1322822218,
|
|
773
|
-
1537002063,
|
|
774
|
-
1747873779,
|
|
775
|
-
1955562222,
|
|
776
|
-
2024104815,
|
|
777
|
-
2227730452,
|
|
778
|
-
2361852424,
|
|
779
|
-
2428436474,
|
|
780
|
-
2756734187,
|
|
781
|
-
3204031479,
|
|
782
|
-
3329325298
|
|
783
|
-
]);
|
|
784
|
-
/** The first hash value of SHA-256. */
|
|
785
|
-
const H0 = [
|
|
786
|
-
1779033703,
|
|
787
|
-
3144134277,
|
|
788
|
-
1013904242,
|
|
789
|
-
2773480762,
|
|
790
|
-
1359893119,
|
|
791
|
-
2600822924,
|
|
792
|
-
528734635,
|
|
793
|
-
1541459225
|
|
794
|
-
];
|
|
795
|
-
/** This function turns a 32-bit word to the right by `n` bits. */
|
|
796
|
-
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
805
|
+
let rowTop = 0;
|
|
806
|
+
let rowBottom = 0;
|
|
807
|
+
let cursor = 0;
|
|
808
|
+
let rowEmpty = true;
|
|
809
|
+
for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
|
|
810
|
+
if (rect.wasPacked) continue;
|
|
811
|
+
const w = rect.size.x;
|
|
812
|
+
const h = rect.size.y;
|
|
813
|
+
if (!(w > 0 && h > 0)) continue;
|
|
814
|
+
const overflows = w > width;
|
|
815
|
+
if (overflows && outer.fixedSize) continue;
|
|
816
|
+
if (overflows && !rowEmpty) {
|
|
817
|
+
rowTop = rowBottom;
|
|
818
|
+
cursor = 0;
|
|
819
|
+
rowEmpty = true;
|
|
820
|
+
}
|
|
821
|
+
let x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
|
|
822
|
+
while (!overflows && x + w > width) {
|
|
823
|
+
rowTop = rowEmpty ? nextTop(obstacles, rowTop, rowTop + h) : rowBottom;
|
|
824
|
+
rowBottom = rowTop;
|
|
825
|
+
cursor = 0;
|
|
826
|
+
rowEmpty = true;
|
|
827
|
+
x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
|
|
828
|
+
}
|
|
829
|
+
while (overflows && x !== 0) {
|
|
830
|
+
rowTop = nextTop(obstacles, rowTop, rowTop + h);
|
|
831
|
+
rowBottom = rowTop;
|
|
832
|
+
x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
|
|
833
|
+
}
|
|
834
|
+
if (outer.fixedSize && rowTop + h > outer.size.y) continue;
|
|
835
|
+
rect.position.set(x + outer.position.x, rowTop + outer.position.y);
|
|
836
|
+
rect.wasPacked = true;
|
|
837
|
+
rowEmpty = false;
|
|
838
|
+
cursor = x + w;
|
|
839
|
+
rowBottom = Math.max(rowBottom, rowTop + h);
|
|
840
|
+
contentBottom = Math.max(contentBottom, rowTop + h);
|
|
841
|
+
if (overflows) {
|
|
842
|
+
rowTop = rowBottom;
|
|
843
|
+
cursor = 0;
|
|
844
|
+
rowEmpty = true;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
848
|
+
return outer;
|
|
849
|
+
}
|
|
797
850
|
/**
|
|
798
|
-
* This function gives the
|
|
799
|
-
*
|
|
851
|
+
* This function gives the first x from `start` where a rectangle of width `w` fits. There, it crosses no locked rectangle,
|
|
852
|
+
* and it is in a free span of its band. It takes `firstFreeX` and `firstFit` in turn until they agree. It gives `Infinity` when no x fits.
|
|
800
853
|
*/
|
|
801
|
-
function
|
|
802
|
-
|
|
803
|
-
for (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
854
|
+
function placeInBand(obstacles, spans, start, w, top, bottom) {
|
|
855
|
+
let x = start;
|
|
856
|
+
for (;;) {
|
|
857
|
+
const free = firstFreeX(obstacles, x, w, top, bottom);
|
|
858
|
+
const fit = firstFit(spans, free, w);
|
|
859
|
+
if (fit === free || !Number.isFinite(fit)) return fit;
|
|
860
|
+
x = fit;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* This function packs rectangles into rows inside a free space. `packShelf` and `packRows` use it when the space is not
|
|
865
|
+
* plain. It sets the position of each rectangle and gives the outer rectangle.
|
|
866
|
+
*
|
|
867
|
+
* - A rectangle goes to the first x from the row cursor where it fits in the space and crosses no locked rectangle.
|
|
868
|
+
* - When it does not fit, a new row starts under the current row. An empty row steps down to `nextY` of the space.
|
|
869
|
+
* - When the steps of an empty row find no place, the row goes back to its last top edge. Later rectangles can use it.
|
|
870
|
+
* - A rectangle wider than the outer rectangle overflows. Without `fixedSize`, it goes on its own row at the left edge.
|
|
871
|
+
*/
|
|
872
|
+
function packRowsInSpace(rects, outer, order, space) {
|
|
873
|
+
const width = outer.size.x;
|
|
874
|
+
const ox = outer.position.x;
|
|
875
|
+
const oy = outer.position.y;
|
|
876
|
+
const limit = outer.fixedSize ? outer.size.y : Infinity;
|
|
877
|
+
const obstacles = [];
|
|
878
|
+
let contentBottom = 0;
|
|
879
|
+
for (const rect of rects) {
|
|
880
|
+
if (!rect.locked) continue;
|
|
881
|
+
rect.wasPacked = true;
|
|
882
|
+
const left = rect.position.x - ox;
|
|
883
|
+
const top = rect.position.y - oy;
|
|
884
|
+
obstacles.push({
|
|
885
|
+
left,
|
|
886
|
+
top,
|
|
887
|
+
right: left + rect.size.x,
|
|
888
|
+
bottom: top + rect.size.y
|
|
889
|
+
});
|
|
890
|
+
contentBottom = Math.max(contentBottom, top + rect.size.y);
|
|
891
|
+
}
|
|
892
|
+
const place = (start, w, top, h) => {
|
|
893
|
+
const spans = space.band(top + oy, top + h + oy).map((s) => ({
|
|
894
|
+
start: s.start - ox,
|
|
895
|
+
end: s.end - ox
|
|
896
|
+
}));
|
|
897
|
+
return placeInBand(obstacles, spans, start, w, top, top + h);
|
|
898
|
+
};
|
|
899
|
+
let rowTop = 0;
|
|
900
|
+
let rowBottom = 0;
|
|
901
|
+
let cursor = 0;
|
|
902
|
+
let rowEmpty = true;
|
|
903
|
+
for (const rect of rects.toSorted(compareRects(order))) {
|
|
904
|
+
if (rect.wasPacked) continue;
|
|
905
|
+
const w = rect.size.x;
|
|
906
|
+
const h = rect.size.y;
|
|
907
|
+
if (!(w > 0 && h > 0)) continue;
|
|
908
|
+
if (w > width) {
|
|
909
|
+
if (outer.fixedSize) continue;
|
|
910
|
+
const top = rowEmpty ? rowTop : rowBottom;
|
|
911
|
+
rect.position.set(ox, top + oy);
|
|
912
|
+
rect.wasPacked = true;
|
|
913
|
+
rowTop = top + h;
|
|
914
|
+
rowBottom = rowTop;
|
|
915
|
+
cursor = 0;
|
|
916
|
+
rowEmpty = true;
|
|
917
|
+
contentBottom = Math.max(contentBottom, top + h);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
let x = place(cursor, w, rowTop, h);
|
|
921
|
+
if (!(x + w <= width) && !rowEmpty) {
|
|
922
|
+
rowTop = rowBottom;
|
|
923
|
+
cursor = 0;
|
|
924
|
+
rowEmpty = true;
|
|
925
|
+
x = place(0, w, rowTop, h);
|
|
926
|
+
}
|
|
927
|
+
let top = rowTop;
|
|
928
|
+
while (!(x + w <= width) && top + h <= limit) {
|
|
929
|
+
const next = Math.min(space.nextY(top + oy, top + h + oy, w) - oy, nextTop(obstacles, top, top + h));
|
|
930
|
+
if (!Number.isFinite(next)) break;
|
|
931
|
+
top = next;
|
|
932
|
+
x = place(0, w, top, h);
|
|
933
|
+
}
|
|
934
|
+
if (!(x + w <= width) || top + h > limit) continue;
|
|
935
|
+
if (top !== rowTop) {
|
|
936
|
+
rowTop = top;
|
|
937
|
+
rowBottom = top;
|
|
938
|
+
}
|
|
939
|
+
rect.position.set(x + ox, rowTop + oy);
|
|
940
|
+
rect.wasPacked = true;
|
|
941
|
+
rowEmpty = false;
|
|
942
|
+
cursor = x + w;
|
|
943
|
+
rowBottom = Math.max(rowBottom, rowTop + h);
|
|
944
|
+
contentBottom = Math.max(contentBottom, rowTop + h);
|
|
945
|
+
}
|
|
946
|
+
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
947
|
+
return outer;
|
|
948
|
+
}
|
|
949
|
+
// src/skyline.ts
|
|
950
|
+
/**
|
|
951
|
+
* This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
|
|
952
|
+
* of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A
|
|
953
|
+
* monotonic deque makes the search O(width) for one rectangle.
|
|
954
|
+
*
|
|
955
|
+
* With `limit`, a window is valid only when its height plus `rh` is not more than the smallest value of
|
|
956
|
+
* `limit[x, x + rw)`. A second deque keeps that smallest value. No valid window gives the height `Infinity`.
|
|
957
|
+
*/
|
|
958
|
+
function lowestWindow(skyline, width, rw, limit, rh = 0) {
|
|
959
|
+
if (limit !== void 0) return lowestLimitedWindow(skyline, limit, width, rw, rh);
|
|
960
|
+
const deque = new Int32Array(width);
|
|
961
|
+
let head = 0;
|
|
962
|
+
let tail = 0;
|
|
963
|
+
let bestX = 0;
|
|
964
|
+
let bestY = Infinity;
|
|
965
|
+
for (let i = 0; i < width; i++) {
|
|
966
|
+
while (tail > head && skyline[deque[tail - 1]] <= skyline[i]) tail--;
|
|
967
|
+
deque[tail++] = i;
|
|
968
|
+
if (deque[head] <= i - rw) head++;
|
|
969
|
+
if (i >= rw - 1) {
|
|
970
|
+
const max = skyline[deque[head]];
|
|
971
|
+
if (max < bestY) {
|
|
972
|
+
bestY = max;
|
|
973
|
+
bestX = i - rw + 1;
|
|
810
974
|
}
|
|
811
975
|
}
|
|
812
|
-
if (c >= 55296 && c <= 57343) c = 65533;
|
|
813
|
-
if (c < 128) out.push(c);
|
|
814
|
-
else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
|
|
815
|
-
else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
|
|
816
|
-
else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
|
|
817
976
|
}
|
|
818
|
-
return
|
|
977
|
+
return {
|
|
978
|
+
x: bestX,
|
|
979
|
+
y: bestY
|
|
980
|
+
};
|
|
819
981
|
}
|
|
820
|
-
/** This function
|
|
821
|
-
function
|
|
822
|
-
const
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
982
|
+
/** This function is `lowestWindow` with a limit for each cell. */
|
|
983
|
+
function lowestLimitedWindow(skyline, limit, width, rw, rh) {
|
|
984
|
+
const high = new Int32Array(width);
|
|
985
|
+
const low = new Int32Array(width);
|
|
986
|
+
let hh = 0;
|
|
987
|
+
let ht = 0;
|
|
988
|
+
let lh = 0;
|
|
989
|
+
let lt = 0;
|
|
990
|
+
let bestX = 0;
|
|
991
|
+
let bestY = Infinity;
|
|
992
|
+
for (let i = 0; i < width; i++) {
|
|
993
|
+
while (ht > hh && skyline[high[ht - 1]] <= skyline[i]) ht--;
|
|
994
|
+
high[ht++] = i;
|
|
995
|
+
if (high[hh] <= i - rw) hh++;
|
|
996
|
+
while (lt > lh && limit[low[lt - 1]] >= limit[i]) lt--;
|
|
997
|
+
low[lt++] = i;
|
|
998
|
+
if (low[lh] <= i - rw) lh++;
|
|
999
|
+
if (i >= rw - 1) {
|
|
1000
|
+
const max = skyline[high[hh]];
|
|
1001
|
+
if (max < bestY && max + rh <= limit[low[lh]]) {
|
|
1002
|
+
bestY = max;
|
|
1003
|
+
bestX = i - rw + 1;
|
|
1004
|
+
}
|
|
841
1005
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
x: bestX,
|
|
1009
|
+
y: bestY
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place
|
|
1014
|
+
* where it fits, and it does not change the width of the outer rectangle.
|
|
1015
|
+
*
|
|
1016
|
+
* - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other
|
|
1017
|
+
* rectangles go under it. The space above it stays empty, because a skyline has no holes.
|
|
1018
|
+
* - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer
|
|
1019
|
+
* rectangle has goes under all other rectangles, at the left edge.
|
|
1020
|
+
* - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that
|
|
1021
|
+
* does not fit is not packed: its `wasPacked` stays `false`.
|
|
1022
|
+
* - The default sort order is `height-desc`. The order of the array of the caller stays.
|
|
1023
|
+
* - With a space that is not plain, each cell gets its start and its limit from `column` of the space. The space holds
|
|
1024
|
+
* the obstacles and the locked items, thus the locked rectangles only keep their positions.
|
|
1025
|
+
*/
|
|
1026
|
+
function packSkyline(rects, outer, options = {}) {
|
|
1027
|
+
if (options.space !== void 0 && !options.space.plain) return packSkylineInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
|
|
1028
|
+
const width = Math.floor(outer.size.x);
|
|
1029
|
+
if (width <= 0) return outer;
|
|
1030
|
+
const skyline = new Float64Array(width);
|
|
1031
|
+
for (const rect of rects) {
|
|
1032
|
+
if (!rect.locked) continue;
|
|
1033
|
+
rect.wasPacked = true;
|
|
1034
|
+
const x0 = Math.max(0, Math.floor(rect.position.x - outer.position.x));
|
|
1035
|
+
const x1 = Math.min(width, Math.ceil(rect.position.x - outer.position.x + rect.size.x));
|
|
1036
|
+
const bottom = rect.position.y - outer.position.y + rect.size.y;
|
|
1037
|
+
for (let i = x0; i < x1; i++) skyline[i] = Math.max(skyline[i], bottom);
|
|
1038
|
+
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, bottom);
|
|
1039
|
+
}
|
|
1040
|
+
for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
|
|
1041
|
+
if (rect.wasPacked) continue;
|
|
1042
|
+
const rw = Math.ceil(rect.size.x);
|
|
1043
|
+
const rh = rect.size.y;
|
|
1044
|
+
if (rw <= 0 || rh <= 0) continue;
|
|
1045
|
+
let x;
|
|
1046
|
+
let y;
|
|
1047
|
+
if (rw > width) {
|
|
1048
|
+
if (outer.fixedSize) continue;
|
|
1049
|
+
x = 0;
|
|
1050
|
+
y = 0;
|
|
1051
|
+
for (let i = 0; i < width; i++) if (skyline[i] > y) y = skyline[i];
|
|
1052
|
+
} else {
|
|
1053
|
+
({x, y} = lowestWindow(skyline, width, rw));
|
|
1054
|
+
if (outer.fixedSize && y + rh > outer.size.y) continue;
|
|
1055
|
+
}
|
|
1056
|
+
rect.position.set(x + outer.position.x, y + outer.position.y);
|
|
1057
|
+
rect.wasPacked = true;
|
|
1058
|
+
const top = y + rh;
|
|
1059
|
+
for (let i = x; i < Math.min(width, x + rw); i++) skyline[i] = top;
|
|
1060
|
+
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
|
|
1061
|
+
}
|
|
1062
|
+
return outer;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* This function packs rectangles with a skyline in a free space. Each cell has a free range: the skyline is its start,
|
|
1066
|
+
* and `limit` is its end. A cell with less room than the lowest rectangle left goes to its next free range.
|
|
1067
|
+
*/
|
|
1068
|
+
function packSkylineInSpace(rects, outer, order, space) {
|
|
1069
|
+
const width = Math.floor(outer.size.x);
|
|
1070
|
+
if (width <= 0) return outer;
|
|
1071
|
+
const ox = outer.position.x;
|
|
1072
|
+
const oy = outer.position.y;
|
|
1073
|
+
const end = outer.fixedSize ? outer.size.y : Infinity;
|
|
1074
|
+
for (const rect of rects) if (rect.locked) rect.wasPacked = true;
|
|
1075
|
+
const rangeOf = (i, from) => {
|
|
1076
|
+
const r = space.column(ox + i, ox + i + 1, from + oy);
|
|
1077
|
+
if (r === void 0) return [Infinity, -Infinity];
|
|
1078
|
+
return [r.start - oy, Math.min(r.end - oy, end)];
|
|
1079
|
+
};
|
|
1080
|
+
const skyline = new Float64Array(width);
|
|
1081
|
+
const limit = new Float64Array(width);
|
|
1082
|
+
for (let i = 0; i < width; i++) [skyline[i], limit[i]] = rangeOf(i, 0);
|
|
1083
|
+
const sorted = rects.toSorted(compareRects(order)).filter((r) => !r.wasPacked && Math.ceil(r.size.x) > 0 && r.size.y > 0);
|
|
1084
|
+
const lowest = new Float64Array(sorted.length + 1).fill(Infinity);
|
|
1085
|
+
for (let k = sorted.length - 1; k >= 0; k--) lowest[k] = Math.min(lowest[k + 1], sorted[k].size.y);
|
|
1086
|
+
const advance = (k) => {
|
|
1087
|
+
const need = lowest[k];
|
|
1088
|
+
if (!Number.isFinite(need)) return;
|
|
1089
|
+
for (let i = 0; i < width; i++) while (Number.isFinite(limit[i]) && limit[i] - skyline[i] < need && limit[i] < end) [skyline[i], limit[i]] = rangeOf(i, limit[i]);
|
|
1090
|
+
};
|
|
1091
|
+
for (const [k, rect] of sorted.entries()) {
|
|
1092
|
+
advance(k);
|
|
1093
|
+
const rw = Math.ceil(rect.size.x);
|
|
1094
|
+
const rh = rect.size.y;
|
|
1095
|
+
let x;
|
|
1096
|
+
let y;
|
|
1097
|
+
if (rw > width) {
|
|
1098
|
+
if (outer.fixedSize) continue;
|
|
1099
|
+
x = 0;
|
|
1100
|
+
y = 0;
|
|
1101
|
+
for (let i = 0; i < width; i++) if (Number.isFinite(skyline[i]) && skyline[i] > y) y = skyline[i];
|
|
1102
|
+
rect.position.set(x + ox, y + oy);
|
|
1103
|
+
rect.wasPacked = true;
|
|
1104
|
+
outer.size.y = Math.max(outer.size.y, y + rh);
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
({x, y} = lowestWindow(skyline, width, rw, limit, rh));
|
|
1108
|
+
let sky = skyline;
|
|
1109
|
+
let lim = limit;
|
|
1110
|
+
if (!Number.isFinite(y)) {
|
|
1111
|
+
sky = skyline.slice();
|
|
1112
|
+
lim = limit.slice();
|
|
1113
|
+
for (let i = 0; i < width; i++) while (Number.isFinite(lim[i]) && lim[i] - sky[i] < rh && lim[i] < end) [sky[i], lim[i]] = rangeOf(i, lim[i]);
|
|
1114
|
+
({x, y} = lowestWindow(sky, width, rw, lim, rh));
|
|
1115
|
+
if (!Number.isFinite(y)) continue;
|
|
1116
|
+
}
|
|
1117
|
+
rect.position.set(x + ox, y + oy);
|
|
1118
|
+
rect.wasPacked = true;
|
|
1119
|
+
const top = y + rh;
|
|
1120
|
+
for (let i = x; i < x + rw; i++) {
|
|
1121
|
+
skyline[i] = top;
|
|
1122
|
+
limit[i] = lim[i];
|
|
1123
|
+
}
|
|
1124
|
+
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
|
|
1125
|
+
}
|
|
1126
|
+
return outer;
|
|
1127
|
+
}
|
|
1128
|
+
// src/pack.ts
|
|
1129
|
+
function packInto(rects, region, offset, order) {
|
|
1130
|
+
const unpacked = Rect.unpacked(rects);
|
|
1131
|
+
pack(unpacked, region, { sortOrder: order });
|
|
1132
|
+
for (const r of Rect.packed(unpacked)) r.translateBy(offset);
|
|
1133
|
+
}
|
|
1134
|
+
function packRow(rects, outer, order) {
|
|
1135
|
+
const row = [];
|
|
1136
|
+
const byHeight = rects.toSorted(compareRects(order));
|
|
1137
|
+
const fillGaps = order !== "input";
|
|
1138
|
+
const rowRect = new Rect();
|
|
1139
|
+
rowRect.size.set(outer.size.x, 0);
|
|
1140
|
+
for (const rect of byHeight) {
|
|
1141
|
+
if (rect.wasPacked) continue;
|
|
1142
|
+
if (rect.size.x > rowRect.size.x) {
|
|
1143
|
+
if (outer.fixedSize && rect.size.x > outer.size.x) continue;
|
|
1144
|
+
const outerLeft = new Rect();
|
|
1145
|
+
outerLeft.position.setTo(rowRect.position);
|
|
1146
|
+
outerLeft.size.set(outer.size.x - rowRect.position.x, rowRect.size.y);
|
|
1147
|
+
outerLeft.fixedSize = true;
|
|
1148
|
+
if (fillGaps) packInto(rects, outerLeft, outer.position, order);
|
|
1149
|
+
rowRect.position.set(0, rowRect.bottom());
|
|
1150
|
+
rowRect.size.set(outer.size.x, 0);
|
|
1151
|
+
row.length = 0;
|
|
1152
|
+
}
|
|
1153
|
+
if (rowRect.position.y + rect.size.y > outer.size.y) {
|
|
1154
|
+
if (outer.fixedSize) continue;
|
|
1155
|
+
outer.size.y += rect.size.y;
|
|
1156
|
+
if (!outer.fixedWidth && !outer.size.x) outer.size.x = rect.size.x;
|
|
1157
|
+
Rect.resetPacked(rects);
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
row.push(rect);
|
|
1161
|
+
rect.position.setTo(rowRect.position.add(outer.position));
|
|
1162
|
+
rowRect.shrinkLeft(rect.size.x);
|
|
1163
|
+
rect.wasPacked = true;
|
|
1164
|
+
if (rect.size.y > rowRect.size.y) rowRect.size.y = rect.size.y;
|
|
1165
|
+
if (fillGaps && row.length > 1) {
|
|
1166
|
+
const rectInRow = row[row.length - 1];
|
|
1167
|
+
const outerLeft = new Rect();
|
|
1168
|
+
outerLeft.position.set(rectInRow.position.x - outer.position.x, rectInRow.bottom() - outer.position.y);
|
|
1169
|
+
outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);
|
|
1170
|
+
outerLeft.fixedWidth = true;
|
|
1171
|
+
outerLeft.fixedSize = true;
|
|
1172
|
+
packInto(rects, outerLeft, outer.position, order);
|
|
856
1173
|
}
|
|
857
|
-
hash[0] = hash[0] + a | 0;
|
|
858
|
-
hash[1] = hash[1] + b | 0;
|
|
859
|
-
hash[2] = hash[2] + c | 0;
|
|
860
|
-
hash[3] = hash[3] + d | 0;
|
|
861
|
-
hash[4] = hash[4] + e | 0;
|
|
862
|
-
hash[5] = hash[5] + f | 0;
|
|
863
|
-
hash[6] = hash[6] + g | 0;
|
|
864
|
-
hash[7] = hash[7] + h | 0;
|
|
865
1174
|
}
|
|
866
|
-
return
|
|
867
|
-
}
|
|
868
|
-
// src/layout-input.ts
|
|
869
|
-
/** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
|
|
870
|
-
function fnv1a32(text) {
|
|
871
|
-
let h = 2166136261;
|
|
872
|
-
for (const b of utf8(text)) h = Math.imul(h ^ b, 16777619) >>> 0;
|
|
873
|
-
return h;
|
|
874
|
-
}
|
|
875
|
-
/** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
|
|
876
|
-
function round3(x) {
|
|
877
|
-
const v = Math.round(x * 1e3) / 1e3;
|
|
878
|
-
return Object.is(v, -0) ? 0 : v;
|
|
1175
|
+
return true;
|
|
879
1176
|
}
|
|
880
1177
|
/**
|
|
881
|
-
* This function
|
|
882
|
-
*
|
|
1178
|
+
* This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
|
|
1179
|
+
* not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps
|
|
1180
|
+
* its position, but the rows do not go around it. `pack` sets the width first and then uses this function.
|
|
1181
|
+
*
|
|
1182
|
+
* With `options.space`, the rows go around the locked rectangles and the holes of the space, inside its outline. A space
|
|
1183
|
+
* that is plain, with no locked rectangle, gives the rows of 0.1.
|
|
883
1184
|
*/
|
|
884
|
-
function
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const rest = { ...file };
|
|
892
|
-
delete rest.hash;
|
|
893
|
-
return sha256Hex(canonicalJson(rest)).slice(0, 16);
|
|
894
|
-
}
|
|
895
|
-
/** 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`. */
|
|
896
|
-
function countCell(count) {
|
|
897
|
-
if (count <= 3) return "1";
|
|
898
|
-
if (count <= 31) return "10";
|
|
899
|
-
if (count <= 316) return "100";
|
|
900
|
-
if (count <= 3162) return "1k";
|
|
901
|
-
return "10k";
|
|
902
|
-
}
|
|
903
|
-
/** This function gives the packing input of a file. The boxes become the items, and the edges go. */
|
|
904
|
-
function toPackInput(file) {
|
|
905
|
-
const input = {
|
|
906
|
-
container: file.container,
|
|
907
|
-
items: file.boxes
|
|
908
|
-
};
|
|
909
|
-
if (file.obstacles !== void 0) input.obstacles = file.obstacles;
|
|
910
|
-
if (file.gap !== void 0) input.gap = file.gap;
|
|
911
|
-
return input;
|
|
1185
|
+
function packRows(rects, outer, options = {}) {
|
|
1186
|
+
const order = options.sortOrder ?? "height-desc";
|
|
1187
|
+
const space = options.space;
|
|
1188
|
+
if (space !== void 0 && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
|
|
1189
|
+
for (const r of rects) if (r.locked) r.wasPacked = true;
|
|
1190
|
+
while (!packRow(rects, outer, order));
|
|
1191
|
+
return outer;
|
|
912
1192
|
}
|
|
913
|
-
// src/measures.ts
|
|
914
|
-
const spec = (id, family, unit, direction, min, max, guard) => ({
|
|
915
|
-
id,
|
|
916
|
-
family,
|
|
917
|
-
unit,
|
|
918
|
-
direction,
|
|
919
|
-
min,
|
|
920
|
-
max,
|
|
921
|
-
...guard
|
|
922
|
-
});
|
|
923
1193
|
/**
|
|
924
|
-
*
|
|
925
|
-
* and the
|
|
1194
|
+
* This function packs rectangles into an outer rectangle with a guillotine row packing. It sets the position
|
|
1195
|
+
* of each rectangle and the size of the outer rectangle, and it gives the outer rectangle.
|
|
1196
|
+
*
|
|
1197
|
+
* - Without fixed sizes, the outer rectangle starts with a width of 1.5 times the square root of the total area.
|
|
1198
|
+
* This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
|
|
1199
|
+
* - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
|
|
1200
|
+
* - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
1201
|
+
* - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
|
|
926
1202
|
*/
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
floor: 0,
|
|
938
|
-
tol: 0
|
|
939
|
-
}),
|
|
940
|
-
"pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity),
|
|
941
|
-
"pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1),
|
|
942
|
-
"pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, {
|
|
943
|
-
floor: 0,
|
|
944
|
-
tol: 0
|
|
945
|
-
}),
|
|
946
|
-
"pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, {
|
|
947
|
-
floor: 0,
|
|
948
|
-
tol: .02
|
|
949
|
-
}),
|
|
950
|
-
"pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, {
|
|
951
|
-
floor: 0,
|
|
952
|
-
tol: .005
|
|
953
|
-
}),
|
|
954
|
-
"pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, {
|
|
955
|
-
floor: 0,
|
|
956
|
-
tol: .01
|
|
957
|
-
}),
|
|
958
|
-
"pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1),
|
|
959
|
-
"pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity),
|
|
960
|
-
"pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity)
|
|
961
|
-
});
|
|
962
|
-
/** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
|
|
963
|
-
function hullOf(boxes) {
|
|
964
|
-
if (boxes.length === 0) return {
|
|
965
|
-
x: 0,
|
|
966
|
-
y: 0,
|
|
967
|
-
w: 0,
|
|
968
|
-
h: 0
|
|
969
|
-
};
|
|
970
|
-
let left = Infinity;
|
|
971
|
-
let top = Infinity;
|
|
972
|
-
let right = -Infinity;
|
|
973
|
-
let bottom = -Infinity;
|
|
974
|
-
for (const b of boxes) {
|
|
975
|
-
left = Math.min(left, b.x);
|
|
976
|
-
top = Math.min(top, b.y);
|
|
977
|
-
right = Math.max(right, b.x + b.w);
|
|
978
|
-
bottom = Math.max(bottom, b.y + b.h);
|
|
1203
|
+
function pack(rects, outer = new Rect(), options = {}) {
|
|
1204
|
+
let totalArea = 0;
|
|
1205
|
+
let widest = 0;
|
|
1206
|
+
let minX = rects.length > 0 ? Infinity : 0;
|
|
1207
|
+
let minY = rects.length > 0 ? Infinity : 0;
|
|
1208
|
+
for (const r of rects) {
|
|
1209
|
+
totalArea += r.area();
|
|
1210
|
+
widest = Math.max(widest, r.size.x);
|
|
1211
|
+
minX = Math.min(minX, r.size.x);
|
|
1212
|
+
minY = Math.min(minY, r.size.y);
|
|
979
1213
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
function rowsOf$1(boxes, tolerance) {
|
|
992
|
-
const rows = [];
|
|
993
|
-
for (const b of boxes.toSorted((p, q) => p.y - q.y)) {
|
|
994
|
-
const row = rows.at(-1);
|
|
995
|
-
if (row && Math.abs(b.y - row.top) <= tolerance) {
|
|
996
|
-
row.right = Math.max(row.right, b.x + b.w);
|
|
997
|
-
row.count += 1;
|
|
998
|
-
} else rows.push({
|
|
999
|
-
top: b.y,
|
|
1000
|
-
right: b.x + b.w,
|
|
1001
|
-
count: 1
|
|
1002
|
-
});
|
|
1214
|
+
const idealWidth = outer.fixedWidth ? outer.size.x : Math.max(widest, Math.min(1.5 * Math.sqrt(totalArea), outer.size.x || Infinity));
|
|
1215
|
+
const ideal = Vector.of(idealWidth).clamp(new Vector(minX, minY), Vector.Infinity);
|
|
1216
|
+
if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
|
|
1217
|
+
if (options.strategy === "skyline") packSkyline(rects, outer, options);
|
|
1218
|
+
else if (options.strategy === "shelf") packShelf(rects, outer, options);
|
|
1219
|
+
else packRows(rects, outer, options);
|
|
1220
|
+
if (!outer.fixedSize) {
|
|
1221
|
+
const contentRight = Rect.maxRight(rects);
|
|
1222
|
+
if (!outer.size.x) outer.size.x = contentRight;
|
|
1223
|
+
else if (!outer.fixedWidth) outer.size.x = Math.min(contentRight, outer.size.x);
|
|
1224
|
+
outer.size.y = Rect.maxBottom(rects);
|
|
1003
1225
|
}
|
|
1004
|
-
return
|
|
1226
|
+
return outer;
|
|
1227
|
+
}
|
|
1228
|
+
// src/free.ts
|
|
1229
|
+
/** This function gives the distance between an aspect and a target aspect on a log scale. */
|
|
1230
|
+
function aspectError(width, height, target = 1) {
|
|
1231
|
+
if (!(width > 0 && height > 0 && target > 0)) return Infinity;
|
|
1232
|
+
return Math.abs(Math.log(width / height) - Math.log(target));
|
|
1005
1233
|
}
|
|
1006
1234
|
/**
|
|
1007
|
-
* This function
|
|
1008
|
-
*
|
|
1235
|
+
* This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps
|
|
1236
|
+
* the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
|
|
1237
|
+
*
|
|
1238
|
+
* - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
|
|
1239
|
+
* - The second pass tries the widths between the two neighbours of the best width of the first pass.
|
|
1240
|
+
* - The search is deterministic: the same input gives the same widths and the same result.
|
|
1241
|
+
* - A `locked` rectangle keeps its position. The width is not less than its right edge.
|
|
1242
|
+
* - Each strategy of `pack` works with this function, because it only changes the width.
|
|
1009
1243
|
*/
|
|
1010
|
-
function
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1244
|
+
function packFree(rects, outer = new Rect(), options = {}) {
|
|
1245
|
+
const target = options.aspect ?? 1;
|
|
1246
|
+
const weight = options.aspectWeight ?? 1;
|
|
1247
|
+
const samples = Math.max(2, Math.floor(options.samples ?? 48));
|
|
1248
|
+
const ox = outer.position.x;
|
|
1249
|
+
const oy = outer.position.y;
|
|
1250
|
+
let itemArea = 0;
|
|
1251
|
+
let minWidth = 0;
|
|
1252
|
+
let rowWidth = 0;
|
|
1253
|
+
for (const r of rects) {
|
|
1254
|
+
if (r.locked) {
|
|
1255
|
+
minWidth = Math.max(minWidth, r.right() - ox);
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (!(r.size.x > 0 && r.size.y > 0)) continue;
|
|
1259
|
+
itemArea += r.area();
|
|
1260
|
+
minWidth = Math.max(minWidth, r.size.x);
|
|
1261
|
+
rowWidth += r.size.x;
|
|
1262
|
+
}
|
|
1263
|
+
for (const r of rects) if (r.locked) itemArea += r.area();
|
|
1264
|
+
const maxWidth = Math.max(minWidth, rowWidth);
|
|
1265
|
+
const tried = [];
|
|
1266
|
+
const run = (width) => {
|
|
1267
|
+
Rect.resetPacked(rects);
|
|
1268
|
+
const box = new Rect();
|
|
1269
|
+
box.position.setTo(outer.position);
|
|
1270
|
+
box.size.set(width, 0);
|
|
1271
|
+
box.fixedWidth = true;
|
|
1272
|
+
pack(rects, box, options);
|
|
1273
|
+
const placed = Rect.packed(rects);
|
|
1274
|
+
const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
|
|
1275
|
+
const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
|
|
1276
|
+
const area = contentWidth * contentHeight;
|
|
1277
|
+
const t = {
|
|
1278
|
+
width,
|
|
1279
|
+
contentWidth,
|
|
1280
|
+
contentHeight,
|
|
1281
|
+
objective: area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0
|
|
1282
|
+
};
|
|
1283
|
+
tried.push(t);
|
|
1284
|
+
return t;
|
|
1285
|
+
};
|
|
1286
|
+
const widths = [];
|
|
1287
|
+
if (maxWidth > 0) {
|
|
1288
|
+
const ratio = maxWidth / minWidth;
|
|
1289
|
+
for (let i = 0; i < samples; i++) {
|
|
1290
|
+
const w = minWidth * ratio ** (i / (samples - 1));
|
|
1291
|
+
const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
|
|
1292
|
+
if (widths.at(-1) !== rounded) widths.push(rounded);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
let best = void 0;
|
|
1296
|
+
let bestIndex = 0;
|
|
1297
|
+
for (const [i, w] of widths.entries()) {
|
|
1298
|
+
const t = run(w);
|
|
1299
|
+
if (best === void 0 || t.objective < best.objective) {
|
|
1300
|
+
best = t;
|
|
1301
|
+
bestIndex = i;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (widths.length > 2) {
|
|
1305
|
+
const low = widths[Math.max(0, bestIndex - 1)];
|
|
1306
|
+
const high = widths[Math.min(widths.length - 1, bestIndex + 1)];
|
|
1307
|
+
const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
|
|
1308
|
+
for (let i = 1; i <= steps; i++) {
|
|
1309
|
+
const w = Math.round(low + (high - low) * i / (steps + 1));
|
|
1310
|
+
if (widths.includes(w)) continue;
|
|
1311
|
+
const t = run(w);
|
|
1312
|
+
if (best === void 0 || t.objective < best.objective) best = t;
|
|
1313
|
+
}
|
|
1025
1314
|
}
|
|
1026
|
-
|
|
1315
|
+
const chosen = best === void 0 ? void 0 : run(best.width);
|
|
1316
|
+
tried.pop();
|
|
1317
|
+
const w = chosen?.contentWidth ?? 0;
|
|
1318
|
+
const h = chosen?.contentHeight ?? 0;
|
|
1319
|
+
if (!outer.fixedSize) outer.size.set(w, h);
|
|
1320
|
+
const area = w * h;
|
|
1321
|
+
return {
|
|
1322
|
+
outer,
|
|
1323
|
+
area,
|
|
1324
|
+
fill: area > 0 ? itemArea / area : 0,
|
|
1325
|
+
aspect: h > 0 ? w / h : 0,
|
|
1326
|
+
aspectError: aspectError(w, h, target),
|
|
1327
|
+
objective: chosen?.objective ?? 0,
|
|
1328
|
+
tried
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
// src/push.ts
|
|
1332
|
+
/** This function gives the overlap of two rectangles on each axis. A value of 0 or less is no overlap. */
|
|
1333
|
+
function overlapOf(a, b) {
|
|
1334
|
+
return {
|
|
1335
|
+
x: Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x),
|
|
1336
|
+
y: Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y)
|
|
1337
|
+
};
|
|
1027
1338
|
}
|
|
1028
1339
|
/**
|
|
1029
|
-
* This function
|
|
1030
|
-
*
|
|
1340
|
+
* This function gives the direction of a first shove. The axis has the smaller overlap. The sign points away from the
|
|
1341
|
+
* center of the pusher. When that shove crosses 0, the sign is positive.
|
|
1031
1342
|
*/
|
|
1032
|
-
function
|
|
1033
|
-
|
|
1034
|
-
|
|
1343
|
+
function firstDirection(pusher, r, gap) {
|
|
1344
|
+
const overlap = overlapOf(pusher, r);
|
|
1345
|
+
if (overlap.x < overlap.y) return {
|
|
1346
|
+
axis: "x",
|
|
1347
|
+
sign: r.position.x + r.size.x / 2 < pusher.position.x + pusher.size.x / 2 && pusher.position.x - r.size.x - gap >= 0 ? -1 : 1
|
|
1348
|
+
};
|
|
1349
|
+
return {
|
|
1350
|
+
axis: "y",
|
|
1351
|
+
sign: r.position.y + r.size.y / 2 < pusher.position.y + pusher.size.y / 2 && pusher.position.y - r.size.y - gap >= 0 ? -1 : 1
|
|
1352
|
+
};
|
|
1035
1353
|
}
|
|
1036
|
-
/** This function
|
|
1037
|
-
function
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1354
|
+
/** This function puts a rectangle past the pusher, along a direction. The rectangle does not go past 0. */
|
|
1355
|
+
function shove(pusher, r, direction, gap) {
|
|
1356
|
+
if (direction.axis === "x") {
|
|
1357
|
+
const x = direction.sign > 0 ? pusher.right() + gap : pusher.position.x - r.size.x - gap;
|
|
1358
|
+
r.position.set(Math.max(0, x), r.position.y);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const y = direction.sign > 0 ? pusher.bottom() + gap : pusher.position.y - r.size.y - gap;
|
|
1362
|
+
r.position.set(r.position.x, Math.max(0, y));
|
|
1041
1363
|
}
|
|
1042
1364
|
/**
|
|
1043
|
-
* This function
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
* - An empty layout gives the area 0, the aspect 1 and the aspect error 0. Its fill is 0 and its whitespace is 1.
|
|
1048
|
-
* - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
|
|
1365
|
+
* This function moves each rectangle that a placed rectangle overlaps, and the rectangles that those overlap in turn.
|
|
1366
|
+
* A shove takes the axis of the smaller overlap, away from the pusher. A shoved rectangle keeps the direction of its first
|
|
1367
|
+
* shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.
|
|
1368
|
+
* The function moves the rectangles in place and gives the rectangles that moved.
|
|
1049
1369
|
*
|
|
1050
1370
|
* @example
|
|
1051
1371
|
* ```ts
|
|
1052
|
-
* const
|
|
1053
|
-
* m["pack.fill"]; // the area of the boxes divided by the area of the hull
|
|
1372
|
+
* const moved = pushCascade(dropped, cards, { gap: 8 });
|
|
1054
1373
|
* ```
|
|
1055
1374
|
*/
|
|
1056
|
-
function
|
|
1057
|
-
const
|
|
1058
|
-
const
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (prev.rank !== next.rank && readsBefore(next, prev)) breaks += 1;
|
|
1080
|
-
}
|
|
1081
|
-
const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
|
|
1082
|
-
const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
|
|
1083
|
-
return {
|
|
1084
|
-
"pack.area": hullArea,
|
|
1085
|
-
"pack.fill": fill,
|
|
1086
|
-
"pack.whitespace": 1 - fill,
|
|
1087
|
-
"pack.aspect": empty ? 1 : Math.max(hull.w / hull.h, hull.h / hull.w),
|
|
1088
|
-
"pack.aspectError": empty ? 0 : aspectError(hull.w, hull.h, target),
|
|
1089
|
-
"pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
|
|
1090
|
-
"pack.order.breaks": breaks,
|
|
1091
|
-
"pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
|
|
1092
|
-
"pack.overflow": new Set(output.overflow).size,
|
|
1093
|
-
"pack.search.widths": traceSum(output, "pack.search.widths"),
|
|
1094
|
-
"pack.search.ms": traceSum(output, "pack.search.ms")
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
/**
|
|
1098
|
-
* This function gives the stability measures of a change pair: the run on the base input and the run on the changed
|
|
1099
|
-
* input. The items match by id. The values are not rounded.
|
|
1100
|
-
*
|
|
1101
|
-
* - An item stays when both inputs have it and both outputs place it. The added and the removed items do not count.
|
|
1102
|
-
* - A staying item moved when its `x` or its `y` changed by more than `RECORD_EPS`, or when its turn changed.
|
|
1103
|
-
* - The distance of an item is the length of the move of its top-left corner. The unit is the width `W`.
|
|
1104
|
-
* - `W` is the larger width of the 2 output sizes. A `W` of 0 becomes 1, so the unit is then the unit of the input.
|
|
1105
|
-
* - No staying item gives 0 for each measure.
|
|
1106
|
-
*/
|
|
1107
|
-
function measureChange(baseInput, baseOutput, changeInput, changeOutput) {
|
|
1108
|
-
const placed = (input, output) => {
|
|
1109
|
-
const ids = new Set(input.items.map((i) => i.id));
|
|
1110
|
-
const map = /* @__PURE__ */ new Map();
|
|
1111
|
-
for (const p of output.placements) if (ids.has(p.id) && !map.has(p.id)) map.set(p.id, p);
|
|
1112
|
-
return map;
|
|
1113
|
-
};
|
|
1114
|
-
const before = placed(baseInput, baseOutput);
|
|
1115
|
-
const after = placed(changeInput, changeOutput);
|
|
1116
|
-
const width = Math.max(baseOutput.size.w, changeOutput.size.w);
|
|
1117
|
-
const unit = width > 0 ? width : 1;
|
|
1118
|
-
const distances = [];
|
|
1119
|
-
let moved = 0;
|
|
1120
|
-
for (const [id, p] of before) {
|
|
1121
|
-
const q = after.get(id);
|
|
1122
|
-
if (q === void 0) continue;
|
|
1123
|
-
const dx = q.x - p.x;
|
|
1124
|
-
const dy = q.y - p.y;
|
|
1125
|
-
if (Math.abs(dx) > .001 || Math.abs(dy) > .001 || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
|
|
1126
|
-
distances.push(Math.hypot(dx, dy) / unit);
|
|
1375
|
+
function pushCascade(placed, rects, options = {}) {
|
|
1376
|
+
const gap = options.gap ?? 8;
|
|
1377
|
+
const maxShoves = options.maxShoves ?? rects.length * 8;
|
|
1378
|
+
const moved = /* @__PURE__ */ new Set();
|
|
1379
|
+
const queue = [{ pusher: placed }];
|
|
1380
|
+
let shoves = 0;
|
|
1381
|
+
while (shoves < maxShoves) {
|
|
1382
|
+
const step = queue.shift();
|
|
1383
|
+
if (!step) break;
|
|
1384
|
+
const { pusher, direction } = step;
|
|
1385
|
+
for (const r of rects) {
|
|
1386
|
+
if (r === pusher || r === placed || r.locked) continue;
|
|
1387
|
+
const overlap = overlapOf(pusher, r);
|
|
1388
|
+
if (overlap.x <= 0 || overlap.y <= 0) continue;
|
|
1389
|
+
const next = direction ?? firstDirection(pusher, r, gap);
|
|
1390
|
+
shove(pusher, r, next, gap);
|
|
1391
|
+
moved.add(r);
|
|
1392
|
+
queue.push({
|
|
1393
|
+
pusher: r,
|
|
1394
|
+
direction: next
|
|
1395
|
+
});
|
|
1396
|
+
if (++shoves >= maxShoves) break;
|
|
1397
|
+
}
|
|
1127
1398
|
}
|
|
1128
|
-
|
|
1129
|
-
"pack.stability.moved": 0,
|
|
1130
|
-
"pack.stability.mean": 0,
|
|
1131
|
-
"pack.stability.q95": 0
|
|
1132
|
-
};
|
|
1133
|
-
return {
|
|
1134
|
-
"pack.stability.moved": moved / distances.length,
|
|
1135
|
-
"pack.stability.mean": distances.reduce((sum, d) => sum + d, 0) / distances.length,
|
|
1136
|
-
"pack.stability.q95": quantile(distances, .95)
|
|
1137
|
-
};
|
|
1399
|
+
return [...moved];
|
|
1138
1400
|
}
|
|
1401
|
+
// src/sha256.ts
|
|
1402
|
+
/** The round constants of SHA-256. */
|
|
1403
|
+
const K = new Uint32Array([
|
|
1404
|
+
1116352408,
|
|
1405
|
+
1899447441,
|
|
1406
|
+
3049323471,
|
|
1407
|
+
3921009573,
|
|
1408
|
+
961987163,
|
|
1409
|
+
1508970993,
|
|
1410
|
+
2453635748,
|
|
1411
|
+
2870763221,
|
|
1412
|
+
3624381080,
|
|
1413
|
+
310598401,
|
|
1414
|
+
607225278,
|
|
1415
|
+
1426881987,
|
|
1416
|
+
1925078388,
|
|
1417
|
+
2162078206,
|
|
1418
|
+
2614888103,
|
|
1419
|
+
3248222580,
|
|
1420
|
+
3835390401,
|
|
1421
|
+
4022224774,
|
|
1422
|
+
264347078,
|
|
1423
|
+
604807628,
|
|
1424
|
+
770255983,
|
|
1425
|
+
1249150122,
|
|
1426
|
+
1555081692,
|
|
1427
|
+
1996064986,
|
|
1428
|
+
2554220882,
|
|
1429
|
+
2821834349,
|
|
1430
|
+
2952996808,
|
|
1431
|
+
3210313671,
|
|
1432
|
+
3336571891,
|
|
1433
|
+
3584528711,
|
|
1434
|
+
113926993,
|
|
1435
|
+
338241895,
|
|
1436
|
+
666307205,
|
|
1437
|
+
773529912,
|
|
1438
|
+
1294757372,
|
|
1439
|
+
1396182291,
|
|
1440
|
+
1695183700,
|
|
1441
|
+
1986661051,
|
|
1442
|
+
2177026350,
|
|
1443
|
+
2456956037,
|
|
1444
|
+
2730485921,
|
|
1445
|
+
2820302411,
|
|
1446
|
+
3259730800,
|
|
1447
|
+
3345764771,
|
|
1448
|
+
3516065817,
|
|
1449
|
+
3600352804,
|
|
1450
|
+
4094571909,
|
|
1451
|
+
275423344,
|
|
1452
|
+
430227734,
|
|
1453
|
+
506948616,
|
|
1454
|
+
659060556,
|
|
1455
|
+
883997877,
|
|
1456
|
+
958139571,
|
|
1457
|
+
1322822218,
|
|
1458
|
+
1537002063,
|
|
1459
|
+
1747873779,
|
|
1460
|
+
1955562222,
|
|
1461
|
+
2024104815,
|
|
1462
|
+
2227730452,
|
|
1463
|
+
2361852424,
|
|
1464
|
+
2428436474,
|
|
1465
|
+
2756734187,
|
|
1466
|
+
3204031479,
|
|
1467
|
+
3329325298
|
|
1468
|
+
]);
|
|
1469
|
+
/** The first hash value of SHA-256. */
|
|
1470
|
+
const H0 = [
|
|
1471
|
+
1779033703,
|
|
1472
|
+
3144134277,
|
|
1473
|
+
1013904242,
|
|
1474
|
+
2773480762,
|
|
1475
|
+
1359893119,
|
|
1476
|
+
2600822924,
|
|
1477
|
+
528734635,
|
|
1478
|
+
1541459225
|
|
1479
|
+
];
|
|
1480
|
+
/** This function turns a 32-bit word to the right by `n` bits. */
|
|
1481
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
1139
1482
|
/**
|
|
1140
|
-
* This function gives the
|
|
1141
|
-
* `
|
|
1142
|
-
*
|
|
1143
|
-
* - The placements match by id, so their order in the list does not count. The numbers must be equal.
|
|
1144
|
-
* - The trace is not part of the rule, because its times change each time.
|
|
1483
|
+
* This function gives the UTF-8 bytes of a string. A lone surrogate becomes U+FFFD, as with `TextEncoder`. The
|
|
1484
|
+
* package has no `TextEncoder` type, because its code operates on each runtime with no global types.
|
|
1145
1485
|
*/
|
|
1146
|
-
function
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if (
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const overflowA = new Set(first.overflow);
|
|
1165
|
-
const overflowB = new Set(second.overflow);
|
|
1166
|
-
return first.size.w === second.size.w && first.size.h === second.size.h && overflowA.size === overflowB.size && [...overflowA].every((id) => overflowB.has(id)) && samePlacements(first.placements, second.placements) && sameMoved(first.moved, second.moved);
|
|
1486
|
+
function utf8(text) {
|
|
1487
|
+
const out = [];
|
|
1488
|
+
for (let i = 0; i < text.length; i++) {
|
|
1489
|
+
let c = text.charCodeAt(i);
|
|
1490
|
+
if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
|
|
1491
|
+
const next = text.charCodeAt(i + 1);
|
|
1492
|
+
if (next >= 56320 && next <= 57343) {
|
|
1493
|
+
c = 65536 + (c - 55296 << 10) + (next - 56320);
|
|
1494
|
+
i++;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
if (c >= 55296 && c <= 57343) c = 65533;
|
|
1498
|
+
if (c < 128) out.push(c);
|
|
1499
|
+
else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
|
|
1500
|
+
else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
|
|
1501
|
+
else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
|
|
1502
|
+
}
|
|
1503
|
+
return Uint8Array.from(out);
|
|
1167
1504
|
}
|
|
1168
|
-
/**
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1505
|
+
/** This function gives the SHA-256 of the UTF-8 bytes of a string, as 64 lowercase hex characters. */
|
|
1506
|
+
function sha256Hex(text) {
|
|
1507
|
+
const bytes = utf8(text);
|
|
1508
|
+
const length = bytes.length;
|
|
1509
|
+
const padded = new Uint8Array(((length + 9 + 63) / 64 | 0) * 64);
|
|
1510
|
+
padded.set(bytes);
|
|
1511
|
+
padded[length] = 128;
|
|
1512
|
+
const view = new DataView(padded.buffer);
|
|
1513
|
+
const bits = length * 8;
|
|
1514
|
+
view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
|
|
1515
|
+
view.setUint32(padded.length - 4, bits >>> 0);
|
|
1516
|
+
const hash = [...H0];
|
|
1517
|
+
const w = /* @__PURE__ */ new Uint32Array(64);
|
|
1518
|
+
for (let block = 0; block < padded.length; block += 64) {
|
|
1519
|
+
for (let t = 0; t < 16; t++) w[t] = view.getUint32(block + t * 4);
|
|
1520
|
+
for (let t = 16; t < 64; t++) {
|
|
1521
|
+
const a = w[t - 15];
|
|
1522
|
+
const b = w[t - 2];
|
|
1523
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
|
|
1524
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
|
|
1525
|
+
w[t] = w[t - 16] + s0 + w[t - 7] + s1 | 0;
|
|
1526
|
+
}
|
|
1527
|
+
let [a, b, c, d, e, f, g, h] = hash;
|
|
1528
|
+
for (let t = 0; t < 64; t++) {
|
|
1529
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
1530
|
+
const ch = e & f ^ ~e & g;
|
|
1531
|
+
const t1 = h + s1 + ch + K[t] + w[t] | 0;
|
|
1532
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
|
|
1533
|
+
h = g;
|
|
1534
|
+
g = f;
|
|
1535
|
+
f = e;
|
|
1536
|
+
e = d + t1 | 0;
|
|
1537
|
+
d = c;
|
|
1538
|
+
c = b;
|
|
1539
|
+
b = a;
|
|
1540
|
+
a = t1 + t2 | 0;
|
|
1541
|
+
}
|
|
1542
|
+
hash[0] = hash[0] + a | 0;
|
|
1543
|
+
hash[1] = hash[1] + b | 0;
|
|
1544
|
+
hash[2] = hash[2] + c | 0;
|
|
1545
|
+
hash[3] = hash[3] + d | 0;
|
|
1546
|
+
hash[4] = hash[4] + e | 0;
|
|
1547
|
+
hash[5] = hash[5] + f | 0;
|
|
1548
|
+
hash[6] = hash[6] + g | 0;
|
|
1549
|
+
hash[7] = hash[7] + h | 0;
|
|
1550
|
+
}
|
|
1551
|
+
return hash.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
|
|
1176
1552
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
*/
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
const bottom = direction === "lower" ? current : candidate;
|
|
1189
|
-
if (bottom === 0) return top === 0 ? 1 : Infinity;
|
|
1190
|
-
return top / bottom;
|
|
1553
|
+
// src/layout-input.ts
|
|
1554
|
+
/** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
|
|
1555
|
+
function fnv1a32(text) {
|
|
1556
|
+
let h = 2166136261;
|
|
1557
|
+
for (const b of utf8(text)) h = Math.imul(h ^ b, 16777619) >>> 0;
|
|
1558
|
+
return h;
|
|
1559
|
+
}
|
|
1560
|
+
/** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
|
|
1561
|
+
function round3(x) {
|
|
1562
|
+
const v = Math.round(x * 1e3) / 1e3;
|
|
1563
|
+
return Object.is(v, -0) ? 0 : v;
|
|
1191
1564
|
}
|
|
1192
1565
|
/**
|
|
1193
|
-
* This function gives the
|
|
1194
|
-
*
|
|
1566
|
+
* This function gives the canonical form of a value: sorted keys, no whitespace and the numbers of `JSON.stringify`
|
|
1567
|
+
* after `round3`. A key with the value `undefined` is not in the form.
|
|
1195
1568
|
*/
|
|
1196
|
-
function
|
|
1197
|
-
if (
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1200
|
-
const at = (sorted.length - 1) * q;
|
|
1201
|
-
const low = Math.floor(at);
|
|
1202
|
-
const part = at - low;
|
|
1203
|
-
const a = sorted[low];
|
|
1204
|
-
if (part === 0) return a;
|
|
1205
|
-
const b = sorted[low + 1];
|
|
1206
|
-
if (a === b) return a;
|
|
1207
|
-
return a + part * (b - a);
|
|
1208
|
-
}
|
|
1209
|
-
/** This function gives the quantile `q` of a list, and 0 for an empty list. */
|
|
1210
|
-
function quantileOrZero(values, q) {
|
|
1211
|
-
return values.length === 0 ? 0 : quantile(values, q);
|
|
1569
|
+
function canonicalJson(value) {
|
|
1570
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
1571
|
+
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
1572
|
+
return JSON.stringify(typeof value === "number" ? round3(value) : value);
|
|
1212
1573
|
}
|
|
1213
|
-
/** This function gives the
|
|
1214
|
-
function
|
|
1215
|
-
|
|
1574
|
+
/** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
|
|
1575
|
+
function hashInput(file) {
|
|
1576
|
+
const { hash: _hash, ...rest } = file;
|
|
1577
|
+
return sha256Hex(canonicalJson(rest)).slice(0, 16);
|
|
1216
1578
|
}
|
|
1217
|
-
/** This function gives the
|
|
1218
|
-
function
|
|
1219
|
-
if (
|
|
1220
|
-
|
|
1579
|
+
/** 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`. */
|
|
1580
|
+
function countCell(count) {
|
|
1581
|
+
if (count <= 3) return "1";
|
|
1582
|
+
if (count <= 31) return "10";
|
|
1583
|
+
if (count <= 316) return "100";
|
|
1584
|
+
if (count <= 3162) return "1k";
|
|
1585
|
+
return "10k";
|
|
1221
1586
|
}
|
|
1222
|
-
// src/metrics.ts
|
|
1223
1587
|
/**
|
|
1224
|
-
* This function
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
* @example
|
|
1228
|
-
* ```ts
|
|
1229
|
-
* const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
|
|
1230
|
-
* ```
|
|
1588
|
+
* This function gives the packing input of a file. The boxes become the items, and the edges go.
|
|
1589
|
+
* A file without a container gets `{ fixed: "none" }`, so the packer picks the size. It is `toPackInput` of the contract.
|
|
1231
1590
|
*/
|
|
1232
|
-
function
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
h: r.size.y
|
|
1239
|
-
}));
|
|
1240
|
-
if (boxes.length === 0) return {
|
|
1241
|
-
count: 0,
|
|
1242
|
-
hull: {
|
|
1243
|
-
width: 0,
|
|
1244
|
-
height: 0
|
|
1245
|
-
},
|
|
1246
|
-
hullAspect: 0,
|
|
1247
|
-
rows: [],
|
|
1248
|
-
raggedness: 0,
|
|
1249
|
-
utilization: 0,
|
|
1250
|
-
widestOverMedian: 0
|
|
1591
|
+
function toPackInput(file) {
|
|
1592
|
+
return {
|
|
1593
|
+
container: file.container ?? { fixed: "none" },
|
|
1594
|
+
items: file.boxes,
|
|
1595
|
+
...file.obstacles === void 0 ? {} : { obstacles: file.obstacles },
|
|
1596
|
+
...file.gap === void 0 ? {} : { gap: file.gap }
|
|
1251
1597
|
};
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1598
|
+
}
|
|
1599
|
+
// src/measures.ts
|
|
1600
|
+
const spec = (id, family, unit, direction, min, max, guard) => ({
|
|
1601
|
+
id,
|
|
1602
|
+
family,
|
|
1603
|
+
unit,
|
|
1604
|
+
direction,
|
|
1605
|
+
min,
|
|
1606
|
+
max,
|
|
1607
|
+
...guard
|
|
1608
|
+
});
|
|
1609
|
+
/**
|
|
1610
|
+
* The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. This record is the source of the floor
|
|
1611
|
+
* and the tolerance of each guard of the packer.
|
|
1612
|
+
*/
|
|
1613
|
+
const MEASURE_SPECS = Object.freeze({
|
|
1614
|
+
"pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity),
|
|
1615
|
+
"pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1),
|
|
1616
|
+
"pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1),
|
|
1617
|
+
"pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity),
|
|
1618
|
+
"pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, {
|
|
1619
|
+
floor: 0,
|
|
1620
|
+
tol: .01
|
|
1621
|
+
}),
|
|
1622
|
+
"pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, {
|
|
1623
|
+
floor: 0,
|
|
1624
|
+
tol: 0
|
|
1625
|
+
}),
|
|
1626
|
+
"pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity),
|
|
1627
|
+
"pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1),
|
|
1628
|
+
"pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, {
|
|
1629
|
+
floor: 0,
|
|
1630
|
+
tol: 0
|
|
1631
|
+
}),
|
|
1632
|
+
"pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, {
|
|
1633
|
+
floor: 0,
|
|
1634
|
+
tol: .02
|
|
1635
|
+
}),
|
|
1636
|
+
"pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, {
|
|
1637
|
+
floor: 0,
|
|
1638
|
+
tol: .005
|
|
1639
|
+
}),
|
|
1640
|
+
"pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, {
|
|
1641
|
+
floor: 0,
|
|
1642
|
+
tol: .01
|
|
1643
|
+
}),
|
|
1644
|
+
"pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1),
|
|
1645
|
+
"pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity),
|
|
1646
|
+
"pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity)
|
|
1647
|
+
});
|
|
1648
|
+
/** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
|
|
1649
|
+
function hullOf(boxes) {
|
|
1650
|
+
if (boxes.length === 0) return {
|
|
1651
|
+
x: 0,
|
|
1652
|
+
y: 0,
|
|
1653
|
+
w: 0,
|
|
1654
|
+
h: 0
|
|
1256
1655
|
};
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1656
|
+
let left = Infinity;
|
|
1657
|
+
let top = Infinity;
|
|
1658
|
+
let right = -Infinity;
|
|
1659
|
+
let bottom = -Infinity;
|
|
1660
|
+
for (const b of boxes) {
|
|
1661
|
+
left = Math.min(left, b.x);
|
|
1662
|
+
top = Math.min(top, b.y);
|
|
1663
|
+
right = Math.max(right, b.x + b.w);
|
|
1664
|
+
bottom = Math.max(bottom, b.y + b.h);
|
|
1665
|
+
}
|
|
1263
1666
|
return {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
raggedness: Math.max(...rights) - Math.min(...rights),
|
|
1269
|
-
utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
|
|
1270
|
-
widestOverMedian: median > 0 ? widest / median : 0
|
|
1667
|
+
x: left,
|
|
1668
|
+
y: top,
|
|
1669
|
+
w: right - left,
|
|
1670
|
+
h: bottom - top
|
|
1271
1671
|
};
|
|
1272
1672
|
}
|
|
1273
|
-
// src/strategy.ts
|
|
1274
1673
|
/**
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1674
|
+
* This function puts the boxes in rows. It sorts the boxes by their top, and keeps the order of the list for equal tops.
|
|
1675
|
+
* A box joins the last row when its top is within `tolerance` of the top of the first box of that row.
|
|
1277
1676
|
*/
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1677
|
+
function rowsOf$1(boxes, tolerance) {
|
|
1678
|
+
const rows = [];
|
|
1679
|
+
for (const b of boxes.toSorted((p, q) => p.y - q.y)) {
|
|
1680
|
+
const row = rows.at(-1);
|
|
1681
|
+
if (row && Math.abs(b.y - row.top) <= tolerance) {
|
|
1682
|
+
row.right = Math.max(row.right, b.x + b.w);
|
|
1683
|
+
row.count += 1;
|
|
1684
|
+
} else rows.push({
|
|
1685
|
+
top: b.y,
|
|
1686
|
+
right: b.x + b.w,
|
|
1687
|
+
count: 1
|
|
1688
|
+
});
|
|
1288
1689
|
}
|
|
1289
|
-
|
|
1290
|
-
/** This function gives a generator of random numbers in `[0, 1)`: `mulberry32` of `docs/FORMAT.md`. */
|
|
1291
|
-
function mulberry32(seed) {
|
|
1292
|
-
let s = seed >>> 0;
|
|
1293
|
-
return () => {
|
|
1294
|
-
s = s + 1831565813 | 0;
|
|
1295
|
-
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
1296
|
-
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1297
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
/** This function makes the context of one packing from a seed. */
|
|
1301
|
-
function createContext(seed = 0, more = {}) {
|
|
1302
|
-
return {
|
|
1303
|
-
...more,
|
|
1304
|
-
seed,
|
|
1305
|
-
rng: mulberry32(seed)
|
|
1306
|
-
};
|
|
1690
|
+
return rows;
|
|
1307
1691
|
}
|
|
1308
|
-
/**
|
|
1309
|
-
function
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
if (
|
|
1319
|
-
|
|
1692
|
+
/**
|
|
1693
|
+
* This function gives the placed boxes of an output, in the order of the items of the input. A box has the size of its
|
|
1694
|
+
* item after the turn. The list has no item without a placement, and no item with a width or a height of 0.
|
|
1695
|
+
*/
|
|
1696
|
+
function boxesOf$1(input, output) {
|
|
1697
|
+
const placements = /* @__PURE__ */ new Map();
|
|
1698
|
+
for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
|
|
1699
|
+
const boxes = [];
|
|
1700
|
+
for (const [index, item] of input.items.entries()) {
|
|
1701
|
+
const p = placements.get(item.id);
|
|
1702
|
+
if (p === void 0 || !(item.w > 0 && item.h > 0)) continue;
|
|
1703
|
+
const turned = p.turn === 90;
|
|
1704
|
+
boxes.push({
|
|
1705
|
+
x: p.x,
|
|
1706
|
+
y: p.y,
|
|
1707
|
+
w: turned ? item.h : item.w,
|
|
1708
|
+
h: turned ? item.w : item.h,
|
|
1709
|
+
rank: item.rank ?? index
|
|
1710
|
+
});
|
|
1320
1711
|
}
|
|
1321
|
-
return
|
|
1712
|
+
return boxes;
|
|
1322
1713
|
}
|
|
1323
|
-
/**
|
|
1324
|
-
function
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
if (region.holes && region.holes.length > 0) return "holes";
|
|
1331
|
-
return isConvex(region.points) ? "convex" : "polygon";
|
|
1332
|
-
}
|
|
1714
|
+
/**
|
|
1715
|
+
* This function tells if box `a` comes before box `b` in the reading order. Two boxes whose heights overlap by more than
|
|
1716
|
+
* `RECORD_EPS` are on one line, and the left box comes first. Otherwise the upper box comes first.
|
|
1717
|
+
*/
|
|
1718
|
+
function readsBefore(a, b) {
|
|
1719
|
+
if (Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > RECORD_EPS) return a.x + RECORD_EPS < b.x;
|
|
1720
|
+
return a.y < b.y;
|
|
1333
1721
|
}
|
|
1334
|
-
/** This function
|
|
1335
|
-
function
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
case "circle": return region.kind;
|
|
1340
|
-
case "ellipse": return "convex";
|
|
1341
|
-
case "polygon": return !region.holes?.length && isConvex(region.points) ? "convex" : "polygon";
|
|
1342
|
-
}
|
|
1722
|
+
/** This function sums one measure over the stages of a trace. A trace without the measure gives 0. */
|
|
1723
|
+
function traceSum(output, id) {
|
|
1724
|
+
let sum = 0;
|
|
1725
|
+
for (const stage of output.trace ?? []) sum += stage.measures?.[id] ?? 0;
|
|
1726
|
+
return sum;
|
|
1343
1727
|
}
|
|
1344
1728
|
/**
|
|
1345
|
-
* This function gives the
|
|
1729
|
+
* This function gives the measures of one layout, by the ids of `docs/MEASURES.md`. The values are not rounded. A
|
|
1730
|
+
* record rounds them with `roundMeasures`.
|
|
1346
1731
|
*
|
|
1347
|
-
* -
|
|
1348
|
-
* -
|
|
1349
|
-
* - `
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1732
|
+
* - The boxes are the placed items with a width and a height above 0. The hull is the smallest rectangle around them.
|
|
1733
|
+
* - An empty layout gives the area 0, the aspect 1 and the aspect error 0. Its fill is 0 and its whitespace is 1.
|
|
1734
|
+
* - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
|
|
1735
|
+
*
|
|
1736
|
+
* @example
|
|
1737
|
+
* ```ts
|
|
1738
|
+
* const m = measure(input, packWith("shelf", input));
|
|
1739
|
+
* m["pack.fill"]; // the area of the boxes divided by the area of the hull
|
|
1740
|
+
* ```
|
|
1352
1741
|
*/
|
|
1353
|
-
function
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
let
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const itemShape = shapes.size > 1 ? "mixed" : [...shapes][0] ?? "rect";
|
|
1371
|
-
const cell = {
|
|
1372
|
-
containerSize,
|
|
1373
|
-
containerShape,
|
|
1374
|
-
itemShape,
|
|
1375
|
-
rotation: items.some((i) => i.turnable === true) ? "quarter" : "none",
|
|
1376
|
-
order: items.some((i) => i.rank !== void 0) ? "soft" : "free",
|
|
1377
|
-
groups: items.some((i) => i.parent !== void 0) ? "nested" : items.some((i) => i.group !== void 0) ? "flat" : "none"
|
|
1378
|
-
};
|
|
1379
|
-
if (obstacleMove !== void 0) cell.obstacleMove = obstacleMove;
|
|
1380
|
-
return cell;
|
|
1381
|
-
}
|
|
1382
|
-
/** This function gives the first dimension of a cell that `supports` does not hold, or `undefined` when it holds each one. */
|
|
1383
|
-
function unsupportedOf(supports, cell) {
|
|
1384
|
-
for (const dimension of Object.keys(supports)) {
|
|
1385
|
-
const value = cell[dimension];
|
|
1386
|
-
if (value === void 0) continue;
|
|
1387
|
-
if (!supports[dimension].includes(value)) return {
|
|
1388
|
-
dimension,
|
|
1389
|
-
value
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
/** This function tells if `supports` holds each value of a cell. A dimension that the cell does not have is held. */
|
|
1394
|
-
function supportsCell(supports, cell) {
|
|
1395
|
-
return unsupportedOf(supports, cell) === void 0;
|
|
1396
|
-
}
|
|
1397
|
-
/** This function throws `UnsupportedCell` for the first value of the cell of an input that `supports` does not hold. */
|
|
1398
|
-
function assertSupported(supports, input) {
|
|
1399
|
-
const miss = unsupportedOf(supports, cellOf(input));
|
|
1400
|
-
if (miss) throw new UnsupportedCell(miss.dimension, miss.value);
|
|
1401
|
-
}
|
|
1402
|
-
// src/frame.ts
|
|
1403
|
-
const clock = globalThis.performance;
|
|
1404
|
-
/** This function gives the time in milliseconds. */
|
|
1405
|
-
function now() {
|
|
1406
|
-
return clock ? clock.now() : Date.now();
|
|
1407
|
-
}
|
|
1408
|
-
/** This function gives the bounding box of a region. */
|
|
1409
|
-
function boundsOf(region) {
|
|
1410
|
-
switch (region.kind) {
|
|
1411
|
-
case "rect": return {
|
|
1412
|
-
x: region.x,
|
|
1413
|
-
y: region.y,
|
|
1414
|
-
w: region.w,
|
|
1415
|
-
h: region.h
|
|
1416
|
-
};
|
|
1417
|
-
case "circle": return {
|
|
1418
|
-
x: region.cx - region.r,
|
|
1419
|
-
y: region.cy - region.r,
|
|
1420
|
-
w: 2 * region.r,
|
|
1421
|
-
h: 2 * region.r
|
|
1422
|
-
};
|
|
1423
|
-
case "ellipse": return {
|
|
1424
|
-
x: region.cx - region.rx,
|
|
1425
|
-
y: region.cy - region.ry,
|
|
1426
|
-
w: 2 * region.rx,
|
|
1427
|
-
h: 2 * region.ry
|
|
1428
|
-
};
|
|
1429
|
-
case "polygon": {
|
|
1430
|
-
const xs = region.points.map((p) => p[0]);
|
|
1431
|
-
const ys = region.points.map((p) => p[1]);
|
|
1432
|
-
const x = Math.min(...xs);
|
|
1433
|
-
const y = Math.min(...ys);
|
|
1434
|
-
return {
|
|
1435
|
-
x,
|
|
1436
|
-
y,
|
|
1437
|
-
w: Math.max(...xs) - x,
|
|
1438
|
-
h: Math.max(...ys) - y
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1742
|
+
function measure(input, output, options = {}) {
|
|
1743
|
+
const boxes = boxesOf$1(input, output);
|
|
1744
|
+
const hull = hullOf(boxes);
|
|
1745
|
+
const hullArea = hull.w * hull.h;
|
|
1746
|
+
const boxArea = boxes.reduce((sum, b) => sum + b.w * b.h, 0);
|
|
1747
|
+
const fill = hullArea > 0 ? boxArea / hullArea : 0;
|
|
1748
|
+
const empty = boxes.length === 0;
|
|
1749
|
+
const target = input.container.aspect ?? 1;
|
|
1750
|
+
let pairs = 0;
|
|
1751
|
+
let inversions = 0;
|
|
1752
|
+
for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
|
|
1753
|
+
const a = boxes[i];
|
|
1754
|
+
const b = boxes[j];
|
|
1755
|
+
if (a.rank === b.rank) continue;
|
|
1756
|
+
pairs += 1;
|
|
1757
|
+
const first = a.rank < b.rank ? a : b;
|
|
1758
|
+
if (readsBefore(first === a ? b : a, first)) inversions += 1;
|
|
1441
1759
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
const
|
|
1450
|
-
const
|
|
1451
|
-
if (fixedW && w === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a width.`);
|
|
1452
|
-
if (fixedH && h === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a height.`);
|
|
1760
|
+
const byRank = boxes.toSorted((a, b) => a.rank - b.rank);
|
|
1761
|
+
let breaks = 0;
|
|
1762
|
+
for (let k = 1; k < byRank.length; k++) {
|
|
1763
|
+
const prev = byRank[k - 1];
|
|
1764
|
+
const next = byRank[k];
|
|
1765
|
+
if (prev.rank !== next.rank && readsBefore(next, prev)) breaks += 1;
|
|
1766
|
+
}
|
|
1767
|
+
const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
|
|
1768
|
+
const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
|
|
1453
1769
|
return {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1770
|
+
"pack.area": hullArea,
|
|
1771
|
+
"pack.fill": fill,
|
|
1772
|
+
"pack.whitespace": 1 - fill,
|
|
1773
|
+
"pack.aspect": empty ? 1 : Math.max(hull.w / hull.h, hull.h / hull.w),
|
|
1774
|
+
"pack.aspectError": empty ? 0 : aspectError(hull.w, hull.h, target),
|
|
1775
|
+
"pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
|
|
1776
|
+
"pack.order.breaks": breaks,
|
|
1777
|
+
"pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
|
|
1778
|
+
"pack.overflow": new Set(output.overflow).size,
|
|
1779
|
+
"pack.search.widths": traceSum(output, "pack.search.widths"),
|
|
1780
|
+
"pack.search.ms": traceSum(output, "pack.search.ms")
|
|
1458
1781
|
};
|
|
1459
1782
|
}
|
|
1460
|
-
/**
|
|
1461
|
-
function
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1783
|
+
/**
|
|
1784
|
+
* This function gives the stability measures of a change pair: the run on the base input and the run on the changed
|
|
1785
|
+
* input. The items match by id. The values are not rounded.
|
|
1786
|
+
*
|
|
1787
|
+
* - An item stays when both inputs have it and both outputs place it. The added and the removed items do not count.
|
|
1788
|
+
* - A staying item moved when its `x` or its `y` changed by more than `RECORD_EPS`, or when its turn changed.
|
|
1789
|
+
* - The distance of an item is the length of the move of its top-left corner. The unit is the width `W`.
|
|
1790
|
+
* - `W` is the larger width of the 2 output sizes. A `W` of 0 becomes 1, so the unit is then the unit of the input.
|
|
1791
|
+
* - No staying item gives 0 for each measure.
|
|
1792
|
+
*/
|
|
1793
|
+
function measureChange(baseInput, baseOutput, changeInput, changeOutput) {
|
|
1794
|
+
const placed = (input, output) => {
|
|
1795
|
+
const ids = new Set(input.items.map((i) => i.id));
|
|
1796
|
+
const map = /* @__PURE__ */ new Map();
|
|
1797
|
+
for (const p of output.placements) if (ids.has(p.id) && !map.has(p.id)) map.set(p.id, p);
|
|
1798
|
+
return map;
|
|
1799
|
+
};
|
|
1800
|
+
const before = placed(baseInput, baseOutput);
|
|
1801
|
+
const after = placed(changeInput, changeOutput);
|
|
1802
|
+
const width = Math.max(baseOutput.size.w, changeOutput.size.w);
|
|
1803
|
+
const unit = width > 0 ? width : 1;
|
|
1804
|
+
const distances = [];
|
|
1805
|
+
let moved = 0;
|
|
1806
|
+
for (const [id, p] of before) {
|
|
1807
|
+
const q = after.get(id);
|
|
1808
|
+
if (q === void 0) continue;
|
|
1809
|
+
const dx = q.x - p.x;
|
|
1810
|
+
const dy = q.y - p.y;
|
|
1811
|
+
if (Math.abs(dx) > RECORD_EPS || Math.abs(dy) > RECORD_EPS || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
|
|
1812
|
+
distances.push(Math.hypot(dx, dy) / unit);
|
|
1813
|
+
}
|
|
1814
|
+
if (distances.length === 0) return {
|
|
1815
|
+
"pack.stability.moved": 0,
|
|
1816
|
+
"pack.stability.mean": 0,
|
|
1817
|
+
"pack.stability.q95": 0
|
|
1818
|
+
};
|
|
1819
|
+
return {
|
|
1820
|
+
"pack.stability.moved": moved / distances.length,
|
|
1821
|
+
"pack.stability.mean": distances.reduce((sum, d) => sum + d, 0) / distances.length,
|
|
1822
|
+
"pack.stability.q95": quantile(distances, .95)
|
|
1823
|
+
};
|
|
1466
1824
|
}
|
|
1467
1825
|
/**
|
|
1468
|
-
* This function
|
|
1469
|
-
*
|
|
1826
|
+
* This function gives the measure `pack.deterministic` of two runs with the same input, options and seed. It gives
|
|
1827
|
+
* `true` when the runs have the same placements, overflow, size and moved obstacles.
|
|
1828
|
+
*
|
|
1829
|
+
* - The placements match by id, so their order in the list does not count. The numbers must be equal.
|
|
1830
|
+
* - The trace is not part of the rule, because its times change each time.
|
|
1470
1831
|
*/
|
|
1471
|
-
function
|
|
1472
|
-
const
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
const entries = [];
|
|
1480
|
-
const tooLarge = [];
|
|
1481
|
-
for (const item of byRank(input.items)) {
|
|
1482
|
-
const m = marginOf(item);
|
|
1483
|
-
const rect = new Rect();
|
|
1484
|
-
rect.size.set(item.w + m.left + m.right + 2 * half, item.h + m.top + m.bottom + 2 * half);
|
|
1485
|
-
const dx = m.left + half;
|
|
1486
|
-
const dy = m.top + half;
|
|
1487
|
-
if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
|
|
1488
|
-
rect.locked = true;
|
|
1489
|
-
rect.position.set(item.x - dx - ox, item.y - dy - oy);
|
|
1490
|
-
} else if (rect.size.x > w + 1e-6 || rect.size.y > h + 1e-6) {
|
|
1491
|
-
tooLarge.push(item.id);
|
|
1492
|
-
continue;
|
|
1493
|
-
}
|
|
1494
|
-
entries.push({
|
|
1495
|
-
rect,
|
|
1496
|
-
item,
|
|
1497
|
-
dx,
|
|
1498
|
-
dy
|
|
1832
|
+
function isDeterministic(first, second) {
|
|
1833
|
+
const key = (list) => new Map(list.map((e) => [e.id, e]));
|
|
1834
|
+
const samePlacements = (a, b) => {
|
|
1835
|
+
if (a.length !== b.length) return false;
|
|
1836
|
+
const other = key(b);
|
|
1837
|
+
return a.every((p) => {
|
|
1838
|
+
const q = other.get(p.id);
|
|
1839
|
+
return q !== void 0 && q.x === p.x && q.y === p.y && (q.turn ?? 0) === (p.turn ?? 0);
|
|
1499
1840
|
});
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
entries.push({
|
|
1508
|
-
rect,
|
|
1509
|
-
obstacle,
|
|
1510
|
-
dx: half,
|
|
1511
|
-
dy: half
|
|
1841
|
+
};
|
|
1842
|
+
const sameMoved = (a, b) => {
|
|
1843
|
+
if (a.length !== b.length) return false;
|
|
1844
|
+
const other = key(b);
|
|
1845
|
+
return a.every((m) => {
|
|
1846
|
+
const n = other.get(m.id);
|
|
1847
|
+
return n !== void 0 && n.x === m.x && n.y === m.y;
|
|
1512
1848
|
});
|
|
1513
|
-
}
|
|
1514
|
-
return {
|
|
1515
|
-
ox,
|
|
1516
|
-
oy,
|
|
1517
|
-
w,
|
|
1518
|
-
h,
|
|
1519
|
-
bounds,
|
|
1520
|
-
inset,
|
|
1521
|
-
entries,
|
|
1522
|
-
tooLarge
|
|
1523
1849
|
};
|
|
1850
|
+
const overflowA = new Set(first.overflow);
|
|
1851
|
+
const overflowB = new Set(second.overflow);
|
|
1852
|
+
return first.size.w === second.size.w && first.size.h === second.size.h && overflowA.size === overflowB.size && [...overflowA].every((id) => overflowB.has(id)) && samePlacements(first.placements, second.placements) && sameMoved(first.moved, second.moved);
|
|
1524
1853
|
}
|
|
1525
|
-
/**
|
|
1526
|
-
function
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
return
|
|
1854
|
+
/**
|
|
1855
|
+
* This function rounds each number of a record of measures with `round3`, as `docs/FORMAT.md` tells. A half goes up,
|
|
1856
|
+
* and `-0` becomes 0. The function keeps the other values, and it does not change its argument.
|
|
1857
|
+
*/
|
|
1858
|
+
function roundMeasures(measures) {
|
|
1859
|
+
const out = {};
|
|
1860
|
+
for (const [id, value] of Object.entries(measures)) out[id] = typeof value === "number" ? round3(value) : value;
|
|
1861
|
+
return out;
|
|
1533
1862
|
}
|
|
1534
|
-
/**
|
|
1535
|
-
function
|
|
1536
|
-
|
|
1863
|
+
/**
|
|
1864
|
+
* This function gives the paired ratio of a measure on one input: the value of the candidate against the value of the
|
|
1865
|
+
* current default. A ratio above 1 is worse, in the direction of the measure.
|
|
1866
|
+
*
|
|
1867
|
+
* - For `lower`, the ratio is `candidate / current`. For `higher`, it is `current / candidate`.
|
|
1868
|
+
* - When both values are 0, the ratio is 1. When only the divisor is 0, the ratio is `Infinity`.
|
|
1869
|
+
* - The values are 0 or more. A negative value or `NaN` throws a `RangeError`.
|
|
1870
|
+
*/
|
|
1871
|
+
function pairedRatio(candidate, current, direction) {
|
|
1872
|
+
if (!(candidate >= 0 && current >= 0)) throw new RangeError("A paired ratio needs two values of 0 or more.");
|
|
1873
|
+
const top = direction === "lower" ? candidate : current;
|
|
1874
|
+
const bottom = direction === "lower" ? current : candidate;
|
|
1875
|
+
if (bottom === 0) return top === 0 ? 1 : Infinity;
|
|
1876
|
+
return top / bottom;
|
|
1537
1877
|
}
|
|
1538
1878
|
/**
|
|
1539
|
-
* This function
|
|
1540
|
-
* position
|
|
1879
|
+
* This function gives the quantile `q` of a list of numbers, with linear interpolation between the sorted values. The
|
|
1880
|
+
* position in the sorted list is `(n - 1) * q`. An empty list or a `q` outside 0 to 1 throws a `RangeError`.
|
|
1541
1881
|
*/
|
|
1542
|
-
function
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1882
|
+
function quantile(values, q) {
|
|
1883
|
+
if (values.length === 0) throw new RangeError("A quantile needs one value or more.");
|
|
1884
|
+
if (!(q >= 0 && q <= 1)) throw new RangeError("A quantile needs a q from 0 to 1.");
|
|
1885
|
+
const sorted = values.toSorted((a, b) => a - b);
|
|
1886
|
+
const at = (sorted.length - 1) * q;
|
|
1887
|
+
const low = Math.floor(at);
|
|
1888
|
+
const part = at - low;
|
|
1889
|
+
const a = sorted[low];
|
|
1890
|
+
if (part === 0) return a;
|
|
1891
|
+
const b = sorted[low + 1];
|
|
1892
|
+
if (a === b) return a;
|
|
1893
|
+
return a + part * (b - a);
|
|
1894
|
+
}
|
|
1895
|
+
/** This function gives the quantile `q` of a list, and 0 for an empty list. */
|
|
1896
|
+
function quantileOrZero(values, q) {
|
|
1897
|
+
return values.length === 0 ? 0 : quantile(values, q);
|
|
1898
|
+
}
|
|
1899
|
+
/** This function gives the median of the paired ratios of a corpus: the quantile 0.5. It is not a ratio of two medians. */
|
|
1900
|
+
function medianRatio(ratios) {
|
|
1901
|
+
return quantile(ratios, .5);
|
|
1902
|
+
}
|
|
1903
|
+
/** This function gives the share of the paired ratios above a limit. An empty list throws a `RangeError`. */
|
|
1904
|
+
function tailShare(ratios, limit) {
|
|
1905
|
+
if (ratios.length === 0) throw new RangeError("A tail share needs one ratio or more.");
|
|
1906
|
+
return ratios.filter((r) => r > limit).length / ratios.length;
|
|
1907
|
+
}
|
|
1908
|
+
// src/metrics.ts
|
|
1909
|
+
/**
|
|
1910
|
+
* This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
|
|
1911
|
+
* It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
|
|
1912
|
+
*
|
|
1913
|
+
* @example
|
|
1914
|
+
* ```ts
|
|
1915
|
+
* const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
|
|
1916
|
+
* ```
|
|
1917
|
+
*/
|
|
1918
|
+
function packingMetrics(rects, options = {}) {
|
|
1919
|
+
const tolerance = options.rowTolerance ?? 40;
|
|
1920
|
+
const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0).map((r) => ({
|
|
1921
|
+
x: r.position.x,
|
|
1922
|
+
y: r.position.y,
|
|
1923
|
+
w: r.size.x,
|
|
1924
|
+
h: r.size.y
|
|
1925
|
+
}));
|
|
1926
|
+
if (boxes.length === 0) return {
|
|
1927
|
+
count: 0,
|
|
1928
|
+
hull: {
|
|
1929
|
+
width: 0,
|
|
1930
|
+
height: 0
|
|
1931
|
+
},
|
|
1932
|
+
hullAspect: 0,
|
|
1933
|
+
rows: [],
|
|
1934
|
+
raggedness: 0,
|
|
1935
|
+
utilization: 0,
|
|
1936
|
+
widestOverMedian: 0
|
|
1937
|
+
};
|
|
1938
|
+
const box = hullOf(boxes);
|
|
1939
|
+
const hull = {
|
|
1940
|
+
width: box.w,
|
|
1941
|
+
height: box.h
|
|
1942
|
+
};
|
|
1943
|
+
const rows = rowsOf$1(boxes, tolerance);
|
|
1944
|
+
const rights = rows.map((row) => row.right);
|
|
1945
|
+
const widths = boxes.map((r) => r.w).toSorted((a, b) => a - b);
|
|
1946
|
+
const median = widths[Math.floor(widths.length / 2)] ?? 0;
|
|
1947
|
+
const widest = widths.at(-1) ?? 0;
|
|
1948
|
+
const area = boxes.reduce((sum, r) => sum + r.w * r.h, 0);
|
|
1560
1949
|
return {
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1950
|
+
count: boxes.length,
|
|
1951
|
+
hull,
|
|
1952
|
+
hullAspect: hull.height > 0 ? hull.width / hull.height : 0,
|
|
1953
|
+
rows: rows.map((row) => row.count),
|
|
1954
|
+
raggedness: Math.max(...rights) - Math.min(...rights),
|
|
1955
|
+
utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
|
|
1956
|
+
widestOverMedian: median > 0 ? widest / median : 0
|
|
1566
1957
|
};
|
|
1567
1958
|
}
|
|
1959
|
+
// src/strategy.ts
|
|
1568
1960
|
/**
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1961
|
+
* The error of a strategy for an input outside its cells. The benchmark records this result. It is not a failure of the
|
|
1962
|
+
* strategy.
|
|
1571
1963
|
*/
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
const movedById = new Map(moved.map((m) => [m.id, m]));
|
|
1583
|
-
for (const o of input.obstacles ?? []) {
|
|
1584
|
-
const b = boundsOf(o.region);
|
|
1585
|
-
const at = movedById.get(o.id) ?? b;
|
|
1586
|
-
right = Math.max(right, at.x + b.w - frame.bounds.x);
|
|
1587
|
-
bottom = Math.max(bottom, at.y + b.h - frame.bounds.y);
|
|
1964
|
+
var UnsupportedCell = class extends Error {
|
|
1965
|
+
/** The dimension of the taxonomy. */
|
|
1966
|
+
dimension;
|
|
1967
|
+
/** The value of the input that the strategy does not support. */
|
|
1968
|
+
value;
|
|
1969
|
+
constructor(dimension, value) {
|
|
1970
|
+
super(`The strategy does not support the value "${value}" of the dimension "${dimension}".`);
|
|
1971
|
+
this.name = "UnsupportedCell";
|
|
1972
|
+
this.dimension = dimension;
|
|
1973
|
+
this.value = value;
|
|
1588
1974
|
}
|
|
1975
|
+
};
|
|
1976
|
+
/** This function gives a generator of random numbers in `[0, 1)`: `mulberry32` of `docs/FORMAT.md`. */
|
|
1977
|
+
function mulberry32(seed) {
|
|
1978
|
+
let s = seed >>> 0;
|
|
1979
|
+
return () => {
|
|
1980
|
+
s = s + 1831565813 | 0;
|
|
1981
|
+
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
1982
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1983
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
/** This function makes the context of one packing from a seed. */
|
|
1987
|
+
function createContext(seed = 0, more = {}) {
|
|
1589
1988
|
return {
|
|
1590
|
-
|
|
1591
|
-
|
|
1989
|
+
...more,
|
|
1990
|
+
seed,
|
|
1991
|
+
rng: mulberry32(seed)
|
|
1592
1992
|
};
|
|
1593
1993
|
}
|
|
1594
|
-
/** This function
|
|
1595
|
-
function
|
|
1994
|
+
/** This function tells if a polygon ring is convex. A ring with fewer than 3 points is convex. */
|
|
1995
|
+
function isConvex(points) {
|
|
1996
|
+
let sign = 0;
|
|
1997
|
+
const n = points.length;
|
|
1998
|
+
for (let i = 0; i < n; i++) {
|
|
1999
|
+
const [ax, ay] = points[i];
|
|
2000
|
+
const [bx, by] = points[(i + 1) % n];
|
|
2001
|
+
const [cx, cy] = points[(i + 2) % n];
|
|
2002
|
+
const cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx);
|
|
2003
|
+
if (cross === 0) continue;
|
|
2004
|
+
if (sign === 0) sign = Math.sign(cross);
|
|
2005
|
+
else if (Math.sign(cross) !== sign) return false;
|
|
2006
|
+
}
|
|
2007
|
+
return true;
|
|
2008
|
+
}
|
|
2009
|
+
/** This function gives the value of `containerShape` of one region. */
|
|
2010
|
+
function shapeOf$1(region) {
|
|
1596
2011
|
switch (region.kind) {
|
|
1597
|
-
case "rect":
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
};
|
|
1604
|
-
case "circle": return {
|
|
1605
|
-
kind: "circle",
|
|
1606
|
-
cx: region.cy,
|
|
1607
|
-
cy: region.cx,
|
|
1608
|
-
r: region.r
|
|
1609
|
-
};
|
|
1610
|
-
case "ellipse": return {
|
|
1611
|
-
kind: "ellipse",
|
|
1612
|
-
cx: region.cy,
|
|
1613
|
-
cy: region.cx,
|
|
1614
|
-
rx: region.ry,
|
|
1615
|
-
ry: region.rx
|
|
1616
|
-
};
|
|
1617
|
-
case "polygon": {
|
|
1618
|
-
const swap = (ring) => ring.map(([x, y]) => [y, x]);
|
|
1619
|
-
const turned = {
|
|
1620
|
-
kind: "polygon",
|
|
1621
|
-
points: swap(region.points)
|
|
1622
|
-
};
|
|
1623
|
-
if (region.holes) turned.holes = region.holes.map(swap);
|
|
1624
|
-
return turned;
|
|
1625
|
-
}
|
|
2012
|
+
case "rect":
|
|
2013
|
+
case "circle":
|
|
2014
|
+
case "ellipse": return region.kind;
|
|
2015
|
+
case "polygon":
|
|
2016
|
+
if (region.holes && region.holes.length > 0) return "holes";
|
|
2017
|
+
return isConvex(region.points) ? "convex" : "polygon";
|
|
1626
2018
|
}
|
|
1627
2019
|
}
|
|
1628
|
-
/** This function gives
|
|
1629
|
-
function
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
2020
|
+
/** This function gives the value of `itemShape` of one item shape. An ellipse is convex. */
|
|
2021
|
+
function itemShapeOf(region) {
|
|
2022
|
+
if (region === void 0) return "rect";
|
|
2023
|
+
switch (region.kind) {
|
|
2024
|
+
case "rect":
|
|
2025
|
+
case "circle": return region.kind;
|
|
2026
|
+
case "ellipse": return "convex";
|
|
2027
|
+
case "polygon": return !region.holes?.length && isConvex(region.points) ? "convex" : "polygon";
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* This function gives the cell of an input: the values of the packer dimensions that the input tells.
|
|
2032
|
+
*
|
|
2033
|
+
* - `containerSize` comes from `fixed`. The value `none` gives `freeArea` when `aspectWeight` is 0, and `freeAspect` if not.
|
|
2034
|
+
* - `containerShape` comes from the shape. A rectangle container with an obstacle of another region gives `holes`.
|
|
2035
|
+
* - `obstacleMove` is the hardest value of the obstacles and the locked items: `costly`, then `free`, then `fixed`.
|
|
2036
|
+
* - `order` is `soft` when an item has a rank, and `free` if not. The input does not tell a hard order.
|
|
2037
|
+
* - `groups` is `nested` when an item has a parent, `flat` when an item has a group, and `none` if not.
|
|
2038
|
+
*/
|
|
2039
|
+
function cellOf(input) {
|
|
2040
|
+
const { container, items } = input;
|
|
2041
|
+
const obstacles = input.obstacles ?? [];
|
|
2042
|
+
const containerSize = {
|
|
2043
|
+
size: "fixed",
|
|
2044
|
+
width: "freeHeight",
|
|
2045
|
+
height: "freeWidth",
|
|
2046
|
+
none: container.aspectWeight === 0 ? "freeArea" : "freeAspect"
|
|
2047
|
+
}[container.fixed];
|
|
2048
|
+
let containerShape = container.shape ? shapeOf$1(container.shape) : "rect";
|
|
2049
|
+
if (containerShape === "rect" && obstacles.some((o) => o.region.kind !== "rect")) containerShape = "holes";
|
|
2050
|
+
let obstacleMove = items.some((i) => i.locked === true) ? "fixed" : void 0;
|
|
2051
|
+
for (const o of obstacles) {
|
|
2052
|
+
const move = o.move === void 0 ? "fixed" : o.move.cost > 0 ? "costly" : "free";
|
|
2053
|
+
if (move === "costly" || obstacleMove === void 0 || move === "free" && obstacleMove === "fixed") obstacleMove = move;
|
|
2054
|
+
}
|
|
2055
|
+
const shapes = new Set(items.map((i) => itemShapeOf(i.shape)));
|
|
2056
|
+
const itemShape = shapes.size > 1 ? "mixed" : [...shapes][0] ?? "rect";
|
|
2057
|
+
const cell = {
|
|
2058
|
+
containerSize,
|
|
2059
|
+
containerShape,
|
|
2060
|
+
itemShape,
|
|
2061
|
+
rotation: items.some((i) => i.turnable === true) ? "quarter" : "none",
|
|
2062
|
+
order: items.some((i) => i.rank !== void 0) ? "soft" : "free",
|
|
2063
|
+
groups: items.some((i) => i.parent !== void 0) ? "nested" : items.some((i) => i.group !== void 0) ? "flat" : "none"
|
|
1635
2064
|
};
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
left: m.top
|
|
2065
|
+
if (obstacleMove !== void 0) cell.obstacleMove = obstacleMove;
|
|
2066
|
+
return cell;
|
|
2067
|
+
}
|
|
2068
|
+
/** This function gives the first dimension of a cell that `supports` does not hold, or `undefined` when it holds each one. */
|
|
2069
|
+
function unsupportedOf(supports, cell) {
|
|
2070
|
+
for (const dimension of Object.keys(supports)) {
|
|
2071
|
+
const value = cell[dimension];
|
|
2072
|
+
if (value === void 0) continue;
|
|
2073
|
+
if (!supports[dimension].includes(value)) return {
|
|
2074
|
+
dimension,
|
|
2075
|
+
value
|
|
1648
2076
|
};
|
|
1649
2077
|
}
|
|
1650
|
-
return turned;
|
|
1651
2078
|
}
|
|
1652
|
-
/** This function
|
|
1653
|
-
function
|
|
1654
|
-
|
|
1655
|
-
const container = { fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed };
|
|
1656
|
-
if (c.h !== void 0) container.w = c.h;
|
|
1657
|
-
if (c.w !== void 0) container.h = c.w;
|
|
1658
|
-
if (c.aspect !== void 0) container.aspect = 1 / c.aspect;
|
|
1659
|
-
if (c.aspectWeight !== void 0) container.aspectWeight = c.aspectWeight;
|
|
1660
|
-
if (c.shape !== void 0) container.shape = transposeRegion(c.shape);
|
|
1661
|
-
if (c.inset !== void 0) container.inset = c.inset;
|
|
1662
|
-
const turned = {
|
|
1663
|
-
container,
|
|
1664
|
-
items: input.items.map(transposeItem)
|
|
1665
|
-
};
|
|
1666
|
-
if (input.obstacles) turned.obstacles = input.obstacles.map((o) => ({
|
|
1667
|
-
...o,
|
|
1668
|
-
region: transposeRegion(o.region)
|
|
1669
|
-
}));
|
|
1670
|
-
if (input.gap !== void 0) turned.gap = input.gap;
|
|
1671
|
-
return turned;
|
|
2079
|
+
/** This function tells if `supports` holds each value of a cell. A dimension that the cell does not have is held. */
|
|
2080
|
+
function supportsCell(supports, cell) {
|
|
2081
|
+
return unsupportedOf(supports, cell) === void 0;
|
|
1672
2082
|
}
|
|
1673
|
-
/** This function
|
|
1674
|
-
function
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
...p,
|
|
1678
|
-
x: p.y,
|
|
1679
|
-
y: p.x
|
|
1680
|
-
})),
|
|
1681
|
-
overflow: [...output.overflow],
|
|
1682
|
-
size: {
|
|
1683
|
-
w: output.size.h,
|
|
1684
|
-
h: output.size.w
|
|
1685
|
-
},
|
|
1686
|
-
moved: output.moved.map((m) => ({
|
|
1687
|
-
id: m.id,
|
|
1688
|
-
x: m.y,
|
|
1689
|
-
y: m.x
|
|
1690
|
-
})),
|
|
1691
|
-
trace: output.trace
|
|
1692
|
-
};
|
|
2083
|
+
/** This function throws `UnsupportedCell` for the first value of the cell of an input that `supports` does not hold. */
|
|
2084
|
+
function assertSupported(supports, input) {
|
|
2085
|
+
const miss = unsupportedOf(supports, cellOf(input));
|
|
2086
|
+
if (miss) throw new UnsupportedCell(miss.dimension, miss.value);
|
|
1693
2087
|
}
|
|
2088
|
+
// src/adapters.ts
|
|
1694
2089
|
/**
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
2090
|
+
* The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. The container layer of `src/space.ts`
|
|
2091
|
+
* gives them each container shape.
|
|
1697
2092
|
*/
|
|
1698
|
-
function viaAxes(input, previous, run) {
|
|
1699
|
-
const start = now();
|
|
1700
|
-
const inner = run(transposeInput(input), previous === void 0 ? void 0 : transposeOutput(previous));
|
|
1701
|
-
const output = transposeOutput(inner);
|
|
1702
|
-
output.trace = [{
|
|
1703
|
-
id: "axes",
|
|
1704
|
-
ms: now() - start,
|
|
1705
|
-
measures: {}
|
|
1706
|
-
}, ...inner.trace];
|
|
1707
|
-
return output;
|
|
1708
|
-
}
|
|
1709
|
-
// src/adapters.ts
|
|
1710
|
-
/** The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. */
|
|
1711
2093
|
const LAYOUT_CELLS = {
|
|
1712
2094
|
containerSize: [
|
|
1713
2095
|
"fixed",
|
|
@@ -1716,7 +2098,14 @@ const LAYOUT_CELLS = {
|
|
|
1716
2098
|
"freeAspect",
|
|
1717
2099
|
"freeArea"
|
|
1718
2100
|
],
|
|
1719
|
-
containerShape: [
|
|
2101
|
+
containerShape: [
|
|
2102
|
+
"rect",
|
|
2103
|
+
"convex",
|
|
2104
|
+
"polygon",
|
|
2105
|
+
"circle",
|
|
2106
|
+
"ellipse",
|
|
2107
|
+
"holes"
|
|
2108
|
+
],
|
|
1720
2109
|
itemShape: ["rect"],
|
|
1721
2110
|
rotation: ["none"],
|
|
1722
2111
|
groups: ["none"],
|
|
@@ -1742,15 +2131,18 @@ function packFixedWidth(id, input, options) {
|
|
|
1742
2131
|
const rects = rectsOf(frame);
|
|
1743
2132
|
if (frame.w > 0) pack(rects, outerOf(frame), {
|
|
1744
2133
|
strategy: id,
|
|
1745
|
-
sortOrder: options.sortOrder
|
|
2134
|
+
sortOrder: options.sortOrder,
|
|
2135
|
+
space: spaceOf(input, frame)
|
|
1746
2136
|
});
|
|
1747
2137
|
const output = readFrame(input, frame, []);
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2138
|
+
return {
|
|
2139
|
+
...output,
|
|
2140
|
+
trace: [{
|
|
2141
|
+
id,
|
|
2142
|
+
ms: now() - start,
|
|
2143
|
+
measures: { "pack.overflow": output.overflow.length }
|
|
2144
|
+
}]
|
|
2145
|
+
};
|
|
1754
2146
|
}
|
|
1755
2147
|
/** This function makes the strategy of one function of `pack`. */
|
|
1756
2148
|
function layoutStrategy(id, family, catalog, cells) {
|
|
@@ -1783,11 +2175,12 @@ function layoutStrategy(id, family, catalog, cells) {
|
|
|
1783
2175
|
return self;
|
|
1784
2176
|
}
|
|
1785
2177
|
/**
|
|
1786
|
-
* The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill.
|
|
1787
|
-
*
|
|
2178
|
+
* The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
|
|
2179
|
+
* container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
|
|
2180
|
+
* shapes throw `UnsupportedCell`.
|
|
1788
2181
|
*/
|
|
1789
2182
|
const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
|
|
1790
|
-
obstacleMove: [],
|
|
2183
|
+
obstacleMove: ["fixed"],
|
|
1791
2184
|
order: [
|
|
1792
2185
|
"free",
|
|
1793
2186
|
"soft",
|
|
@@ -1796,7 +2189,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
|
|
|
1796
2189
|
});
|
|
1797
2190
|
/**
|
|
1798
2191
|
* The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
|
|
1799
|
-
* locked items and the fixed obstacles. Turnable items, groups and shapes throw `UnsupportedCell`.
|
|
2192
|
+
* locked items and the fixed obstacles, in each container shape. Turnable items, groups and item shapes throw `UnsupportedCell`.
|
|
1800
2193
|
*/
|
|
1801
2194
|
const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
|
|
1802
2195
|
obstacleMove: ["fixed"],
|
|
@@ -1804,7 +2197,7 @@ const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
|
|
|
1804
2197
|
});
|
|
1805
2198
|
/**
|
|
1806
2199
|
* The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
|
|
1807
|
-
* With the sort order `input`, the rows keep the rank order. Turnable items, groups and shapes throw `UnsupportedCell`.
|
|
2200
|
+
* With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
|
|
1808
2201
|
*/
|
|
1809
2202
|
const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
|
|
1810
2203
|
obstacleMove: ["fixed"],
|
|
@@ -1833,6 +2226,7 @@ const logSweep = {
|
|
|
1833
2226
|
const result = packFree(rectsOf(frame), new Rect(), {
|
|
1834
2227
|
strategy: id,
|
|
1835
2228
|
sortOrder: o.sortOrder,
|
|
2229
|
+
space: spaceOf(input, frame),
|
|
1836
2230
|
samples: o.samples,
|
|
1837
2231
|
aspect: input.container.aspect ?? 1,
|
|
1838
2232
|
aspectWeight: input.container.aspectWeight ?? 1
|
|
@@ -1840,24 +2234,26 @@ const logSweep = {
|
|
|
1840
2234
|
const ms = now() - start;
|
|
1841
2235
|
const runs = result.tried.length + 1;
|
|
1842
2236
|
const output = readFrame(input, frame, []);
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2237
|
+
return {
|
|
2238
|
+
...output,
|
|
2239
|
+
trace: [{
|
|
2240
|
+
id: "log-sweep",
|
|
2241
|
+
ms,
|
|
2242
|
+
measures: {
|
|
2243
|
+
"pack.search.widths": result.tried.length,
|
|
2244
|
+
"pack.search.ms": ms
|
|
2245
|
+
}
|
|
2246
|
+
}, {
|
|
2247
|
+
id,
|
|
2248
|
+
ms: ms / runs,
|
|
2249
|
+
measures: { "pack.overflow": output.overflow.length }
|
|
2250
|
+
}]
|
|
2251
|
+
};
|
|
1856
2252
|
}
|
|
1857
2253
|
};
|
|
1858
2254
|
/** This function tells if the insides of two rectangles overlap by more than `EPS`. */
|
|
1859
2255
|
function overlaps(a, b) {
|
|
1860
|
-
return Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x) >
|
|
2256
|
+
return Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x) > EPS && Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y) > EPS;
|
|
1861
2257
|
}
|
|
1862
2258
|
/** This function packs an input with a fixed size or a fixed width with a push cascade. */
|
|
1863
2259
|
function pushFixedWidth(input, options, previous) {
|
|
@@ -1899,9 +2295,9 @@ function pushFixedWidth(input, options, previous) {
|
|
|
1899
2295
|
const startAt = starts.get(e);
|
|
1900
2296
|
const x = e.rect.position.x + e.dx + frame.ox;
|
|
1901
2297
|
const y = e.rect.position.y + e.dy + frame.oy;
|
|
1902
|
-
const b = boundsOf(e.obstacle.region);
|
|
1903
|
-
if (x < bounds.x -
|
|
1904
|
-
else if (Math.abs(e.rect.position.x - startAt.x) >
|
|
2298
|
+
const b = boundsOf$1(e.obstacle.region);
|
|
2299
|
+
if (x < bounds.x - EPS || y < bounds.y - EPS || x + b.w > bounds.x + bounds.w + EPS || y + b.h > bounds.y + bounds.h + EPS || lockedItems.some((l) => overlaps(l.rect, e.rect))) e.rect.position.set(startAt.x, startAt.y);
|
|
2300
|
+
else if (Math.abs(e.rect.position.x - startAt.x) > EPS || Math.abs(e.rect.position.y - startAt.y) > EPS) moved.push({
|
|
1905
2301
|
id: e.obstacle.id,
|
|
1906
2302
|
x,
|
|
1907
2303
|
y
|
|
@@ -1911,7 +2307,7 @@ function pushFixedWidth(input, options, previous) {
|
|
|
1911
2307
|
const kept = [...obstacles, ...lockedItems].map((e) => e.rect);
|
|
1912
2308
|
for (const e of positioned) {
|
|
1913
2309
|
const r = e.rect;
|
|
1914
|
-
if (r.position.x >= -
|
|
2310
|
+
if (r.position.x >= -EPS && r.position.y >= -EPS && r.right() <= frame.w + EPS && r.bottom() <= frame.h + EPS && !kept.some((k) => overlaps(k, r))) kept.push(r);
|
|
1915
2311
|
else pending.push(e);
|
|
1916
2312
|
}
|
|
1917
2313
|
const skylineStart = now();
|
|
@@ -1927,19 +2323,21 @@ function pushFixedWidth(input, options, previous) {
|
|
|
1927
2323
|
packSkyline([...pending.map((e) => e.rect), ...kept], outerOf(frame), { sortOrder: options.sortOrder });
|
|
1928
2324
|
}
|
|
1929
2325
|
const output = readFrame(input, frame, [], moved);
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2326
|
+
return {
|
|
2327
|
+
...output,
|
|
2328
|
+
trace: [{
|
|
2329
|
+
id: "push",
|
|
2330
|
+
ms: pushMs,
|
|
2331
|
+
measures: { "pack.push.moved": moved.length }
|
|
2332
|
+
}, {
|
|
2333
|
+
id: "skyline",
|
|
2334
|
+
ms: now() - skylineStart,
|
|
2335
|
+
measures: {
|
|
2336
|
+
"pack.push.replaced": pending.length,
|
|
2337
|
+
"pack.overflow": output.overflow.length
|
|
2338
|
+
}
|
|
2339
|
+
}]
|
|
2340
|
+
};
|
|
1943
2341
|
}
|
|
1944
2342
|
/**
|
|
1945
2343
|
* The strategy `push` uses `pushCascade`. Each item starts at its `x` and `y`, or at its place in `context.previous`.
|
|
@@ -1989,31 +2387,29 @@ const push = {
|
|
|
1989
2387
|
*/
|
|
1990
2388
|
function fromRects(rects, outer) {
|
|
1991
2389
|
const fixed = outer.fixedSize ? "size" : outer.fixedWidth ? "width" : "none";
|
|
1992
|
-
const container = { fixed };
|
|
1993
|
-
if (fixed !== "none") container.w = outer.size.x;
|
|
1994
|
-
if (fixed === "size") container.h = outer.size.y;
|
|
1995
|
-
if (outer.position.x !== 0 || outer.position.y !== 0) container.shape = {
|
|
1996
|
-
kind: "rect",
|
|
1997
|
-
x: outer.position.x,
|
|
1998
|
-
y: outer.position.y,
|
|
1999
|
-
w: outer.size.x,
|
|
2000
|
-
h: outer.size.y
|
|
2001
|
-
};
|
|
2002
2390
|
return {
|
|
2003
|
-
container
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2391
|
+
container: {
|
|
2392
|
+
fixed,
|
|
2393
|
+
...fixed === "none" ? {} : { w: outer.size.x },
|
|
2394
|
+
...fixed === "size" ? { h: outer.size.y } : {},
|
|
2395
|
+
...outer.position.x !== 0 || outer.position.y !== 0 ? { shape: {
|
|
2396
|
+
kind: "rect",
|
|
2397
|
+
x: outer.position.x,
|
|
2398
|
+
y: outer.position.y,
|
|
2399
|
+
w: outer.size.x,
|
|
2400
|
+
h: outer.size.y
|
|
2401
|
+
} } : {}
|
|
2402
|
+
},
|
|
2403
|
+
items: rects.map((r, i) => ({
|
|
2404
|
+
id: String(i),
|
|
2405
|
+
w: r.size.x,
|
|
2406
|
+
h: r.size.y,
|
|
2407
|
+
...r.locked ? {
|
|
2408
|
+
locked: true,
|
|
2409
|
+
x: r.position.x,
|
|
2410
|
+
y: r.position.y
|
|
2411
|
+
} : {}
|
|
2412
|
+
}))
|
|
2017
2413
|
};
|
|
2018
2414
|
}
|
|
2019
2415
|
/**
|
|
@@ -2068,6 +2464,15 @@ function packWith(id, input, options, seed = 0) {
|
|
|
2068
2464
|
function overlap(a, b, tolerance) {
|
|
2069
2465
|
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;
|
|
2070
2466
|
}
|
|
2467
|
+
/** This function gives the box made smaller by a tolerance on each side, as a rectangle. */
|
|
2468
|
+
function inner(box, tolerance) {
|
|
2469
|
+
return {
|
|
2470
|
+
x: box.l + tolerance,
|
|
2471
|
+
y: box.t + tolerance,
|
|
2472
|
+
w: box.r - box.l - 2 * tolerance,
|
|
2473
|
+
h: box.b - box.t - 2 * tolerance
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2071
2476
|
/**
|
|
2072
2477
|
* This function counts the breaks of the 4 hard rules of the packer for an output.
|
|
2073
2478
|
*
|
|
@@ -2078,38 +2483,36 @@ function overlap(a, b, tolerance) {
|
|
|
2078
2483
|
* - `complete`: each item with no placement that is not in the overflow. A locked item breaks `locked-kept` in its place.
|
|
2079
2484
|
*
|
|
2080
2485
|
* A locked item with no placement stays at its input position for `no-overlap` and `in-container`. A locked item with no
|
|
2081
|
-
* `x` or no `y` in the input is
|
|
2486
|
+
* `x` or no `y` in the input is a bad input. The rules skip it, as `packHard` of the contract does. `checkInput` finds it.
|
|
2082
2487
|
*
|
|
2083
|
-
* The
|
|
2084
|
-
*
|
|
2488
|
+
* The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
|
|
2489
|
+
* The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
|
|
2490
|
+
* shape is relative to the position of the item. The result has no tolerance in it.
|
|
2085
2491
|
*/
|
|
2086
2492
|
function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
|
|
2087
2493
|
const tol = tolerance.overlap;
|
|
2088
2494
|
const placements = /* @__PURE__ */ new Map();
|
|
2089
2495
|
for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
|
|
2090
2496
|
const overflow = new Set(output.overflow);
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
"in-container": 0,
|
|
2094
|
-
"locked-kept": 0,
|
|
2095
|
-
complete: 0
|
|
2096
|
-
};
|
|
2497
|
+
let lockedKept = 0;
|
|
2498
|
+
let complete = 0;
|
|
2097
2499
|
const boxes = [];
|
|
2098
2500
|
for (const item of input.items) {
|
|
2099
|
-
|
|
2501
|
+
if (item.locked === true && (item.x === void 0 || item.y === void 0)) continue;
|
|
2502
|
+
const lockedAt = item.locked === true ? {
|
|
2100
2503
|
x: item.x,
|
|
2101
2504
|
y: item.y
|
|
2102
2505
|
} : void 0;
|
|
2103
2506
|
let p = placements.get(item.id);
|
|
2104
2507
|
if (p === void 0) {
|
|
2105
2508
|
if (lockedAt === void 0) {
|
|
2106
|
-
if (!overflow.has(item.id))
|
|
2509
|
+
if (!overflow.has(item.id)) complete += 1;
|
|
2107
2510
|
continue;
|
|
2108
2511
|
}
|
|
2109
|
-
|
|
2512
|
+
lockedKept += 1;
|
|
2110
2513
|
p = lockedAt;
|
|
2111
2514
|
} else if (lockedAt !== void 0) {
|
|
2112
|
-
if (Math.abs(p.x - lockedAt.x) >
|
|
2515
|
+
if (Math.abs(p.x - lockedAt.x) > RECORD_EPS || Math.abs(p.y - lockedAt.y) > RECORD_EPS) lockedKept += 1;
|
|
2113
2516
|
}
|
|
2114
2517
|
const placed = p.turn === 90 ? turnItem(item) : item;
|
|
2115
2518
|
const m = marginOf(placed);
|
|
@@ -2130,18 +2533,17 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
|
|
|
2130
2533
|
}
|
|
2131
2534
|
const moved = new Map(output.moved.map((m) => [m.id, m]));
|
|
2132
2535
|
const obstacles = (input.obstacles ?? []).map((o) => {
|
|
2536
|
+
const at = moved.get(o.id);
|
|
2537
|
+
if (at === void 0) return o.region;
|
|
2133
2538
|
const b = boundsOf(o.region);
|
|
2134
|
-
|
|
2135
|
-
return {
|
|
2136
|
-
l: at.x,
|
|
2137
|
-
t: at.y,
|
|
2138
|
-
r: at.x + b.w,
|
|
2139
|
-
b: at.y + b.h
|
|
2140
|
-
};
|
|
2539
|
+
return translate(o.region, at.x - b.x, at.y - b.y);
|
|
2141
2540
|
});
|
|
2541
|
+
let overlaps = 0;
|
|
2142
2542
|
for (const [i, a] of boxes.entries()) {
|
|
2143
|
-
for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol))
|
|
2144
|
-
|
|
2543
|
+
for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) overlaps += 1;
|
|
2544
|
+
if (obstacles.length === 0) continue;
|
|
2545
|
+
const rect = inner(a, tol);
|
|
2546
|
+
for (const region of obstacles) if (regionHitsRect(region, rect)) overlaps += 1;
|
|
2145
2547
|
}
|
|
2146
2548
|
const { container } = input;
|
|
2147
2549
|
const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
|
|
@@ -2152,12 +2554,18 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
|
|
|
2152
2554
|
const inset = container.inset ?? 0;
|
|
2153
2555
|
const fixedW = container.fixed === "size" || container.fixed === "width";
|
|
2154
2556
|
const fixedH = container.fixed === "size" || container.fixed === "height";
|
|
2557
|
+
let outside = 0;
|
|
2155
2558
|
for (const box of boxes) {
|
|
2156
2559
|
const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
|
|
2157
2560
|
const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
|
|
2158
|
-
if (outX || outY)
|
|
2561
|
+
if (outX || outY) outside += 1;
|
|
2159
2562
|
}
|
|
2160
|
-
return
|
|
2563
|
+
return {
|
|
2564
|
+
"no-overlap": overlaps,
|
|
2565
|
+
"in-container": outside,
|
|
2566
|
+
"locked-kept": lockedKept,
|
|
2567
|
+
complete
|
|
2568
|
+
};
|
|
2161
2569
|
}
|
|
2162
2570
|
// src/generators.ts
|
|
2163
2571
|
/** The largest count of boxes that a generator makes. */
|
|
@@ -2378,8 +2786,10 @@ function containerOf(size, w, h, shape) {
|
|
|
2378
2786
|
aspectWeight: 0
|
|
2379
2787
|
}
|
|
2380
2788
|
}[size];
|
|
2381
|
-
|
|
2382
|
-
|
|
2789
|
+
return shape === void 0 ? container : {
|
|
2790
|
+
...container,
|
|
2791
|
+
shape
|
|
2792
|
+
};
|
|
2383
2793
|
}
|
|
2384
2794
|
/** This function gives the 4 corners of a rectangle, clockwise from the top left. */
|
|
2385
2795
|
const corners = (x, y, w, h) => [
|
|
@@ -3071,6 +3481,6 @@ function corpus(name, specs) {
|
|
|
3071
3481
|
inputs
|
|
3072
3482
|
};
|
|
3073
3483
|
}
|
|
3074
|
-
export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, compareRects, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, fromRects, gallery, generator, generators, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, measure, measureChange, medianRatio, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
|
|
3484
|
+
export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, checkInput, compareRects, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, freeSpace, fromRects, gallery, generator, generators, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, measure, measureChange, medianRatio, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
|
|
3075
3485
|
|
|
3076
3486
|
//# sourceMappingURL=index.js.map
|