@jetta/blocks-allocator 0.2.0 → 0.2.1-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.
Files changed (25) hide show
  1. package/package.json +3 -2
  2. package/src/Allocation_Attempt.js +4 -1
  3. package/src/Allocation_Optimization/Block_Allocator.js +12 -6
  4. package/src/Allocation_Optimization/block_builder/Block_Wall_Builder.js +8 -1
  5. package/src/Allocation_Optimization/block_builder/Blocks_Builder.js +3 -1
  6. package/src/Allocation_Optimization/block_builder/Slot_Item_Assigner.js +4 -2
  7. package/src/Allocation_Optimization/item_consumption_heuristic/Item_Chooser.js +25 -17
  8. package/src/Allocation_Optimization/item_consumption_heuristic/Slots_Items_Assigner.js +59 -32
  9. package/src/Allocation_Optimization/item_consumption_heuristic/assign_one_plan_slot.js +53 -0
  10. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/assert_layout_fits_axle_load.js +26 -0
  11. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/axle_load_exceeds_limit.js +51 -0
  12. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/candidate_item_fits_axle_load.js +19 -0
  13. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/clone_env_with_layout_items.js +26 -0
  14. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/evaluate_layout_axle_load.js +43 -0
  15. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/layout_fits_axle_load.js +35 -0
  16. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/map_allocated_item_to_axle_wire.js +21 -0
  17. package/src/Allocation_Optimization/tie_ratio/Same_Height_Tie_Ratio_Filter.js +28 -0
  18. package/src/Allocation_Optimization/tie_ratio/filter_out_mixed_height_tie_ratio.js +20 -0
  19. package/src/Allocation_Optimization/tie_ratio/has_stocked_same_height_tie_ratio.js +40 -0
  20. package/src/Allocation_Optimization/tie_ratio/is_tie_ratio_block.js +12 -0
  21. package/src/Allocation_Optimization/tie_ratio/max_tie_ratio_slot_count.js +24 -0
  22. package/src/Allocation_Optimization/tie_ratio/tie_ratio_block_has_shape_stock.js +113 -0
  23. package/src/models/Cargo_Profile.js +42 -0
  24. package/src/models/transport/Transport.js +12 -0
  25. package/src/space_and_block_chooser/blocks_filters/pos_selection_filters/Post_Selection_Filter_List.js +4 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetta/blocks-allocator",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-1",
4
4
  "description": "Blocks wall allocation library",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -40,13 +40,14 @@
40
40
  "author": "",
41
41
  "license": "ISC",
