@jetta/blocks-allocator 0.2.1-1 → 0.2.1-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetta/blocks-allocator",
3
- "version": "0.2.1-1",
3
+ "version": "0.2.1-2",
4
4
  "description": "Blocks wall allocation library",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -1,24 +1,11 @@
1
1
  const No_Available_Items_Exception = require('./exceptions/No_Available_Items_Exception')
2
- const No_Slots_For_Items_Exception = require('./exceptions/No_Slots_For_Items_Exception')
3
2
  const Cargo_Profile = require('../../models/Cargo_Profile')
4
- const No_Suitable_Item_Exception = require('../block_builder/exceptions/No_Suitable_Item_Exception')
5
- const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
6
- const Allocated_Item = require('../../models/Allocated_Item')
7
- const { distribute_items_by_weight_range } = require('./Weight_Slot_Distribution')
8
- const {
9
- resolve_choose_item_heuristic_context,
10
- format_slot_label_for_exception
11
- } = require('./Choose_Item_Heuristic_Context')
12
- const {
13
- calculate_total_stack_slots,
14
- sort_stacking_slots_by_proximity
15
- } = require('./Stack_Slot_Calculator')
16
- const { order_items_by_capacity_bias } = require('./Capacity_Bias_Item_Order')
17
- const candidate_item_fits_axle_load = require('./axle_load/candidate_item_fits_axle_load')
3
+ const { select_slot_item, select_preferred_or_heuristic } = require('./select_slot_item')
18
4
 
