Add Windows build script and update README with build instructions
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
# A1 Swap Mod Packer
|
# 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.
|
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.
|
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.
|
- External SwapMod G-code template folder.
|
||||||
- Editable G-code patch file.
|
- Editable G-code patch file.
|
||||||
- Optional remaining-time plate-number encoding through `M73 R...`.
|
- 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.
|
- Combined mode: all input rows become one packed 3MF.
|
||||||
- Individual batch mode: every input row becomes its own packed 3MF.
|
- Individual batch mode: every input row becomes its own packed 3MF.
|
||||||
- zlib-ng Deflate ZIP compression level, defaulting to Level 7.
|
- 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.
|
Python 3.10 or newer is recommended.
|
||||||
|
|
||||||
Install dependency:
|
Install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
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:
|
Run the GUI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -62,6 +77,105 @@ or
|
|||||||
python run_cli.py
|
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
|
## Swap G-code templates
|
||||||
|
|
||||||
The GUI automatically scans this fixed folder:
|
The GUI automatically scans this fixed folder:
|
||||||
@@ -70,7 +184,16 @@ The GUI automatically scans this fixed folder:
|
|||||||
swap_gcode/
|
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:
|
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 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
|
### Build 3MF button
|
||||||
|
|
||||||
The **Build 3MF** button is placed below the input list on the right side for fast batch operation.
|
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**
|
- **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.
|
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
|
### Batch mode
|
||||||
|
|
||||||
#### Combined mode
|
#### Combined mode
|
||||||
@@ -363,6 +503,12 @@ Saved options include:
|
|||||||
|
|
||||||
## CLI examples
|
## 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:
|
Build one source with five copies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -405,3 +551,51 @@ python -m a1_swap_mod_packer.cli build \
|
|||||||
-o "5 Plates - A.3mf"
|
-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` |
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
APP_NAME = "A1 Swap Mod Packer"
|
APP_NAME = "A1 Swap Mod Packer"
|
||||||
__version__ = "0.4.0"
|
__version__ = "0.5.0"
|
||||||
APP_TITLE = f"{APP_NAME} v{__version__}"
|
APP_TITLE = f"{APP_NAME} v{__version__}"
|
||||||
|
|||||||
@@ -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("--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("--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("--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.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)
|
build.set_defaults(func=build_command)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ PATCH_CONFIG_FILE_NAME = "gcode_patches.ini"
|
|||||||
|
|
||||||
|
|
||||||
def program_root() -> Path:
|
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):
|
if getattr(sys, "frozen", False):
|
||||||
return Path(sys.executable).resolve().parent
|
return Path(sys.executable).resolve().parent
|
||||||
return Path(__file__).resolve().parents[1]
|
return Path(__file__).resolve().parents[1]
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user