fix(ams): crash in _build_auto_ams_box_mapping for plain/no-dialog prints

This commit is contained in:
Pavulon87
2026-07-14 13:37:28 +02:00
committed by Your Name
parent 9c7b7c5116
commit be12f342c2

View File

@@ -1747,7 +1747,41 @@ class KobraXBridge:
return [int(color[0]), int(color[1]), int(color[2]), 255]
return [255, 255, 255, 255]
def _build_auto_ams_box_mapping(
@staticmethod
def _hex_to_rgba(hex_color: str | None) -> list[int]:
"""Convert a '#RRGGBB' (or 'RRGGBB') string from GCode metadata to [r,g,b,255]."""
if not hex_color:
return [255, 255, 255, 255]
h = str(hex_color).strip().lstrip("#")
if len(h) < 6:
return [255, 255, 255, 255]
try:
return [int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), 255]
except ValueError:
return [255, 255, 255, 255]
@staticmethod
def _gcode_filament_by_index(gcode_filaments: list[dict] | None) -> dict[int, dict]:
"""Index parsed GCode filament metadata (_extract_filament_info) by paint/slot index."""
by_index: dict[int, dict] = {}
if not gcode_filaments:
return by_index
for f in gcode_filaments:
try:
by_index[int(f.get("slot_index"))] = f
except (TypeError, ValueError):
continue
return by_index
def _color_distance(self, rgb1: list[int], rgb2: list[int]) -> float:
"""Calculate RGB Euclidean distance."""
return (
(rgb1[0] - rgb2[0]) ** 2 +
(rgb1[1] - rgb2[1]) ** 2 +
(rgb1[2] - rgb2[2]) ** 2
) ** 0.5
def _build_auto_ams_box_mapping_old(
self,
warn_on_empty_default: bool = False,
loaded_slots: list[tuple[int, dict]] | None = None,
@@ -1783,6 +1817,128 @@ class KobraXBridge:
})
return result
def _build_auto_ams_box_mapping(
self,
warn_on_empty_default: bool = False,
loaded_slots: list[tuple[int, dict]] | None = None,
gcode_filaments: list[dict] | None = None,
) -> list[dict]:
"""Build print mapping by matching G-code filament colors
with currently loaded AMS slots using RGB distance.
"""
# IMPORTANT: loaded_slots is intentionally ignored whenever gcode_filaments
# is provided. Color matching needs access to ALL currently loaded slots,
# not just the ones whose index happens to coincide with the gcode's
# paint_index - otherwise a black filament physically loaded in slot 3
# could never be matched to gcode paint_index 1 just because slot 1
# holds a different color. loaded_slots is only used as a fallback pool
# when the caller has no gcode filament info to match colors against
# (see _build_auto_ams_box_mapping_old for that plain, index-based case).
loaded = loaded_slots if ( gcode_filaments is None or len(gcode_filaments) == 0 ) else None
if loaded is None:
loaded = self._select_loaded_slots_for_print( warn_on_empty_default=warn_on_empty_default )
if not loaded:
return []
loaded_map = {gidx: s for gidx, s in loaded}
log.debug(f"Loaded map: {loaded_map}")
gcode_by_index = self._gcode_filament_by_index(gcode_filaments)
log.debug(f"GCode filaments by index: {gcode_by_index}")
# Slots available for assignment.
available_slots = list(loaded_map.items())
result = []
max_idx = max(gcode_by_index.keys(), default=-1)
# Maximum RGB distance:
# sqrt(255² + 255² + 255²) = 441
# A threshold of 80100 is reasonable for visually similar colors.
MAX_COLOR_DISTANCE = 100
for paint_index in range(max_idx + 1):
req = gcode_by_index.get(paint_index)
paint_color = (
self._hex_to_rgba(req.get("color_hex"))
if req else
[255, 255, 255, 255]
)
default_material = (
req.get("material", "PLA")
if req else
"PLA"
)
best_match = None
best_distance = float("inf")
if req:
requested_rgb = paint_color[:3]
for slot_idx, slot in available_slots:
slot_color = slot.get("color")
if not slot_color or len(slot_color) < 3:
continue
# Skip slots with a different material type.
if slot.get("type") != default_material:
continue
distance = self._color_distance(
requested_rgb,
slot_color[:3],
)
if distance < best_distance:
best_distance = distance
best_match = (slot_idx, slot)
if best_match and best_distance <= MAX_COLOR_DISTANCE:
slot_idx, slot = best_match
available_slots.remove(best_match)
log.debug(
f"Matched paint index {paint_index}: "
f"GCODE={paint_color[:3]} "
f"AMS={slot.get('color')} "
f"distance={best_distance:.2f} "
f"slot={slot_idx}"
)
result.append({
"paint_index": paint_index,
"ams_index": self._slot_to_print_ams_index(slot_idx),
"paint_color": paint_color,
"ams_color": self._slot_color_rgba(slot),
"material_type": slot.get("type", default_material),
})
else:
log.debug(
f"No AMS match for paint index {paint_index}: "
f"color={paint_color[:3]} "
f"best_distance={best_distance:.2f}"
)
result.append({
"paint_index": paint_index,
"ams_index": self._slot_to_print_ams_index(paint_index),
"paint_color": paint_color,
"ams_color": [255, 255, 255, 255],
"material_type": default_material,
})
return result
def _build_assigned_ams_box_mapping(self, assignments: list) -> tuple[list[dict], int, int]:
"""Build print mapping from UI filament assignments.
@@ -3170,7 +3326,7 @@ class KobraXBridge:
return self._json_cors({"error": "no usable filament assignments for current filament mode"}, status=400)
else:
# No dialog -> all occupied slots as with a normal upload print
ams_box_mapping = self._build_auto_ams_box_mapping()
ams_box_mapping = self._build_auto_ams_box_mapping_old()
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
filename = gcode_file["filename"]
@@ -3708,7 +3864,12 @@ class KobraXBridge:
# used slots; used-but-unloaded -> a warning may follow later.
loaded = [(gidx, s) for (gidx, s) in loaded if gidx in used_paint_indices]
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded)
used_gcode_filaments = [
f for f in (gcode_filaments or [])
if used_paint_indices is not None and f.get("slot_index") in used_paint_indices
]
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded, gcode_filaments=used_gcode_filaments)
log.debug(f"AMS slots: {len(loaded)} mapped (used paints: {used_paint_indices}) -> {[i for i, _ in loaded]}")
payload = self._build_print_payload(
filename, url, md5, filesize,