42
42
  "dependencies": {
43
+ "@jetta/axle-load-calculator": "^0.4.1",
43
44
  "@jetta/empty-spaces-manager": "^0.1.1-0",
44
45
  "@jetta/item-amount-controller": "0.1.0-0",
45
46
  "@jetta/logger": "^2.0.1",
46
47
  "@jetta/lp-framework": "2.1.0-6",
47
48
  "@jetta/number-compare": "^3.0.2-4",
48
49
  "@jetta/stacking-constraints": "^1.1.0",
49
- "@jetta/tie-ratio-block-creator-core": "1.2.2-3",
50
+ "@jetta/tie-ratio-block-creator-core": "1.2.2-5",
50
51
  "@jetta/z-intersected-block-creator": "^0.0.1-42",
51
52
  "@jettacargo/single-item-block-creator-core": "2.0.1-0",
52
53
  "@jettacargo/spiral-block-creator-core": "2.1.2",
@@ -22,12 +22,14 @@ class Allocation_Attempt
22
22
  #item_list
23
23
  #amount_controller
24
24
  #allocator
25
+ #transport
25
26
 
26
27
  constructor(input_state)
27
28
  {
28
29
  this.#item_list = input_state.items_to_allocate
29
30
  this.#amount_controller = new Item_Amount_Controller(input_state.items_to_allocate)
30
31
  this.#allocator = new Block_Allocator()
32
+ this.#transport = input_state.transport
31
33
  }
32
34
 
33
35
  /**
@@ -103,7 +105,8 @@ class Allocation_Attempt
103
105
  this.#item_list,
104
106
  block_amount_map,
105
107
  effective_item_list,
106
- picking_continuity_seed
108
+ picking_continuity_seed,
109
+ this.#transport
107
110
  )
108
111
  this.#update_state(container, allocation_output)
109
112
  mark_trace_outcome(TRACE_CONTEXT.BLOCK_WALL_ALLOCATED)
@@ -19,7 +19,8 @@ class Block_Allocator
19
19
  item_list,
20
20
  block_amount_map,
21
21
  effective_item_list,
22
- picking_continuity_seed
22
+ picking_continuity_seed,
23
+ transport
23
24
  )
24
25
  {
25
26
  this.#next_group_id = resolve_next_group_id(container, this.#next_group_id)
@@ -47,7 +48,8 @@ class Block_Allocator
47
48
  item_amount_map,
48
49
  container,
49
50
  picking_continuity_seed,
50
- optimized_blocks.block_instance_item_queues
51
+ optimized_blocks.block_instance_item_queues,
52
+ transport
51
53
  )
52
54
 
53
55
  if (block_builder_output.allocated_item_list.length === 0)
@@ -85,7 +87,8 @@ class Block_Allocator
85
87
  item_amount_map,
86
88
  container,
87
89
  picking_continuity_seed,
88
- block_instance_item_queues
90
+ block_instance_item_queues,
91
+ transport
89
92
  )
90
93
  {
91
94
  const block_builder_data = this.#create_blocks_builder_data(
@@ -94,7 +97,8 @@ class Block_Allocator
94
97
  item_list,
95
98
  item_amount_map,
96
99
  container,
97
- picking_continuity_seed
100
+ picking_continuity_seed,
101
+ transport
98
102
  )
99
103
  block_builder_data.block_instance_item_queues = block_instance_item_queues ?? new Map()
100
104
  const block_builder = new Block_Builder(block_builder_data)
@@ -109,7 +113,8 @@ class Block_Allocator
109
113
  item_list,
110
114
  item_amount_map,
111
115
  container,
112
- picking_continuity_seed
116
+ picking_continuity_seed,
117
+ transport
113
118
  )
114
119
  {
115
120
  return {
@@ -119,7 +124,8 @@ class Block_Allocator
119
124
  item_amount_map: item_amount_map,
120
125
  container: container,
121
126
  picking_continuity_seed: picking_continuity_seed,
122
- next_group_id: this.#next_group_id
127
+ next_group_id: this.#next_group_id,
128
+ transport: transport
123
129
  }
124
130
  }
125
131
 
@@ -12,6 +12,7 @@ const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
12
12
  const assert_tie_ratio_block_matches_orientation_below = require('./assert_tie_ratio_block_matches_orientation_below')
13
13
  const snap_elevated_block_wall_items = require('./snap_elevated_block_wall_items')
14
14
  const should_omit_block_group_id = require('./should_omit_block_group_id')
15
+ const assert_layout_fits_axle_load = require('../item_consumption_heuristic/axle_load/assert_layout_fits_axle_load')
15
16
  const allocated_items_weight_sum = (item_list) => item_list.reduce((acc, item) => acc + item.get_weight(), 0)
16
17
 
17
18
  /**
@@ -124,7 +125,8 @@ function build_a_block(chosen_block, is_single_slot, state, wall_data, pending_w
124
125
  wall_data.allocated_blocks_amount,
125
126
  state.container_supported_weight,
126
127
  item_position,
127
- state.last_chosen_item_id
128
+ state.last_chosen_item_id,
129
+ pending_wall_allocated_items
128
130
  )
129
131
  let realigned_item_list = item_in_block_list
130
132
  try
@@ -143,6 +145,11 @@ function build_a_block(chosen_block, is_single_slot, state, wall_data, pending_w
143
145
  state,
144
146
  pending_wall_allocated_items
145
147
  )
148
+ assert_layout_fits_axle_load(
149
+ wall_data,
150
+ pending_wall_allocated_items,
151
+ realigned_item_list
152
+ )
146
153
  }
147
154
  catch (error)
148
155
  {
@@ -15,11 +15,13 @@ module.exports = class Block_Builder
15
15
  container: block_builder_data.container,
16
16
  picking_continuity_seed: block_builder_data.picking_continuity_seed,
17
17
  block_instance_item_queues: block_builder_data.block_instance_item_queues ?? new Map(),
18
+ transport: block_builder_data.transport,
18
19
  heuristic: new Slots_Items_Assigner(
19
20
  block_builder_data.space_to_allocate,
20
21
  block_builder_data.item_list,
21
22
  block_builder_data.item_amount_map.copy(),
22
- block_builder_data.container
23
+ block_builder_data.container,
24
+ block_builder_data.transport
23
25
  ),
24
26
  allocated_blocks_amount: {},
25
27
  next_group_id: block_builder_data.next_group_id ?? 0
@@ -9,7 +9,8 @@ module.exports = function assign_item_to_slots(
9
9
  allocated_blocks_amount,
10
10
  container_supported_weight,
11
11
  item_position,
12
- picking_continuity_seed
12
+ picking_continuity_seed,
13
+ pending_wall_allocated_items
13
14
  )
14
15
  {
15
16
  try
@@ -18,7 +19,8 @@ module.exports = function assign_item_to_slots(
18
19
  block,
19
20
  container_supported_weight,
20
21
  item_position,
21
- picking_continuity_seed
22
+ picking_continuity_seed,
23
+ pending_wall_allocated_items
22
24
  )
23
25
  return assigned_item_list
24
26
  }
@@ -14,6 +14,7 @@ const {
14
14
  sort_stacking_slots_by_proximity
15
15
  } = require('./Stack_Slot_Calculator')
16
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')
17
18
 
18
19
  /**
19
20
  * Chooses an item from available items by distributing them into stacking slots by weight
@@ -50,10 +51,10 @@ function prefer_same_id(data)
50
51
  const allocation_item = item_list.find(item => item.get_id() === previous_chosen_item_id)
51
52
  if (allocation_item)
52
53
  {
53
- const result = is_stacking_valid(container, allocation_item, item_position, data.block)
54
- if (result.is_valid)
54
+ const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
55
+ if (accepted_item)
55
56
  {
56
- return result.allocated_item
57
+ return accepted_item
57
58
  }
58
59
  }
59
60
 
@@ -83,10 +84,10 @@ function choose_item_with_heuristic(data)
83
84
  while (candidate_items.length > 0)
84
85
  {
85
86
  const allocation_item = get_item(candidate_items, qty_of_slots, container)
86
- const result = is_stacking_valid(container, allocation_item, item_position, data.block)
87
- if (result.is_valid)
87
+ const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
88
+ if (accepted_item)
88
89
  {
89
- return result.allocated_item
90
+ return accepted_item
90
91
  }
91
92
 
92
93
  remove_item_from_candidate_list(candidate_items, allocation_item)
@@ -97,7 +98,14 @@ function choose_item_with_heuristic(data)
97
98
  throw new No_Suitable_Item_Exception(ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM, [slot_label])
98
99
  }
99
100
 
100
- function is_stacking_valid(container, allocation_item, item_position, block)
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)
101
109
  {
102
110
  const allocated_item = new Allocated_Item(
103
111
  allocation_item,
@@ -105,19 +113,19 @@ function is_stacking_valid(container, allocation_item, item_position, block)
105
113
  item_position.y,
106
114
  item_position.z
107
115
  )
108
- if (should_defer_loose_on_block_stacking_validation(block))
116
+ if (!candidate_item_fits_axle_load(data, allocated_item))
109
117
  {
110
- return {
111
- is_valid: true,
112
- allocated_item
113
- }
118
+ return undefined
114
119
  }
115
- const is_valid = container.is_a_valid_stacking(allocated_item)
116
-
117
- return {
118
- is_valid,
119
- allocated_item
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
120
127
  }
128
+ return allocated_item
121
129
  }
122
130
 
123
131
  function should_defer_loose_on_block_stacking_validation(block)
@@ -1,7 +1,7 @@
1
1
  const choose_item_for_slot = require('./Item_For_Slot_Chooser')
2
2
  const No_Suitable_Item_Exception = require('../block_builder/exceptions/No_Suitable_Item_Exception')
3
3
  const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
4
- const Allocated_Item = require('../../models/Allocated_Item')
4
+ const assign_one_plan_slot = require('./assign_one_plan_slot')
5
5
  const { block_has_sufficient_shape_stock } = require('../block_builder/block_shape_stock')
6
6
  const { Logger } = require('@jetta/logger')
7
7
  const logger = new Logger('Item Consumption Heuristic')
@@ -26,6 +26,12 @@ function process_one_slot(data)
26
26
  return { allocated_item, next_weight }
27
27
  }
28
28
 
29
+ function pending_items_for_slot(pending_wall_allocated_items, allocated_item_list, index)
30
+ {
31
+ const filled_slots = allocated_item_list.slice(0, index).filter(Boolean)
32
+ return (pending_wall_allocated_items ?? []).concat(filled_slots)
33
+ }
34
+
29
35
  /**
30
36
  * Assigns items to block slots using a consumption heuristic (slot order and stacking).
31
37
  */
@@ -35,18 +41,21 @@ class Item_Consumption_Heuristic
35
41
  #item_map = {}
36
42
  #item_amount_map
37
43
  #container
44
+ #transport
38
45
 
39
46
  /**
40
47
  * @param {object[]} item_list - Items available for allocation.
41
48
  * @param {object} item_amount_map - Tracks remaining amount per item.
42
49
  * @param {object} container - Target container.
50
+ * @param {object} [transport] - Transport holding the axle env template.
43
51
  */
44
- constructor(empty_space, item_list, item_amount_map, container)
52
+ constructor(empty_space, item_list, item_amount_map, container, transport)
45
53
  {
46
54
  this.#empty_space = empty_space
47
55
  this.#item_map = build_item_map(item_list)
48
56
  this.#item_amount_map = item_amount_map
49
57
  this.#container = container
58
+ this.#transport = transport
50
59
  }
51
60
 
52
61
  /**
@@ -55,9 +64,16 @@ class Item_Consumption_Heuristic
55
64
  * @param {number} container_supported_weight - Supported weight for the layer.
56
65
  * @param {object} item_position - Base block position { x, y, z } before per-slot offsets (from space / block placement).
57
66
  * @param {string|undefined} picking_continuity_seed - Prior SKU id to prefer on the first slot.
67
+ * @param {object[]} [pending_wall_allocated_items] - Items already chosen in this wall.
58
68
  * @returns {object[]|undefined} Allocated items, or undefined if a non-rethrown error occurred.
59
69
  */
60
- assign_items_to_slots(block, container_supported_weight, item_position, picking_continuity_seed)
70
+ assign_items_to_slots(
71
+ block,
72
+ container_supported_weight,
73
+ item_position,
74
+ picking_continuity_seed,
75
+ pending_wall_allocated_items
76
+ )
61
77
  {
62
78
  const slot_count = block.get_slot_list().length
63
79
  if (slot_count === 0)
@@ -76,7 +92,8 @@ class Item_Consumption_Heuristic
76
92
  block,
77
93
  Number(container_supported_weight),
78
94
  item_position,
79
- picking_continuity_seed
95
+ picking_continuity_seed,
96
+ pending_wall_allocated_items
80
97
  )
81
98
  }
