From 6ac53319324f0ebacebba7e1abb7d6cd571b4754 Mon Sep 17 00:00:00 2001 From: hyyz17200 Date: Fri, 8 May 2026 12:42:37 +0800 Subject: [PATCH] Add Windows build script and update README with build instructions --- README.md | 198 ++++++++++++++++++++++++++++++++- a1_swap_mod_packer/__init__.py | 2 +- a1_swap_mod_packer/cli.py | 2 +- a1_swap_mod_packer/paths.py | 10 ++ build_win.cmd | 43 +++++++ 5 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 build_win.cmd diff --git a/README.md b/README.md index 2079d11..b8b09b6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # A1 Swap Mod Packer +Current version: **v0.5.0** + A1 Swap Mod Packer is an open-source implementation 3MF packer for Bambu Lab A1 SwapMod workflows. It takes one or more A1-sliced `.3mf` files, repeats their plate G-code according to copy counts, inserts an external SwapMod ejection/swap G-code block, and writes a new packed `.3mf` that can be sent to the printer. @@ -24,6 +26,8 @@ It takes one or more A1-sliced `.3mf` files, repeats their plate G-code accordin - External SwapMod G-code template folder. - Editable G-code patch file. - Optional remaining-time plate-number encoding through `M73 R...`. +- Selected-input thumbnail preview in the GUI. +- Packed preview image composition from up to 9 unique input files, with a short `{plates} P` label. - Combined mode: all input rows become one packed 3MF. - Individual batch mode: every input row becomes its own packed 3MF. - zlib-ng Deflate ZIP compression level, defaulting to Level 7. @@ -33,11 +37,22 @@ It takes one or more A1-sliced `.3mf` files, repeats their plate G-code accordin Python 3.10 or newer is recommended. -Install dependency: +Install dependencies: ```bash pip install -r requirements.txt ``` + +Current runtime dependencies: + +| Dependency | Used for | +|---|---| +| `PySide6` | GUI application, drag-and-drop table, file dialogs, thumbnail preview, and settings UI | +| `Pillow` | Reading and composing PNG preview images inside the output 3MF, including the `{plates} P` label | +| `zlib-ng` | Required ZIP Deflate backend for writing compressed 3MF files and honoring the compression level | + +No Java runtime, Bambu Studio SDK, external archive tool, or encrypted/template decoder is required by the current Python source version. + Run the GUI: ```bash @@ -62,6 +77,105 @@ or python run_cli.py ``` +Show the version: + +```bash +python -m a1_swap_mod_packer.cli --version +``` + +## Build Windows executables + +The repository includes a Windows build script: + +```cmd +build_win.cmd +``` + +The script uses Nuitka onefile mode and creates this portable release folder: + +```text +build/onefile/ +``` + +Expected output: + +```text +build/onefile/ + a1packer.exe + a1packer-cli.exe + gcode_patches.ini + swap_gcode/ + settings.json Optional; created by the GUI after settings are saved +``` + +External resources are intentionally kept outside the executables. Do not pack these files into the onefile binary: + +```text +swap_gcode/ +gcode_patches.ini +settings.json +``` + +The application resolves these paths from the executable folder when running as a packaged exe. + +### Windows build requirements + +Tested build environment: + +- Windows 10. +- Python 3.10 or newer. +- A virtual environment at `.venv/`. +- Project runtime dependencies from `requirements.txt`. +- Nuitka with onefile support. +- Microsoft C++ Build Tools / Visual Studio Build Tools with the MSVC compiler and Windows SDK. + +Install the Python dependencies in a fresh virtual environment: + +```cmd +py -3.12 -m venv .venv +.venv\Scripts\python.exe -m pip install --upgrade pip +.venv\Scripts\python.exe -m pip install -r requirements.txt +.venv\Scripts\python.exe -m pip install "Nuitka[onefile]" +``` + +Then run: + +```cmd +build_win.cmd +``` + +The build script performs two Nuitka builds: + +- GUI: `run_gui.py` -> `build/onefile/a1packer.exe` +- CLI: `run_cli.py` -> `build/onefile/a1packer-cli.exe` + +The GUI build enables the PySide6 plugin and includes the Qt plugin groups needed by the current interface: + +```text +platforms,imageformats,styles,iconengines +``` + +### After building + +Before publishing the folder, verify the CLI can see the external resources: + +```cmd +build\onefile\a1packer-cli.exe --version +build\onefile\a1packer-cli.exe list-swap-gcode +``` + +The second command should list files from: + +```text +build/onefile/swap_gcode/ +``` + +Also start `build/onefile/a1packer.exe` once and confirm that: + +- The Swap G-code dropdown lists the copied templates. +- Editing GUI options creates or updates `build/onefile/settings.json`. +- The app still runs if `settings.json` is absent before first launch. + ## Swap G-code templates The GUI automatically scans this fixed folder: @@ -70,7 +184,16 @@ The GUI automatically scans this fixed folder: swap_gcode/ ``` -Plain UTF-8 G-code files are read directly. +Plain UTF-8 text files are read directly. The current scanner accepts these suffixes: + +```text +.gcode +.nc +.ngc +.txt +``` + +The current source version does not decode encrypted or vendor-template G-code archives. In the GUI: @@ -143,6 +266,8 @@ If the source 3MF lacks this metadata, the GUI shows `Unknown`. The summary line under the input list shows the total plate count, estimated time, and filament for the current table. +The right-side thumbnail panel shows the selected input file's active plate preview when one is available in the 3MF. + ### Build 3MF button The **Build 3MF** button is placed below the input list on the right side for fast batch operation. @@ -236,6 +361,21 @@ Options: - **Sum prediction and filament** Updates the first plate metadata using the sum of all repeated source plates. This is more logically correct for statistics, but may differ from the vendor software. +### Preview image handling + +By default, the output 3MF keeps the base archive preview members for the active output plate and rewrites those PNG previews with: + +- a composite of up to 9 unique input-file preview images; +- a short green label such as `5 P`, applied once to the final composite. + +If preview images are missing or cannot be read, the packer keeps the available base preview and still tries to apply the short plate label. + +The CLI can disable preview rewriting with: + +```bash +--no-preview-label +``` + ### Batch mode #### Combined mode @@ -363,6 +503,12 @@ Saved options include: ## CLI examples +List available Swap G-code files: + +```bash +python -m a1_swap_mod_packer.cli list-swap-gcode +``` + Build one source with five copies: ```bash @@ -405,3 +551,51 @@ python -m a1_swap_mod_packer.cli build \ -o "5 Plates - A.3mf" ``` +Build positional inputs with the same copy count: + +```bash +python -m a1_swap_mod_packer.cli build \ + "A.3mf" "B.3mf" \ + --copies 2 \ + --swap-gcode "a1_swap.gcode" \ + -o "4 Plates - A_and_B.3mf" +``` + +Use an explicit Swap G-code folder and ZIP compression level: + +```bash +python -m a1_swap_mod_packer.cli build \ + --item "A.3mf" 5 \ + --swap-gcode "a1_swap.gcode" \ + --swap-gcode-dir "D:\SwapMod\swap_gcode" \ + --zip-level 9 \ + -o "5 Plates - A.3mf" +``` + +Disable final swap and preview label rewriting: + +```bash +python -m a1_swap_mod_packer.cli build \ + --item "A.3mf" 5 \ + --swap-gcode "a1_swap.gcode" \ + --no-swap-after-final \ + --no-preview-label \ + -o "5 Plates - A.3mf" +``` + +Useful CLI options: + +| Option | Meaning | +|---|---| +| `--version` | Print the current version | +| `list-swap-gcode` | List files found in the Swap G-code folder | +| `--item PATH COPIES` | Add one input with its own copy count; can be repeated | +| positional `inputs` + `--copies N` | Add multiple inputs with the same copy count | +| `--swap-gcode-dir DIR` | Use a custom Swap G-code folder | +| `--no-bed-cooldown` | Do not insert `M190` before the swap block | +| `--no-swap-after-final` | Do not run the swap block after the final plate | +| `--line-ending lf|crlf` | Choose generated G-code line endings; default is `crlf` | +| `--zip-level 1-9` | zlib-ng Deflate compression level; default is `7` | +| `--no-preview-label` | Do not rewrite the preview image label/composite | +| `--no-gcode-patches` | Do not apply rules from `gcode_patches.ini` | + diff --git a/a1_swap_mod_packer/__init__.py b/a1_swap_mod_packer/__init__.py index 50fc328..d7dd811 100644 --- a/a1_swap_mod_packer/__init__.py +++ b/a1_swap_mod_packer/__init__.py @@ -1,3 +1,3 @@ APP_NAME = "A1 Swap Mod Packer" -__version__ = "0.4.0" +__version__ = "0.5.0" APP_TITLE = f"{APP_NAME} v{__version__}" diff --git a/a1_swap_mod_packer/cli.py b/a1_swap_mod_packer/cli.py index 96a0af9..e14716b 100644 --- a/a1_swap_mod_packer/cli.py +++ b/a1_swap_mod_packer/cli.py @@ -94,7 +94,7 @@ def create_parser() -> argparse.ArgumentParser: build.add_argument("--metadata-mode", choices=("source", "sum"), default="source", help="How to write slice_info prediction and weight.") build.add_argument("--line-ending", choices=("lf", "crlf"), default="crlf", help="Line ending for the generated G-code.") build.add_argument("--zip-level", type=int, choices=range(1, 10), default=DEFAULT_ZIP_COMPRESS_LEVEL, metavar="1-9", help="zlib-ng Deflate compression level for the output 3MF. Default: 7.") - build.add_argument("--no-preview-label", action="store_true", help="Do not add a plate-count label to the first preview image.") + build.add_argument("--no-preview-label", action="store_true", help="Do not rewrite the preview image label/composite.") build.add_argument("--no-gcode-patches", action="store_true", help=f"Do not apply editable patches from {default_patch_config_path()}.") build.set_defaults(func=build_command) diff --git a/a1_swap_mod_packer/paths.py b/a1_swap_mod_packer/paths.py index 742bddf..6847c98 100644 --- a/a1_swap_mod_packer/paths.py +++ b/a1_swap_mod_packer/paths.py @@ -9,6 +9,16 @@ PATCH_CONFIG_FILE_NAME = "gcode_patches.ini" def program_root() -> Path: + compiled = globals().get("__compiled__") + if compiled is not None and getattr(compiled, "onefile", False): + containing_dir = getattr(compiled, "containing_dir", None) + if containing_dir: + return Path(containing_dir).resolve() + + original_argv0 = getattr(compiled, "original_argv0", None) + if original_argv0: + return Path(original_argv0).resolve().parent + if getattr(sys, "frozen", False): return Path(sys.executable).resolve().parent return Path(__file__).resolve().parents[1] diff --git a/build_win.cmd b/build_win.cmd new file mode 100644 index 0000000..65e3b05 --- /dev/null +++ b/build_win.cmd @@ -0,0 +1,43 @@ +@echo off +chcp 65001 >nul +setlocal + +set "PY=.venv\Scripts\python.exe" +set "DIST=build\onefile" + +if not exist "%DIST%" mkdir "%DIST%" + +"%PY%" -m nuitka ^ + --mode=onefile ^ + --msvc=latest ^ + --enable-plugin=pyside6 ^ + --include-qt-plugins=platforms,imageformats,styles,iconengines ^ + --windows-console-mode=disable ^ + --output-dir="%DIST%" ^ + --output-filename=a1packer.exe ^ + --remove-output ^ + run_gui.py +if errorlevel 1 exit /b %ERRORLEVEL% + +echo. +echo GUI build done. +echo Next step is building CLI. + +"%PY%" -m nuitka ^ + --mode=onefile ^ + --msvc=latest ^ + --output-dir="%DIST%" ^ + --output-filename=a1packer-cli.exe ^ + --remove-output ^ + run_cli.py +if errorlevel 1 exit /b %ERRORLEVEL% + +robocopy "swap_gcode" "%DIST%\swap_gcode" /E +if %ERRORLEVEL% GEQ 8 exit /b %ERRORLEVEL% + +copy /Y "gcode_patches.ini" "%DIST%\gcode_patches.ini" >nul +if errorlevel 1 exit /b %ERRORLEVEL% + +echo. +echo Build done: %DIST% +exit /b 0