mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Fix multi-plate 3MF metadata extraction (Issue #92)
When exporting individual plates from a multi-plate 3MF in Bambu Studio, all uploaded archives showed plate 1's name and thumbnail regardless of which plate was actually exported. Root cause: The 3MF parser used an incorrect XPath lookup (plate_idx attribute) and didn't extract the plate index from slice_info.config metadata. Changes: - Extract plate index from <metadata key="index" value="N"/> in slice_info.config - Remove incorrect plate[@plate_idx='N'] XPath lookup that doesn't work for single-plate exports - Set self.plate_number from extracted index so _extract_thumbnail() uses the correct plate thumbnail (e.g., plate_5.png instead of plate_1.png) - Append " - Plate N" to print_name when plate index > 1 to distinguish multi-plate exports - Add 7 unit tests for plate index extraction and print_name enhancement
This commit is contained in:
@@ -219,7 +219,9 @@ async def list_ha_entities(db: AsyncSession = Depends(get_db)):
|
||||
ha_token = await get_setting(db, "ha_token") or ""
|
||||
|
||||
if not ha_url or not ha_token:
|
||||
raise HTTPException(400, "Home Assistant not configured. Please set HA URL and token in Settings.")
|
||||
raise HTTPException(
|
||||
400, "Home Assistant not configured. Please set HA URL and token in Settings → Network → Home Assistant."
|
||||
)
|
||||
|
||||
entities = await homeassistant_service.list_entities(ha_url, ha_token)
|
||||
return [HAEntity(**e) for e in entities]
|
||||
|
||||
@@ -31,11 +31,19 @@ class ThreeMFParser:
|
||||
"""Extract metadata from 3MF file."""
|
||||
try:
|
||||
with zipfile.ZipFile(self.file_path, "r") as zf:
|
||||
self._parse_slice_info(zf)
|
||||
self._parse_slice_info(zf) # Now sets self.plate_number from slice_info
|
||||
self._parse_project_settings(zf)
|
||||
self._parse_gcode_header(zf)
|
||||
self._parse_3dmodel(zf)
|
||||
self._extract_thumbnail(zf)
|
||||
self._extract_thumbnail(zf) # Uses correct plate_number for thumbnail
|
||||
|
||||
# Enhance print_name with plate info if this is a multi-plate export
|
||||
plate_index = self.metadata.get("_plate_index")
|
||||
if plate_index and plate_index > 1:
|
||||
# Append plate number to distinguish from other plates
|
||||
existing_name = self.metadata.get("print_name", "")
|
||||
if existing_name and f"Plate {plate_index}" not in existing_name:
|
||||
self.metadata["print_name"] = f"{existing_name} - Plate {plate_index}"
|
||||
|
||||
# ALWAYS prefer slice_info values - they contain ONLY filaments actually used in print
|
||||
# project_settings contains ALL configured filaments (AMS slots), not just used ones
|
||||
@@ -47,6 +55,7 @@ class ThreeMFParser:
|
||||
# Clean up internal keys
|
||||
self.metadata.pop("_slice_filament_type", None)
|
||||
self.metadata.pop("_slice_filament_color", None)
|
||||
self.metadata.pop("_plate_index", None)
|
||||
except Exception:
|
||||
pass
|
||||
return self.metadata
|
||||
@@ -58,21 +67,26 @@ class ThreeMFParser:
|
||||
content = zf.read("Metadata/slice_info.config").decode()
|
||||
root = ET.fromstring(content)
|
||||
|
||||
# Get the correct plate's metadata (use plate_number if specified)
|
||||
if self.plate_number:
|
||||
plate = root.find(f".//plate[@plate_idx='{self.plate_number}']")
|
||||
if plate is None:
|
||||
# Fallback to first plate if specific plate not found
|
||||
plate = root.find(".//plate")
|
||||
else:
|
||||
plate = root.find(".//plate")
|
||||
# Find the plate element (single-plate exports only have one plate)
|
||||
plate = root.find(".//plate")
|
||||
|
||||
if plate is not None:
|
||||
# Get prediction and weight from metadata elements
|
||||
# Extract metadata from plate element
|
||||
for meta in plate.findall("metadata"):
|
||||
key = meta.get("key")
|
||||
value = meta.get("value")
|
||||
if key == "prediction" and value:
|
||||
if key == "index" and value:
|
||||
# Extract plate index - this tells us which plate was exported
|
||||
try:
|
||||
extracted_index = int(value)
|
||||
# Set plate_number if not already set from filename
|
||||
if not self.plate_number:
|
||||
self.plate_number = extracted_index
|
||||
# Store in metadata for print_name generation
|
||||
self.metadata["_plate_index"] = extracted_index
|
||||
except ValueError:
|
||||
pass
|
||||
elif key == "prediction" and value:
|
||||
self.metadata["print_time_seconds"] = int(value)
|
||||
elif key == "weight" and value:
|
||||
self.metadata["filament_used_grams"] = float(value)
|
||||
|
||||
@@ -292,3 +292,137 @@ class TestPrintableObjectsExtraction:
|
||||
count += 1
|
||||
|
||||
assert count == 0 # All objects skipped
|
||||
|
||||
|
||||
class TestThreeMFPlateIndexExtraction:
|
||||
"""Tests for extracting plate index from multi-plate 3MF exports (Issue #92)."""
|
||||
|
||||
def test_extract_plate_index_from_slice_info(self):
|
||||
"""Test parsing plate index from slice_info.config metadata."""
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
# Single-plate export from plate 5 of a multi-plate project
|
||||
slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config>
|
||||
<plate>
|
||||
<metadata key="index" value="5" />
|
||||
<metadata key="prediction" value="3600" />
|
||||
<metadata key="weight" value="50.5" />
|
||||
<object identify_id="1" name="Part_A" skipped="false" />
|
||||
</plate>
|
||||
</config>
|
||||
"""
|
||||
root = ET.fromstring(slice_info_xml)
|
||||
plate = root.find(".//plate")
|
||||
|
||||
plate_index = None
|
||||
for meta in plate.findall("metadata"):
|
||||
if meta.get("key") == "index":
|
||||
plate_index = int(meta.get("value"))
|
||||
break
|
||||
|
||||
assert plate_index == 5
|
||||
|
||||
def test_extract_plate_index_plate_1(self):
|
||||
"""Test parsing plate index when it's plate 1."""
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config>
|
||||
<plate>
|
||||
<metadata key="index" value="1" />
|
||||
<metadata key="prediction" value="1800" />
|
||||
</plate>
|
||||
</config>
|
||||
"""
|
||||
root = ET.fromstring(slice_info_xml)
|
||||
plate = root.find(".//plate")
|
||||
|
||||
plate_index = None
|
||||
for meta in plate.findall("metadata"):
|
||||
if meta.get("key") == "index":
|
||||
plate_index = int(meta.get("value"))
|
||||
break
|
||||
|
||||
assert plate_index == 1
|
||||
|
||||
def test_thumbnail_path_uses_plate_number(self):
|
||||
"""Test that thumbnail path correctly uses the extracted plate number."""
|
||||
plate_number = 5
|
||||
thumbnail_paths = []
|
||||
|
||||
if plate_number:
|
||||
thumbnail_paths.append(f"Metadata/plate_{plate_number}.png")
|
||||
|
||||
thumbnail_paths.extend(
|
||||
[
|
||||
"Metadata/plate_1.png",
|
||||
"Metadata/thumbnail.png",
|
||||
]
|
||||
)
|
||||
|
||||
# First priority should be plate_5.png
|
||||
assert thumbnail_paths[0] == "Metadata/plate_5.png"
|
||||
|
||||
def test_print_name_enhanced_for_plate_greater_than_1(self):
|
||||
"""Test that print_name is enhanced with plate info for plate > 1."""
|
||||
plate_index = 5
|
||||
print_name = "Benchy"
|
||||
|
||||
# Logic from archive.py
|
||||
if plate_index and plate_index > 1:
|
||||
if print_name and f"Plate {plate_index}" not in print_name:
|
||||
print_name = f"{print_name} - Plate {plate_index}"
|
||||
|
||||
assert print_name == "Benchy - Plate 5"
|
||||
|
||||
def test_print_name_not_enhanced_for_plate_1(self):
|
||||
"""Test that print_name is NOT enhanced for plate 1."""
|
||||
plate_index = 1
|
||||
print_name = "Benchy"
|
||||
|
||||
# Logic from archive.py
|
||||
if plate_index and plate_index > 1:
|
||||
if print_name and f"Plate {plate_index}" not in print_name:
|
||||
print_name = f"{print_name} - Plate {plate_index}"
|
||||
|
||||
assert print_name == "Benchy" # Unchanged for plate 1
|
||||
|
||||
def test_print_name_not_duplicated(self):
|
||||
"""Test that plate info is not added if already present in print_name."""
|
||||
plate_index = 5
|
||||
print_name = "Benchy - Plate 5"
|
||||
|
||||
# Logic from archive.py
|
||||
if plate_index and plate_index > 1:
|
||||
if print_name and f"Plate {plate_index}" not in print_name:
|
||||
print_name = f"{print_name} - Plate {plate_index}"
|
||||
|
||||
assert print_name == "Benchy - Plate 5" # Not duplicated
|
||||
|
||||
def test_high_plate_number_extraction(self):
|
||||
"""Test extracting high plate numbers (e.g., plate 28)."""
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config>
|
||||
<plate>
|
||||
<metadata key="index" value="28" />
|
||||
<metadata key="prediction" value="7200" />
|
||||
</plate>
|
||||
</config>
|
||||
"""
|
||||
root = ET.fromstring(slice_info_xml)
|
||||
plate = root.find(".//plate")
|
||||
|
||||
plate_index = None
|
||||
for meta in plate.findall("metadata"):
|
||||
if meta.get("key") == "index":
|
||||
plate_index = int(meta.get("value"))
|
||||
break
|
||||
|
||||
assert plate_index == 28
|
||||
|
||||
# Verify thumbnail would use correct plate
|
||||
thumbnail_path = f"Metadata/plate_{plate_index}.png"
|
||||
assert thumbnail_path == "Metadata/plate_28.png"
|
||||
|
||||
Reference in New Issue
Block a user