mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
Add multi-plate 3MF plate selection for reprinting (Issue #93)
When reprinting a multi-plate 3MF file exported with "All sliced file",
the system now asks which plate to print and only maps filaments for
that specific plate.
Backend changes:
- Add GET /archives/{id}/plates endpoint to list available plates
with thumbnails, print times, and per-plate filament requirements
- Add GET /archives/{id}/plate-thumbnail/{index} for plate thumbnails
- Update GET /archives/{id}/filament-requirements to accept plate_id
query parameter for filtering filaments by plate
- Add plate_id field to ReprintRequest schema
- Update POST /archives/{id}/reprint to use plate_id from request body
instead of auto-detecting (maintains backward compatibility)
Frontend changes:
- Add getArchivePlates API method
- Update getArchiveFilamentRequirements to accept optional plateId
- Update reprintArchive to accept plate_id in options
- Add plate selection UI to ReprintModal when multi-plate 3MF detected
- Show plate thumbnails, names, and filament counts in selection grid
- Require plate selection before printing multi-plate files
- Filter filament requirements to show only selected plate's filaments
Tests:
- Add unit tests for multi-plate slice_info.config parsing
- Add unit tests for plate detection from gcode files
- Add integration tests for new plate endpoints
This commit is contained in:
@@ -426,3 +426,188 @@ class TestThreeMFPlateIndexExtraction:
|
||||
# Verify thumbnail would use correct plate
|
||||
thumbnail_path = f"Metadata/plate_{plate_index}.png"
|
||||
assert thumbnail_path == "Metadata/plate_28.png"
|
||||
|
||||
|
||||
class TestMultiPlate3MFParsing:
|
||||
"""Tests for parsing multi-plate 3MF files (Issue #93)."""
|
||||
|
||||
def test_parse_multiple_plates_from_slice_info(self):
|
||||
"""Test parsing multiple plates from slice_info.config."""
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
# Multi-plate 3MF with 3 plates
|
||||
slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config>
|
||||
<plate>
|
||||
<metadata key="index" value="1" />
|
||||
<metadata key="prediction" value="3600" />
|
||||
<metadata key="weight" value="50.0" />
|
||||
<filament id="1" type="PLA" color="#FF0000" used_g="25.0" used_m="8.5" />
|
||||
<object identify_id="1" name="Part_A" skipped="false" />
|
||||
</plate>
|
||||
<plate>
|
||||
<metadata key="index" value="2" />
|
||||
<metadata key="prediction" value="7200" />
|
||||
<metadata key="weight" value="100.0" />
|
||||
<filament id="2" type="PETG" color="#00FF00" used_g="50.0" used_m="17.0" />
|
||||
<object identify_id="2" name="Part_B" skipped="false" />
|
||||
</plate>
|
||||
<plate>
|
||||
<metadata key="index" value="3" />
|
||||
<metadata key="prediction" value="1800" />
|
||||
<metadata key="weight" value="25.0" />
|
||||
<filament id="1" type="PLA" color="#FF0000" used_g="12.5" used_m="4.2" />
|
||||
<filament id="3" type="TPU" color="#0000FF" used_g="12.5" used_m="4.2" />
|
||||
<object identify_id="3" name="Part_C" skipped="false" />
|
||||
</plate>
|
||||
</config>
|
||||
"""
|
||||
root = ET.fromstring(slice_info_xml)
|
||||
plates = root.findall(".//plate")
|
||||
|
||||
assert len(plates) == 3
|
||||
|
||||
# Parse each plate
|
||||
plate_data = []
|
||||
for plate_elem in plates:
|
||||
plate_info = {"index": None, "filaments": []}
|
||||
|
||||
for meta in plate_elem.findall("metadata"):
|
||||
if meta.get("key") == "index":
|
||||
plate_info["index"] = int(meta.get("value"))
|
||||
|
||||
for filament_elem in plate_elem.findall("filament"):
|
||||
used_g = float(filament_elem.get("used_g", "0"))
|
||||
if used_g > 0:
|
||||
plate_info["filaments"].append(
|
||||
{
|
||||
"slot_id": int(filament_elem.get("id")),
|
||||
"type": filament_elem.get("type"),
|
||||
"color": filament_elem.get("color"),
|
||||
"used_grams": used_g,
|
||||
}
|
||||
)
|
||||
|
||||
plate_data.append(plate_info)
|
||||
|
||||
# Verify plate 1
|
||||
assert plate_data[0]["index"] == 1
|
||||
assert len(plate_data[0]["filaments"]) == 1
|
||||
assert plate_data[0]["filaments"][0]["slot_id"] == 1
|
||||
assert plate_data[0]["filaments"][0]["type"] == "PLA"
|
||||
|
||||
# Verify plate 2
|
||||
assert plate_data[1]["index"] == 2
|
||||
assert len(plate_data[1]["filaments"]) == 1
|
||||
assert plate_data[1]["filaments"][0]["slot_id"] == 2
|
||||
assert plate_data[1]["filaments"][0]["type"] == "PETG"
|
||||
|
||||
# Verify plate 3 (has 2 filaments)
|
||||
assert plate_data[2]["index"] == 3
|
||||
assert len(plate_data[2]["filaments"]) == 2
|
||||
filament_types = {f["type"] for f in plate_data[2]["filaments"]}
|
||||
assert filament_types == {"PLA", "TPU"}
|
||||
|
||||
def test_filter_filaments_by_plate_id(self):
|
||||
"""Test filtering filaments for a specific plate."""
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config>
|
||||
<plate>
|
||||
<metadata key="index" value="1" />
|
||||
<filament id="1" type="PLA" color="#FF0000" used_g="25.0" />
|
||||
</plate>
|
||||
<plate>
|
||||
<metadata key="index" value="2" />
|
||||
<filament id="2" type="PETG" color="#00FF00" used_g="50.0" />
|
||||
</plate>
|
||||
</config>
|
||||
"""
|
||||
root = ET.fromstring(slice_info_xml)
|
||||
|
||||
# Filter for plate 2 only
|
||||
target_plate_id = 2
|
||||
filaments = []
|
||||
|
||||
for plate_elem in root.findall(".//plate"):
|
||||
plate_index = None
|
||||
for meta in plate_elem.findall("metadata"):
|
||||
if meta.get("key") == "index":
|
||||
plate_index = int(meta.get("value", "0"))
|
||||
break
|
||||
|
||||
if plate_index == target_plate_id:
|
||||
for filament_elem in plate_elem.findall("filament"):
|
||||
used_g = float(filament_elem.get("used_g", "0"))
|
||||
if used_g > 0:
|
||||
filaments.append(
|
||||
{
|
||||
"slot_id": int(filament_elem.get("id")),
|
||||
"type": filament_elem.get("type"),
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
# Should only have plate 2's filament
|
||||
assert len(filaments) == 1
|
||||
assert filaments[0]["slot_id"] == 2
|
||||
assert filaments[0]["type"] == "PETG"
|
||||
|
||||
def test_detect_multi_plate_from_gcode_files(self):
|
||||
"""Test detecting multiple plates from gcode file presence."""
|
||||
# Simulate namelist from a multi-plate 3MF
|
||||
namelist = [
|
||||
"Metadata/plate_1.gcode",
|
||||
"Metadata/plate_2.gcode",
|
||||
"Metadata/plate_3.gcode",
|
||||
"Metadata/plate_1.png",
|
||||
"Metadata/plate_2.png",
|
||||
"Metadata/plate_3.png",
|
||||
"Metadata/slice_info.config",
|
||||
"3D/3dmodel.model",
|
||||
]
|
||||
|
||||
# Extract plate indices from gcode files
|
||||
gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
|
||||
plate_indices = []
|
||||
for gf in gcode_files:
|
||||
plate_str = gf[15:-6] # Remove "Metadata/plate_" and ".gcode"
|
||||
plate_indices.append(int(plate_str))
|
||||
|
||||
plate_indices.sort()
|
||||
|
||||
assert len(plate_indices) == 3
|
||||
assert plate_indices == [1, 2, 3]
|
||||
|
||||
# Verify it's a multi-plate file
|
||||
is_multi_plate = len(plate_indices) > 1
|
||||
assert is_multi_plate is True
|
||||
|
||||
def test_single_plate_export_not_multi_plate(self):
|
||||
"""Test that single-plate exports are not detected as multi-plate."""
|
||||
# Simulate namelist from a single-plate export (plate 5 only)
|
||||
namelist = [
|
||||
"Metadata/plate_5.gcode",
|
||||
"Metadata/plate_1.png",
|
||||
"Metadata/plate_2.png",
|
||||
"Metadata/plate_3.png",
|
||||
"Metadata/plate_4.png",
|
||||
"Metadata/plate_5.png", # All thumbnails present
|
||||
"Metadata/slice_info.config",
|
||||
"3D/3dmodel.model",
|
||||
]
|
||||
|
||||
# Extract plate indices from gcode files (not thumbnails!)
|
||||
gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
|
||||
plate_indices = []
|
||||
for gf in gcode_files:
|
||||
plate_str = gf[15:-6]
|
||||
plate_indices.append(int(plate_str))
|
||||
|
||||
# Only one gcode file = single plate export
|
||||
assert len(plate_indices) == 1
|
||||
assert plate_indices[0] == 5
|
||||
|
||||
is_multi_plate = len(plate_indices) > 1
|
||||
assert is_multi_plate is False
|
||||
|
||||
Reference in New Issue
Block a user