19
5
  /**
20
6
  * Chooses an item from available items by distributing them into stacking slots by weight
21
7
  * and returning the item in the slot closest to the block slot position.
8
+ * When the axle gate is on, legal SKUs in that band are ranked by hottest-axle ratio.
22
9
  * @param {object} data - Input bag.
23
10
  * @param {object} [data.slot] - Slot with get_y() and get_height() (production).
24
11
  * @param {object} [data.block_slot] - Legacy alias for slot (tests).
@@ -41,127 +28,8 @@ module.exports = function choose_item(data)
41
28
  {
42
29
  throw new No_Available_Items_Exception()
43
30
  }
44
- const is_to_prefer_same_id = data.block.get_slot_list().length > 0 && previous_chosen_item_id !== undefined && Cargo_Profile.prefer_block_with_same_item()
45
- return is_to_prefer_same_id ? prefer_same_id(data) : choose_item_with_heuristic(data)
46
- }
47
-
48
- function prefer_same_id(data)
49
- {
50
- const { container, item_list, previous_chosen_item_id, item_position } = data
51
- const allocation_item = item_list.find(item => item.get_id() === previous_chosen_item_id)
52
- if (allocation_item)
53
- {
54
- const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
55
- if (accepted_item)
56
- {
57
- return accepted_item
58
- }
59
- }
60
-
61
- return choose_item_with_heuristic(data)
62
- }
63
-
64
- /**
65
- * Heuristic path: weight bands, proximity-ordered bands, stacking validation per candidate.
66
- * @param {object} data - Input bag (see choose_item).
67
- * @returns {object} Chosen item.
68
- */
69
- function choose_item_with_heuristic(data)
70
- {
71
- const { slot, container, item_position } = resolve_choose_item_heuristic_context(data)
72
- const { item_list, qty_of_slots } = data
73
- const total_slots = calculate_total_stack_slots(container.get_height(), slot)
74
- const stacking_slots = distribute_items_by_weight_range(item_list, total_slots)
75
- const ordered_bands = sort_stacking_slots_by_proximity(
76
- stacking_slots,
77
- slot.get_y(),
78
- slot.get_height()
79
- )
80
-
81
- for (const band of ordered_bands)
82
- {
83
- const candidate_items = band.items
84
- while (candidate_items.length > 0)
85
- {
86
- const allocation_item = get_item(candidate_items, qty_of_slots, container)
87
- const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
88
- if (accepted_item)
89
- {
90
- return accepted_item
91
- }
92
-
93
- remove_item_from_candidate_list(candidate_items, allocation_item)
94
- }
95
- }
96
-
97
- const slot_label = format_slot_label_for_exception(slot)
98
- throw new No_Suitable_Item_Exception(ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM, [slot_label])
99
- }
100
-
101
- /**
102
- * @param {object} data
103
- * @param {object} container
104
- * @param {object} allocation_item
105
- * @param {object} item_position
106
- * @returns {object|undefined}
107
- */
108
- function try_accept_candidate(data, container, allocation_item, item_position)
109
- {
110
- const allocated_item = new Allocated_Item(
111
- allocation_item,
112
- item_position.x,
113
- item_position.y,
114
- item_position.z
115
- )
116
- if (!candidate_item_fits_axle_load(data, allocated_item))
117
- {
118
- return undefined
119
- }
120
- if (should_defer_loose_on_block_stacking_validation(data.block))
121
- {
122
- return allocated_item
123
- }
124
- if (!container.is_a_valid_stacking(allocated_item))
125
- {
126
- return undefined
127
- }
128
- return allocated_item
129
- }
130
-
131
- function should_defer_loose_on_block_stacking_validation(block)
132
- {
133
- return Cargo_Profile.stack_loose_on_tie_ratio_blocks()
134
- && block?.get_slot_list?.().length === 1
135
- }
136
-
137
- function remove_item_from_candidate_list(candidate_item_list, allocation_item)
138
- {
139
- const index = candidate_item_list.indexOf(allocation_item)
140
- if (index > -1)
141
- {
142
- candidate_item_list.splice(index, 1)
143
- }
144
- }
145
-
146
- function get_item(item_list, qty_of_slots, container)
147
- {
148
- if (qty_of_slots === undefined)
149
- {
150
- throw new No_Slots_For_Items_Exception()
151
- }
152
-
153
- if (item_list.length === 0)
154
- {
155
- throw new No_Available_Items_Exception()
156
- }
157
-
158
- const ordered_item_list = order_items_by_capacity_bias(item_list, container)
159
- const preferable_item_list = ordered_item_list.filter(item => item.get_amount() >= qty_of_slots)
160
-
161
- if (preferable_item_list.length > 0)
162
- {
163
- return preferable_item_list[0]
164
- }
165
-
166
- return ordered_item_list[0]
31
+ const is_to_prefer_same_id = data.block.get_slot_list().length > 0
32
+ && previous_chosen_item_id !== undefined
33
+ && Cargo_Profile.prefer_block_with_same_item()
34
+ return is_to_prefer_same_id ? select_preferred_or_heuristic(data) : select_slot_item(data)
167
35
  }
@@ -1,5 +1,21 @@
1
1
  const { gt } = require('@jetta/number-compare')
2
2
 
3
+ /**
4
+ * Hottest axle load as a fraction of the folga-adjusted cap. Above 1 is over limit.
5
+ * @param {object} env - Calculator output env with containers[].pesoEixos.
6
+ * @param {number} folga_percent - Slack percent (10 means 110% of pesoMaxFinal).
7
+ * @returns {number}
8
+ */
9
+ function max_axle_load_ratio(env, folga_percent)
10
+ {
11
+ let hottest_ratio = 0
12
+ for (const axle of collect_peso_eixos_rows(env))
13
+ {
14
+ hottest_ratio = Math.max(hottest_ratio, axle_row_ratio(axle, folga_percent))
15
+ }
16
+ return hottest_ratio
17
+ }
18
+
3
19
  /**
4
20
  * Whether any container axle row exceeds the folga-adjusted final max.
5
21
  * @param {object} env - Calculator output env with containers[].pesoEixos.
@@ -8,15 +24,7 @@ const { gt } = require('@jetta/number-compare')
8
24
  */
9
25
  function axle_load_exceeds_limit(env, folga_percent)
10
26
  {
11
- const axle_rows = collect_peso_eixos_rows(env)
12
- for (const axle of axle_rows)
13
- {
14
- if (axle_row_exceeds_limit(axle, folga_percent))
15
- {
16
- return true
17
- }
18
- }
19
- return false
27
+ return gt(max_axle_load_ratio(env, folga_percent), 1)
20
28
  }