82
99
 
@@ -86,9 +103,16 @@ class Item_Consumption_Heuristic
86
103
  * @param {string[]} slot_item_ids - Item ids per slot (same order as ordered slots).
87
104
  * @param {number} container_supported_weight
88
105
  * @param {object} item_position
106
+ * @param {object[]} [pending_wall_allocated_items]
89
107
  * @returns {object[]}
90
108
  */
91
- assign_items_from_plan(block, slot_item_ids, container_supported_weight, item_position)
109
+ assign_items_from_plan(
110
+ block,
111
+ slot_item_ids,
112
+ container_supported_weight,
113
+ item_position,
114
+ pending_wall_allocated_items
115
+ )
92
116
  {
93
117
  const slot_list = order_slots_by_y(block.get_slot_list())
94
118
  const allocated_item_list = []
@@ -97,31 +121,22 @@ class Item_Consumption_Heuristic
97
121
  {
98
122
  const slot = slot_list[index]
99
123
  const item_id = slot_item_ids[index]
100
- const allocation_item = this.#item_map[item_id]
101
- if (!allocation_item || allocation_item.get_amount() <= 0)
102
- {
103
- throw new No_Suitable_Item_Exception(
104
- ALLOCATOR_MESSAGE.LP_PLAN_ITEM_UNAVAILABLE,
105
- [String(item_id), slot.to_string()]
106
- )
107
- }
108
-
109
- const current_item_position = this.#update_item_position(item_position, slot)
110
- const allocated_item = new Allocated_Item(
111
- allocation_item,
112
- current_item_position.x,
113
- current_item_position.y,
114
- current_item_position.z
115
- )
116
-
117
- if (!this.#container.is_a_valid_stacking(allocated_item))
118
- {
119
- throw new No_Suitable_Item_Exception(
120
- ALLOCATOR_MESSAGE.LP_PLAN_ITEM_STACKING_FAILED,
121
- [String(item_id), slot.to_string()]
122
- )
123
- }
124
-
124
+ const allocated_item = assign_one_plan_slot({
125
+ item_id,
126
+ slot,
127
+ allocation_item: this.#item_map[item_id],
128
+ item_position: this.#update_item_position(item_position, slot),
129
+ container: this.#container,
130
+ axle_probe: {
131
+ transport: this.#transport,
132
+ container: this.#container,
133
+ pending_axle_items: pending_items_for_slot(
134
+ pending_wall_allocated_items,
135
+ allocated_item_list,
136
+ allocated_item_list.length
137
+ )
138
+ }
139
+ })
125
140
  this.#item_amount_map.discount_single_item(allocated_item)
126
141
  allocated_item_list.push(allocated_item)
127
142
  }
@@ -139,7 +154,13 @@ class Item_Consumption_Heuristic
139
154
  }
140
155
  }
