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()