@packvium/engine 0.1.1 → 0.1.3

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 CHANGED
@@ -120,6 +120,25 @@ node examples/basic.mjs
120
120
  The native addon is optional. `npm install` works on unsupported platforms too; call
121
121
  `backend()` if your application needs to know which implementation handled a request.
122
122
 
123
+ ## The Packvium family
124
+
125
+ One request and result contract, implemented independently in four engines (Rust,
126
+ Python, PHP, JavaScript) and held to identical placements on a shared fixture set.
127
+ Pick the package for your stack; mixing them in one system is safe.
128
+
129
+ Documentation, the constraint reference and the benchmarks are at
130
+ [packvium.com](https://packvium.com).
131
+
132
+ | Package | Install | Source |
133
+ | --- | --- | --- |
134
+ | Python — [`packvium`](https://pypi.org/project/packvium/) | `pip install packvium` | [packvium-python](https://github.com/toxakara/packvium-python) |
135
+ | PHP — [`packvium/packvium`](https://packagist.org/packages/packvium/packvium) | `composer require packvium/packvium` | [packvium-php](https://github.com/toxakara/packvium-php) |
136
+ | Rust — [`packvium`](https://crates.io/crates/packvium) | `packvium = "0.1"` | [packvium-rust](https://github.com/toxakara/packvium-rust) |
137
+ | Node.js — [`@packvium/engine`](https://www.npmjs.com/package/@packvium/engine) | `npm install @packvium/engine` | [packvium-node](https://github.com/toxakara/packvium-node) |
138
+ | Browser / WebAssembly — [`@packvium/browser`](https://www.npmjs.com/package/@packvium/browser) | `npm install @packvium/browser` | [packvium-wasm](https://github.com/toxakara/packvium-wasm) |
139
+ | 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) |
140
+ | Python native selector — `packvium-native` | from source until the native wheels ship | [packvium-python-adapter](https://github.com/toxakara/packvium-python-adapter) |
141
+
123
142
  ## API and support
124
143
 
125
144
  TypeScript declarations are included. See the package's `index.d.ts` for the complete
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Objectives: six ways to be "best", and the scenes where they disagree.
3
+ *
4
+ * Run it:
5
+ *
6
+ * node examples/objectives.mjs
7
+ *
8
+ * Every solve returns the arrangement that scores best -- but "best" is a choice, and it
9
+ * is the one setting most likely to make the library look wrong when it is merely
10
+ * answering a different question than you meant to ask. This example builds scenes where
11
+ * two objectives genuinely pick different containers, so the difference is visible
12
+ * rather than asserted.
13
+ *
14
+ * The score is always a lexicographic array of exact integers, never a float, and its
15
+ * first key is always the unpacked count: no objective will ever leave an item behind to
16
+ * save money. The same request handed to the Python, PHP or Rust engine prints the same
17
+ * vector.
18
+ */
19
+
20
+ import { pack } from '../index.js';
21
+
22
+ const widgets = [{
23
+ id: 'widget', quantity: 8,
24
+ dimensions: { length: '100', width: '100', height: '100' },
25
+ weight: '500 g',
26
+ }];
27
+
28
+ const solve = (configuration, containers) => {
29
+ const result = pack({ units: { length: 'mm' }, configuration, items: widgets, containers });
30
+ const chosen = result.containers.length > 0 ? result.containers[0].container_type : 'none';
31
+ return `${chosen.padEnd(6)} score=${JSON.stringify(result.score)}`;
32
+ };
33
+
34
+ const box = (id, side, extra = {}) => ({
35
+ id,
36
+ inner_dimensions: { length: side, width: side, height: side },
37
+ max_payload: '20 kg',
38
+ ...extra,
39
+ });
40
+
41
+ const weightPricing = {
42
+ dimensional_weight_divisor: 5000,
43
+ dimensional_weight_length_unit: 'cm',
44
+ dimensional_weight_weight_unit: 'kg',
45
+ };
46
+
47
+ const snug = box('snug', '300', { cost_minor: 500 });
48
+ const roomy = box('roomy', '400', { cost_minor: 150 });
49
+
50
+ // `default` -- fewest containers, then tightest fit. What you want when the boxes are
51
+ // interchangeable and you are simply trying not to open another one.
52
+ console.log('default ', solve({ seed: 42 }, [snug, roomy]));
53
+
54
+ // `lowest_cost` -- the cheapest *packaging*. `cost_minor` is what the box costs you, so
55
+ // this is the objective for a warehouse buying cartons, not a shipper paying a carrier.
56
+ console.log('lowest_cost ', solve({ seed: 42, objective: 'lowest_cost' }, [snug, roomy]));
57
+
58
+ // `shipping_cost` -- carrier-billable *weight*: the greater of actual gross weight and
59
+ // dimensional weight. A big light box can bill more than a small heavy one, which is why
60
+ // this is not the same objective as `lowest_cost`. It needs a divisor and refuses rather
61
+ // than guessing one, because a wrong divisor silently misprices every shipment.
62
+ console.log('shipping_cost ',
63
+ solve({ seed: 42, objective: 'shipping_cost', ...weightPricing }, [snug, roomy]));
64
+
65
+ // `lowest_landed_cost` -- carrier-billable *money*, with the rate card arriving as
66
+ // request data. Weight and money do not always agree: a bracket step, or a minimum
67
+ // charge, can make the cheaper shipment the heavier one. Below the roomy box bills
68
+ // heavier (12,800 g of dimensional weight against the snug box's 5,400) and still costs
69
+ // less, because the snug box's carrier charges a steep first bracket.
70
+ const dearPerGram = box('snug', '300', {
71
+ rate_table: { weight_brackets_g: [6000, 20000], prices_minor: [2400, 3100] },
72
+ });
73
+ const cheapPerGram = box('roomy', '400', {
74
+ rate_table: { weight_brackets_g: [6000, 20000], prices_minor: [900, 1500] },
75
+ });
76
+ const byMoney = { seed: 42, objective: 'lowest_landed_cost', ...weightPricing };
77
+ console.log('lowest_landed_cost ', solve(byMoney, [dearPerGram, cheapPerGram]));
78
+
79
+ // A rate card that stops short of the shipment is a refusal, never a silent clamp to the
80
+ // top bracket -- you would otherwise be quoted a price the carrier never published.
81
+ const tooNarrow = box('roomy', '400', {
82
+ rate_table: { weight_brackets_g: [2000], prices_minor: [900] },
83
+ });
84
+ try {
85
+ solve(byMoney, [tooNarrow]);
86
+ } catch (refusal) {
87
+ console.log('lowest_landed_cost* ', `refused: ${refusal.message}`);
88
+ }
89
+
90
+ // `open_dimension_height` -- pack into the shortest stack, for a lidless container or a
91
+ // pallet that has to clear a doorway.
92
+ console.log('open_dimension_height',
93
+ solve({ seed: 42, objective: 'open_dimension_height' }, [snug, roomy]));
94
+
95
+ // `maximum_value` -- when not everything fits, leave the *cheap* things behind. It orders
96
+ // by value; it does not solve the knapsack problem to optimality. `quantity: 1` on the
97
+ // container is what makes it a choice at all -- with an unlimited supply the packer
98
+ // simply opens another box.
99
+ const scarce = pack({
100
+ units: { length: 'mm' },
101
+ configuration: { seed: 42, objective: 'maximum_value' },
102
+ items: [
103
+ { id: 'gold', quantity: 2, dimensions: { length: '100', width: '100', height: '100' }, weight: '500 g', value: 90000 },
104
+ { id: 'gravel', quantity: 2, dimensions: { length: '100', width: '100', height: '100' }, weight: '500 g', value: 10 },
105
+ ],
106
+ containers: [{
107
+ id: 'tiny', quantity: 1,
108
+ inner_dimensions: { length: '200', width: '100', height: '100' },
109
+ max_payload: '20 kg',
110
+ }],
111
+ });
112
+ const kept = scarce.containers.flatMap((c) => c.placements.map((p) => p.item_type)).sort();
113
+ const left = (scarce.unpacked_items ?? []).map((u) => u.item_type ?? u.item_id).sort();
114
+ console.log('maximum_value ', `packed=${JSON.stringify(kept)} left behind=${JSON.stringify(left)}`);
package/index.js CHANGED
@@ -29,7 +29,7 @@ export function rebalanceWeight(request,result,{maxMoves=64}={}){
29
29
  }
30
30
  return rebalanceFallback(request,result,{maxMoves});
31
31
  }
32
- export const version=()=>native?.version?.()??'0.1.1-js-fallback';
32
+ export const version=()=>native?.version?.()??'0.1.3-js-fallback';
33
33
 
34
34
  /**
35
35
  * The exported commercial and control-plane API: a quote, a policy decision and catalog
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name":"@packvium/engine",
3
- "version":"0.1.1",
3
+ "version":"0.1.3",
4
4
  "description":"Native-first 3D cartonization with deterministic JS fallback",
5
+ "keywords":["3d-bin-packing","bin-packing","cartonization","packing","container-loading","logistics","shipping","deterministic"],
6
+ "homepage":"https://packvium.com",
7
+ "repository":{"type":"git","url":"git+https://github.com/toxakara/packvium-node.git"},
8
+ "bugs":{"url":"https://github.com/toxakara/packvium-node/issues"},
5
9
  "type":"module",
6
10
  "main":"index.js",
7
11
  "types":"index.d.ts",
8
12
  "exports":{".":{"types":"./index.d.ts","import":"./index.js"}},
9
13
  "files":["index.js","fallback.js","contact-graph.js","policy.js","commerce.js","commerce-model.js","examples","index.d.ts","README.md","SECURITY.md"],
10
14
  "engines":{"node":">=16"},
11
- "optionalDependencies":{"@packvium/native":"0.1.1"},
15
+ "optionalDependencies":{"@packvium/native":"0.1.3"},
12
16
  "scripts":{
13
17
  "test":"node --test \"test/*.test.mjs\"",
14
18
  "test:legacy":"node test/legacy-conformance.mjs"