@packvium/engine 0.1.2 → 1.0.0
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 +9 -1
- package/SECURITY.md +2 -2
- package/contact-graph.js +143 -29
- package/examples/shapes.mjs +155 -0
- package/fallback.js +766 -20
- package/index.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
Deterministic 3D cartonization for Node.js. It uses the optional native engine when
|
|
4
4
|
available and automatically falls back to the bundled JavaScript implementation.
|
|
5
5
|
|
|
6
|
+
Full documentation, the constraint reference and benchmarks live at
|
|
7
|
+
[packvium.com](https://packvium.com).
|
|
8
|
+
|
|
6
9
|
## Install
|
|
7
10
|
|
|
8
11
|
```bash
|
|
@@ -100,6 +103,8 @@ and execute without a project around it.
|
|
|
100
103
|
| File | What it shows |
|
|
101
104
|
| --- | --- |
|
|
102
105
|
| [`basic.mjs`](examples/basic.mjs) | Pack an order, read placements, and see why an item was refused. |
|
|
106
|
+
| [`objectives.mjs`](examples/objectives.mjs) | All six objectives on scenes where they genuinely disagree — the same scores the Python, PHP and Rust engines print for the same request. |
|
|
107
|
+
| [`shapes.mjs`](examples/shapes.mjs) | Items that are not their box: complementary wedges sharing one crate as `convex_hull`, and a cushion that compresses under load until the crush limit refuses it. |
|
|
103
108
|
| [`commerce.mjs`](examples/commerce.mjs) | Rate a shipment, apply an eligibility rule, and pin a catalog version. |
|
|
104
109
|
|
|
105
110
|
```bash
|
|
@@ -126,11 +131,14 @@ One request and result contract, implemented independently in four engines (Rust
|
|
|
126
131
|
Python, PHP, JavaScript) and held to identical placements on a shared fixture set.
|
|
127
132
|
Pick the package for your stack; mixing them in one system is safe.
|
|
128
133
|
|
|
134
|
+
Documentation, the constraint reference and the benchmarks are at
|
|
135
|
+
[packvium.com](https://packvium.com).
|
|
136
|
+
|
|
129
137
|
| Package | Install | Source |
|
|
130
138
|
| --- | --- | --- |
|
|
131
139
|
| Python — [`packvium`](https://pypi.org/project/packvium/) | `pip install packvium` | [packvium-python](https://github.com/toxakara/packvium-python) |
|
|
132
140
|
| PHP — [`packvium/packvium`](https://packagist.org/packages/packvium/packvium) | `composer require packvium/packvium` | [packvium-php](https://github.com/toxakara/packvium-php) |
|
|
133
|
-
| Rust — [`packvium`](https://crates.io/crates/packvium) | `packvium = "0
|
|
141
|
+
| Rust — [`packvium`](https://crates.io/crates/packvium) | `packvium = "1.0"` | [packvium-rust](https://github.com/toxakara/packvium-rust) |
|
|
134
142
|
| Node.js — [`@packvium/engine`](https://www.npmjs.com/package/@packvium/engine) | `npm install @packvium/engine` | [packvium-node](https://github.com/toxakara/packvium-node) |
|
|
135
143
|
| Browser / WebAssembly — [`@packvium/browser`](https://www.npmjs.com/package/@packvium/browser) | `npm install @packvium/browser` | [packvium-wasm](https://github.com/toxakara/packvium-wasm) |
|
|
136
144
|
| PHP FFI bridge — [`packvium/native-bridge`](https://packagist.org/packages/packvium/native-bridge) | `composer require packvium/native-bridge` | [packvium-php-bridge](https://github.com/toxakara/packvium-php-bridge) |
|
package/SECURITY.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Supported versions
|
|
4
4
|
|
|
5
|
-
Only the latest `
|
|
6
|
-
long-term support branch
|
|
5
|
+
Only the latest `1.x` release receives fixes. The `0.1.x` line is superseded by `1.0.0`
|
|
6
|
+
and receives none. There is no long-term support branch for older majors.
|
|
7
7
|
|
|
8
8
|
## Reporting a vulnerability
|
|
9
9
|
|
package/contact-graph.js
CHANGED
|
@@ -6,45 +6,106 @@
|
|
|
6
6
|
* representation can avoid. The exact overlap function remains authoritative.
|
|
7
7
|
* This module is package-internal: package.json exports only the root entry point.
|
|
8
8
|
*/
|
|
9
|
-
export function buildContactGraph(boxes, overlapXY) {
|
|
10
|
-
const supporters = boxes.map(() => []);
|
|
11
|
-
const children = boxes.map(() => []);
|
|
12
|
-
if (boxes.length === 0) return { supporters, children, candidateChecks: 0 };
|
|
13
|
-
|
|
14
|
-
const cell = Math.max(1, ...boxes.map(box => Math.max(box.d[0], box.d[1])));
|
|
15
|
-
const byTop = new Map();
|
|
16
|
-
const levels = new Map();
|
|
17
|
-
const cells = box => {
|
|
18
|
-
const x1 = Math.floor(box.x / cell);
|
|
19
|
-
const x2 = Math.floor((box.x + box.d[0] - 1) / cell);
|
|
20
|
-
const y1 = Math.floor(box.y / cell);
|
|
21
|
-
const y2 = Math.floor((box.y + box.d[1] - 1) / cell);
|
|
22
|
-
return [...new Set([`${x1}:${y1}`, `${x2}:${y1}`, `${x1}:${y2}`, `${x2}:${y2}`])];
|
|
23
|
-
};
|
|
24
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The at-most-four cells `box` occupies.
|
|
12
|
+
*
|
|
13
|
+
* `cell` must be at least as large as the largest footprint dimension of every box
|
|
14
|
+
* hashed into the index or queried against it -- not just the ones being indexed. Only
|
|
15
|
+
* then is a box guaranteed to span no more than a 2x2 block, which is what makes two
|
|
16
|
+
* overlapping boxes always share a cell. Sizing it from the indexed boxes alone would be
|
|
17
|
+
* exactly wrong: a larger querying box could step over cells in the middle of its own
|
|
18
|
+
* footprint and silently miss a real overlap.
|
|
19
|
+
*/
|
|
20
|
+
function cellsOf(box, cell) {
|
|
21
|
+
const x1 = Math.floor(box.x / cell);
|
|
22
|
+
const x2 = Math.floor((box.x + box.d[0] - 1) / cell);
|
|
23
|
+
const y1 = Math.floor(box.y / cell);
|
|
24
|
+
const y2 = Math.floor((box.y + box.d[1] - 1) / cell);
|
|
25
|
+
return [...new Set([`${x1}:${y1}`, `${x2}:${y1}`, `${x1}:${y2}`, `${x2}:${y2}`])];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function bucketsByPlane(boxes, plane) {
|
|
29
|
+
const byPlane = new Map();
|
|
25
30
|
boxes.forEach((box, index) => {
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
28
|
-
|
|
31
|
+
const key = plane(box);
|
|
32
|
+
if (!byPlane.has(key)) byPlane.set(key, []);
|
|
33
|
+
byPlane.get(key).push(index);
|
|
29
34
|
});
|
|
35
|
+
return byPlane;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const topOf = box => box.z + box.d[2];
|
|
39
|
+
const bottomOf = box => box.z;
|
|
30
40
|
|
|
41
|
+
function levelIndex(boxes, indices, cell) {
|
|
42
|
+
const level = new Map();
|
|
43
|
+
for (const index of indices) {
|
|
44
|
+
for (const key of cellsOf(boxes[index], cell)) {
|
|
45
|
+
if (!level.has(key)) level.set(key, []);
|
|
46
|
+
level.get(key).push(index);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return level;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Every box in `buckets` on `plane` that really overlaps `box`, ascending by index.
|
|
54
|
+
*
|
|
55
|
+
* Ascending order is contract, not presentation: `topLoads` splits a conserved integer
|
|
56
|
+
* across the supporter list and hands the rounding remainder to whichever edge is last.
|
|
57
|
+
*/
|
|
58
|
+
function overlapsOnPlane(graph, buckets, cache, plane, box, overlapXY) {
|
|
59
|
+
const indices = buckets.get(plane);
|
|
60
|
+
if (!indices) return [];
|
|
61
|
+
let level = cache.get(plane);
|
|
62
|
+
if (level == null) {
|
|
63
|
+
level = levelIndex(graph.boxes, indices, graph.cell);
|
|
64
|
+
cache.set(plane, level);
|
|
65
|
+
}
|
|
66
|
+
const nearby = new Set();
|
|
67
|
+
for (const key of cellsOf(box, graph.cell)) {
|
|
68
|
+
for (const index of level.get(key) ?? []) nearby.add(index);
|
|
69
|
+
}
|
|
70
|
+
const found = [];
|
|
71
|
+
for (const other of [...nearby].sort((left, right) => left - right)) {
|
|
72
|
+
const area = overlapXY(graph.boxes[other], box);
|
|
73
|
+
if (area > 0) found.push([other, area]);
|
|
74
|
+
}
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `cellHint` is an upper bound on the footprint of any box that may later be appended
|
|
80
|
+
* with `appendContactBox`.
|
|
81
|
+
*
|
|
82
|
+
* Without it the cell is sized from the boxes present now, and appending anything wider
|
|
83
|
+
* has to fall back to a full rebuild -- correct, but it defeats the point, because in a
|
|
84
|
+
* search the base is what is already placed and the candidate is a *new* item that may
|
|
85
|
+
* well be the widest in the request. A caller that knows the item set passes its widest
|
|
86
|
+
* footprint once and the delta path then always applies. Too large a hint only makes
|
|
87
|
+
* each bucket coarser; too small a one cannot give a wrong answer, because the fallback
|
|
88
|
+
* covers it.
|
|
89
|
+
*/
|
|
90
|
+
export function buildContactGraph(boxes, overlapXY, cellHint = 1) {
|
|
91
|
+
const supporters = boxes.map(() => []);
|
|
92
|
+
const children = boxes.map(() => []);
|
|
93
|
+
const cell = Math.max(1, cellHint, ...boxes.map(box => Math.max(box.d[0], box.d[1])));
|
|
94
|
+
const byTop = bucketsByPlane(boxes, topOf);
|
|
95
|
+
const byBottom = bucketsByPlane(boxes, bottomOf);
|
|
96
|
+
const topLevels = new Map();
|
|
31
97
|
let candidateChecks = 0;
|
|
98
|
+
|
|
32
99
|
boxes.forEach((upper, upperIndex) => {
|
|
33
100
|
const candidates = byTop.get(upper.z);
|
|
34
101
|
if (!candidates) return;
|
|
35
|
-
let level =
|
|
102
|
+
let level = topLevels.get(upper.z);
|
|
36
103
|
if (level == null) {
|
|
37
|
-
level =
|
|
38
|
-
|
|
39
|
-
for (const key of cells(boxes[index])) {
|
|
40
|
-
if (!level.has(key)) level.set(key, []);
|
|
41
|
-
level.get(key).push(index);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
levels.set(upper.z, level);
|
|
104
|
+
level = levelIndex(boxes, candidates, cell);
|
|
105
|
+
topLevels.set(upper.z, level);
|
|
45
106
|
}
|
|
46
107
|
const nearby = new Set();
|
|
47
|
-
for (const key of
|
|
108
|
+
for (const key of cellsOf(upper, cell)) {
|
|
48
109
|
for (const index of level.get(key) ?? []) nearby.add(index);
|
|
49
110
|
}
|
|
50
111
|
for (const lowerIndex of [...nearby].sort((left, right) => left - right)) {
|
|
@@ -57,5 +118,58 @@ export function buildContactGraph(boxes, overlapXY) {
|
|
|
57
118
|
}
|
|
58
119
|
}
|
|
59
120
|
});
|
|
60
|
-
|
|
121
|
+
// The downward-facing indexes stay empty here: only an append queries them, and a graph
|
|
122
|
+
// built once and read once would otherwise pay for an index nothing looks at.
|
|
123
|
+
return { supporters, children, candidateChecks, boxes, cell, byTop, byBottom, topLevels,
|
|
124
|
+
bottomLevels: new Map() };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* `graph` plus one more box, appended at the next index.
|
|
129
|
+
*
|
|
130
|
+
* Adding a box cannot create or destroy contact between two boxes already in the graph:
|
|
131
|
+
* contact is a pairwise geometric predicate over two boxes and nothing else. That is the
|
|
132
|
+
* whole reason a delta is sound, and it is why only the new box's own two planes are
|
|
133
|
+
* queried instead of every box being re-examined.
|
|
134
|
+
*
|
|
135
|
+
* The result is required to be identical to `buildContactGraph([...boxes, box])`, not
|
|
136
|
+
* merely equivalent -- see `overlapsOnPlane` on why edge order is contract. The new box
|
|
137
|
+
* takes the highest index, so appending it to an existing list keeps that list ascending.
|
|
138
|
+
*
|
|
139
|
+
* `graph` is not modified: the returned graph shares every edge list the append did not
|
|
140
|
+
* touch, and copies the two or three it did.
|
|
141
|
+
*/
|
|
142
|
+
export function appendContactBox(graph, box, overlapXY) {
|
|
143
|
+
const index = graph.boxes.length;
|
|
144
|
+
const footprint = Math.max(box.d[0], box.d[1]);
|
|
145
|
+
if (footprint > graph.cell) {
|
|
146
|
+
// The broad phase is only correct while its cell covers every box hashed into it or
|
|
147
|
+
// queried against it, so this is a correctness fallback, not an optimisation choice.
|
|
148
|
+
return buildContactGraph([...graph.boxes, box], overlapXY, footprint);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const below = overlapsOnPlane(graph, graph.byTop, graph.topLevels, box.z, box, overlapXY);
|
|
152
|
+
const above = overlapsOnPlane(graph, graph.byBottom, graph.bottomLevels, topOf(box), box, overlapXY);
|
|
153
|
+
|
|
154
|
+
const supporters = graph.supporters.slice();
|
|
155
|
+
const children = graph.children.slice();
|
|
156
|
+
supporters.push(below.map(([lower, area]) => [lower, area]));
|
|
157
|
+
children.push(above.map(([upper]) => upper));
|
|
158
|
+
for (const [lower] of below) children[lower] = [...children[lower], index];
|
|
159
|
+
for (const [upper, area] of above) supporters[upper] = [...supporters[upper], [index, area]];
|
|
160
|
+
|
|
161
|
+
// One box joins exactly two planes, so only those two buckets change, and only the two
|
|
162
|
+
// level indexes describing them are invalidated. A level index is never mutated after
|
|
163
|
+
// it is built, so every other one is shared with the base rather than rebuilt.
|
|
164
|
+
const byTop = new Map(graph.byTop);
|
|
165
|
+
byTop.set(topOf(box), [...(byTop.get(topOf(box)) ?? []), index]);
|
|
166
|
+
const byBottom = new Map(graph.byBottom);
|
|
167
|
+
byBottom.set(box.z, [...(byBottom.get(box.z) ?? []), index]);
|
|
168
|
+
const topLevels = new Map(graph.topLevels);
|
|
169
|
+
topLevels.delete(topOf(box));
|
|
170
|
+
const bottomLevels = new Map(graph.bottomLevels);
|
|
171
|
+
bottomLevels.delete(box.z);
|
|
172
|
+
|
|
173
|
+
return { supporters, children, candidateChecks: graph.candidateChecks,
|
|
174
|
+
boxes: [...graph.boxes, box], cell: graph.cell, byTop, byBottom, topLevels, bottomLevels };
|
|
61
175
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shapes: when an item is not its box.
|
|
3
|
+
*
|
|
4
|
+
* Run it:
|
|
5
|
+
*
|
|
6
|
+
* node examples/shapes.mjs
|
|
7
|
+
*
|
|
8
|
+
* Every other example treats an item as the box it declares. That is the default and it
|
|
9
|
+
* is right for almost everything, because a carton *is* a cuboid. Two kinds of goods are
|
|
10
|
+
* not: a moulded or tapered part that leaves a usable void beside it, and a soft one that
|
|
11
|
+
* gives way under whatever is stacked on it.
|
|
12
|
+
*
|
|
13
|
+
* `shape_type` narrows the box in one direction each -- `convex_hull` in space,
|
|
14
|
+
* `compressible` in height under load -- and neither is ever inferred. An engine that
|
|
15
|
+
* quietly packed a hull as its bounding box would return a plan that validates and does
|
|
16
|
+
* not physically fit, so the value must be asked for.
|
|
17
|
+
*
|
|
18
|
+
* These fields are part of the shared request contract, so the same document runs
|
|
19
|
+
* unchanged against the Python, PHP and Rust engines. It does not follow that all four
|
|
20
|
+
* print the same numbers -- see the note on the compressible section below, which is the
|
|
21
|
+
* more useful half of the lesson.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { pack } from '../index.js';
|
|
25
|
+
|
|
26
|
+
const MM = { units: { length: 'mm' } };
|
|
27
|
+
const crate = (length, width, height) => [
|
|
28
|
+
{ id: 'crate', inner_dimensions: { length, width, height } },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** Run one request and print only what the shape changed: containers and refusals. */
|
|
32
|
+
const summarise = (label, request) => {
|
|
33
|
+
const result = pack({ ...MM, ...request });
|
|
34
|
+
const placed = result.containers.reduce((n, c) => n + c.placements.length, 0);
|
|
35
|
+
console.log(
|
|
36
|
+
` ${label.padEnd(22)} ${result.status.padEnd(10)} ` +
|
|
37
|
+
`${result.containers.length} container(s), ${placed} placed, ` +
|
|
38
|
+
`${result.unpacked_items.length} refused`,
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ------------------------------------------------------------------ convex_hull
|
|
43
|
+
//
|
|
44
|
+
// Two triangular prisms, each cut from the same 100 mm cube along the diagonal. Their
|
|
45
|
+
// bounding boxes are identical and fill the crate on their own, so as cuboids the second
|
|
46
|
+
// one has nowhere to go. As hulls they are complementary halves and share the crate
|
|
47
|
+
// exactly -- the collision test is an exact integer separating-axis test on the vertices,
|
|
48
|
+
// not a box overlap.
|
|
49
|
+
//
|
|
50
|
+
// The hull is given in the item's own coordinates, in the request's length unit, and must
|
|
51
|
+
// fit inside the declared dimensions. It is not a replacement for them: the box still
|
|
52
|
+
// bounds the item, the hull only says how much of that box is solid.
|
|
53
|
+
|
|
54
|
+
const LOWER_WEDGE = [
|
|
55
|
+
{ x: '0', y: '0', z: '0' }, { x: '100', y: '0', z: '0' },
|
|
56
|
+
{ x: '0', y: '100', z: '0' }, { x: '0', y: '0', z: '100' },
|
|
57
|
+
{ x: '100', y: '0', z: '100' }, { x: '0', y: '100', z: '100' },
|
|
58
|
+
];
|
|
59
|
+
const UPPER_WEDGE = [
|
|
60
|
+
{ x: '100', y: '100', z: '0' }, { x: '100', y: '0', z: '0' },
|
|
61
|
+
{ x: '0', y: '100', z: '0' }, { x: '100', y: '100', z: '100' },
|
|
62
|
+
{ x: '100', y: '0', z: '100' }, { x: '0', y: '100', z: '100' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const wedge = (id, vertices) => ({
|
|
66
|
+
id,
|
|
67
|
+
quantity: 1,
|
|
68
|
+
dimensions: { length: '100', width: '100', height: '100' },
|
|
69
|
+
weight: { value: '1', unit: 'kg' },
|
|
70
|
+
...(vertices ? { shape_type: 'convex_hull', hull_vertices: vertices } : {}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log('convex_hull -- two complementary wedges cut from one cube');
|
|
74
|
+
summarise('as cuboids', {
|
|
75
|
+
items: [wedge('wedge-lower', null), wedge('wedge-upper', null)],
|
|
76
|
+
containers: crate('100', '100', '100'),
|
|
77
|
+
});
|
|
78
|
+
summarise('as hulls', {
|
|
79
|
+
items: [wedge('wedge-lower', LOWER_WEDGE), wedge('wedge-upper', UPPER_WEDGE)],
|
|
80
|
+
containers: crate('100', '100', '100'),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// One crate instead of two, for the same goods and the same crate. Nothing about the
|
|
84
|
+
// request changed except the claim that the items are wedges rather than blocks.
|
|
85
|
+
|
|
86
|
+
// ----------------------------------------------------------------- compressible
|
|
87
|
+
//
|
|
88
|
+
// `compression_ratio` is the fraction of its own height an item may lose when something
|
|
89
|
+
// rests on it -- 0.25 means it can give up a quarter. The mass above it is what decides
|
|
90
|
+
// how much it actually gives, so the occupied height of a compressible item is not a
|
|
91
|
+
// property of the item alone; it depends on what the solver put on top.
|
|
92
|
+
//
|
|
93
|
+
// `max_compression_pressure_kpa` is the other half of the same field. Past that pressure
|
|
94
|
+
// the item is not compressed further, it is crushed, and the load is refused instead.
|
|
95
|
+
//
|
|
96
|
+
// Note `must_be_on_floor` on the cushion. Without it the solver is free to put the brick
|
|
97
|
+
// underneath, nothing bears on the cushion, and the feature never engages -- which is the
|
|
98
|
+
// honest reason the rule is here and not an incidental detail of the example.
|
|
99
|
+
|
|
100
|
+
const cushion = (crushKpa) => ({
|
|
101
|
+
id: 'cushion',
|
|
102
|
+
quantity: 1,
|
|
103
|
+
dimensions: { length: '100', width: '100', height: '100' },
|
|
104
|
+
weight: { value: '2', unit: 'kg' },
|
|
105
|
+
must_be_on_floor: true,
|
|
106
|
+
shape_type: 'compressible',
|
|
107
|
+
compression_ratio: 0.25,
|
|
108
|
+
max_compression_pressure_kpa: crushKpa,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const brick = (kilograms) => ({
|
|
112
|
+
id: 'brick',
|
|
113
|
+
quantity: 1,
|
|
114
|
+
dimensions: { length: '100', width: '100', height: '100' },
|
|
115
|
+
weight: { value: String(kilograms), unit: 'kg' },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** One crate, one cushion, one brick -- only the brick's mass changes. */
|
|
119
|
+
const load = (label, kilograms) => {
|
|
120
|
+
const result = pack({
|
|
121
|
+
...MM,
|
|
122
|
+
items: [cushion(100), brick(kilograms)],
|
|
123
|
+
containers: crate('100', '100', '200'),
|
|
124
|
+
});
|
|
125
|
+
console.log(
|
|
126
|
+
` ${label.padEnd(22)} ${result.containers.length} container(s), ` +
|
|
127
|
+
`unused volume ${result.score[3]} ppm`,
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// The crate is 100x100x200 and the two items are 100 mm cubes, so rigidly they fill it
|
|
132
|
+
// exactly and nothing is unused. At 102 kg the brick crosses 100 kPa over the cushion's
|
|
133
|
+
// 0.01 m^2 face: the stack is refused, the brick opens a second crate, and half of each
|
|
134
|
+
// crate is empty.
|
|
135
|
+
//
|
|
136
|
+
// At 101 kg this engine also opens two crates -- and the Python, PHP and Rust engines
|
|
137
|
+
// return one, with the cushion compressed. Both answers are valid: every item is placed,
|
|
138
|
+
// no rule is broken, and an independent validator accepts each. This one is simply worse,
|
|
139
|
+
// and it is recorded as such in the suite's quality budget rather than left to be
|
|
140
|
+
// discovered here.
|
|
141
|
+
//
|
|
142
|
+
// That is the guarantee, stated exactly. What the shared contract fixes is the request
|
|
143
|
+
// shape, the validity rules and the objective vector -- not which of several valid
|
|
144
|
+
// arrangements a given engine finds. An engine may return a worse-scoring valid packing;
|
|
145
|
+
// none may return an invalid one. If you need the best answer these fields can give,
|
|
146
|
+
// solve on the Rust or Python engine and treat the JavaScript fallback as the portable
|
|
147
|
+
// one.
|
|
148
|
+
console.log('\ncompressible -- a cushion that yields to the load above it');
|
|
149
|
+
load('brick 101 kg', 101);
|
|
150
|
+
load('brick 102 kg', 102);
|
|
151
|
+
|
|
152
|
+
// Both shapes are refused rather than approximated wherever an engine cannot honour them
|
|
153
|
+
// exactly -- a hull on a route, a hull under a configured clearance, a compressible item
|
|
154
|
+
// with `nesting_height`. A wrong answer that validates is worse than a refusal that does
|
|
155
|
+
// not, which is the whole reason these are opt-in.
|