21
29
 
22
30
  /**
@@ -25,12 +33,10 @@ function axle_load_exceeds_limit(env, folga_percent)
25
33
  */
26
34
  function collect_peso_eixos_rows(env)
27
35
  {
28
- const containers = env.containers ?? []
29
36
  const rows = []
30
- for (const container of containers)
37
+ for (const container of env.containers ?? [])
31
38
  {
32
- const axle_list = container.pesoEixos ?? []
33
- rows.push(...axle_list)
39
+ rows.push(...(container.pesoEixos ?? []))
34
40
  }
35
41
  return rows
36
42
  }
@@ -38,14 +44,19 @@ function collect_peso_eixos_rows(env)
38
44
  /**
39
45
  * @param {object} axle
40
46
  * @param {number} folga_percent
41
- * @returns {boolean}
47
+ * @returns {number}
42
48
  */
43
- function axle_row_exceeds_limit(axle, folga_percent)
49
+ function axle_row_ratio(axle, folga_percent)
44
50
  {
45
51
  const loaded_kilograms = Number(axle.pesoCarregadoFinal ?? 0)
46
52
  const max_kilograms = Number(axle.pesoMaxFinal ?? 0)
47
53
  const limit_kilograms = max_kilograms * (1 + folga_percent / 100)
48
- return gt(loaded_kilograms, limit_kilograms)
54
+ if (limit_kilograms === 0)
55
+ {
56
+ return loaded_kilograms > 0 ? Number.POSITIVE_INFINITY : 0
57
+ }
58
+ return loaded_kilograms / limit_kilograms
49
59
  }
50
60
 
51
61
  module.exports = axle_load_exceeds_limit
62
+ module.exports.max_axle_load_ratio = max_axle_load_ratio
@@ -0,0 +1,42 @@
1
+ const { gt } = require('@jetta/number-compare')
2
+ const Cargo_Profile = require('../../../models/Cargo_Profile')
3
+ const { score_layout_axle_load } = require('./evaluate_layout_axle_load')
4
+ const layout_fits_axle_load = require('./layout_fits_axle_load')
5
+
6
+ /**
7
+ * Axle score for one candidate. fits is false when the cap is exceeded or physics fails.
8
+ * When the gate is off, ratio is 0 and fits is true.
9
+ * @param {object} data
10
+ * @param {object} allocated_item
11
+ * @returns {{ fits: boolean, ratio: number|undefined }}
12
+ */
13
+ function candidate_item_axle_score(data, allocated_item)
14
+ {
15
+ if (!Cargo_Profile.enable_axle_load_slot_gate() || !layout_fits_axle_load.transport_has_axles(data.transport))
16
+ {
17
+ return { fits: true, ratio: 0 }
18
+ }
19
+ const ratio = score_layout_axle_load(layout_probe(data, allocated_item))
20
+ if (ratio === undefined)
21
+ {
22
+ return { fits: false, ratio: undefined }
23
+ }
24
+ return { fits: gt(ratio, 1) === false, ratio }
25
+ }
26
+
27
+ /**
28
+ * @param {object} data
29
+ * @param {object} allocated_item
30
+ * @returns {object}
31
+ */
32
+ function layout_probe(data, allocated_item)
33
+ {
34
+ const pending_axle_items = data.pending_axle_items ?? []
35
+ return {
36
+ transport: data.transport,
37
+ container: data.container,
38
+ extra_items: pending_axle_items.concat(allocated_item)
39
+ }
40
+ }
41
+
42
+ module.exports = candidate_item_axle_score
@@ -1,43 +1,63 @@
1
1
  const { calculate_weight_on_axles_physics } = require('@jetta/axle-load-calculator')
2
+ const { gt } = require('@jetta/number-compare')
2
3
  const { Logger } = require('@jetta/logger')
3
4
  const Cargo_Profile = require('../../../models/Cargo_Profile')
