diff --git a/.gitignore b/.gitignore
index 172cf32..624a889 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@ Thumbs.db
*.bak
/settings.json
+/reference
codec.py
\ No newline at end of file
diff --git a/a1_swap_mod_packer/builder.py b/a1_swap_mod_packer/builder.py
index 5736d86..334fae2 100644
--- a/a1_swap_mod_packer/builder.py
+++ b/a1_swap_mod_packer/builder.py
@@ -17,7 +17,13 @@ from .gcode import (
read_swap_gcode_file,
resolve_swap_gcode_path,
)
-from .metadata import read_filament_metadata, read_slice_plate_metadata, safe_float, update_first_slice_info
+from .metadata import (
+ read_filament_metadata,
+ read_model_settings_gcode_members,
+ read_slice_plate_metadata,
+ safe_float,
+ update_first_slice_info,
+)
from .models import BuildOptions, BuildResult, GcodePatchConfig, PlateJob, PlateSource
from .patches import apply_gcode_patches, parse_patch_config
@@ -128,9 +134,35 @@ def update_preview_label(png_bytes: bytes, label: str, small: bool = False) -> b
return output.getvalue()
+def resolve_output_gcode_member(archive: zipfile.ZipFile, fallback_member: str) -> str:
+ configured_members = read_model_settings_gcode_members(archive)
+ if fallback_member in configured_members:
+ return fallback_member
+ existing_members = set(list_gcode_members(archive))
+ for member in configured_members:
+ if member in existing_members:
+ return member
+ if configured_members:
+ return configured_members[0]
+ return fallback_member
+
+
+def preview_members_for_gcode_member(gcode_member: str) -> set[str]:
+ match = GCODE_MEMBER_RE.match(gcode_member)
+ if not match:
+ return set()
+ plate_index = match.group(1)
+ return {
+ f"Metadata/plate_{plate_index}.png",
+ f"Metadata/plate_{plate_index}_small.png",
+ }
+
+
def write_output_3mf(base_3mf: Path, output_3mf: Path, gcode_bytes: bytes, sources: list[PlateSource], options: BuildOptions) -> str:
md5 = hashlib.md5(gcode_bytes).hexdigest()
with zipfile.ZipFile(base_3mf, "r") as src, zipfile.ZipFile(output_3mf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=1) as dst:
+ gcode_member = resolve_output_gcode_member(src, sources[0].member_name)
+ preview_members = preview_members_for_gcode_member(gcode_member)
for item in src.infolist():
name = item.filename
if GCODE_MEMBER_RE.match(name) or MD5_MEMBER_RE.match(name):
@@ -138,11 +170,11 @@ def write_output_3mf(base_3mf: Path, output_3mf: Path, gcode_bytes: bytes, sourc
data = src.read(name)
if name == "Metadata/slice_info.config":
data = update_first_slice_info(data, sources, options)
- elif options.add_preview_label and name in {"Metadata/plate_1.png", "Metadata/plate_1_small.png"}:
+ elif options.add_preview_label and name in preview_members:
data = update_preview_label(data, f"{len(sources)} plates", small=name.endswith("_small.png"))
dst.writestr(item, data)
- dst.writestr("Metadata/plate_1.gcode", gcode_bytes)
- dst.writestr("Metadata/plate_1.gcode.md5", md5)
+ dst.writestr(gcode_member, gcode_bytes)
+ dst.writestr(f"{gcode_member}.md5", md5)
return md5
diff --git a/a1_swap_mod_packer/metadata.py b/a1_swap_mod_packer/metadata.py
index 64aaa0e..ec2145a 100644
--- a/a1_swap_mod_packer/metadata.py
+++ b/a1_swap_mod_packer/metadata.py
@@ -54,6 +54,27 @@ def read_filament_metadata(archive: zipfile.ZipFile) -> dict[int, dict[str, str]
return result
+def read_model_settings_gcode_members(archive: zipfile.ZipFile) -> list[str]:
+ try:
+ data = archive.read("Metadata/model_settings.config")
+ except KeyError:
+ return []
+ try:
+ root = ET.fromstring(data.decode("utf-8-sig", errors="replace"))
+ except Exception:
+ return []
+ members: list[str] = []
+ for plate in root.findall("plate"):
+ for metadata in plate.findall("metadata"):
+ if metadata.attrib.get("key") != "gcode_file":
+ continue
+ value = metadata.attrib.get("value", "").strip().replace("\\", "/").lstrip("/")
+ if value and GCODE_MEMBER_RE.match(value):
+ members.append(value)
+ break
+ return members
+
+
def safe_float(value: str | None) -> float | None:
if value is None:
return None
diff --git a/tests/test_active_plate_member.py b/tests/test_active_plate_member.py
new file mode 100644
index 0000000..b139ead
--- /dev/null
+++ b/tests/test_active_plate_member.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+
+import hashlib
+import tempfile
+import unittest
+import zipfile
+from pathlib import Path
+
+from a1_swap_mod_packer.builder import (
+ preview_members_for_gcode_member,
+ resolve_output_gcode_member,
+ write_output_3mf,
+)
+from a1_swap_mod_packer.metadata import read_model_settings_gcode_members
+from a1_swap_mod_packer.models import BuildOptions, PlateSource
+
+
+class ActivePlateMemberTest(unittest.TestCase):
+ def test_write_output_preserves_configured_plate_member(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source_3mf = root / "plate2.3mf"
+ output_3mf = root / "out.3mf"
+ self.write_plate2_archive(source_3mf)
+
+ with zipfile.ZipFile(source_3mf, "r") as archive:
+ self.assertEqual(read_model_settings_gcode_members(archive), ["Metadata/plate_2.gcode"])
+ self.assertEqual(resolve_output_gcode_member(archive, "Metadata/plate_2.gcode"), "Metadata/plate_2.gcode")
+ self.assertEqual(
+ preview_members_for_gcode_member("Metadata/plate_2.gcode"),
+ {"Metadata/plate_2.png", "Metadata/plate_2_small.png"},
+ )
+
+ gcode_bytes = b"G1 X1\n"
+ sources = [
+ PlateSource(
+ source_3mf=source_3mf,
+ member_name="Metadata/plate_2.gcode",
+ gcode_text="G1 X0\n",
+ )
+ ]
+ options = BuildOptions(
+ swap_gcode="",
+ output_3mf=output_3mf,
+ add_preview_label=False,
+ apply_gcode_patches=False,
+ )
+ md5 = write_output_3mf(source_3mf, output_3mf, gcode_bytes, sources, options)
+
+ with zipfile.ZipFile(output_3mf, "r") as archive:
+ names = set(archive.namelist())
+ self.assertIn("Metadata/plate_2.gcode", names)
+ self.assertIn("Metadata/plate_2.gcode.md5", names)
+ self.assertNotIn("Metadata/plate_1.gcode", names)
+ self.assertEqual(archive.read("Metadata/plate_2.gcode"), gcode_bytes)
+ self.assertEqual(archive.read("Metadata/plate_2.gcode.md5").decode(), md5)
+ self.assertEqual(md5, hashlib.md5(gcode_bytes).hexdigest())
+
+ @staticmethod
+ def write_plate2_archive(path: Path) -> None:
+ model_settings = """
+
+
+
+
+
+
+
+
+
+
+"""
+ slice_info = """
+
+
+
+
+
+"""
+ with zipfile.ZipFile(path, "w") as archive:
+ archive.writestr("Metadata/model_settings.config", model_settings)
+ archive.writestr("Metadata/slice_info.config", slice_info)
+ archive.writestr("Metadata/plate_2.gcode", "G1 X0\n")
+ archive.writestr("Metadata/plate_2.gcode.md5", "old")
+ archive.writestr("Metadata/plate_2.png", b"png")
+ archive.writestr("Metadata/plate_2_small.png", b"png")
+
+
+if __name__ == "__main__":
+ unittest.main()