forked from viewit/KX-Bridge-Release
fix(ams): crash in _build_auto_ams_box_mapping for plain/no-dialog prints
This commit is contained in:
@@ -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 80–100 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,
|
||||
|
||||
Reference in New Issue
Block a user