4
5
  const clone_env_with_layout_items = require('./clone_env_with_layout_items')
5
6
  const axle_load_exceeds_limit = require('./axle_load_exceeds_limit')
7
+ const { max_axle_load_ratio } = axle_load_exceeds_limit
6
8
 
7
9
  const logger = new Logger('Axle Load Slot Gate')
8
10
 
9
11
  /**
10
- * Runs axle physics for the current layout plus extra items on the active container.
12
+ * Hottest axle ratio for the layout, or undefined when physics cannot run.
11
13
  * @param {object} data
12
14
  * @param {object} data.transport
13
15
  * @param {object} data.container
14
16
  * @param {object[]} data.extra_items
15
- * @returns {boolean} True when no axle exceeds the folga-adjusted cap.
17
+ * @returns {number|undefined}
16
18
  */
17
- function evaluate_layout_axle_load(data)
19
+ function score_layout_axle_load(data)
18
20
  {
19
21
  const env = clone_env_with_layout_items(data.transport, data.container, data.extra_items)
20
22
  try
21
23
  {
22
- return layout_is_within_axle_limit(env)
24
+ return ratio_from_physics(env)
23
25
  }
24
26
  catch (error)
25
27
  {
26
28
  logger.warn(`axle calculator rejected layout: ${error.message}`)
29
+ return undefined
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Runs axle physics for the current layout plus extra items on the active container.
35
+ * @param {object} data
36
+ * @param {object} data.transport
37
+ * @param {object} data.container
38
+ * @param {object[]} data.extra_items
39
+ * @returns {boolean} True when no axle exceeds the folga-adjusted cap.
40
+ */
41
+ function evaluate_layout_axle_load(data)
42
+ {
43
+ const ratio = score_layout_axle_load(data)
44
+ if (ratio === undefined)
45
+ {
27
46
  return false
28
47
  }
48
+ return gt(ratio, 1) === false
29
49
  }
30
50
 
31
51
  /**
32
52
  * @param {object} env
33
- * @returns {boolean}
53
+ * @returns {number}
34
54
  */
35
- function layout_is_within_axle_limit(env)
55
+ function ratio_from_physics(env)
36
56
  {
37
57
  const output = calculate_weight_on_axles_physics({ envs: [env] })
38
- const calculated_env = output.envs[0]
39
58
  const folga_percent = Cargo_Profile.axle_weight_gap_percent()
40
- return axle_load_exceeds_limit(calculated_env, folga_percent) === false
59
+ return max_axle_load_ratio(output.envs[0], folga_percent)
41
60
  }
42
61
 
43
62
  module.exports = evaluate_layout_axle_load
63
+ module.exports.score_layout_axle_load = score_layout_axle_load
@@ -15,7 +15,7 @@ function layout_fits_axle_load(data)
15
15
  {
16
16
  return true
17
17
  }
18
- if (!env_has_axles(data.transport))
18
+ if (!transport_has_axles(data.transport))
19
19
  {
20
20
  return true
21
21
  }
@@ -26,10 +26,11 @@ function layout_fits_axle_load(data)
26
26
  * @param {object|undefined} transport
27
27
  * @returns {boolean}
28
28
  */
29
- function env_has_axles(transport)
29
+ function transport_has_axles(transport)
30
30
  {
31
31
  const env = transport?.get_axle_env_template?.()
32
32
  return Array.isArray(env?.eixos) && env.eixos.length > 0
33
33
  }
34
34
 
35
35
  module.exports = layout_fits_axle_load
36
+ module.exports.transport_has_axles = transport_has_axles
@@ -0,0 +1,143 @@
1
+ const { lt } = require('@jetta/number-compare')
2
+ const { get_item, remove_item_from_candidate_list } = require('./slot_candidate_order')
3
+ const { score_without_commit, commit_scored_candidate } = require('./slot_candidate_acceptance')
4
+
5
+ /**
6
+ * Legal SKU across every weight band with the lowest hottest-axle ratio.
7
+ * Ties keep band order, then candidate order inside the band.
8
+ * @param {object} data
9
+ * @param {object} context
10
+ * @returns {object|undefined}
11
+ */
12
+ function commit_lowest_ratio_across_bands(data, context)
13
+ {
14
+ const ranked = rank_every_band(data, context)
15
+ for (const selected of ranked)
16
+ {
17
+ const committed = commit_scored_candidate(data, context.container, selected)
18
+ if (committed)
19
+ {
20
+ return committed
21
+ }
22
+ }
23
+ return undefined
24
+ }
25
+
26
+ /**
27
+ * Best legal SKU across every band. Does not commit stacking.
28
+ * @param {object} data
29
+ * @param {object} context
30
+ * @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
31
+ */
32
+ function lowest_ratio_across_bands(data, context)
33
+ {
34
+ const ranked = rank_every_band(data, context)
35
+ return ranked[0]
36
+ }
37
+
38
+ /**
39
+ * Preferred SKU scored without a stacking commit.
40
+ * @param {object} data
41
+ * @param {object} context
42
+ * @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
43
+ */
44
+ function score_preferred(data, context)
45
+ {
46
+ const allocation_item = data.item_list.find(item => item.get_id() === data.previous_chosen_item_id)
47
+ if (!allocation_item)
48
+ {
49
+ return undefined
50
+ }
51
+ return score_without_commit(data, context, allocation_item)
52
+ }
53
+
54
+ /**
55
+ * @param {{ ratio: number }|undefined} preferred
56
+ * @param {{ ratio: number }|undefined} best
57
+ * @returns {boolean}
58
+ */
59
+ function preferred_wins(preferred, best)
60
+ {
61
+ if (!preferred)
62
+ {
63
+ return false
64
+ }
65
+ if (!best)
66
+ {
67
+ return true
68
+ }
69
+ return lt(best.ratio, preferred.ratio) === false
70
+ }
71
+
72
+ /**
73
+ * @param {object} data
74
+ * @param {object} context
75
+ * @returns {object[]}
76
+ */
77
+ function rank_every_band(data, context)
78
+ {
79
+ const scored = []
80
+ for (const band of context.ordered_bands)
81
+ {
82
+ scored.push(...rank_band(data, context, band.items.slice()))
83
+ }
84
+ return rank_by_ratio(scored)
85
+ }
86
+
87
+ /**
88
+ * @param {object} data
89
+ * @param {object} context
90
+ * @param {object[]} pending
91
+ * @returns {object[]}
92
+ */
93
+ function rank_band(data, context, pending)
94
+ {
95
+ const scored = []
96
+ while (pending.length > 0)
97
+ {
98
+ const allocation_item = get_item(pending, data.qty_of_slots, context.container)
99
+ const candidate = score_without_commit(data, context, allocation_item)
100
+ if (candidate)
101
+ {
102
+ scored.push(candidate)
103
+ }
104
+ remove_item_from_candidate_list(pending, allocation_item)
105
+ }
106
+ return rank_by_ratio(scored)
107
+ }
108
+
109
+ /**
110
+ * @param {{ ratio: number }[]} scored
111
+ * @returns {{ ratio: number }[]}
112
+ */
113
+ function rank_by_ratio(scored)
114
+ {
115
+ const ranked = []
116
+ for (const candidate of scored)
117
+ {
118
+ insert_by_ratio(ranked, candidate)
119
+ }
120
+ return ranked
121
+ }
122
+
123
+ /**
124
+ * @param {{ ratio: number }[]} ranked
125
+ * @param {{ ratio: number }} candidate
126
+ * @returns {void}
127
+ */
128
+ function insert_by_ratio(ranked, candidate)
129
+ {
130
+ let index = 0
131
+ while (index < ranked.length && lt(ranked[index].ratio, candidate.ratio))
132
+ {
133
+ index += 1
134
+ }
135
+ ranked.splice(index, 0, candidate)
136
+ }
137
+
138
+ module.exports = {
139
+ commit_lowest_ratio_across_bands,
140
+ lowest_ratio_across_bands,
141
+ score_preferred,
142
+ preferred_wins
143
+ }
@@ -0,0 +1,149 @@
1
+ const Cargo_Profile = require('../../models/Cargo_Profile')
2
+ const No_Suitable_Item_Exception = require('../block_builder/exceptions/No_Suitable_Item_Exception')
3
+ const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
4
+ const { distribute_items_by_weight_range } = require('./Weight_Slot_Distribution')
5
+ const {
6
+ resolve_choose_item_heuristic_context,
7
+ format_slot_label_for_exception
8
+ } = require('./Choose_Item_Heuristic_Context')
9
+ const {
10
+ calculate_total_stack_slots,
11
+ sort_stacking_slots_by_proximity
12
+ } = require('./Stack_Slot_Calculator')
13
+ const { get_item, remove_item_from_candidate_list } = require('./slot_candidate_order')
14
+ const { try_accept_candidate, commit_scored_candidate } = require('./slot_candidate_acceptance')
15
+ const {
16
+ commit_lowest_ratio_across_bands,
17
+ lowest_ratio_across_bands,
18
+ score_preferred,
19
+ preferred_wins
20
+ } = require('./select_axle_balanced_slot_item')
21
+
22
+ /**
23
+ * Chooses a slot item. When the axle gate is on, the legal SKU with the lowest hottest-axle ratio wins.
24
+ * @param {object} data
25
+ * @returns {object}
26
+ */
27
+ function select_slot_item(data)
28
+ {
29
+ const context = build_band_context(data)
30
+ if (Cargo_Profile.enable_axle_load_slot_gate())
31
+ {
32
+ const selected = commit_lowest_ratio_across_bands(data, context)
33
+ if (!selected)
34
+ {
35
+ throw unsuitable(context.slot)
36
+ }
37
+ return selected
38
+ }
39
+ for (const band of context.ordered_bands)
40
+ {
41
+ const selected = first_accepted_in_band(data, context, band.items)
42
+ if (selected)
43
+ {
44
+ return selected
45
+ }
46
+ }
47
+ throw unsuitable(context.slot)
48
+ }
49
+
50
+ /**
51
+ * Same-id preference. Under the axle gate, that SKU wins only when it is not strictly hotter.
52
+ * @param {object} data
53
+ * @returns {object}
54
+ */
55
+ function select_preferred_or_heuristic(data)
56
+ {
57
+ if (!Cargo_Profile.enable_axle_load_slot_gate())
58
+ {
59
+ return preferred_or_first(data)
60
+ }
61
+ return preferred_or_balanced(data)
62
+ }
63
+
64
+ /**
65
+ * @param {object} data
66
+ * @returns {object}
67
+ */
68
+ function preferred_or_first(data)
69
+ {
70
+ const { container, item_list, previous_chosen_item_id, item_position } = data
71
+ const allocation_item = item_list.find(item => item.get_id() === previous_chosen_item_id)
72
+ if (allocation_item)
73
+ {
74
+ const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
75
+ if (accepted_item)
76
+ {
77
+ return accepted_item
78
+ }
79
+ }
80
+ return select_slot_item(data)
81
+ }
82
+
83
+ /**
84
+ * @param {object} data
85
+ * @returns {object}
86
+ */
87
+ function preferred_or_balanced(data)
88
+ {
89
+ const context = build_band_context(data)
90
+ const preferred = score_preferred(data, context)
91
+ const best = lowest_ratio_across_bands(data, context)
92
+ const winner = preferred_wins(preferred, best) ? preferred : best
93
+ if (!winner)
94
+ {
95
+ throw unsuitable(context.slot)
96
+ }
97
+ return commit_scored_candidate(data, context.container, winner)
98
+ }
99
+
100
+ /**
101
+ * @param {object} data
102
+ * @returns {object}
103
+ */
104
+ function build_band_context(data)
105
+ {
106
+ const { slot, container, item_position } = resolve_choose_item_heuristic_context(data)
107
+ const total_slots = calculate_total_stack_slots(container.get_height(), slot)
108
+ const stacking_slots = distribute_items_by_weight_range(data.item_list, total_slots)
109
+ const ordered_bands = sort_stacking_slots_by_proximity(stacking_slots, slot.get_y(), slot.get_height())
110
+ return { slot, container, item_position, ordered_bands }
111
+ }
112
+
113
+ /**
114
+ * @param {object} data
115
+ * @param {object} context
116
+ * @param {object[]} candidate_items
117
+ * @returns {object|undefined}
118
+ */
119
+ function first_accepted_in_band(data, context, candidate_items)
120
+ {
121
+ while (candidate_items.length > 0)
122
+ {
123
+ const allocation_item = get_item(candidate_items, data.qty_of_slots, context.container)
124
+ const accepted_item = try_accept_candidate(data, context.container, allocation_item, context.item_position)
125
+ if (accepted_item)
126
+ {
127
+ return accepted_item
128
+ }
129
+ remove_item_from_candidate_list(candidate_items, allocation_item)
130
+ }
131
+ return undefined
132
+ }
133
+
134
+ /**
135
+ * @param {object} slot
136
+ * @returns {No_Suitable_Item_Exception}
137
+ */
138
+ function unsuitable(slot)
139
+ {
140
+ return new No_Suitable_Item_Exception(
141
+ ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM,
142
+ [format_slot_label_for_exception(slot)]
143
+ )
144
+ }
145
+
146
+ module.exports = {
147
+ select_slot_item,
148
+ select_preferred_or_heuristic
149
+ }
@@ -0,0 +1,110 @@
1
+ const Cargo_Profile = require('../../models/Cargo_Profile')
2
+ const Allocated_Item = require('../../models/Allocated_Item')
3
+ const candidate_item_fits_axle_load = require('./axle_load/candidate_item_fits_axle_load')
4
+ const candidate_item_axle_score = require('./axle_load/candidate_item_axle_score')
5
+
6
+ /**
7
+ * Builds the allocated item at the slot position.
8
+ * @param {object} allocation_item
9
+ * @param {object} item_position
10
+ * @returns {object}
11
+ */
12
+ function build_allocated_item(allocation_item, item_position)
13
+ {
14
+ return new Allocated_Item(allocation_item, item_position.x, item_position.y, item_position.z)
15
+ }
16
+
17
+ /**
18
+ * Accepts the first legal candidate and commits stacking.
19
+ * @param {object} data
20
+ * @param {object} container
21
+ * @param {object} allocation_item
22
+ * @param {object} item_position
23
+ * @returns {object|undefined}
24
+ */
25
+ function try_accept_candidate(data, container, allocation_item, item_position)
26
+ {
27
+ const allocated_item = build_allocated_item(allocation_item, item_position)
28
+ if (!candidate_item_fits_axle_load(data, allocated_item))
29
+ {
30
+ return undefined
31
+ }
32
+ if (should_defer_loose_on_block_stacking_validation(data.block))
33
+ {
34
+ return allocated_item
35
+ }
36
+ if (!container.is_a_valid_stacking(allocated_item))
37
+ {
38
+ return undefined
39
+ }
40
+ return allocated_item
41
+ }
42
+
43
+ /**
44
+ * Scores a candidate without writing the stacking index.
45
+ * @param {object} data
46
+ * @param {object} context
47
+ * @param {object} allocation_item
48
+ * @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
49
+ */
50
+ function score_without_commit(data, context, allocation_item)
51
+ {
52
+ const allocated_item = build_allocated_item(allocation_item, context.item_position)
53
+ const score = candidate_item_axle_score(data, allocated_item)
54
+ if (!score.fits || !stacking_probe_allows(data, context.container, allocated_item))
55
+ {
56
+ return undefined
57
+ }
58
+ return { allocated_item, ratio: score.ratio, allocation_item }
59
+ }
60
+
61
+ /**
62
+ * Commits the chosen candidate into the live stacking index.
63
+ * @param {object} data
64
+ * @param {object} container
65
+ * @param {{ allocated_item: object }} scored
66
+ * @returns {object|undefined}
67
+ */
68
+ function commit_scored_candidate(data, container, scored)
69
+ {
70
+ if (should_defer_loose_on_block_stacking_validation(data.block))
71
+ {
72
+ return scored.allocated_item
73
+ }
74
+ if (!container.is_a_valid_stacking(scored.allocated_item))
75
+ {
76
+ return undefined
77
+ }
78
+ return scored.allocated_item
79
+ }
80
+
81
+ /**
82
+ * @param {object} data
83
+ * @param {object} container
84
+ * @param {object} allocated_item
85
+ * @returns {boolean}
86
+ */
87
+ function stacking_probe_allows(data, container, allocated_item)
88
+ {
89
+ if (should_defer_loose_on_block_stacking_validation(data.block))
90
+ {
91
+ return true
92
+ }
93
+ if (typeof container.probe_stacking === 'function')
94
+ {
95
+ return container.probe_stacking(allocated_item)
96
+ }
97
+ return container.is_a_valid_stacking(allocated_item)
98
+ }
99
+
100
+ function should_defer_loose_on_block_stacking_validation(block)
101
+ {
102
+ return Cargo_Profile.stack_loose_on_tie_ratio_blocks()
103
+ && block?.get_slot_list?.().length === 1
104
+ }
105
+
106
+ module.exports = {
107
+ try_accept_candidate,
108
+ score_without_commit,
109
+ commit_scored_candidate
110
+ }
@@ -0,0 +1,49 @@
1
+ const No_Available_Items_Exception = require('./exceptions/No_Available_Items_Exception')
2
+ const No_Slots_For_Items_Exception = require('./exceptions/No_Slots_For_Items_Exception')
3
+ const { order_items_by_capacity_bias } = require('./Capacity_Bias_Item_Order')
4
+
5
+ /**
6
+ * Removes one candidate from the working list.
7
+ * @param {object[]} candidate_item_list
8
+ * @param {object} allocation_item
9
+ * @returns {void}
10
+ */
11
+ function remove_item_from_candidate_list(candidate_item_list, allocation_item)
12
+ {
13
+ const index = candidate_item_list.indexOf(allocation_item)
14
+ if (index > -1)
15
+ {
16
+ candidate_item_list.splice(index, 1)
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Next candidate in the existing capacity-bias order.
22
+ * @param {object[]} item_list
23
+ * @param {number|undefined} qty_of_slots
24
+ * @param {object} container
25
+ * @returns {object}
26
+ */
27
+ function get_item(item_list, qty_of_slots, container)
28
+ {
29
+ if (qty_of_slots === undefined)
30
+ {
31
+ throw new No_Slots_For_Items_Exception()
32
+ }
33
+ if (item_list.length === 0)
34
+ {
35
+ throw new No_Available_Items_Exception()
36
+ }
37
+ const ordered_item_list = order_items_by_capacity_bias(item_list, container)
38
+ const preferable_item_list = ordered_item_list.filter(item => item.get_amount() >= qty_of_slots)
39
+ if (preferable_item_list.length > 0)
40
+ {
41
+ return preferable_item_list[0]
42
+ }
43
+ return ordered_item_list[0]
44
+ }
45
+
46
+ module.exports = {
47
+ get_item,
48
+ remove_item_from_candidate_list
49
+ }
@@ -87,6 +87,17 @@ class Container extends Space
87
87
  return this.#stacking_feasibility_checker.validate_stackings(item_data)
88
88
  }
89
89
 
90
+ /**
91
+ * Checks stacking on a clone so the live index stays unchanged.
92
+ * @param {object} item
93
+ * @returns {boolean}
94
+ */
95
+ probe_stacking(item)
96
+ {
97
+ const probe = this.#stacking_feasibility_checker.clone()
98
+ return probe.validate_stackings(create_input(item))
99
+ }
100
+
90
101
  /**
91
102
  * Drops provisional stacking commits left by failed slot/wall attempts.
92
103
  * Restores the pre-attempt checkpoint when stacking was touched; otherwise a no-op.