141
156
 
142
- #assign_items_for_ordered_slots(block, initial_weight, starting_item_position, picking_continuity_seed)
157
+ #assign_items_for_ordered_slots(
158
+ block,
159
+ initial_weight,
160
+ starting_item_position,
161
+ picking_continuity_seed,
162
+ pending_wall_allocated_items
163
+ )
143
164
  {
144
165
  const slot_list = order_slots_by_y(block.get_slot_list())
145
166
  const allocated_item_list = new Array(slot_list.length)
@@ -162,7 +183,13 @@ class Item_Consumption_Heuristic
162
183
  item_amount_map: provisional_amount_map,
163
184
  previous_chosen_item_id,
164
185
  qty_of_slots,
165
- item_position: current_item_position
186
+ item_position: current_item_position,
187
+ transport: this.#transport,
188
+ pending_axle_items: pending_items_for_slot(
189
+ pending_wall_allocated_items,
190
+ allocated_item_list,
191
+ index
192
+ )
166
193
  })
167
194
  allocated_item_list[index] = result.allocated_item
168
195
  current_weight = result.next_weight
@@ -0,0 +1,53 @@
1
+ const Allocated_Item = require('../../models/Allocated_Item')
2
+ const No_Suitable_Item_Exception = require('../block_builder/exceptions/No_Suitable_Item_Exception')
3
+ const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
4
+ const candidate_item_fits_axle_load = require('./axle_load/candidate_item_fits_axle_load')
5
+
6
+ /**
7
+ * Places one LP-plan item onto a slot, validating stacking and axle load.
8
+ * @param {object} data
9
+ * @returns {object}
10
+ */
11
+ function assign_one_plan_slot(data)
12
+ {
13
+ const allocated_item = create_plan_allocated_item(data)
14
+ if (!data.container.is_a_valid_stacking(allocated_item))
15
+ {
16
+ throw new No_Suitable_Item_Exception(
17
+ ALLOCATOR_MESSAGE.LP_PLAN_ITEM_STACKING_FAILED,
18
+ [String(data.item_id), data.slot.to_string()]
19
+ )
20
+ }
21
+ if (!candidate_item_fits_axle_load(data.axle_probe, allocated_item))
22
+ {
23
+ throw new No_Suitable_Item_Exception(
24
+ ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM,
25
+ [data.slot.to_string()]
26
+ )
27
+ }
28
+ return allocated_item
29
+ }
30
+
31
+ /**
32
+ * @param {object} data
33
+ * @returns {object}
34
+ */
35
+ function create_plan_allocated_item(data)
36
+ {
37
+ const allocation_item = data.allocation_item
38
+ if (!allocation_item || allocation_item.get_amount() <= 0)
39
+ {
40
+ throw new No_Suitable_Item_Exception(
41
+ ALLOCATOR_MESSAGE.LP_PLAN_ITEM_UNAVAILABLE,
42
+ [String(data.item_id), data.slot.to_string()]
43
+ )
44
+ }
45
+ return new Allocated_Item(
46
+ allocation_item,
47
+ data.item_position.x,
48
+ data.item_position.y,
49
+ data.item_position.z
50
+ )
51
+ }
52
+
53
+ module.exports = assign_one_plan_slot
@@ -0,0 +1,26 @@
1
+ const No_Suitable_Item_Exception = require('../../block_builder/exceptions/No_Suitable_Item_Exception')
2
+ const { ALLOCATOR_MESSAGE } = require('../../../messages/Allocator_Message')
3
+ const layout_fits_axle_load = require('./layout_fits_axle_load')
4
+
5
+ /**
6
+ * Throws when the realigned block items would overload axles.
7
+ * @param {object} wall_data
8
+ * @param {object[]} pending_wall_allocated_items
9
+ * @param {object[]} realigned_item_list
10
+ * @returns {void}
11
+ */
12
+ function assert_layout_fits_axle_load(wall_data, pending_wall_allocated_items, realigned_item_list)
13
+ {
14
+ const extra_items = (pending_wall_allocated_items ?? []).concat(realigned_item_list)
15
+ const fits = layout_fits_axle_load({
16
+ transport: wall_data.transport,
17
+ container: wall_data.container,
18
+ extra_items
19
+ })
20
+ if (!fits)
21
+ {
22
+ throw new No_Suitable_Item_Exception(ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM)
23
+ }
24
+ }
25
+
26
+ module.exports = assert_layout_fits_axle_load
@@ -0,0 +1,51 @@
1
+ const { gt } = require('@jetta/number-compare')
2
+
3
+ /**
4
+ * Whether any container axle row exceeds the folga-adjusted final max.
5
+ * @param {object} env - Calculator output env with containers[].pesoEixos.
6
+ * @param {number} folga_percent - Slack percent (10 means 110% of pesoMaxFinal).
7
+ * @returns {boolean}
8
+ */
9
+ function axle_load_exceeds_limit(env, folga_percent)
10
+ {
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
20
+ }
21
+
22
+ /**
23
+ * @param {object} env
24
+ * @returns {object[]}
25
+ */
26
+ function collect_peso_eixos_rows(env)
27
+ {
28
+ const containers = env.containers ?? []
29
+ const rows = []
30
+ for (const container of containers)
31
+ {
32
+ const axle_list = container.pesoEixos ?? []
33
+ rows.push(...axle_list)
34
+ }
35
+ return rows
36
+ }
37
+
38
+ /**
39
+ * @param {object} axle
40
+ * @param {number} folga_percent
41
+ * @returns {boolean}
42
+ */
43
+ function axle_row_exceeds_limit(axle, folga_percent)
44
+ {
45
+ const loaded_kilograms = Number(axle.pesoCarregadoFinal ?? 0)
46
+ const max_kilograms = Number(axle.pesoMaxFinal ?? 0)
47
+ const limit_kilograms = max_kilograms * (1 + folga_percent / 100)
48
+ return gt(loaded_kilograms, limit_kilograms)
49
+ }
50
+
51
+ module.exports = axle_load_exceeds_limit
@@ -0,0 +1,19 @@
1
+ const layout_fits_axle_load = require('./layout_fits_axle_load')
2
+
3
+ /**
4
+ * Whether placing this allocated item on top of pending axle items stays within axle caps.
5
+ * @param {object} data - Chooser bag with transport, container, pending_axle_items.
6
+ * @param {object} allocated_item
7
+ * @returns {boolean}
8
+ */
9
+ function candidate_item_fits_axle_load(data, allocated_item)
10
+ {
11
+ const pending_axle_items = data.pending_axle_items ?? []
12
+ return layout_fits_axle_load({
13
+ transport: data.transport,
14
+ container: data.container,
15
+ extra_items: pending_axle_items.concat(allocated_item)
16
+ })
17
+ }
18
+
19
+ module.exports = candidate_item_fits_axle_load
@@ -0,0 +1,26 @@
1
+ const clone = require('../../../tools/Clone')
2
+ const map_allocated_item_to_axle_wire = require('./map_allocated_item_to_axle_wire')
3
+
4
+ /**
5
+ * Deep-clones the axle env template and writes current plus extra items per container.
6
+ * @param {object} transport
7
+ * @param {object} current_container
8
+ * @param {object[]} extra_items - Pending wall, earlier slots, and candidate for current container.
9
+ * @returns {object}
10
+ */
11
+ function clone_env_with_layout_items(transport, current_container, extra_items)
12
+ {
13
+ const env = clone(transport.get_axle_env_template())
14
+ const extra_item_list = extra_items ?? []
15
+ for (const container_data of env.containers ?? [])
16
+ {
17
+ const sequence = Number(container_data.sequencia)
18
+ const live_container = transport.get_containers().get(sequence)
19
+ const committed_items = live_container?.get_allocated_items?.() ?? []
20
+ const pending_items = live_container === current_container ? extra_item_list : []
21
+ container_data.items = committed_items.concat(pending_items).map(map_allocated_item_to_axle_wire)
22
+ }
23
+ return env
24
+ }
25
+
26
+ module.exports = clone_env_with_layout_items
@@ -0,0 +1,43 @@
1
+ const { calculate_weight_on_axles_physics } = require('@jetta/axle-load-calculator')
2
+ const { Logger } = require('@jetta/logger')
3
+ const Cargo_Profile = require('../../../models/Cargo_Profile')
4
+ const clone_env_with_layout_items = require('./clone_env_with_layout_items')
5
+ const axle_load_exceeds_limit = require('./axle_load_exceeds_limit')
6
+
7
+ const logger = new Logger('Axle Load Slot Gate')
8
+
9
+ /**
10
+ * Runs axle physics for the current layout plus extra items on the active container.
11
+ * @param {object} data
12
+ * @param {object} data.transport
13
+ * @param {object} data.container
14
+ * @param {object[]} data.extra_items
15
+ * @returns {boolean} True when no axle exceeds the folga-adjusted cap.
16
+ */
17
+ function evaluate_layout_axle_load(data)
18
+ {
19
+ const env = clone_env_with_layout_items(data.transport, data.container, data.extra_items)
20
+ try
21
+ {
22
+ return layout_is_within_axle_limit(env)
23
+ }
24
+ catch (error)
25
+ {
26
+ logger.warn(`axle calculator rejected layout: ${error.message}`)
27
+ return false
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {object} env
33
+ * @returns {boolean}
34
+ */
35
+ function layout_is_within_axle_limit(env)
36
+ {
37
+ const output = calculate_weight_on_axles_physics({ envs: [env] })
38
+ const calculated_env = output.envs[0]
39
+ const folga_percent = Cargo_Profile.axle_weight_gap_percent()
40
+ return axle_load_exceeds_limit(calculated_env, folga_percent) === false
41
+ }
42
+
43
+ module.exports = evaluate_layout_axle_load
@@ -0,0 +1,35 @@
1
+ const Cargo_Profile = require('../../../models/Cargo_Profile')
2
+ const evaluate_layout_axle_load = require('./evaluate_layout_axle_load')
3
+
4
+ /**
5
+ * Returns whether the layout may be committed under the optional axle slot gate.
6
+ * @param {object} data
7
+ * @param {object} [data.transport]
8
+ * @param {object} [data.container]
9
+ * @param {object[]} [data.extra_items]
10
+ * @returns {boolean}
11
+ */
12
+ function layout_fits_axle_load(data)
13
+ {
14
+ if (!Cargo_Profile.enable_axle_load_slot_gate())
15
+ {
16
+ return true
17
+ }
18
+ if (!env_has_axles(data.transport))
19
+ {
20
+ return true
21
+ }
22
+ return evaluate_layout_axle_load(data)
23
+ }
24
+
25
+ /**
26
+ * @param {object|undefined} transport
27
+ * @returns {boolean}
28
+ */
29
+ function env_has_axles(transport)
30
+ {
31
+ const env = transport?.get_axle_env_template?.()
32
+ return Array.isArray(env?.eixos) && env.eixos.length > 0
33
+ }
34
+
35
+ module.exports = layout_fits_axle_load
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Maps an allocated item to the ms-faixas axle-calculator item contract.
3
+ * Longitudinal chassis position is JSON `y` (allocator `z`).
4
+ * @param {object} allocated_item
5
+ * @returns {object}
6
+ */
7
+ function map_allocated_item_to_axle_wire(allocated_item)
8
+ {
9
+ return {
10
+ id: allocated_item.get_id(),
11
+ x: allocated_item.get_x(),
12
+ y: allocated_item.get_z(),
13
+ z: allocated_item.get_y(),
14
+ peso: allocated_item.get_weight(),
15
+ comprimento: allocated_item.get_length(),
16
+ largura: allocated_item.get_width(),
17
+ altura: allocated_item.get_height()
18
+ }
19
+ }
20
+
21
+ module.exports = map_allocated_item_to_axle_wire
@@ -0,0 +1,28 @@
1
+ const Block_Filter_Interface = require('../../space_and_block_chooser/blocks_filters/Block_Filter_Interface')
2
+ const Cargo_Profile = require('../../models/Cargo_Profile')
3
+ const filter_out_mixed_height_tie_ratio = require('./filter_out_mixed_height_tie_ratio')
4
+ const has_stocked_same_height_tie_ratio = require('./has_stocked_same_height_tie_ratio')
5
+
6
+ /**
7
+ * Post-selection filter: prefer same-height tie-ratio over mixed-height when stock allows.
8
+ */
9
+ module.exports = class Same_Height_Tie_Ratio_Filter extends Block_Filter_Interface
10
+ {
11
+ static run(filter_data)
12
+ {
13
+ if (!Cargo_Profile.prefer_same_height_tie_ratio_slots())
14
+ {
15
+ return filter_data.block_list
16
+ }
17
+
18
+ if (!has_stocked_same_height_tie_ratio(
19
+ filter_data.block_list,
20
+ filter_data.item_list || []
21
+ ))
22
+ {
23
+ return filter_data.block_list
24
+ }
25
+
26
+ return filter_out_mixed_height_tie_ratio(filter_data.block_list)
27
+ }
28
+ }
@@ -0,0 +1,20 @@
1
+ const is_tie_ratio_block = require('./is_tie_ratio_block')
2
+
3
+ /**
4
+ * Drops mixed-height tie-ratio blocks; keeps same-height tie-ratio and non-tie-ratio.
5
+ * @param {Array<object>} block_list
6
+ * @returns {Array<object>}
7
+ */
8
+ function filter_out_mixed_height_tie_ratio(block_list)
9
+ {
10
+ return block_list.filter((block) =>
11
+ {
12
+ if (!is_tie_ratio_block(block))
13
+ {
14
+ return true
15
+ }
16
+ return block.is_slots_of_same_measure()
17
+ })
18
+ }
19
+
20
+ module.exports = filter_out_mixed_height_tie_ratio
@@ -0,0 +1,40 @@
1
+ const is_tie_ratio_block = require('./is_tie_ratio_block')
2
+ const max_tie_ratio_slot_count = require('./max_tie_ratio_slot_count')
3
+ const tie_ratio_block_has_shape_stock = require('./tie_ratio_block_has_shape_stock')
4
+
5
+ /**
6
+ * True when a same-height tie-ratio at the max slot count has enough shape stock.
7
+ * Smaller same-height partials must not hide larger mixed walls.
8
+ * @param {Array<object>} block_list
9
+ * @param {Array<object>} item_list
10
+ * @returns {boolean}
11
+ */
12
+ function has_stocked_same_height_tie_ratio(block_list, item_list)
13
+ {
14
+ const target_slot_count = max_tie_ratio_slot_count(block_list)
15
+
16
+ if (target_slot_count === 0)
17
+ {
18
+ return false
19
+ }
20
+
21
+ for (const block of block_list)
22
+ {
23
+ if (!is_tie_ratio_block(block) || !block.is_slots_of_same_measure())
24
+ {
25
+ continue
26
+ }
27
+ if (block.get_slot_list().length !== target_slot_count)
28
+ {
29
+ continue
30
+ }
31
+ if (tie_ratio_block_has_shape_stock(block, item_list))
32
+ {
33
+ return true
34
+ }
35
+ }
36
+
37
+ return false
38
+ }
39
+
40
+ module.exports = has_stocked_same_height_tie_ratio
@@ -0,0 +1,12 @@
1
+ /**
2
+ * True when the block reports itself as a tie-ratio block.
3
+ * Probe objects without the method are not tie-ratio blocks.
4
+ * @param {object} block
5
+ * @returns {boolean}
6
+ */
7
+ function is_tie_ratio_block(block)
8
+ {
9
+ return typeof block.is_tie_ratio_block === 'function' && block.is_tie_ratio_block()
10
+ }
11
+
12
+ module.exports = is_tie_ratio_block
@@ -0,0 +1,24 @@
1
+ const is_tie_ratio_block = require('./is_tie_ratio_block')
2
+
3
+ /**
4
+ * Returns the largest slot count among tie-ratio blocks in the list.
5
+ * @param {Array<object>} block_list
6
+ * @returns {number}
7
+ */
8
+ function max_tie_ratio_slot_count(block_list)
9
+ {
10
+ let maximum = 0
11
+
12
+ for (const block of block_list)
13
+ {
14
+ if (!is_tie_ratio_block(block))
15
+ {
16
+ continue
17
+ }
18
+ maximum = Math.max(maximum, block.get_slot_list().length)
19
+ }
20
+
21
+ return maximum
22
+ }
23
+
24
+ module.exports = max_tie_ratio_slot_count
@@ -0,0 +1,113 @@
1
+ const { cube_map_function } = require('../../Blocks_Preparation/Cube_Map_Function')
2
+
3
+ /**
4
+ * Builds required shape amounts from a block's slots.
5
+ * @param {object} block
6
+ * @returns {Map<string, number>}
7
+ */
8
+ function build_required_shapes(block)
9
+ {
10
+ const required_shapes = new Map()
11
+ for (const slot of block.get_slot_list())
12
+ {
13
+ const shape_hash = cube_map_function(slot)
14
+ required_shapes.set(shape_hash, (required_shapes.get(shape_hash) || 0) + 1)
15
+ }
16
+ return required_shapes
17
+ }
18
+
19
+ /**
20
+ * Builds available shape amounts (amount counted once per SKU per unique rotation hash).
21
+ * @param {Array<object>} item_list
22
+ * @returns {Map<string, number>}
23
+ */
24
+ function build_available_shapes(item_list)
25
+ {
26
+ const available_shapes = new Map()
27
+ for (const item of item_list)
28
+ {
29
+ const unique_shape_hashes = new Set()
30
+ for (const rotated_item of item.get_all_rotated_options())
31
+ {
32
+ unique_shape_hashes.add(cube_map_function(rotated_item))
33
+ }
34
+ const item_amount = item.get_amount()
35
+ for (const shape_hash of unique_shape_hashes)
36
+ {
37
+ available_shapes.set(
38
+ shape_hash,
39
+ (available_shapes.get(shape_hash) || 0) + item_amount
40
+ )
41
+ }
42
+ }
43
+ return available_shapes
44
+ }
45
+
46
+ /**
47
+ * Counts units that can form any required shape (once per SKU).
48
+ * @param {Map<string, number>} required_shapes
49
+ * @param {Array<object>} item_list
50
+ * @returns {number}
51
+ */
52
+ function count_capable_item_amount(required_shapes, item_list)
53
+ {
54
+ let capable_amount = 0
55
+
56
+ for (const item of item_list)
57
+ {
58
+ const amount = item.get_amount()
59
+ if (amount <= 0)
60
+ {
61
+ continue
62
+ }
63
+ const can_form = item.get_all_rotated_options().some((rotated_item) =>
64
+ required_shapes.has(cube_map_function(rotated_item)))
65
+ if (can_form)
66
+ {
67
+ capable_amount += amount
68
+ }
69
+ }
70
+
71
+ return capable_amount
72
+ }
73
+
74
+ /**
75
+ * True when remaining item_list stock can fill the block.
76
+ * Combines total capable units with per-shape availability so rotation
77
+ * double-counting cannot keep a stale same-height five-slot alive.
78
+ * @param {object} block
79
+ * @param {Array<object>} item_list
80
+ * @returns {boolean}
81
+ */
82
+ function tie_ratio_block_has_shape_stock(block, item_list)
83
+ {
84
+ const required_shapes = build_required_shapes(block)
85
+ if (required_shapes.size === 0)
86
+ {
87
+ return false
88
+ }
89
+
90
+ let total_required = 0
91
+ for (const required_amount of required_shapes.values())
92
+ {
93
+ total_required += required_amount
94
+ }
95
+
96
+ if (count_capable_item_amount(required_shapes, item_list) < total_required)
97
+ {
98
+ return false
99
+ }
100
+
101
+ const available_shapes = build_available_shapes(item_list)
102
+ for (const [shape_hash, required_amount] of required_shapes)
103
+ {
104
+ if ((available_shapes.get(shape_hash) || 0) < required_amount)
105
+ {
106
+ return false
107
+ }
108
+ }
109
+
110
+ return true
111
+ }
112
+
113
+ module.exports = tie_ratio_block_has_shape_stock
@@ -81,6 +81,33 @@ class Cargo_Profile
81
81
  return str_to_bool(this.#instance['allocation.blocks.enable_item_continuation_lock'], true)
82
82
  }
83
83
 
84
+ /**
85
+ * Whether slot item choice rejects SKUs that would overload axles.
86
+ * Defaults to false when omitted or when no cargo profile is loaded.
87
+ * @returns {boolean}
88
+ */
89
+ static enable_axle_load_slot_gate()
90
+ {
91
+ if (Cargo_Profile.#instance === null)
92
+ {
93
+ return false
94
+ }
95
+ return str_to_bool(this.#instance['allocation.blocks.enable_axle_load_slot_gate'], false)
96
+ }
97
+
98
+ /**
99
+ * Axle slack percent applied as pesoMaxFinal * (1 + percent / 100).
100
+ * @returns {number}
101
+ */
102
+ static axle_weight_gap_percent()
103
+ {
104
+ if (Cargo_Profile.#instance === null)
105
+ {
106
+ return 0
107
+ }
108
+ return finite_float_from_string_or_number(this.#instance.folga_de_peso_nos_eixos, 0)
109
+ }
110
+
84
111
  static enable_key_item_mapping()
85
112
  {
86
113
  return str_to_bool(this.#instance['allocation.blocks.preferential.enable'], true)
@@ -362,6 +389,21 @@ class Cargo_Profile
362
389
  return str_to_bool(this.#instance[key], false)
363
390
  }
364
391
 
392
+ /**
393
+ * Prefer same-height tie-ratio blocks over mixed-height when both fit.
394
+ * Default true when omitted.
395
+ * @returns {boolean}
396
+ */
397
+ static prefer_same_height_tie_ratio_slots()
398
+ {
399
+ if (Cargo_Profile.#instance === null)
400
+ {
401
+ return true
402
+ }
403
+ const key = 'allocation.blocks.prefer_same_height_tie_ratio_slots'
404
+ return str_to_bool(this.#instance[key], true)
405
+ }
406
+
365
407
  static tie_ratio_allocate_remainder_as_partial_blocks()
366
408
  {
367
409
  if (Cargo_Profile.#instance === null)
@@ -1,4 +1,5 @@
1
1
  const create_container_map = require('./factory/Container_Factory')
2
+ const clone = require('../../tools/Clone')
2
3
 
3
4
  /**
4
5
  * In-memory representation of one transport environment: a map of sequence → container model.
@@ -6,6 +7,7 @@ const create_container_map = require('./factory/Container_Factory')
6
7
  class Transport
7
8
  {
8
9
  #containers = new Map()
10
+ #axle_env_template
9
11
  /**
10
12
  * @param {object} transport_data - First element shape from `input_data.envs` (includes `containers` array).
11
13
  * @param {object|undefined} empty_space_finder_options - Optional merge tolerance for each {@link Container} finder.
@@ -13,6 +15,16 @@ class Transport
13
15
  constructor(data)
14
16
  {
15
17
  this.#containers = create_container_map(data)
18
+ this.#axle_env_template = clone(data.transport_data)
19
+ }
20
+
21
+ /**
22
+ * Frozen-at-construction clone of the env JSON used by axle-load physics.
23
+ * @returns {object}
24
+ */
25
+ get_axle_env_template()
26
+ {
27
+ return this.#axle_env_template
16
28
  }
17
29
 
18
30
  /**
@@ -1,9 +1,11 @@
1
1
  const Blocks_Bigger_Than_Space_Filter = require('./Blocks_Bigger_Than_Space_Filter')
2
2
  const Tie_Ratio_Orientation_Filter = require('../../../Allocation_Optimization/tie_ratio/Tie_Ratio_Orientation_Filter')
3
3
  const Tie_Ratio_Vertical_Stack_Filter = require('../../../Allocation_Optimization/tie_ratio/Tie_Ratio_Vertical_Stack_Filter')
4
+ const Same_Height_Tie_Ratio_Filter = require('../../../Allocation_Optimization/tie_ratio/Same_Height_Tie_Ratio_Filter')
4
5
 
5
6
  module.exports = [
6
7
  Blocks_Bigger_Than_Space_Filter,
7
8
  Tie_Ratio_Orientation_Filter,
8
- Tie_Ratio_Vertical_Stack_Filter
9
- ]
9
+ Tie_Ratio_Vertical_Stack_Filter,
10
+ Same_Height_Tie_Ratio_Filter
11
+ ]