mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +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:
@@ -1964,15 +1964,189 @@ async def upload_archives_bulk(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{archive_id}/plates")
|
||||
async def get_archive_plates(
|
||||
archive_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get available plates from a multi-plate 3MF archive.
|
||||
|
||||
Returns a list of plates with their index, name, thumbnail availability,
|
||||
and filament requirements. For single-plate exports, returns a single plate.
|
||||
"""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
service = ArchiveService(db)
|
||||
archive = await service.get_archive(archive_id)
|
||||
if not archive:
|
||||
raise HTTPException(404, "Archive not found")
|
||||
|
||||
file_path = settings.base_dir / archive.file_path
|
||||
if not file_path.exists():
|
||||
raise HTTPException(404, "Archive file not found")
|
||||
|
||||
plates = []
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(file_path, "r") as zf:
|
||||
namelist = zf.namelist()
|
||||
|
||||
# Find all plate gcode files to determine available plates
|
||||
gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
|
||||
|
||||
if not gcode_files:
|
||||
# No sliced plates found
|
||||
return {"archive_id": archive_id, "filename": archive.filename, "plates": []}
|
||||
|
||||
# Extract plate indices from gcode filenames
|
||||
plate_indices = []
|
||||
for gf in gcode_files:
|
||||
# "Metadata/plate_5.gcode" -> 5
|
||||
try:
|
||||
plate_str = gf[15:-6] # Remove "Metadata/plate_" and ".gcode"
|
||||
plate_indices.append(int(plate_str))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
plate_indices.sort()
|
||||
|
||||
# Parse slice_info.config for plate metadata
|
||||
plate_metadata = {} # plate_index -> {filaments, prediction, weight, name}
|
||||
if "Metadata/slice_info.config" in namelist:
|
||||
content = zf.read("Metadata/slice_info.config").decode()
|
||||
root = ET.fromstring(content)
|
||||
|
||||
for plate_elem in root.findall(".//plate"):
|
||||
plate_info = {"filaments": [], "prediction": None, "weight": None, "name": None}
|
||||
|
||||
# Get plate index from metadata
|
||||
plate_index = None
|
||||
for meta in plate_elem.findall("metadata"):
|
||||
key = meta.get("key")
|
||||
value = meta.get("value")
|
||||
if key == "index" and value:
|
||||
try:
|
||||
plate_index = int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
elif key == "prediction" and value:
|
||||
try:
|
||||
plate_info["prediction"] = int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
elif key == "weight" and value:
|
||||
try:
|
||||
plate_info["weight"] = float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get filaments used in this plate
|
||||
for filament_elem in plate_elem.findall("filament"):
|
||||
filament_id = filament_elem.get("id")
|
||||
filament_type = filament_elem.get("type", "")
|
||||
filament_color = filament_elem.get("color", "")
|
||||
used_g = filament_elem.get("used_g", "0")
|
||||
used_m = filament_elem.get("used_m", "0")
|
||||
|
||||
try:
|
||||
used_grams = float(used_g)
|
||||
except (ValueError, TypeError):
|
||||
used_grams = 0
|
||||
|
||||
if used_grams > 0 and filament_id:
|
||||
plate_info["filaments"].append(
|
||||
{
|
||||
"slot_id": int(filament_id),
|
||||
"type": filament_type,
|
||||
"color": filament_color,
|
||||
"used_grams": round(used_grams, 1),
|
||||
"used_meters": float(used_m) if used_m else 0,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort filaments by slot ID
|
||||
plate_info["filaments"].sort(key=lambda x: x["slot_id"])
|
||||
|
||||
# Get first object name as plate name hint
|
||||
first_obj = plate_elem.find("object")
|
||||
if first_obj is not None:
|
||||
plate_info["name"] = first_obj.get("name")
|
||||
|
||||
if plate_index is not None:
|
||||
plate_metadata[plate_index] = plate_info
|
||||
|
||||
# Build plate list
|
||||
for idx in plate_indices:
|
||||
meta = plate_metadata.get(idx, {})
|
||||
has_thumbnail = f"Metadata/plate_{idx}.png" in namelist
|
||||
|
||||
plates.append(
|
||||
{
|
||||
"index": idx,
|
||||
"name": meta.get("name"),
|
||||
"has_thumbnail": has_thumbnail,
|
||||
"thumbnail_url": f"/api/v1/archives/{archive_id}/plate-thumbnail/{idx}"
|
||||
if has_thumbnail
|
||||
else None,
|
||||
"print_time_seconds": meta.get("prediction"),
|
||||
"filament_used_grams": meta.get("weight"),
|
||||
"filaments": meta.get("filaments", []),
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse plates from archive {archive_id}: {e}")
|
||||
|
||||
return {
|
||||
"archive_id": archive_id,
|
||||
"filename": archive.filename,
|
||||
"plates": plates,
|
||||
"is_multi_plate": len(plates) > 1,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{archive_id}/plate-thumbnail/{plate_index}")
|
||||
async def get_plate_thumbnail(
|
||||
archive_id: int,
|
||||
plate_index: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get the thumbnail image for a specific plate."""
|
||||
service = ArchiveService(db)
|
||||
archive = await service.get_archive(archive_id)
|
||||
if not archive:
|
||||
raise HTTPException(404, "Archive not found")
|
||||
|
||||
file_path = settings.base_dir / archive.file_path
|
||||
if not file_path.exists():
|
||||
raise HTTPException(404, "Archive file not found")
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(file_path, "r") as zf:
|
||||
thumb_path = f"Metadata/plate_{plate_index}.png"
|
||||
if thumb_path in zf.namelist():
|
||||
data = zf.read(thumb_path)
|
||||
return Response(content=data, media_type="image/png")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise HTTPException(404, f"Thumbnail for plate {plate_index} not found")
|
||||
|
||||
|
||||
@router.get("/{archive_id}/filament-requirements")
|
||||
async def get_filament_requirements(
|
||||
archive_id: int,
|
||||
plate_id: int | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get filament requirements from the archived 3MF file.
|
||||
|
||||
Returns the filaments used in this print with their slot IDs, types, colors,
|
||||
and usage amounts. This can be compared with current AMS state before reprinting.
|
||||
|
||||
Args:
|
||||
archive_id: The archive ID
|
||||
plate_id: Optional plate index to filter filaments for (for multi-plate files)
|
||||
"""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
@@ -1994,31 +2168,70 @@ async def get_filament_requirements(
|
||||
content = zf.read("Metadata/slice_info.config").decode()
|
||||
root = ET.fromstring(content)
|
||||
|
||||
# Extract filament elements
|
||||
# Format: <filament id="1" type="PLA" color="#FFFFFF" used_g="100" used_m="10" />
|
||||
for filament_elem in root.findall(".//filament"):
|
||||
filament_id = filament_elem.get("id")
|
||||
filament_type = filament_elem.get("type", "")
|
||||
filament_color = filament_elem.get("color", "")
|
||||
used_g = filament_elem.get("used_g", "0")
|
||||
used_m = filament_elem.get("used_m", "0")
|
||||
# If plate_id is specified, find filaments for that specific plate
|
||||
if plate_id is not None:
|
||||
# Find the plate element with matching index
|
||||
for plate_elem in root.findall(".//plate"):
|
||||
plate_index = None
|
||||
for meta in plate_elem.findall("metadata"):
|
||||
if meta.get("key") == "index":
|
||||
try:
|
||||
plate_index = int(meta.get("value", "0"))
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
|
||||
# Only include filaments that are actually used
|
||||
try:
|
||||
used_grams = float(used_g)
|
||||
except (ValueError, TypeError):
|
||||
used_grams = 0
|
||||
if plate_index == plate_id:
|
||||
# Extract filaments from this plate element
|
||||
for filament_elem in plate_elem.findall("filament"):
|
||||
filament_id = filament_elem.get("id")
|
||||
filament_type = filament_elem.get("type", "")
|
||||
filament_color = filament_elem.get("color", "")
|
||||
used_g = filament_elem.get("used_g", "0")
|
||||
used_m = filament_elem.get("used_m", "0")
|
||||
|
||||
if used_grams > 0 and filament_id:
|
||||
filaments.append(
|
||||
{
|
||||
"slot_id": int(filament_id),
|
||||
"type": filament_type,
|
||||
"color": filament_color,
|
||||
"used_grams": round(used_grams, 1),
|
||||
"used_meters": float(used_m) if used_m else 0,
|
||||
}
|
||||
)
|
||||
try:
|
||||
used_grams = float(used_g)
|
||||
except (ValueError, TypeError):
|
||||
used_grams = 0
|
||||
|
||||
if used_grams > 0 and filament_id:
|
||||
filaments.append(
|
||||
{
|
||||
"slot_id": int(filament_id),
|
||||
"type": filament_type,
|
||||
"color": filament_color,
|
||||
"used_grams": round(used_grams, 1),
|
||||
"used_meters": float(used_m) if used_m else 0,
|
||||
}
|
||||
)
|
||||
break
|
||||
else:
|
||||
# No plate_id specified - extract all filaments with used_g > 0
|
||||
# This is the legacy behavior for single-plate files
|
||||
for filament_elem in root.findall(".//filament"):
|
||||
filament_id = filament_elem.get("id")
|
||||
filament_type = filament_elem.get("type", "")
|
||||
filament_color = filament_elem.get("color", "")
|
||||
used_g = filament_elem.get("used_g", "0")
|
||||
used_m = filament_elem.get("used_m", "0")
|
||||
|
||||
# Only include filaments that are actually used
|
||||
try:
|
||||
used_grams = float(used_g)
|
||||
except (ValueError, TypeError):
|
||||
used_grams = 0
|
||||
|
||||
if used_grams > 0 and filament_id:
|
||||
filaments.append(
|
||||
{
|
||||
"slot_id": int(filament_id),
|
||||
"type": filament_type,
|
||||
"color": filament_color,
|
||||
"used_grams": round(used_grams, 1),
|
||||
"used_meters": float(used_m) if used_m else 0,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort by slot ID
|
||||
filaments.sort(key=lambda x: x["slot_id"])
|
||||
@@ -2029,6 +2242,7 @@ async def get_filament_requirements(
|
||||
return {
|
||||
"archive_id": archive_id,
|
||||
"filename": archive.filename,
|
||||
"plate_id": plate_id,
|
||||
"filaments": filaments,
|
||||
}
|
||||
|
||||
@@ -2129,18 +2343,22 @@ async def reprint_archive(
|
||||
# Register this as an expected print so we don't create a duplicate archive
|
||||
register_expected_print(printer_id, remote_filename, archive_id)
|
||||
|
||||
# Detect plate ID from 3MF file
|
||||
plate_id = 1
|
||||
try:
|
||||
with zipfile.ZipFile(file_path, "r") as zf:
|
||||
for name in zf.namelist():
|
||||
if name.startswith("Metadata/plate_") and name.endswith(".gcode"):
|
||||
# Extract plate number from "Metadata/plate_X.gcode"
|
||||
plate_str = name[15:-6] # Remove "Metadata/plate_" and ".gcode"
|
||||
plate_id = int(plate_str)
|
||||
break
|
||||
except Exception:
|
||||
pass # Default to plate 1 if detection fails
|
||||
# Use plate_id from request if provided, otherwise auto-detect from 3MF file
|
||||
if body.plate_id is not None:
|
||||
plate_id = body.plate_id
|
||||
else:
|
||||
# Auto-detect plate ID from 3MF file (legacy behavior for single-plate files)
|
||||
plate_id = 1
|
||||
try:
|
||||
with zipfile.ZipFile(file_path, "r") as zf:
|
||||
for name in zf.namelist():
|
||||
if name.startswith("Metadata/plate_") and name.endswith(".gcode"):
|
||||
# Extract plate number from "Metadata/plate_X.gcode"
|
||||
plate_str = name[15:-6] # Remove "Metadata/plate_" and ".gcode"
|
||||
plate_id = int(plate_str)
|
||||
break
|
||||
except Exception:
|
||||
pass # Default to plate 1 if detection fails
|
||||
|
||||
logger.info(
|
||||
f"Reprint archive {archive_id}: plate_id={plate_id}, "
|
||||
|
||||
@@ -170,6 +170,10 @@ class ProjectPageUpdate(BaseModel):
|
||||
class ReprintRequest(BaseModel):
|
||||
"""Request body for reprinting an archive."""
|
||||
|
||||
# Plate selection for multi-plate 3MF files
|
||||
# If not specified, auto-detects from file (legacy behavior for single-plate files)
|
||||
plate_id: int | None = None
|
||||
|
||||
# AMS slot mapping: list of tray IDs for each filament slot in the 3MF
|
||||
# Global tray ID = (ams_id * 4) + slot_id, external = 254
|
||||
ams_mapping: list[int] | None = None
|
||||
|
||||
@@ -380,3 +380,35 @@ class TestArchiveF3DEndpoints:
|
||||
assert with_f3d["f3d_path"] == "archives/test/design.f3d"
|
||||
assert without_f3d is not None
|
||||
assert without_f3d["f3d_path"] is None
|
||||
|
||||
# ========================================================================
|
||||
# Multi-Plate 3MF endpoints (Issue #93)
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_archive_plates_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 when fetching plates for non-existent archive."""
|
||||
response = await async_client.get("/api/v1/archives/999999/plates")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_plate_thumbnail_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 when fetching plate thumbnail for non-existent archive."""
|
||||
response = await async_client.get("/api/v1/archives/999999/plate-thumbnail/1")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_filament_requirements_not_found(self, async_client: AsyncClient):
|
||||
"""Verify filament-requirements returns 404 for non-existent archive."""
|
||||
response = await async_client.get("/api/v1/archives/999999/filament-requirements")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_filament_requirements_with_plate_id_not_found(self, async_client: AsyncClient):
|
||||
"""Verify filament-requirements with plate_id returns 404 for non-existent archive."""
|
||||
response = await async_client.get("/api/v1/archives/999999/filament-requirements?plate_id=1")
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -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