Compare commits

..

42 Commits

Author SHA1 Message Date
SoftFever
8d37abae54 Merge branch 'main' into feature/automation 2026-06-09 23:34:34 +08:00
SoftFever
3546dafc18 update doc 2026-06-03 22:41:30 +08:00
SoftFever
2e4cbd4511 fix(automation): view.select 'device' falls back to m_printer_view for third-party printers 2026-06-03 21:45:44 +08:00
SoftFever
411787afb2 docs(automation): add view.select client wrapper, example usage, and docs 2026-06-03 21:37:51 +08:00
SoftFever
00bb52bcd7 fix(automation): select_tab_by_name resolves prepare/preview by page window (viewer-mode safe) 2026-06-03 21:36:03 +08:00
SoftFever
1a960b59ea feat(automation): implement view.select via MainFrame::select_tab_by_name 2026-06-03 21:26:40 +08:00
SoftFever
9a16fb7c2e feat(automation): add view.select dispatcher handler + tests 2026-06-03 21:18:50 +08:00
SoftFever
7ef89fdb9d docs(automation): document file.open method and error 1007 2026-06-03 20:22:17 +08:00
SoftFever
3d813d529e docs(automation): example_slice.py loads model at runtime via file.open 2026-06-03 20:20:25 +08:00
SoftFever
151927ac00 feat(automation): add OrcaClient.open() wrapper for file.open 2026-06-03 20:18:31 +08:00
SoftFever
3f1a2a71bd feat(automation): implement WxUiBackend::open_files via Plater::load_files 2026-06-03 20:14:29 +08:00
SoftFever
b87dd23c74 feat(automation): advertise file.open in automation.version capabilities 2026-06-03 20:06:44 +08:00
SoftFever
cea46ddc7f feat(automation): add file.open dispatcher handler with validation + tests 2026-06-03 20:01:23 +08:00
SoftFever
b3d7a732c5 feat(automation): add open_files to backend interface + kErrLoadFailed (1007) 2026-06-03 19:55:46 +08:00
SoftFever
b70be9178e docs(automation): design spec for file.open runtime model-loading method
Adds the approved design for a `file.open` JSON-RPC method that loads
files into a running OrcaSlicer via Plater::load_files (synchronous, GUI
thread). Dialog-driving approaches deferred.
2026-06-03 18:57:20 +08:00
SoftFever
892b33bac5 refactor(automation): drop screenshot.viewport3d, keep only screenshot.window
The on-screen window capture is composited from the desktop framebuffer, so it
already includes the GL 3D viewport as currently shown (model in the editor,
toolpaths in Preview). The offscreen render_thumbnail path only ever drew the
model GLVolumeCollection — never the gcode toolpaths — and produced a blank image
after slicing because the app switches to the Preview panel. Rather than maintain a
second, more limited capture method, remove it entirely.

Removes the JSON-RPC method, IUiBackend/WxUiBackend implementation, dispatcher
route + capability entry, the now-dead opt_int/thumbnail_to_wximage helpers and
ThumbnailData include, the mock override + unit test, and the Python
screenshot_3d client method. Docs updated accordingly.
2026-06-03 18:05:23 +08:00
SoftFever
952696fd1f fix(automation): capture screenshot.window from the composited screen
Blitting from the MainFrame's own wxClientDC clips out child HWNDs, so all of
OrcaSlicer's custom child-window controls (sidebar buttons/combos/panels) and the
GL canvas came back as uninitialized black bitmap memory. Read the window's
on-screen rectangle from the DWM-composited desktop via wxScreenDC instead, which
includes every child window, the OpenGL surface, and ImGui overlays.

Document the visible/unobscured requirement and the HiDPI logical-vs-physical
pixel caveat; clarify how screenshot.viewport3d differs and why it stays.
2026-06-03 17:19:36 +08:00
SoftFever
6980d9c327 plan 2026-06-03 09:50:34 +08:00
SoftFever
45e93951c1 test(automation): regression verification — unit suites green + disabled-path no-op audit 2026-06-03 04:26:25 +08:00
SoftFever
a4cedde163 docs(automation): protocol reference, ids, and platform caveats 2026-06-03 04:19:20 +08:00
SoftFever
61b4131aee feat(automation): runnable e2e slice example / smoke test 2026-06-03 04:14:24 +08:00
SoftFever
6eb479243d feat(automation): instrument core widgets with stable automation ids 2026-06-03 04:09:42 +08:00
SoftFever
622272e674 feat(automation): GUI_App owns automation server lifecycle (opt-in) 2026-06-03 03:58:01 +08:00
SoftFever
b54cc75362 feat(automation): --automation-server CLI flag plumbed into GUI 2026-06-03 03:48:47 +08:00
SoftFever
9d915c4e76 feat(automation): WxUiBackend input + window/viewport screenshots 2026-06-03 03:35:40 +08:00
SoftFever
d742b10c50 feat(automation): WxUiBackend marshaller + dump_tree + app_state 2026-06-03 03:19:29 +08:00
SoftFever
47467b626c feat(automation): guarded ImGui item/window recording hooks 2026-06-03 03:01:58 +08:00
SoftFever
c0d37bff3a feat(automation): double-buffered ImGui item table 2026-06-03 02:52:52 +08:00
SoftFever
39a29cf865 feat(automation): wxWindow automation-id registry 2026-06-03 02:43:41 +08:00
SoftFever
487e1cb205 feat(automation): localhost beast POST /jsonrpc server 2026-06-03 02:32:51 +08:00
SoftFever
a2e8a90052 feat(automation): Python reference client 2026-06-03 02:23:10 +08:00
SoftFever
8dcbc582fa feat(automation): sync.wait_for poll loop 2026-06-03 02:13:35 +08:00
SoftFever
b0325c999a feat(automation): app.state + screenshot handlers with base64 2026-06-03 02:07:30 +08:00
SoftFever
5a2f03adee feat(automation): input.click / input.type / input.key handlers 2026-06-03 02:01:40 +08:00
SoftFever
a8ed2b8dd5 feat(automation): tree.dump / tree.find / widget.get handlers 2026-06-03 01:55:48 +08:00
SoftFever
aac14ae161 feat(automation): JSON-RPC dispatcher envelope + version + error model 2026-06-03 01:49:49 +08:00
SoftFever
94c356845e test(automation): MockUiBackend recording test double 2026-06-03 01:45:46 +08:00
SoftFever
e449a0b618 feat(automation): resolve_unique + wait-state evaluation 2026-06-03 01:42:38 +08:00
SoftFever
ddd1967bff feat(automation): pure locator (id/path/predicate) with unit tests 2026-06-03 01:38:13 +08:00
SoftFever
02140d2a1e test(automation): lock serializer children/value/imgui/app_state shapes 2026-06-03 01:34:13 +08:00
SoftFever
0be138b981 feat(automation): pure UI node model + JSON serializer with unit test 2026-06-03 01:28:43 +08:00
SoftFever
11301086a7 docs: add UI automation design spec
Design for an opt-in localhost JSON-RPC server that lets an external script/AI agent drive and observe the OrcaSlicer GUI (wxWidgets + 3D viewport + ImGui) via wxUIActionSimulator.
2026-06-03 00:48:27 +08:00
8238 changed files with 211449 additions and 113660 deletions

View File

@@ -5,7 +5,6 @@ on:
branches:
- main
- release/*
- belt-printer
paths:
- 'deps/**'
- 'src/**'
@@ -15,8 +14,6 @@ on:
- 'resources/**'
- ".github/workflows/build_*.yml"
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
pull_request:
branches:
@@ -30,12 +27,9 @@ on:
- 'version.inc'
- ".github/workflows/build_*.yml"
- 'build_linux.sh'
- 'build_release_vs.bat'
- 'build_release_vs2022.bat'
- 'build_release_macos.sh'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
schedule:
@@ -57,42 +51,19 @@ jobs:
build_linux:
strategy:
fail-fast: false
# Build both arches on every event (PRs included), through the same
# build_check_cache -> build_deps -> build_orca chain (the AppImage).
# aarch64 always uses the GitHub-hosted arm runner (there is no arm
# self-hosted server). amd64's empty arch is load-bearing: it keeps the
# historical 'linux-clang' deps cache key and the unsuffixed asset names.
matrix:
include:
- arch: ""
os: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }}
- arch: "aarch64"
os: ubuntu-24.04-arm
# Don't run scheduled builds on forks:
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_check_cache.yml
with:
os: ${{ matrix.os }}
arch: ${{ matrix.arch }}
os: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }}
build-deps-only: ${{ inputs.build-deps-only || false }}
secrets: inherit
build_windows:
name: Build Windows ${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
include:
- arch: x64
os: windows-latest
- arch: arm64
os: windows-11-arm
# Don't run scheduled builds on forks:
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_check_cache.yml
with:
# Self-hosted runner is x64-only; ARM64 always uses the GitHub-hosted runner.
os: ${{ (matrix.arch == 'x64' && vars.SELF_HOSTED) && 'orca-win-server' || matrix.os }}
arch: ${{ matrix.arch }}
os: ${{ vars.SELF_HOSTED && 'orca-win-server' || 'windows-latest' }}
build-deps-only: ${{ inputs.build-deps-only || false }}
force-build: ${{ github.event_name == 'schedule' }}
secrets: inherit
@@ -124,9 +95,7 @@ jobs:
secrets: inherit
unit_tests:
name: Unit Tests
# Tests are built on the aarch64 leg by default (faster GitHub arm runner),
# so run them there; self-hosted builds them on the amd64 server instead.
runs-on: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04-arm' }}
runs-on: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }}
needs: build_linux
if: ${{ !cancelled() && success() }}
steps:
@@ -197,9 +166,6 @@ jobs:
date:
ver:
ver_pure:
# Belt-printer nightlies share the main nightly release but carry a `_belt`
# suffix so they never overwrite the main assets.
nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: "Remove unneeded stuff to free disk space"
run:
@@ -258,12 +224,13 @@ jobs:
name: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
- name: Deploy Flatpak to nightly release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer')
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted

View File

@@ -33,12 +33,9 @@ jobs:
- name: set outputs
id: set_outputs
env:
# Keep macOS/Windows cache keys architecture-specific. amd64 Linux passes
# no arch (key stays 'linux-clang', preserving the existing cache);
# aarch64 gets its own 'linux-clang-aarch64' key.
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}', inputs.arch) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
# ARM64 builds use the build-arm64 tree (see build_release_vs.bat); x64/other use build.
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
# Keep macOS cache keys and paths architecture-specific.
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && 'windows' || 'linux-clang') }}
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || '/OrcaSlicer_dep' }}
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
run: |
echo cache-key=${{ env.cache-os }}-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }} >> ${{ env.output-cmd }}

View File

@@ -45,28 +45,11 @@ jobs:
key: ${{ inputs.cache-key }}
- uses: lukka/get-cmake@latest
# The windows-11-arm runner needs CMake <= 3.31 (handled in the next step).
if: ${{ !(runner.os == 'Windows' && inputs.arch == 'arm64') }}
with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
useLocalCache: true # <--= Use the local cache (default is 'false').
useCloudCache: true
- name: Install CMake 3.31.x (Windows ARM64)
# windows-11-arm ships CMake 4.x, which removed pre-3.5 policy
# compatibility AND has incomplete ASM_ARMASM linker modules
# (breaks Boost.Context on ARM64). Pin to the last 3.x release.
if: runner.os == 'Windows' && inputs.arch == 'arm64'
shell: pwsh
run: |
$ver = "3.31.6"
$url = "https://github.com/Kitware/CMake/releases/download/v$ver/cmake-$ver-windows-arm64.zip"
Invoke-WebRequest -Uri $url -OutFile "$env:RUNNER_TEMP\cmake.zip"
Expand-Archive -Path "$env:RUNNER_TEMP\cmake.zip" -DestinationPath "$env:RUNNER_TEMP\cmake" -Force
$cmakeBin = "$env:RUNNER_TEMP\cmake\cmake-$ver-windows-arm64\bin"
if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" }
Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin
- name: setup dev on Windows
if: runner.os == 'Windows'
uses: microsoft/setup-msbuild@v3
@@ -82,50 +65,6 @@ jobs:
shell: pwsh
- name: Install MSYS2 (clangarm64) with GMP/MPFR and LLVM tools
if: runner.os == 'Windows' && inputs.arch == 'arm64'
uses: msys2/setup-msys2@v2
with:
msystem: CLANGARM64
update: true
install: >-
mingw-w64-clang-aarch64-gmp
mingw-w64-clang-aarch64-mpfr
mingw-w64-clang-aarch64-llvm
- name: Stage ARM64 GMP/MPFR (no prebuilt blobs exist for win-arm64)
# GMP/MPFR ship prebuilt x64/x86 blobs in-tree but none for ARM64.
# Pull them from MSYS2 clangarm64 and generate MSVC import libs via
# llvm-dlltool, then stage into deps/{GMP,MPFR}/.../win-arm64 where the
# MSVC branch of GMP.cmake/MPFR.cmake copies them into the dep prefix.
if: runner.os == 'Windows' && inputs.arch == 'arm64'
shell: msys2 {0}
run: |
set -euo pipefail
BIN=/clangarm64/bin
REPO=$(cygpath -u "$GITHUB_WORKSPACE")
make_import_lib() {
local dll="$1"; local lib="$2"; local def="/tmp/${dll%.dll}.def"
echo "EXPORTS" > "$def"
llvm-readobj --coff-exports "$BIN/$dll" | awk '/Name: /{print $2}' >> "$def"
llvm-dlltool -m arm64 -D "$dll" -d "$def" -l "$BIN/$lib"
}
make_import_lib libgmp-10.dll libgmp-10.lib
# MPFR 4.x ships as libmpfr-6.dll; rename to libmpfr-4 BEFORE generating
# the import lib so the baked-in runtime DLL name is correct.
MPFR_DLL=$(ls $BIN/libmpfr-*.dll | head -1 | xargs basename)
if [ "$MPFR_DLL" != "libmpfr-4.dll" ]; then cp "$BIN/$MPFR_DLL" "$BIN/libmpfr-4.dll"; fi
make_import_lib libmpfr-4.dll libmpfr-4.lib
mkdir -p $REPO/deps/GMP/gmp/lib/win-arm64 $REPO/deps/MPFR/mpfr/lib/win-arm64
cp $BIN/libgmp-10.dll $BIN/libgmp-10.lib $REPO/deps/GMP/gmp/lib/win-arm64/
cp $BIN/libmpfr-4.dll $BIN/libmpfr-4.lib $REPO/deps/MPFR/mpfr/lib/win-arm64/
cp /clangarm64/include/gmp.h $REPO/deps/GMP/gmp/include/
cp /clangarm64/include/mpfr.h $REPO/deps/MPFR/mpfr/include/ || true
# Build Dependencies
- name: Build on Windows
if: runner.os == 'Windows'
@@ -134,14 +73,8 @@ jobs:
if (-not "${{ vars.SELF_HOSTED }}") {
choco install strawberryperl
}
$arch = "${{ inputs.arch }}"
if ($arch -eq "arm64") {
.\build_release_vs.bat deps arm64
.\build_release_vs.bat pack arm64
} else {
.\build_release_vs.bat deps
.\build_release_vs.bat pack
}
.\build_release_vs.bat deps
.\build_release_vs.bat pack
shell: pwsh
- name: Build on Mac ${{ inputs.arch }}

View File

@@ -29,11 +29,6 @@ jobs:
ubuntu-ver: '2404'
ubuntu-ver-str: '_Ubuntu2404'
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
# Branches whose builds are published to the nightly release. The
# belt-printer branch ships alongside main but its assets carry a `_belt`
# suffix (nightly_suffix) so they never overwrite the main nightly assets.
deploy_nightly: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' }}
nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: Checkout
@@ -50,28 +45,11 @@ jobs:
fail-on-cache-miss: true
- uses: lukka/get-cmake@latest
# The windows-11-arm runner needs CMake <= 3.31 (handled in the next step).
if: ${{ !(runner.os == 'Windows' && inputs.arch == 'arm64') }}
with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
useLocalCache: true # <--= Use the local cache (default is 'false').
useCloudCache: true
- name: Install CMake 3.31.x (Windows ARM64)
# windows-11-arm ships CMake 4.x, which removed pre-3.5 policy
# compatibility AND has incomplete ASM_ARMASM linker modules
# (breaks Boost.Context on ARM64). Pin to the last 3.x release.
if: runner.os == 'Windows' && inputs.arch == 'arm64'
shell: pwsh
run: |
$ver = "3.31.6"
$url = "https://github.com/Kitware/CMake/releases/download/v$ver/cmake-$ver-windows-arm64.zip"
Invoke-WebRequest -Uri $url -OutFile "$env:RUNNER_TEMP\cmake.zip"
Expand-Archive -Path "$env:RUNNER_TEMP\cmake.zip" -DestinationPath "$env:RUNNER_TEMP\cmake" -Force
$cmakeBin = "$env:RUNNER_TEMP\cmake\cmake-$ver-windows-arm64\bin"
if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" }
Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin
- name: Get the version and date on Ubuntu and macOS
if: runner.os != 'Windows'
run: |
@@ -87,11 +65,6 @@ jobs:
echo "ver_pure=$ver_pure" >> $GITHUB_ENV
echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV
# Per-arch Linux AppImage naming: amd64 keeps the historical unsuffixed
# name (arch_suffix empty). Unused on macOS/Windows.
if [ '${{ inputs.arch }}' = 'aarch64' ]; then
echo "arch_suffix=_aarch64" >> $GITHUB_ENV
fi
shell: bash
- name: Get the version and date on Windows
@@ -180,9 +153,17 @@ jobs:
run: |
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15
- name: Delete intermediate per-arch artifacts
if: runner.os == 'macOS' && inputs.macos-combine-only
uses: geekyeggo/delete-artifact@v6
with:
name: |
OrcaSlicer_Mac_bundle_arm64_${{ github.sha }}
OrcaSlicer_Mac_bundle_x86_64_${{ github.sha }}
# Thanks to RaySajuuk, it's working now
- name: Sign app and notary
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
@@ -190,8 +171,6 @@ jobs:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
CERTIFICATE_ID: ${{ secrets.MACOS_CERTIFICATE_ID }}
run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
source ${{ github.workspace }}/scripts/retry.sh
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH
@@ -214,7 +193,7 @@ jobs:
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications
retry hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Create separate OrcaSlicer_profile_validator DMG if the app exists
@@ -223,7 +202,7 @@ jobs:
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications
retry hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
@@ -238,16 +217,14 @@ jobs:
fi
- name: Create DMG without notary
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/belt-printer' && runner.os == 'macOS' && inputs.macos-combine-only
if: github.ref != 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
source ${{ github.workspace }}/scripts/retry.sh
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications
retry hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Create separate OrcaSlicer_profile_validator DMG if the app exists
if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
@@ -255,19 +232,9 @@ jobs:
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications
retry hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
# Delete the per-arch bundles only after signing/DMG creation succeeded, so a
# failed run keeps them available for a re-run instead of forcing a full rebuild.
- name: Delete intermediate per-arch artifacts
if: success() && runner.os == 'macOS' && inputs.macos-combine-only
uses: geekyeggo/delete-artifact@v6
with:
name: |
OrcaSlicer_Mac_bundle_arm64_${{ github.sha }}
OrcaSlicer_Mac_bundle_x86_64_${{ github.sha }}
- name: Upload artifacts mac
if: runner.os == 'macOS' && inputs.macos-combine-only
uses: actions/upload-artifact@v7
@@ -284,13 +251,13 @@ jobs:
if-no-files-found: ignore
- name: Deploy Mac release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
asset_name: OrcaSlicer_Mac_universal_nightly${{ env.nightly_suffix }}.dmg
asset_name: OrcaSlicer_Mac_universal_nightly.dmg
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
@@ -306,18 +273,6 @@ jobs:
max_releases: 1
# Windows
- name: Set Windows build variables
if: runner.os == 'Windows'
shell: pwsh
run: |
if ("${{ inputs.arch }}" -eq "arm64") {
"BUILD_DIR=build-arm64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"ARCH_SUFFIX=_arm64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
} else {
"BUILD_DIR=build" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"ARCH_SUFFIX=_x64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
}
- name: setup MSVC
if: runner.os == 'Windows'
uses: microsoft/setup-msbuild@v3
@@ -335,28 +290,23 @@ jobs:
# env:
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
# WindowsSDKVersion: '10.0.26100.0\'
run: |
$arch = "${{ inputs.arch }}"
if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 } else { .\build_release_vs.bat slicer }
shell: pwsh
run: .\build_release_vs.bat slicer
# NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's
# x86 emulation, packaging the native arm64 payload from build-arm64.
- name: Create installer Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
working-directory: ${{ github.workspace }}/build
run: |
cpack -G NSIS
- name: Pack app
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
working-directory: ${{ github.workspace }}/build
shell: cmd
run: '"C:/Program Files/7-Zip/7z.exe" a -tzip OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer'
run: '"C:/Program Files/7-Zip/7z.exe" a -tzip OrcaSlicer_Windows_${{ env.ver }}_portable.zip ${{ github.workspace }}/build/OrcaSlicer'
- name: Pack PDB
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/src/Release
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/build/src/Release
shell: cmd
run: '"C:/Program Files/7-Zip/7z.exe" a -m0=lzma2 -mx9 Debug_PDB_${{ env.ver }}_for_developers_only.7z *.pdb'
@@ -364,54 +314,54 @@ jobs:
if: runner.os == 'Windows'
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer
name: OrcaSlicer_Windows_${{ env.ver }}_portable
path: ${{ github.workspace }}/build/OrcaSlicer
- name: Upload artifacts Win installer
if: runner.os == 'Windows' && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer*.exe
name: OrcaSlicer_Windows_${{ env.ver }}
path: ${{ github.workspace }}/build/OrcaSlicer*.exe
- name: Upload artifacts Win PDB
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
if: runner.os == 'Windows' && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: PDB
path: ${{ github.workspace }}/build/src/Release/Debug_PDB_${{ env.ver }}_for_developers_only.7z
- name: Upload OrcaSlicer_profile_validator Win
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
if: runner.os == 'Windows' && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip
asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}_portable.zip
asset_path: ${{ github.workspace }}/build/OrcaSlicer_Windows_${{ env.ver }}_portable.zip
asset_name: OrcaSlicer_Windows_nightly_portable.zip
asset_content_type: application/x-zip-compressed
max_releases: 1
- name: Deploy Windows release installer
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}.exe
asset_path: ${{ github.workspace }}/build/OrcaSlicer_Windows_Installer_${{ env.ver }}.exe
asset_name: OrcaSlicer_Windows_Installer_nightly.exe
asset_content_type: application/x-msdownload
max_releases: 1
- name: Deploy Windows OrcaSlicer_profile_validator release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
@@ -421,26 +371,6 @@ jobs:
asset_content_type: application/x-msdownload
max_releases: 1
- name: Build MSIX Store package Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}
shell: pwsh
run: |
./scripts/msix/build_msix.ps1 `
-InstallDir "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" `
-OutputPath "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}.msix" `
-Architecture "${{ inputs.arch }}" `
-IdentityName "${{ vars.ORCA_MSIX_IDENTITY_NAME || 'OrcaSlicer.OrcaSlicer' }}" `
-Publisher "${{ vars.ORCA_MSIX_PUBLISHER || 'CN=38F7EA55-C73B-4072-B3B2-C8E0EA15BB82' }}" `
-PublisherDisplayName "${{ vars.ORCA_MSIX_PUBLISHER_DISPLAY_NAME || 'OrcaSlicer' }}"
- name: Upload artifacts Win MSIX
if: runner.os == 'Windows' && !vars.SELF_HOSTED
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}.msix
# Ubuntu
- name: Apt-Install Dependencies
if: runner.os == 'Linux' && !vars.SELF_HOSTED
@@ -452,23 +382,16 @@ jobs:
if: runner.os == 'Linux'
shell: bash
run: |
# Build + tar the unit tests (-t) only on the leg that runs them: the
# aarch64 leg by default (faster GitHub arm runner), or amd64 when using
# self-hosted runners (no arm self-hosted server). unit_tests downloads
# this tarball. The profile validator is built with -s, so amd64 keeps it.
tests=${{ (!vars.SELF_HOSTED && inputs.arch == 'aarch64') || (vars.SELF_HOSTED && inputs.arch != 'aarch64') }}
if $tests; then flags=-istrlL; else flags=-isrlL; fi
./build_linux.sh "$flags"
./build_linux.sh -istrlL
./scripts/check_appimage_libs.sh ./build/package ./build/package/bin/orca-slicer
appimage=./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
mv -n ./build/OrcaSlicer_Linux_V${{ env.ver_pure }}.AppImage "$appimage"
chmod +x "$appimage"
if $tests; then tar -cvpf build_tests.tar build/tests; fi
mv -n ./build/OrcaSlicer_Linux_V${{ env.ver_pure }}.AppImage ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_${{ env.ver }}.AppImage
chmod +x ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_${{ env.ver }}.AppImage
tar -cvpf build_tests.tar build/tests
# Use tar because upload-artifacts won't always preserve directory structure
# and doesn't preserve file permissions
- name: Upload Test Artifact
if: runner.os == 'Linux' && ((!vars.SELF_HOSTED && inputs.arch == 'aarch64') || (vars.SELF_HOSTED && inputs.arch != 'aarch64'))
if: runner.os == 'Linux'
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}-tests
@@ -478,7 +401,7 @@ jobs:
if-no-files-found: error
- name: Run external slicer regression tests
if: runner.os == 'Linux' && inputs.arch != 'aarch64'
if: runner.os == 'Linux'
timeout-minutes: 20
shell: bash
run: |
@@ -488,7 +411,7 @@ jobs:
python3 "$test_repo_dir/run_test.py" "${{ github.workspace }}/build/package/bin/orca-slicer"
- name: Build orca_custom_preset_tests
if: github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64'
if: github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/build/src/Release
shell: bash
run: |
@@ -500,28 +423,28 @@ jobs:
if: ${{ ! env.ACT && runner.os == 'Linux' }}
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Linux_ubuntu_${{ env.ubuntu-ver }}${{ env.arch_suffix }}_${{ env.ver }}
path: "./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage"
name: OrcaSlicer_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_${{ env.ver }}.AppImage'
- name: Upload OrcaSlicer_profile_validator Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/Release/OrcaSlicer_profile_validator'
- name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly${{ env.nightly_suffix }}.AppImage
asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_${{ env.ver }}.AppImage
asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_nightly.AppImage
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
- name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: rickstaa/action-create-tag@v1
with:
tag: "nightly-builds"
@@ -530,7 +453,7 @@ jobs:
message: "nightly-builds"
- name: Deploy Ubuntu OrcaSlicer_profile_validator release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
@@ -541,7 +464,7 @@ jobs:
max_releases: 1
- name: Deploy orca_custom_preset_tests
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}

View File

@@ -38,10 +38,10 @@ jobs:
- name: Download
working-directory: ${{ github.workspace }}
run: |
curl -L -o OrcaSlicer_profile_validator https://github.com/OrcaSlicer/OrcaSlicer/releases/download/nightly-builds/OrcaSlicer_profile_validator_Linux_Ubuntu2404_nightly
curl -LJO https://github.com/SoftFever/Orca_tools/releases/download/1/OrcaSlicer_profile_validator
chmod +x ./OrcaSlicer_profile_validator
# Validate all system profiles.
# validate profiles
- name: validate system profiles
id: validate_system
continue-on-error: true
@@ -49,14 +49,6 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]}
# For now run filament subtype check only for BBL profiles until we fix other vendors' profiles.
- name: validate filament subtype check for BBL profiles
id: validate_filament_subtypes
continue-on-error: true
run: |
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -v BBL -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
exit ${PIPESTATUS[0]}
- name: validate custom presets
id: validate_custom
@@ -76,7 +68,7 @@ jobs:
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
- name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: |
{
# Marker matched by check_profiles_comment.yml to delete prior comments.
@@ -102,15 +94,6 @@ jobs:
echo ""
fi
if [ "${{ steps.validate_filament_subtypes.outcome }}" = "failure" ]; then
echo "### BBL Filament Subtype Validation Failed"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_filament_subtypes.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_custom.outcome }}" = "failure" ]; then
echo "### Custom Preset Validation Failed"
echo ""
@@ -133,7 +116,7 @@ jobs:
retention-days: 1
- name: Fail if any check failed
if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: |
echo "One or more profile checks failed. See above for details."
exit 1

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Ask PR author for label
uses: actions/github-script@v9
uses: actions/github-script@v7
with:
script: |
function isPermissionDenied(error) {
@@ -88,7 +88,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Apply label command from PR author
uses: actions/github-script@v9
uses: actions/github-script@v7
with:
script: |
function isPermissionDenied(error) {

View File

@@ -73,9 +73,8 @@ jobs:
- name: Download release artifacts from build run
run: |
# Windows_V* (not Windows_*) keeps the MSIX Store artifact out: it goes to Partner Center, not GitHub releases.
gh run download "$RUN_ID" --repo "$GITHUB_REPOSITORY" --dir artifacts \
-p 'OrcaSlicer_Windows_V*' \
-p 'OrcaSlicer_Windows_*' \
-p 'OrcaSlicer_Mac_universal_*' \
-p 'OrcaSlicer_Linux_ubuntu_*' \
-p 'OrcaSlicer-Linux-flatpak_*' \
@@ -92,11 +91,9 @@ jobs:
# binaries are already unzipped. Copy the inner binary for each platform.
# -type f is required (some artifact *folders* are named "*.flatpak").
# Windows installers (x64 + arm64): the .exe inside each installer
# artifact, NOT the orca-slicer.exe in the portable app folder. CPack
# now bakes the arch into the filename (…_x64.exe / …_arm64.exe), so
# copy them straight through.
find artifacts -type f -name '*.exe' -path '*OrcaSlicer_Windows_V*' ! -path '*_portable*' -exec cp -v {} upload/ \;
# Windows installer: the .exe inside the installer artifact, NOT the
# orca-slicer.exe that lives in the portable app folder.
find artifacts -type f -name '*.exe' -path '*OrcaSlicer_Windows_*' ! -path '*_portable*' -exec cp -v {} upload/ \;
# macOS universal DMG (profile-validator DMG isn't downloaded).
find artifacts -type f -name '*.dmg' -path '*OrcaSlicer_Mac_universal_*' -exec cp -v {} upload/ \;
# Linux AppImage.
@@ -106,16 +103,15 @@ jobs:
# Windows debug symbols (PDB archive, for developers).
find artifacts -type f -name 'Debug_PDB_*.7z' -exec cp -v {} upload/ \;
# Portable Windows builds (x64 + arm64) are unzipped folder artifacts;
# re-zip each to its released filename (these stay .zip on the release).
mapfile -t portable_dirs < <(find artifacts -maxdepth 1 -type d -name 'OrcaSlicer_Windows_*_portable')
if [ ${#portable_dirs[@]} -eq 0 ]; then
echo "::warning::Windows portable artifact not found."
fi
for portable_dir in "${portable_dirs[@]}"; do
# Portable Windows build is an unzipped folder artifact; re-zip it to the
# released filename (this one stays a .zip on the release).
portable_dir=$(find artifacts -maxdepth 1 -type d -name 'OrcaSlicer_Windows_*_portable' | head -n1)
if [ -n "${portable_dir:-}" ]; then
( cd "$portable_dir" && zip -qr "$GITHUB_WORKSPACE/upload/$(basename "$portable_dir").zip" . )
echo "Zipped portable -> $(basename "$portable_dir").zip"
done
else
echo "::warning::Windows portable artifact not found."
fi
echo "Assets to upload:"
ls -lh upload

4
.gitignore vendored
View File

@@ -46,6 +46,4 @@ test.js
internal_docs/
*.flatpak
/flatpak-repo/
# Python bytecode
__pycache__/
*.pyc
*.pyc

View File

@@ -934,16 +934,6 @@ set (CPACK_PACKAGE_VERSION_MAJOR "${ORCA_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${ORCA_VERSION_MINOR}")
set (CPACK_PACKAGE_VERSION_PATCH "${ORCA_VERSION_PATCH}")
set (CPACK_PACKAGE_FILE_NAME "OrcaSlicer_Windows_Installer_V${SoftFever_VERSION}")
# Suffix the Windows installer with its target arch so the x64 and arm64 builds
# produce distinct filenames (matches ARCH_SUFFIX in build_orca.yml). Same
# CMAKE_SYSTEM_PROCESSOR mapping used by orcaslicer_copy_dlls() above.
if (WIN32)
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
string (APPEND CPACK_PACKAGE_FILE_NAME "_arm64")
else ()
string (APPEND CPACK_PACKAGE_FILE_NAME "_x64")
endif ()
endif ()
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Orca Slicer is an open source slicer for FDM printers")
set (CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/OrcaSlicer/OrcaSlicer")
set (CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})

View File

@@ -161,8 +161,6 @@ It can also be installed through graphical software managers (KDE Discover, GNOM
### AppImage
AppImages are published for both **x86_64** and **aarch64** (ARM64). Pick the file matching your CPU — the ARM64 build has `aarch64` in its name (e.g. `OrcaSlicer_Linux_AppImage_Ubuntu2404_aarch64_*.AppImage`).
1. Download App image from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases).
2. Double click the downloaded file to run it.

View File

@@ -504,24 +504,13 @@ if [[ -n "${USE_LLD}" ]] ; then
fi
fi
# Auto-detect ccache for faster rebuilds
export CMAKE_CCACHE_ARGS=()
CMAKE_CCACHE=${CMAKE_CCACHE:-}
if [ -n "$CMAKE_CCACHE" ]; then
echo "Checking ${CMAKE_CCACHE} environment variable for compiler cache program..."
CMAKE_CCACHE=$(command -v "${CMAKE_CCACHE}") || {
echo "CMAKE_CCACHE environment variable is set to '${CMAKE_CCACHE}' but it was not found in PATH."
CMAKE_CCACHE=""
}
elif command -v sccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v sccache)
elif command -v ccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v ccache)
fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
if command -v ccache >/dev/null 2>&1 ; then
echo "ccache found at $(command -v ccache), enabling compiler caching..."
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache)
else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
echo "Note: ccache not found. Install ccache for faster rebuilds."
fi
if [[ -n "${BUILD_DEPS}" ]] ; then
@@ -536,7 +525,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
fi
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake --build deps/$BUILD_DIR -j1
fi

View File

@@ -3,17 +3,6 @@
set WP=%CD%
set _START_TIME=%TIME%
@REM Default target architecture to the host CPU arch; override by passing
@REM "x64" or "arm64" as an argument. PROCESSOR_ARCHITEW6432 covers a 32-bit
@REM shell running on a 64-bit OS, where PROCESSOR_ARCHITECTURE reads "x86".
set arch=x64
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set arch=ARM64
if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" set arch=ARM64
if /I "%1"=="arm64" set arch=ARM64
if /I "%2"=="arm64" set arch=ARM64
if /I "%1"=="x64" set arch=x64
if /I "%2"=="x64" set arch=x64
@REM Check for Ninja Multi-Config option (-x)
set USE_NINJA=0
for %%a in (%*) do (
@@ -79,13 +68,12 @@ echo Using CMake generator: %CMAKE_GENERATOR%
@REM Pack deps
if "%1"=="pack" (
setlocal ENABLEDELAYEDEXPANSION
setlocal ENABLEDELAYEDEXPANSION
cd %WP%/deps/build
if "%arch%"=="ARM64" cd %WP%/deps/build-arm64
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do set build_date=%%c%%b%%a
echo packing deps: OrcaSlicer_dep_win-!arch!_!build_date!_vs!VS_VERSION!.zip
echo packing deps: OrcaSlicer_dep_win64_!build_date!_vs!VS_VERSION!.zip
%WP%/tools/7z.exe a OrcaSlicer_dep_win-!arch!_!build_date!_vs!VS_VERSION!.zip OrcaSlicer_dep
%WP%/tools/7z.exe a OrcaSlicer_dep_win64_!build_date!_vs!VS_VERSION!.zip OrcaSlicer_dep
goto :done
)
@@ -107,10 +95,9 @@ if "%debug%"=="ON" (
set build_dir=build
)
)
if "%arch%"=="ARM64" set build_dir=%build_dir%-arm64
echo build type set to %build_type%, arch=%arch%
echo build type set to %build_type%
setlocal DISABLEDELAYEDEXPANSION
setlocal DISABLEDELAYEDEXPANSION
cd deps
mkdir %build_dir%
cd %build_dir%
@@ -129,7 +116,7 @@ if "%USE_NINJA%"=="1" (
cmake ../ -G %CMAKE_GENERATOR% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target deps
) else (
cmake ../ -G %CMAKE_GENERATOR% -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
cmake ../ -G %CMAKE_GENERATOR% -A x64 -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target deps -- -m
)
@echo off
@@ -148,7 +135,7 @@ if "%USE_NINJA%"=="1" (
cmake .. -G %CMAKE_GENERATOR% -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target ALL_BUILD
) else (
cmake .. -G %CMAKE_GENERATOR% -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type%
cmake .. -G %CMAKE_GENERATOR% -A x64 -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target ALL_BUILD -- -m
)
@echo off

View File

@@ -15,18 +15,10 @@ if "%1"=="pack" (
set debug=OFF
set debuginfo=OFF
@REM Default target architecture to the host CPU arch; override with x64/arm64 arg.
set arch=x64
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set arch=ARM64
if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" set arch=ARM64
if "%1"=="debug" set debug=ON
if "%2"=="debug" set debug=ON
if "%1"=="debuginfo" set debuginfo=ON
if "%2"=="debuginfo" set debuginfo=ON
if /I "%1"=="arm64" set arch=ARM64
if /I "%2"=="arm64" set arch=ARM64
if /I "%1"=="x64" set arch=x64
if /I "%2"=="x64" set arch=x64
if "%debug%"=="ON" (
set build_type=Debug
set build_dir=build-dbg
@@ -39,8 +31,7 @@ if "%debug%"=="ON" (
set build_dir=build
)
)
if "%arch%"=="ARM64" set build_dir=%build_dir%-arm64
echo build type set to %build_type%, arch=%arch%
echo build type set to %build_type%
setlocal DISABLEDELAYEDEXPANSION
cd deps
@@ -57,7 +48,7 @@ echo "building deps.."
echo on
REM Set minimum CMake policy to avoid <3.5 errors
set CMAKE_POLICY_VERSION_MINIMUM=3.5
cmake ../ -G "Visual Studio 17 2022" -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
cmake ../ -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target deps -- -m
@echo off
@@ -71,7 +62,7 @@ cd %build_dir%
echo on
set CMAKE_POLICY_VERSION_MINIMUM=3.5
cmake .. -G "Visual Studio 17 2022" -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type%
cmake .. -G "Visual Studio 17 2022" -A x64 -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target ALL_BUILD -- -m
@echo off
cd ..

View File

@@ -124,8 +124,6 @@ endif()
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "x64" OR "${CMAKE_GENERATOR}" MATCHES "Win64")
set(_arch "x64")
elseif("${CMAKE_GENERATOR_PLATFORM}" MATCHES "ARM64")
set(_arch "x64") # GLEW ships one header set; ARM64 uses the x64 import path
else()
set(_arch "Win32")
endif()

View File

@@ -10,22 +10,7 @@ if (APPLE AND CMAKE_OSX_ARCHITECTURES)
set(_context_arch_line "-DBOOST_CONTEXT_ARCHITECTURE:STRING=${CMAKE_OSX_ARCHITECTURES}")
endif ()
# Windows ARM64: Boost.Context's default fcontext implementation assembles .asm
# via armasm64, which trips a CMake ASM_ARMASM linker-module bug under the VS
# generator. The winfib implementation (Windows Fiber API) avoids assembly while
# keeping the Boost::context target that Boost.Asio's stackful coroutines need.
set(_context_impl_line "")
if (MSVC AND "${DEPS_ARCH}" STREQUAL "arm64")
set(_context_impl_line "-DBOOST_CONTEXT_IMPLEMENTATION:STRING=winfib")
endif ()
set(_options "")
if (MSVC AND DEP_DEBUG)
set(_options "FORWARD_CONFIG")
endif ()
orcaslicer_add_cmake_project(Boost
${_options}
URL "https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.gz"
URL_HASH SHA256=4d27e9efed0f6f152dc28db6430b9d3dfb40c0345da7342eaa5a987dde57bd95
LIST_SEPARATOR |
@@ -37,7 +22,6 @@ orcaslicer_add_cmake_project(Boost
-DBOOST_IOSTREAMS_ENABLE_ZSTD:BOOL=OFF
"${_context_abi_line}"
"${_context_arch_line}"
"${_context_impl_line}"
)
set(DEP_Boost_DEPENDS ZLIB)

24
deps/CMakeLists.txt vendored
View File

@@ -155,25 +155,17 @@ if (NOT _is_multi AND NOT CMAKE_BUILD_TYPE)
endif ()
function(orcaslicer_add_cmake_project projectname)
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN})
cmake_parse_arguments(P_ARGS "" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN})
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
if (_is_multi OR MSVC)
if (P_ARGS_FORWARD_CONFIG)
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
elseif (ORCA_INCLUDE_DEBUG_INFO AND NOT DEP_DEBUG)
if (ORCA_INCLUDE_DEBUG_INFO AND NOT DEP_DEBUG)
set(_configs_line "-DCMAKE_C_FLAGS_RELEASE:STRING=${CMAKE_C_FLAGS_RELWITHDEBINFO} -DCMAKE_CXX_FLAGS_RELEASE:STRING=${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
else ()
set(_configs_line "")
endif ()
endif ()
if (P_ARGS_FORWARD_CONFIG)
set(_target_config "$<CONFIG>")
else()
set(_target_config "Release")
endif()
if (MSVC)
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}")
else()
@@ -204,8 +196,6 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
-DCMAKE_DEBUG_POSTFIX:STRING=d
-DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER}
-DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER}
-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER}
-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER}
-DCMAKE_TOOLCHAIN_FILE:STRING=${CMAKE_TOOLCHAIN_FILE}
-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS}
-DCMAKE_SHARED_LINKER_FLAGS:STRING=${CMAKE_SHARED_LINKER_FLAGS}
@@ -216,8 +206,8 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
${DEP_CMAKE_OPTS}
${P_ARGS_CMAKE_ARGS}
${P_ARGS_UNPARSED_ARGUMENTS}
BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${_target_config} -- ${_build_j}
INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config ${_target_config}
BUILD_COMMAND ${CMAKE_COMMAND} --build . --config Release -- ${_build_j}
INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config Release
)
if (FLATPAK)
@@ -251,16 +241,14 @@ else()
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER}
-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER}
-DBUILD_SHARED_LIBS:BOOL=OFF
${_cmake_osx_arch}
"${_configs_line}"
${DEP_CMAKE_OPTS}
${P_ARGS_CMAKE_ARGS}
${P_ARGS_UNPARSED_ARGUMENTS}
BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${_target_config} -- ${_build_j}
INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config ${_target_config}
BUILD_COMMAND ${CMAKE_COMMAND} --build . --config Release -- ${_build_j}
INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config Release
)
endif()

View File

@@ -1,10 +1,4 @@
set(_options "")
if (MSVC AND DEP_DEBUG)
set(_options "FORWARD_CONFIG")
endif ()
orcaslicer_add_cmake_project(Draco
${_options}
URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip
URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,13 +1,7 @@
# Intel IPP / IPP-ICV is x86/x64 only — there is no ARM64 build, so enabling it
# leaves ~200 unresolved ippicv* externals at link time on Windows ARM64.
if (MSVC AND NOT "${DEPS_ARCH}" STREQUAL "arm64")
if (MSVC)
set(_use_IPP "-DWITH_IPP=ON")
if (DEP_DEBUG)
set(_options "FORWARD_CONFIG")
endif ()
else ()
set(_use_IPP "-DWITH_IPP=OFF")
set(_options "")
endif ()
if (IN_GIT_REPO)
@@ -15,7 +9,6 @@ if (IN_GIT_REPO)
endif ()
orcaslicer_add_cmake_project(OpenCV
${_options}
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277
PATCH_COMMAND git apply ${OpenCV_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-vs.patch ${CMAKE_CURRENT_LIST_DIR}/0002-clang19-macos.patch

View File

@@ -32,17 +32,6 @@ else()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(_patch_cmd ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/0001-OpenEXR-GCC13.patch)
elseif (MSVC AND "${DEPS_ARCH}" STREQUAL "arm64")
# Windows ARM64: OpenEXR 2.5.5 hard-codes IMF_HAVE_SSE2 for any MSVC
# (ImfSimd.h: `_MSC_VER >= 1300`), pulling in <emmintrin.h> (x86-only) -> C1189.
# Patch the header to require an x86 target, and force the SSE cache vars off.
set(_patch_cmd ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/patch_openexr_arm64.cmake)
set(_openexr_arm64_args
-DOPENEXR_IMF_HAVE_SSE2:BOOL=OFF
-DOPENEXR_IMF_HAVE_SSSE3:BOOL=OFF
-DILMBASE_HAVE_SSE:BOOL=OFF
-DILMBASE_FORCE_DISABLE_INTEL_SSE:BOOL=ON
)
else ()
set(_patch_cmd "")
endif ()
@@ -60,7 +49,6 @@ orcaslicer_add_cmake_project(OpenEXR
-DPYILMBASE_ENABLE:BOOL=OFF
-DOPENEXR_VIEWERS_ENABLE:BOOL=OFF
-DOPENEXR_BUILD_UTILS:BOOL=OFF
${_openexr_arm64_args}
)
endif()

View File

@@ -1,29 +0,0 @@
# Applied as PATCH_COMMAND for OpenEXR 2.5.5 on Windows ARM64.
#
# Root cause of the ARM64 build failure: OpenEXR/IlmImf/ImfSimd.h hard-codes
# #if defined __SSE2__ || (_MSC_VER >= 1300 && !_M_CEE_PURE)
# #define IMF_HAVE_SSE2 1
# #endif
# The `_MSC_VER >= 1300` arm is true for *every* MSVC, including ARM64, so
# IMF_HAVE_SSE2 gets defined and <emmintrin.h> (an x86-only header) is pulled
# in -> error C1189. This is a pure-preprocessor decision, so no CMake cache
# variable can suppress it. Patch the header to also require an x86 target.
set(_simd "OpenEXR/IlmImf/ImfSimd.h")
if(EXISTS "${_simd}")
file(READ "${_simd}" _content)
set(_old "#if defined __SSE2__ || (_MSC_VER >= 1300 && !_M_CEE_PURE)")
set(_new "#if (defined __SSE2__ || (_MSC_VER >= 1300 && !_M_CEE_PURE)) && (defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__x86_64__))")
if(_content MATCHES "_M_IX86")
message(STATUS "[ARM64 patch] ImfSimd.h already guarded")
else()
string(REPLACE "${_old}" "${_new}" _patched "${_content}")
if(_patched STREQUAL _content)
message(FATAL_ERROR "[ARM64 patch] Failed to match SSE2 guard in ${_simd}")
endif()
file(WRITE "${_simd}" "${_patched}")
message(STATUS "[ARM64 patch] Guarded IMF_HAVE_SSE2 with x86 arch check in ${_simd}")
endif()
else()
message(FATAL_ERROR "[ARM64 patch] Not found: ${_simd}")
endif()

View File

@@ -6,11 +6,7 @@ if(DEFINED OPENSSL_ARCH)
set(_cross_arch ${OPENSSL_ARCH})
else()
if(WIN32)
if("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "ARM64")
set(_cross_arch "VC-WIN64-ARM")
else()
set(_cross_arch "VC-WIN64A")
endif()
set(_cross_arch "VC-WIN64A")
elseif(APPLE)
set(_cross_arch "darwin64-${CMAKE_OSX_ARCHITECTURES}-cc")
endif()

View File

@@ -267,7 +267,7 @@ void ImGui::Text(const char* fmt, ...)
void ImGui::TextCentered(const char* text, ...)
{
va_list vaList;
va_start(vaList, text);
va_start(vaList,&text);
float font_size = ImGui::GetFontSize() * strlen(text) / 2;
ImGui::SameLine(ImGui::GetCursorPos().x / 2 - font_size + (font_size / 2));

View File

@@ -1123,17 +1123,18 @@ private:
std::vector<RawShape> objs,excludes;
for (const Item &item : items_) {
if (item.isFixed())
excludes.push_back(item.transformedShape());
else
objs.push_back(item.transformedShape());
if (item.isFixed()) continue;
objs.push_back(item.transformedShape());
}
if (objs.empty())
return;
// Without fixed items this inner-fit NFP can exceed clipper's range and crash MSVC.
if (!excludes.empty())
{ // find a best position inside NFP of fixed items (excluded regions), so the center of pile is cloest to bed center
RawShape objs_convex_hull = sl::convexHull(objs);
for (const Item &item : items_) {
if (item.isFixed()) {
excludes.push_back(item.transformedShape());
}
}
auto nfps = calcnfp(objs_convex_hull, excludes, bbin, Lvl<MaxNfpLevel::value>());
if (nfps.empty()) {

668
doc/automation.md Normal file
View File

@@ -0,0 +1,668 @@
# OrcaSlicer UI Automation Protocol (v1.0.0)
OrcaSlicer ships an **opt-in, localhost-only JSON-RPC server** that lets external
scripts introspect, drive, and screenshot the running OrcaSlicer GUI. It is built
for end-to-end testing and automation: a script can enumerate the live widget
tree, click buttons, type text, send keyboard shortcuts, wait for UI state, query
high-level application state, load models/projects into the running instance,
switch the active view/tab, and capture window images (the on-screen capture
includes the 3D viewport).
This document is the protocol reference. It describes activation, the transport,
the JSON-RPC envelope, every method, the unified node shape, the target/locator
model, error codes, the set of instrumented automation ids, ImGui specifics,
platform caveats, a quick-start snippet, and planned future work.
---
## 1. Overview & activation
The automation server is **OFF by default**. It is enabled with two
command-line flags:
| Flag | Meaning |
|---|---|
| `--automation-server` | Enable the automation server. |
| `--automation-server-port=PORT` | Override the listening port. Optional; default is **13619**. |
Example:
```bash
OrcaSlicer --automation-server --automation-server-port=13619 model.stl
```
The server binds to **`127.0.0.1` only** (the loopback interface). It is never
exposed on an external network interface.
**Security note (v1):** there is **no authentication token** in v1. The localhost
bind is the *only* security boundary. Any process able to run code on the machine
can connect to the port and drive the GUI — including injecting mouse and keyboard
input — while the server is enabled. The feature is intended for testing and
automation environments, not for production or shared/multi-user machines.
When the server is enabled, OrcaSlicer emits a `warning`-level log line at startup
to make the active input-injection surface obvious in logs, for example:
```
UI automation server ENABLED ... input injection is active
```
---
## 2. Transport
The server speaks **HTTP/1.1** over the loopback TCP socket:
| Request | Response |
|---|---|
| `POST /jsonrpc` with a JSON-RPC 2.0 request body | A JSON-RPC 2.0 response with `Content-Type: application/json`. |
| `GET /` | A plain-text health page: `OrcaSlicer automation server v1.0.0` (`Content-Type: text/plain`). |
| Anything else | HTTP `404 Not Found`. |
The server is **single-client / serialized** in v1: it handles one request at a
time on its own dedicated I/O thread. Connections are not kept alive; each request
is answered and the socket is closed. Clients should issue requests sequentially.
---
## 3. JSON-RPC envelope
The protocol follows **JSON-RPC 2.0**.
**Request:**
```json
{ "jsonrpc": "2.0", "id": <id>, "method": "<method>", "params": { ... } }
```
- `params` may be omitted; the server treats a missing `params` as an empty object.
**Success response:**
```json
{ "jsonrpc": "2.0", "id": <id>, "result": { ... } }
```
**Error response:**
```json
{ "jsonrpc": "2.0", "id": <id>, "error": { "code": <int>, "message": "<string>" } }
```
The request `id` is echoed back in the response. When the request has no `id`, or
when the request body cannot be parsed as JSON, the response `id` is `null`.
---
## 4. Methods
There are 12 methods. Capabilities advertised by `automation.version` list the 11
callable feature methods (every method except `automation.version` itself).
### `automation.version`
Returns server identity and the list of supported methods. Takes no parameters.
**Result:**
```json
{
"version": "1.0.0",
"protocol": "2.0",
"capabilities": [
"tree.dump", "tree.find", "widget.get", "input.click", "input.type",
"input.key", "sync.wait_for", "app.state", "screenshot.window", "file.open",
"view.select"
]
}
```
### `tree.dump`
Snapshot the live UI tree as a single root node with nested children.
**Params (all optional):**
| Param | Type | Default | Meaning |
|---|---|---|---|
| `root` | string (id or path) | full tree | Root the dump at the node with this id/path. |
| `max_depth` | int | `-1` | Maximum depth to descend. `-1` = unlimited. |
| `visible_only` | bool | `false` | When true, omit non-visible nodes. |
| `include_imgui` | bool | `true` | When true, include ImGui items. |
**Result:** the serialized root [node](#5-unified-node-shape), with `children`
included.
### `tree.find`
Find all nodes matching a [target predicate](#6-target--locator).
**Params:** a target predicate — any combination of `name`, `class`, `label`,
`value`, `backend` (provided fields are ANDed). The params object is the target
itself (it is *not* wrapped in a `target` key for this method).
**Result:** a **flat JSON array** of matching nodes. The nodes in this array are
returned **without** their `children` (use `widget.get`/`tree.dump` to descend).
### `widget.get`
Fetch a single node by [target](#6-target--locator).
**Params:**
| Param | Type | Required | Meaning |
|---|---|---|---|
| `target` | object | yes | Target spec (id / path / predicate). |
**Result:** a single [node](#5-unified-node-shape), with its `children` included.
**Errors:** `1001` if the target is **not found** *or* **ambiguous** (more than one
match).
### `input.click`
Click a resolved, actionable node.
**Params:**
| Param | Type | Default | Meaning |
|---|---|---|---|
| `target` | object | required | Target spec; must resolve to exactly one node. |
| `button` | string | `"left"` | `"left"`, `"right"`, or `"middle"`. |
| `double` | bool | `false` | Double-click when true. |
| `modifiers` | array of string | `[]` | Held modifiers: any of `"ctrl"`, `"shift"`, `"alt"`, `"cmd"` (`"meta"` is accepted as an alias of `"cmd"`). |
**Result:** `{ "ok": true }`.
**Errors:** `1001` not found / ambiguous; `1002` if the target is disabled or
hidden (not actionable). The click path raises and focuses the target's top-level
window before injecting the click.
### `input.type`
Type text into the currently focused control.
**Params:**
| Param | Type | Required | Meaning |
|---|---|---|---|
| `text` | string | yes | The text to type. |
| `target` | object | no | If given, this node is clicked first (to focus it) before typing. |
**Result:** `{ "ok": true }`.
**Errors:** if `target` is supplied, the same actionability errors as
`input.click` apply (`1001` / `1002`).
### `input.key`
Send a key chord (a key plus optional modifiers) to the focused window.
**Params:**
| Param | Type | Required | Meaning |
|---|---|---|---|
| `keys` | string or array | yes | Either a `"+"`-joined string like `"ctrl+s"`, or an array like `["ctrl", "s"]`. The last token is the key; earlier tokens are modifiers. |
**Result:** `{ "ok": true }`.
**Key names must be lowercase.** Recognized key names include `"enter"`, `"tab"`,
`"esc"`, `"space"`, `"delete"`, `"backspace"`, `"f5"` (and other function keys),
and single characters (e.g. `"s"`, `"a"`). Recognized modifiers are `"ctrl"`,
`"shift"`, `"alt"`, `"cmd"` (with `"meta"` as an alias for `"cmd"`).
**Unrecognized or uppercase key names are silently ignored** — no error is
returned, the key simply does not fire. Use lowercase names exclusively.
### `sync.wait_for`
Poll the UI until a target node reaches a desired state, or time out. This is the
preferred way to synchronize with asynchronous UI changes (it replaces fragile
fixed sleeps). Internally it repeatedly refreshes and dumps the tree, re-resolves
the target, and evaluates the requested state until it is satisfied.
**Params:**
| Param | Type | Default | Meaning |
|---|---|---|---|
| `target` | object | required | Target spec. |
| `state` | string | required | One of `"exists"`, `"visible"`, `"enabled"`, `"value"`. |
| `value` | string | — | Required when `state` is `"value"`; the expected value to match. |
| `timeout_ms` | int | `5000` | Maximum time to wait, in milliseconds. |
| `poll_ms` | int | `100` | Poll interval, in milliseconds (minimum 1). |
State semantics:
- `exists` — the target resolves to a node.
- `visible` — the node exists and is visible.
- `enabled` — the node exists and is **both enabled and visible**.
- `value` — the node has a value and that value equals the supplied `value`.
**Result:** `{ "ok": true, "elapsed_ms": <int> }`.
**Errors:** `1003` on timeout (the state was not reached within `timeout_ms`).
### `app.state`
Return a high-level application-state snapshot. Takes no parameters.
**Result:**
```json
{
"active_tab": "<string>",
"project_loaded": <bool>,
"slicing": <bool>,
"slice_progress": <int>,
"foreground": <bool>,
"modal_dialog": "<string>"
}
```
| Field | Meaning |
|---|---|
| `active_tab` | The active top-level tab/page. |
| `project_loaded` | Whether a project/model is currently loaded. |
| `slicing` | Whether slicing is currently in progress. |
| `slice_progress` | Slicing progress (`-1` when unknown). |
| `foreground` | Whether the main window is in the foreground. |
| `modal_dialog` | Present only when a modal dialog is active; identifies it. Omitted otherwise. |
### `screenshot.window`
Capture a window as a PNG, exactly as it appears on screen.
**Params:**
| Param | Type | Default | Meaning |
|---|---|---|---|
| `target` | object | main frame | If given, capture this window; otherwise capture the main frame. |
**Result:** `{ "png_base64": "<base64 PNG>", "width": <int>, "height": <int> }`.
**Errors:** `1005` on screenshot failure; `1001` if a supplied `target` is not
found or ambiguous.
**How it works:** the window's on-screen rectangle is read back from the
DWM-composited desktop framebuffer (`wxScreenDC`), so the capture includes every
native child control, the OpenGL 3D viewport, and ImGui overlays — it is a faithful
image of what the user sees. (Capturing the parent window's own client DC instead
would clip out child HWNDs and the GL surface, leaving them black; that is why this
method reads from the screen.)
**Caveats:**
- The window must be **visible and unobscured**. Because the source is the on-screen
framebuffer, any overlapping window occludes the captured region. The backend
raises the target window before capturing.
- **HiDPI:** the reported `width`/`height` come from the window's logical client size,
while the screen framebuffer is in physical pixels. On per-monitor-DPI displays the
two can differ; the capture may be cropped or scaled relative to the logical size.
- Because the capture is the live on-screen image, the 3D content reflects the
**current view**: the model in the 3D editor, or the gcode toolpaths in Preview
after a slice. There is no separate offscreen 3D-render method — the window
capture already includes whatever the GL canvas is showing.
### `file.open`
Load one or more files into the **already-running** instance at runtime, by calling
`Plater::load_files(...)` directly on the GUI thread. This is the supported way to add
or swap a model without relaunching the process. Loading is **synchronous**: when the
call returns `ok: true`, `app.state().project_loaded` is already `true` (no polling
race).
**Params:**
| Param | Type | Required | Meaning |
|---|---|---|---|
| `paths` | string or array of strings | yes | One or more **absolute** file paths. A bare string is accepted and treated as a one-element list. Paths are read from the **host (server) filesystem** — client and server are localhost-only. |
`.3mf` files are routed as projects and meshes as models automatically, based on file
content (the same default strategy as drag-drop); there is no `as_project` flag in v1.
**Result:** `{ "ok": true, "loaded": <int> }`, where `loaded` is the number of objects
added to the scene (`load_files(...).size()`).
**Errors:**
- `-32602` (invalid params) — `paths` is missing, is not a string/array, contains a
non-string entry, or yields no non-empty path.
- `1007` (load failed) — `load_files` returned empty or threw (file not found, parse
error, or unsupported format).
- `1004` (GUI busy) — the GUI-thread marshal timed out. An extremely large model can
exceed the marshal timeout and surface here; documented, not mitigated in v1.
### `view.select`
Switch the main window to a top-level view/tab at runtime. Useful to put the UI in a
known state before other actions — e.g. switch to **Prepare** before loading a model,
or to **Preview** after slicing.
**Params:**
| Param | Type | Required | Meaning |
|---|---|---|---|
| `view` | string | yes | The target view. One of: `home`, `prepare` (3D editor), `preview` (sliced G-code), `device`, `multi_device`, `project`, `calibration`. |
**Result:** `{ "ok": true, "view": <string>, "index": <int> }`, where `index` is the
resulting tab index (it can vary with layout, since some tabs — e.g. `multi_device`
are only present in certain configurations).
**Errors:**
- `-32602` (invalid params) — `view` is missing, is not a string, or is empty.
- `1001` (not found) — the view name is unknown, or that view is not available in the
current layout (for example `prepare`/`preview` in G-code-viewer mode, or
`multi_device` when multi-device is disabled).
- `1004` (GUI busy) — the GUI-thread marshal timed out.
---
## 5. Unified node shape
Both wx widgets and ImGui items are reported with the same node schema:
```json
{
"backend": "wx" | "imgui",
"id": "<string>",
"path": "<string>",
"class": "<string>",
"label": "<string>",
"rect": { "x": <int>, "y": <int>, "w": <int>, "h": <int> },
"enabled": <bool>,
"visible": <bool>,
"value": "<string>",
"children": [ <node>, ... ]
}
```
| Field | Meaning |
|---|---|
| `backend` | `"wx"` for native wxWidgets controls, `"imgui"` for immediate-mode ImGui items. |
| `id` | The automation id when one is set, otherwise a derived id. For ImGui items the `path` doubles as the `id`. |
| `path` | Positional path, e.g. `"MainFrame/Panel[2]/Button[0]"`. For ImGui items: `"ImGui/<window>/<label>"`. |
| `class` | wx class name, or the ImGui item type. |
| `label` | The control's label/caption. May include an ImGui `##`-id suffix for ImGui items. |
| `rect` | Bounding rectangle in **screen coordinates**. |
| `enabled` | Whether the control is enabled. |
| `visible` | Whether the control is visible. |
| `value` | The control's value (text/choice/check/slider, etc.). **Omitted entirely** when the control has no applicable value. |
| `children` | Child nodes. **wx only**, and present only when children are included (e.g. `tree.dump`, `widget.get`). ImGui items are flat (no children) and are listed under their window. |
Notes:
- The `value` key is **omitted** (not `null`) when the control has no value.
- `children` is present only for wx nodes when children are requested; ImGui nodes
never carry `children`.
---
## 6. Target / locator
Most methods accept a **target** object that identifies one or more nodes. A
target may specify:
| Field | Meaning |
|---|---|
| `id` | Exact automation id. |
| `path` | Exact positional path. |
| `name` | Predicate: matches either the node's `id` **or** its `label`. |
| `class` | Predicate: exact class name. |
| `label` | Predicate: exact label. |
| `value` | Predicate: node has a value and it equals this string. |
| `backend` | Predicate: `"wx"` or `"imgui"`. |
**Resolution order:** **`id``path` → predicate.**
- If `id` is present, only `id` is used (exact match).
- Else if `path` is present, only `path` is used (exact match).
- Else the predicate fields (`name`, `class`, `label`, `value`, `backend`) are
used, and all provided predicate fields are **ANDed** together.
Action methods (`input.click`, `input.type` with a target, `widget.get`, and
single-target `screenshot.window`) require a **unique** match. If the target
resolves to zero matches or more than one match, the call fails with error `1001`
(not found / ambiguous). `tree.find` is the exception: it returns *all* matches as
an array and never errors on ambiguity.
---
## 7. Error codes
Standard JSON-RPC codes:
| Code | Meaning |
|---|---|
| `-32700` | Parse error — the request body was not valid JSON. |
| `-32600` | Invalid request — missing/invalid `method`. |
| `-32601` | Method not found — unknown method name. |
| `-32602` | Invalid params — missing/invalid parameters for the method. |
Application-specific codes:
| Code | Meaning |
|---|---|
| `1001` | Widget/target not found **or** ambiguous (more than one match). |
| `1002` | Not actionable — the target is disabled or hidden. |
| `1003` | Wait timeout — `sync.wait_for` did not reach the requested state in time. |
| `1004` | GUI thread busy / timeout — a backend call could not be marshaled onto the GUI thread in time (wedged GUI). |
| `1005` | Screenshot failed. |
| `1006` | Disabled. |
| `1007` | Load failed — `file.open`'s `load_files` returned empty or threw (not found, parse error, unsupported format). |
---
## 8. Automation-id naming conventions & instrumented ids
Stable automation ids follow these prefix conventions:
| Prefix | Used for |
|---|---|
| `btn_` | Buttons |
| `combo_` | Preset combo boxes |
| `tab_` | Tabs |
| `canvas_` | Canvases |
| `dlg_` | Dialog buttons |
### Instrumented ids (as-built in v1)
The following controls currently carry stable automation ids:
| id | Control | Note |
|---|---|---|
| `btn_slice` | Slice-plate button | |
| `btn_export` | Print / Export button | Multi-purpose: the action (Print plate / Export G-code / Send) depends on the current mode. |
| `tab_device` | Device / Monitor tab (`MonitorPanel`) | |
| `combo_printer` | Printer preset combo (sidebar) | |
| `combo_filament` | Filament preset combo (sidebar) | First filament row only; extra multi-material rows are not instrumented. |
| `canvas_3d` | 3D editor GL canvas | |
### Controls NOT instrumented in v1
Several controls are intentionally **not** instrumented in v1 because they have no
stable `wxWindow` target to attach an id to:
- **`combo_process`** — process settings are not a sidebar combo box in the current
OrcaSlicer layout, so there is no combo control to instrument.
- **`btn_add`** — the add/import-object control is a `GLToolbar` item rendered
*inside* the GL canvas, not a `wxWindow`.
- **`tab_prepare` / `tab_preview`** — the Prepare and Preview notebook pages are
both backed by the **same** window, and the per-tab buttons are private; there is
no distinct stable window to target.
For controls that are not instrumented, scripts should fall back to class / label /
path lookup (for wx controls) or ImGui-item lookup (for ImGui controls).
---
## 9. ImGui notes
ImGui is **immediate-mode**: an item is addressable only while it is being drawn in
the current frame. The automation backend records ImGui items each frame, and a
`refresh_ui` is forced before every read or action so that the latest frame's items
are captured.
Consequences and conventions:
- Use [`sync.wait_for`](#syncwait_for) to wait for a transient gizmo or panel item
to appear before acting on it.
- ImGui items are reported with `backend: "imgui"`, a `path` of the form
`ImGui/<window>/<label>`, and that **path doubles as the item's `id`** in v1.
- ImGui items are **flat** — they have no `children` and are listed under their
window.
- Labels may include ImGui `##`-id suffixes (the part after `##` that ImGui uses to
disambiguate identically labeled widgets).
- Raw `ImGui::` gizmos that are *not* routed through the instrumented
`ImGuiWrapper` widgets (for example some Emboss / SVG / Text gizmo controls) are
only covered at the **window level** in v1; their individual sub-items are not
enumerated.
---
## 10. Platform & display caveats
- **Input requires a focused, visible window.** OS-level input injection uses
`wxUIActionSimulator`, which requires a focused, visible window. The click path
raises and focuses the target's top-level window first.
- **Linux CI needs a display.** There must be an X display available; wrap test
runs with `xvfb-run` (for example, `xvfb-run -a python example_slice.py ...`).
- **Input is asynchronous.** Do **not** rely on fixed sleeps. Use
[`sync.wait_for`](#syncwait_for) — for example, wait for `btn_export` to become
`enabled` after slicing completes — rather than sleeping for a guessed duration.
- **`screenshot.window` reads the screen.** It captures the on-screen, DWM-composited
framebuffer, so the target window must be visible and unobscured, and the result is
in physical pixels (see HiDPI caveat under [`screenshot.window`](#screenshotwindow)).
The capture includes the GL 3D viewport as currently shown (model or toolpaths).
- **Single-client / serialized.** v1 handles one request at a time; issue requests
sequentially from a single client.
- **GUI-thread marshaling.** Every backend call is marshaled onto the GUI thread
with a timeout. A wedged or unresponsive GUI returns error `1004`.
---
## 11. Quick start
Using the reference client in `tools/automation/orca_automation.py`:
```python
from orca_automation import OrcaClient
orca = OrcaClient(port=13619)
print(orca.version()) # {'version': '1.0.0', ...}
orca.select_view("prepare") # switch to the 3D editor
orca.open(r"C:\models\part.stl") # load a model at runtime (synchronous)
orca.click({"id": "btn_slice"}) # start slicing the plate
orca.wait_for({"id": "btn_export"}, # wait until slicing finishes
state="enabled", timeout_ms=180000)
orca.select_view("preview") # switch to the sliced G-code preview
png = orca.screenshot() # on-screen capture (incl. 3D view)
with open("window.png", "wb") as f:
f.write(png)
```
For a full, runnable end-to-end example — launching OrcaSlicer with the automation
flags, loading a model, slicing, waiting for completion, and saving a window PNG —
see `tools/automation/example_slice.py`.
---
## 12. Future work
Planned enhancements beyond v1:
- **Authentication token** plus a Preferences toggle to enable/disable the server
from the GUI.
- **WebSocket push events** for real-time UI/state notifications (instead of
polling).
- **Per-item ImGui gizmo instrumentation** so individual gizmo sub-controls (Emboss
/ SVG / Text, etc.) are addressable, not just at the window level.
- **More widget ids** — the process combo, the add/import button, and the
Prepare/Preview tabs once they expose stable windows.
- An **MCP wrapper** to expose the automation surface to model-context tooling.
---
## Verification (v1)
This section records the final regression gate for the v1 feature: confirmation
that the protocol core is covered by unit tests, that the existing test suites
are unaffected, and that the **disabled path (automation OFF, the default) is a
true no-op** — zero new threads, zero socket binds, zero allocations, and zero
behavior change.
### Unit-suite results (Release, Windows / MSVC, Ninja Multi-Config)
| Suite | Result |
|---|---|
| `automation` (protocol core) | **32 / 32 passed** |
| `libslic3r` (most affected by the additive `PrintConfig.cpp` CLI options) | **99 / 99 passed** |
| `fff_print` | **14 / 14 passed** |
| `libnest2d` | **14 / 14 passed** |
| `sla_print` | **21 / 21 passed** |
| `slic3rutils` | 3 / 5 passed — 2 pre-existing `[OrcaCloudServiceAgent]` SEGFAULTs, **unrelated to automation** (see note) |
> The two `slic3rutils` failures are `Orca cloud flat/nested session resolves
> display name consistently`. They exercise `Slic3r::OrcaCloudServiceAgent`, which
> the automation branch does **not** touch (verified via `git diff --stat
> main...HEAD` — no change to `src/slic3r/Utils/OrcaCloudServiceAgent.*` or
> `tests/slic3rutils/*`). They are pre-existing and not a regression introduced by
> this feature.
### Static disabled-path audit (the core regression guarantee)
Verified by code reading that with no `--automation-server` flag:
- **Flag defaults off.** `m_automation_port` defaults to `0`
(`src/slic3r/GUI/GUI_App.hpp:249`); `is_automation_enabled()` returns
`m_automation_port > 0` (`GUI_App.hpp:386`) → `false` by default.
- **No server / thread / socket.** `post_init()` calls
`start_automation_server()` **only** when
`init_params->automation_port > 0` (`src/slic3r/GUI/GUI_App.cpp:737-740`), and
`start_automation_server()` itself early-returns when `m_automation_port <= 0`
(`GUI_App.cpp:7097`). The backend / dispatcher / beast server objects are
constructed nowhere else → no `orca_automation` thread and no localhost bind
when the flag is absent.
- **Recording hooks short-circuit.** `ImGuiWrapper::automation_record_last_item`
has as its **first statement** `if (!wxGetApp().is_automation_enabled())
return;` (`src/slic3r/GUI/ImGuiWrapper.cpp:576-577`) — a single bool check, no
`ImGuiItemRecord` allocation and no `ImGuiItemTable` access on the disabled
path. In `ImGuiWrapper::render()` the window-enumeration loop and
`swap_frame()` are fully wrapped in `if (wxGetApp().is_automation_enabled())`
(`ImGuiWrapper.cpp:599-611`); when off, `render()` is its original
`ImGui::Render()` + `render_draw_data()` plus one bool check.
- **Instrumentation is inert.** The ~7 `set_automation_id(...)` calls
(`MainFrame.cpp:1330,1389,1841,1842`; `Plater.cpp:1772,2172,5068`) only store a
pointer into a static registry and bind a `wxEVT_DESTROY` pruning handler
(`src/slic3r/GUI/Automation/AutomationRegistry.cpp:24-36`). The registry is
**read** only via `window_for_automation_id` / `automation_id_of`, which are
called solely by the backend while the server is running → harmless when off.
- **CLI options are purely additive.** `automation_server` (coBool, default
`false`) and `automation_server_port` (coInt, default `13619`) are new `add()`
entries appended after `enable_timelapse`
(`src/libslic3r/PrintConfig.cpp:10794-10805`); no existing option is changed.
`GUI_InitParams::automation_port` defaults to `0`
(`src/slic3r/GUI/GUI_Init.hpp:37`) and is set only when `--automation-server`
is supplied (`src/OrcaSlicer.cpp:1345-1348`).
**Conclusion:** with automation OFF (the default), the feature allocates nothing
and changes nothing — the only added cost on any hot path is a single boolean
comparison.
### Deferred manual runtime checks (require a display / Xvfb)
These need a live GUI and cannot be run headlessly in CI; they are the manual
acceptance steps:
1. Launch **without** `--automation-server``curl http://127.0.0.1:13619/`
fails to connect (no listener); no `orca_automation` thread exists.
2. Launch **with** `--automation-server --automation-server-port=13619`
`GET /` returns the health text; `POST /jsonrpc {"method":"automation.version"}`
returns version / protocol / capabilities; `widget.get {"target":{"id":"btn_slice"}}`
returns a node with a sensible screen rect.
3. Interactive sanity: open a gizmo / move sliders with automation OFF → no
visual or behavior change.
See `tools/automation/example_slice.py` for the runnable end-to-end path.

View File

@@ -0,0 +1,608 @@
# `file.open` Automation Method Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a `file.open` JSON-RPC automation method that loads one or more files into an already-running OrcaSlicer instance by calling `Plater::load_files(...)` synchronously on the GUI thread.
**Architecture:** Follows the existing `screenshot.window` / `app.state` method pattern. A new pure-virtual `open_files(paths)` is added to the wx-free `IUiBackend` interface; `WxUiBackend` implements it via the existing `run_on_gui(...)` GUI-thread marshal calling `Plater::load_files`; the `JsonRpcDispatcher` gains a `file.open` route, a param-parsing helper, and a new `kErrLoadFailed = 1007` error code. The unit-testable surface (dispatcher + param validation + routing) is driven against `MockUiBackend`.
**Tech Stack:** C++17, nlohmann::json, Catch2 v2 (`catch_all.hpp` / `Catch2WithMain`), wxWidgets, CMake + Ninja Multi-Config. Python 3 reference client (stdlib only).
---
## Design-spec note (resolve before coding)
The design spec's error table reads `1002 | kInvalidParams | paths missing/empty…`, but in the codebase `kInvalidParams` is the standard JSON-RPC code **`-32602`**, while `1002` is `kErrNotActionable`. The spec's **Constant column (`kInvalidParams`) is authoritative** and matches every other param-validation path in the dispatcher (e.g. `m_input_type` throws `kInvalidParams` for a bad `text`). This plan therefore validates `file.open` params with **`kInvalidParams` (-32602)**, exactly like the existing handlers, and the tests assert `== kInvalidParams`. The literal "1002" in the spec is a typo; do not emit code 1002 for param errors.
## File Structure
| File | Change | Responsibility |
|---|---|---|
| `src/slic3r/GUI/Automation/IUiBackend.hpp` | modify | Add pure-virtual `int open_files(paths)` to the backend abstraction (stays wx-free). |
| `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp` | modify | Add `kErrLoadFailed = 1007` constant + `m_file_open` declaration. |
| `src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp` | modify | Add `parse_paths` helper, `m_file_open` body, dispatch route, capabilities entry. |
| `src/slic3r/GUI/Automation/WxUiBackend.hpp` | modify | Declare `open_files` override. |
| `src/slic3r/GUI/Automation/WxUiBackend.cpp` | modify | Implement `open_files` via `run_on_gui``Plater::load_files`. |
| `tests/automation/MockUiBackend.hpp` | modify | `open_files` override: record paths + return-count + fail knob. |
| `tests/automation/test_dispatcher.cpp` | modify | Catch2 tests for routing, string/array, validation, failure, capabilities. |
| `tools/automation/orca_automation.py` | modify | `open(self, paths)` client wrapper. |
| `tools/automation/example_slice.py` | modify | Launch without a model arg, then `orca.open([model])`. |
| `doc/automation.md` | modify | Document the method, capabilities, error `1007`. |
**Build/test layout:** Ninja Multi-Config in `build/`. The unit suite target is `automation_tests`; its sources (`tests/automation/CMakeLists.txt`) compile `JsonRpcDispatcher.cpp` + `MockUiBackend` but **not** `WxUiBackend.cpp`. So dispatcher/mock changes are fully unit-testable headlessly; `WxUiBackend.cpp` is verified by the full app build only.
---
## Task 1: Extend the backend abstraction (interface + mock + error code)
Adds the `open_files` contract so tests can be written. Adding a pure virtual to `IUiBackend` forces every implementation to provide it — in the unit-test target that is only `MockUiBackend`, so this task keeps the `automation_tests` build green.
**Files:**
- Modify: `src/slic3r/GUI/Automation/IUiBackend.hpp`
- Modify: `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp:19`
- Modify: `tests/automation/MockUiBackend.hpp`
- [ ] **Step 1: Add the error constant**
In `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp`, after the existing `kErrDisabled` line (currently line 19), add:
```cpp
constexpr int kErrDisabled = 1006;
constexpr int kErrLoadFailed = 1007; // file.open: load_files returned empty / threw
```
- [ ] **Step 2: Add the pure-virtual `open_files` to the interface**
In `src/slic3r/GUI/Automation/IUiBackend.hpp`, inside `class IUiBackend`, immediately after the `screenshot_window` pure virtual (currently line 97), add:
```cpp
// Load one or more files (absolute paths) into the running instance on the GUI
// thread. Returns the number of objects added to the scene (load_files(...).size()).
// Throws AutomationError(kErrLoadFailed) when nothing loads. Header stays wx-free:
// the concrete LoadStrategy is chosen inside WxUiBackend, not exposed here.
virtual int open_files(const std::vector<std::string>& paths) = 0;
```
- [ ] **Step 3: Implement `open_files` in the mock with record + knobs**
In `tests/automation/MockUiBackend.hpp`: add an include for the error constant near the top (after the `IUiBackend.hpp` include on line 2):
```cpp
#include "slic3r/GUI/Automation/IUiBackend.hpp"
#include "slic3r/GUI/Automation/JsonRpcDispatcher.hpp" // kErrLoadFailed
```
Add recorded-call + canned-output members. After the `screenshot_window_count` recorded field (line 20) add:
```cpp
int screenshot_window_count = 0;
std::vector<std::vector<std::string>> opened_paths; // paths of each open_files()
```
After the `click_result` canned field (line 26) add:
```cpp
bool click_result = true;
int open_return_count = 0; // value open_files() returns
bool open_should_fail = false; // when true, open_files() throws kErrLoadFailed
```
Add the override next to the other overrides, after `screenshot_window` (lines 49-51):
```cpp
PngImage screenshot_window(const UiNode*) override {
++screenshot_window_count; return canned_png;
}
int open_files(const std::vector<std::string>& paths) override {
opened_paths.push_back(paths);
if (open_should_fail)
throw AutomationError(kErrLoadFailed, "mock load failed");
return open_return_count;
}
```
- [ ] **Step 4: Build the unit-test target to confirm it still compiles**
Run: `cmake --build build --config RelWithDebInfo --target automation_tests`
Expected: build succeeds (the new pure virtual is satisfied by the mock; no behavior change yet).
- [ ] **Step 5: Commit**
```bash
git add src/slic3r/GUI/Automation/IUiBackend.hpp src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp tests/automation/MockUiBackend.hpp
git commit -m "feat(automation): add open_files to backend interface + kErrLoadFailed (1007)"
```
---
## Task 2: `file.open` dispatcher handler (parse, route, validate, fail)
Implements the full JSON-RPC handler against the mock: param parsing (string or array), validation, routing to `open_files`, and `kErrLoadFailed` propagation.
**Files:**
- Modify: `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp:49` (declaration)
- Modify: `src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp`
- Test: `tests/automation/test_dispatcher.cpp`
- [ ] **Step 1: Write the failing happy-path test (array of paths)**
Append to `tests/automation/test_dispatcher.cpp`:
```cpp
TEST_CASE("file.open with an array of paths routes to backend", "[automation][rpc]") {
MockUiBackend mock;
mock.open_return_count = 3;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","file.open"},
{"params",{{"paths", json::array({"C:/abs/a.stl","C:/abs/b.stl"})}}}});
CHECK(resp.at("result").at("ok") == true);
CHECK(resp.at("result").at("loaded") == 3);
REQUIRE(mock.opened_paths.size() == 1);
REQUIRE(mock.opened_paths[0].size() == 2);
CHECK(mock.opened_paths[0][0] == "C:/abs/a.stl");
CHECK(mock.opened_paths[0][1] == "C:/abs/b.stl");
}
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "file.open with an array of paths routes to backend"`
Expected: FAIL — `file.open` is an unknown method, so the response carries `error.code == -32601` and has no `result` (the `resp.at("result")` access throws). (If the exe path differs on your machine, locate it with `find build -iname automation_tests.exe`.)
- [ ] **Step 3: Declare the handler**
In `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp`, after the `m_screenshot_window` declaration (currently line 49) add:
```cpp
nlohmann::json m_screenshot_window(const nlohmann::json& params);
nlohmann::json m_file_open(const nlohmann::json& params);
```
- [ ] **Step 4: Add the `parse_paths` helper and `m_file_open` body**
In `src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp`, add a `parse_paths` helper. Place it in the anonymous namespace that also holds `parse_keys` — insert it right before that namespace's closing `} // namespace` (currently line 130):
```cpp
// "paths" may be a single string ("C:/a.stl") or an array of strings. Returns the
// non-empty absolute paths; throws kInvalidParams when paths is missing, not a
// string/array, contains a non-string entry, or yields no non-empty path.
std::vector<std::string> parse_paths(const nlohmann::json& params) {
if (!params.is_object() || !params.contains("paths"))
throw AutomationError(kInvalidParams, "file.open requires 'paths'");
const auto& p = params.at("paths");
std::vector<std::string> out;
if (p.is_string()) {
out.push_back(p.get<std::string>());
} else if (p.is_array()) {
for (const auto& e : p) {
if (!e.is_string())
throw AutomationError(kInvalidParams, "'paths' entries must be strings");
out.push_back(e.get<std::string>());
}
} else {
throw AutomationError(kInvalidParams, "'paths' must be a string or array");
}
out.erase(std::remove_if(out.begin(), out.end(),
[](const std::string& s) { return s.empty(); }),
out.end());
if (out.empty())
throw AutomationError(kInvalidParams, "'paths' is empty");
return out;
}
```
(`<algorithm>` for `std::remove_if` is already included at the top of the file, line 4.)
Add the handler body next to the other handlers. After `m_screenshot_window` (currently ends line 343, just before the final `}}} // namespace`), add:
```cpp
nlohmann::json JsonRpcDispatcher::m_file_open(const nlohmann::json& params) {
const std::vector<std::string> paths = parse_paths(params);
const int loaded = m_backend.open_files(paths);
return { {"ok", true}, {"loaded", loaded} };
}
```
- [ ] **Step 5: Add the dispatch route**
In `JsonRpcDispatcher::dispatch`, after the `screenshot.window` route (currently line 195) add:
```cpp
if (method == "screenshot.window") return make_result(id, m_screenshot_window(params));
if (method == "file.open") return make_result(id, m_file_open(params));
```
- [ ] **Step 6: Run the happy-path test to verify it passes**
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "file.open with an array of paths routes to backend"`
Expected: PASS — 1 test case, all assertions passed.
- [ ] **Step 7: Add the remaining handler tests (string, validation, failure)**
Append to `tests/automation/test_dispatcher.cpp`:
```cpp
TEST_CASE("file.open accepts a bare string path", "[automation][rpc]") {
MockUiBackend mock;
mock.open_return_count = 1;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",2},{"method","file.open"},
{"params",{{"paths","C:/abs/a.stl"}}}});
CHECK(resp.at("result").at("loaded") == 1);
REQUIRE(mock.opened_paths.size() == 1);
REQUIRE(mock.opened_paths[0].size() == 1);
CHECK(mock.opened_paths[0][0] == "C:/abs/a.stl");
}
TEST_CASE("file.open with missing paths -> invalid params", "[automation][rpc]") {
MockUiBackend mock;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",3},{"method","file.open"},
{"params", json::object()}});
CHECK(resp.at("error").at("code") == kInvalidParams);
CHECK(mock.opened_paths.empty());
}
TEST_CASE("file.open with empty paths array -> invalid params", "[automation][rpc]") {
MockUiBackend mock;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",4},{"method","file.open"},
{"params",{{"paths", json::array()}}}});
CHECK(resp.at("error").at("code") == kInvalidParams);
CHECK(mock.opened_paths.empty());
}
TEST_CASE("file.open with a non-string entry -> invalid params", "[automation][rpc]") {
MockUiBackend mock;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",5},{"method","file.open"},
{"params",{{"paths", json::array({"C:/a.stl", 42})}}}});
CHECK(resp.at("error").at("code") == kInvalidParams);
CHECK(mock.opened_paths.empty());
}
TEST_CASE("file.open backend load failure -> 1007", "[automation][rpc]") {
MockUiBackend mock;
mock.open_should_fail = true;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",6},{"method","file.open"},
{"params",{{"paths","C:/abs/a.stl"}}}});
CHECK(resp.at("error").at("code") == kErrLoadFailed);
}
```
- [ ] **Step 8: Run all file.open tests to verify they pass**
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "file.open*"`
Expected: PASS — 6 test cases, all assertions passed.
- [ ] **Step 9: Commit**
```bash
git add src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp tests/automation/test_dispatcher.cpp
git commit -m "feat(automation): add file.open dispatcher handler with validation + tests"
```
---
## Task 3: Advertise `file.open` in `automation.version` capabilities
**Files:**
- Modify: `src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp:166-172`
- Test: `tests/automation/test_dispatcher.cpp`
- [ ] **Step 1: Write the failing capabilities test**
Append to `tests/automation/test_dispatcher.cpp`:
```cpp
TEST_CASE("automation.version capabilities include file.open", "[automation][rpc]") {
MockUiBackend mock;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","automation.version"}});
const auto& caps = resp.at("result").at("capabilities");
bool found = false;
for (const auto& c : caps) if (c == "file.open") found = true;
CHECK(found);
}
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "automation.version capabilities include file.open"`
Expected: FAIL — `CHECK(found)` is false; `file.open` is not yet in the capabilities array.
- [ ] **Step 3: Add `file.open` to the capabilities array**
In `JsonRpcDispatcher::m_version` (currently lines 166-172), add `"file.open"` to the array:
```cpp
nlohmann::json JsonRpcDispatcher::m_version(const nlohmann::json&) {
return { {"version", kAutomationVersion},
{"protocol", "2.0"},
{"capabilities", nlohmann::json::array({
"tree.dump","tree.find","widget.get","input.click","input.type",
"input.key","sync.wait_for","app.state","screenshot.window",
"file.open" })} };
}
```
- [ ] **Step 4: Run the test to verify it passes**
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "automation.version capabilities include file.open"`
Expected: PASS.
- [ ] **Step 5: Run the whole automation suite to confirm no regressions**
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe --order rand --warn NoAssertions`
Expected: PASS — all cases green (the pre-existing ~32 plus the 7 new `file.open`/capabilities cases ≈ 39).
- [ ] **Step 6: Commit**
```bash
git add src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp tests/automation/test_dispatcher.cpp
git commit -m "feat(automation): advertise file.open in automation.version capabilities"
```
---
## Task 4: Implement `WxUiBackend::open_files` (real GUI-thread load)
Not covered by the headless unit suite (`WxUiBackend.cpp` is excluded from `automation_tests`); verified by the full app build + the manual runtime check in Task 8.
**Files:**
- Modify: `src/slic3r/GUI/Automation/WxUiBackend.hpp:21`
- Modify: `src/slic3r/GUI/Automation/WxUiBackend.cpp`
- [ ] **Step 1: Declare the override**
In `src/slic3r/GUI/Automation/WxUiBackend.hpp`, after the `screenshot_window` declaration (line 21) add:
```cpp
PngImage screenshot_window(const UiNode* target) override;
int open_files(const std::vector<std::string>& paths) override;
```
- [ ] **Step 2: Implement `open_files`**
In `src/slic3r/GUI/Automation/WxUiBackend.cpp`, add the implementation just before the final `}}} // namespace Slic3r::GUI::Automation` (currently line 306):
```cpp
int WxUiBackend::open_files(const std::vector<std::string>& paths) {
return run_on_gui(m_gui_timeout_ms, [&]() -> int {
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
throw AutomationError(kErrLoadFailed, "no plater to load into");
// Default strategy matches drag-drop / Plater::load_files's own default: it
// routes .3mf as a project and meshes as models based on file content, so no
// as_project flag is needed in v1. ask_multi=false: never prompt.
const LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig;
std::vector<size_t> loaded;
try {
loaded = plater->load_files(paths, strategy, /*ask_multi=*/false);
} catch (const std::exception& e) {
throw AutomationError(kErrLoadFailed,
std::string("load_files failed: ") + e.what());
}
if (loaded.empty())
throw AutomationError(kErrLoadFailed, "load_files loaded nothing");
return static_cast<int>(loaded.size());
});
}
```
Notes for the implementer:
- `LoadStrategy` and its `operator|` (namespace `Slic3r`, from `libslic3r/Format/bbs_3mf.hpp`) are already in scope: `WxUiBackend.cpp` includes `Plater.hpp` (line 7), which transitively pulls in the enum, and this translation unit lives in `Slic3r::GUI::Automation` so unqualified `LoadStrategy` resolves via the enclosing `Slic3r` namespace. No new include is required.
- `Plater::load_files(const std::vector<std::string>&, LoadStrategy, bool)` is the existing string overload (`Plater.hpp:379`) — no `boost::filesystem::path` conversion needed.
- `kErrLoadFailed` comes from `JsonRpcDispatcher.hpp`, already included at line 4.
- An `AutomationError` thrown inside the `run_on_gui` lambda is captured by the helper's `set_exception` and rethrown from `fut.get()`, so the 1007 code propagates to the dispatcher unchanged.
- [ ] **Step 3: Build the full app to verify it compiles and links**
Run: `cmake --build build --config RelWithDebInfo --target OrcaSlicer`
Expected: build succeeds (no missing-symbol / pure-virtual errors; `WxUiBackend` is now concrete).
- [ ] **Step 4: Commit**
```bash
git add src/slic3r/GUI/Automation/WxUiBackend.hpp src/slic3r/GUI/Automation/WxUiBackend.cpp
git commit -m "feat(automation): implement WxUiBackend::open_files via Plater::load_files"
```
---
## Task 5: Python client wrapper `OrcaClient.open`
**Files:**
- Modify: `tools/automation/orca_automation.py:80-82`
- [ ] **Step 1: Add the `open` method**
In `tools/automation/orca_automation.py`, after the `key` method (ends line 82), add:
```python
def key(self, keys) -> dict:
# keys: "ctrl+s" or ["ctrl", "s"]
return self._call("input.key", {"keys": keys})
def open(self, paths) -> dict:
"""Load one or more files into the running instance at runtime.
`paths` is a single absolute path string or a list of them. Paths are read
from the host filesystem by the server (localhost-only). Returns
{"ok": True, "loaded": <count>}. Raises OrcaError 1007 on load failure."""
if isinstance(paths, str):
paths = [paths]
return self._call("file.open", {"paths": list(paths)})
```
- [ ] **Step 2: Smoke-test the wrapper's normalization offline (no server needed)**
Run:
```bash
python -c "import sys; sys.path.insert(0, 'tools/automation'); import orca_automation as m; c = m.OrcaClient.__new__(m.OrcaClient); c._call = lambda meth, params=None: (meth, params); print(c.open('C:/a.stl')); print(c.open(['C:/a.stl','C:/b.stl']))"
```
Expected output:
```
('file.open', {'paths': ['C:/a.stl']})
('file.open', {'paths': ['C:/a.stl', 'C:/b.stl']})
```
- [ ] **Step 3: Commit**
```bash
git add tools/automation/orca_automation.py
git commit -m "feat(automation): add OrcaClient.open() wrapper for file.open"
```
---
## Task 6: Update `example_slice.py` to load at runtime via `file.open`
**Files:**
- Modify: `tools/automation/example_slice.py:26-52`
- [ ] **Step 1: Launch without the model arg, then call `open`**
In `tools/automation/example_slice.py`, change the `subprocess.Popen` call (lines 26-31) to drop the trailing model positional:
```python
proc = subprocess.Popen([
args.orca,
"--automation-server",
f"--automation-server-port={args.port}",
])
```
Then replace the project-load wait block (currently lines 46-51) so the model is loaded at runtime via `file.open` instead of relying on a launch-time positional:
```python
# Load the model into the already-running instance, then wait until the
# project reports loaded. file.open is synchronous, so project_loaded is
# already true on return; the wait is a belt-and-suspenders guard.
orca.open([args.model])
deadline = time.time() + 30
while time.time() < deadline:
if orca.app_state().get("project_loaded"):
break
time.sleep(0.5)
```
- [ ] **Step 2: Byte-compile the script to confirm no syntax errors**
Run: `python -m py_compile tools/automation/example_slice.py`
Expected: no output, exit code 0.
- [ ] **Step 3: Commit**
```bash
git add tools/automation/example_slice.py
git commit -m "docs(automation): example_slice.py loads model at runtime via file.open"
```
---
## Task 7: Document `file.open` in `doc/automation.md`
**Files:**
- Modify: `doc/automation.md` (capabilities example §4 line 111-114; new method subsection after `screenshot.window`; error table §7)
- [ ] **Step 1: Add `file.open` to the capabilities example**
In `doc/automation.md`, update the `automation.version` result example (lines 111-114) to include `file.open`:
```json
"capabilities": [
"tree.dump", "tree.find", "widget.get", "input.click", "input.type",
"input.key", "sync.wait_for", "app.state", "screenshot.window", "file.open"
]
```
The §4 prose count is already written for this: "There are 11 methods … the 10 callable feature methods" now matches exactly (10 capability entries + `automation.version` = 11). Leave that sentence unchanged.
- [ ] **Step 2: Add the `file.open` method subsection**
In `doc/automation.md`, immediately after the `screenshot.window` method subsection (it ends just before the `---` on line 303) and before that `---`, insert:
```markdown
### `file.open`
Load one or more files into the **already-running** instance at runtime, by calling
`Plater::load_files(...)` directly on the GUI thread. This is the supported way to add
or swap a model without relaunching the process. Loading is **synchronous**: when the
call returns `ok: true`, `app.state().project_loaded` is already `true` (no polling
race).
**Params:**
| Param | Type | Required | Meaning |
|---|---|---|---|
| `paths` | string or array of strings | yes | One or more **absolute** file paths. A bare string is accepted and treated as a one-element list. Paths are read from the **host (server) filesystem** — client and server are localhost-only. |
`.3mf` files are routed as projects and meshes as models automatically, based on file
content (the same default strategy as drag-drop); there is no `as_project` flag in v1.
**Result:** `{ "ok": true, "loaded": <int> }`, where `loaded` is the number of objects
added to the scene (`load_files(...).size()`).
**Errors:**
- `-32602` (invalid params) — `paths` is missing, is not a string/array, contains a
non-string entry, or yields no non-empty path.
- `1007` (load failed) — `load_files` returned empty or threw (file not found, parse
error, or unsupported format).
- `1004` (GUI busy) — the GUI-thread marshal timed out. An extremely large model can
exceed the marshal timeout and surface here; documented, not mitigated in v1.
```
- [ ] **Step 3: Add the `1007` row to the error-code table**
In `doc/automation.md` §7, in the application-specific codes table, after the `1006` row (line 395) add:
```markdown
| `1006` | Disabled. |
| `1007` | Load failed — `file.open`'s `load_files` returned empty or threw (not found, parse error, unsupported format). |
```
- [ ] **Step 4: Commit**
```bash
git add doc/automation.md
git commit -m "docs(automation): document file.open method and error 1007"
```
---
## Final verification
- [ ] **Step 1: Full automation unit suite green**
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe --order rand --warn NoAssertions`
Expected: PASS — all cases (pre-existing ~32 + 7 new) green, no `NoAssertions` warnings on the new cases.
- [ ] **Step 2: Full app builds**
Run: `cmake --build build --config RelWithDebInfo --target ALL_BUILD -- -m`
Expected: build succeeds.
- [ ] **Step 3: Manual runtime check (requires a display)**
Launch with `--automation-server` and **no** model arg, then from a Python shell:
```python
from orca_automation import OrcaClient
orca = OrcaClient(port=13619)
print(orca.open(["C:/abs/path/cube.stl"])) # -> {'ok': True, 'loaded': 1}
print(orca.app_state()["project_loaded"]) # -> True
open("window.png","wb").write(orca.screenshot()) # PNG shows the loaded model
```
Expected: `loaded >= 1`, `project_loaded == True`, screenshot shows the model.
- [ ] **Step 4: Gating check (automation OFF is a no-op)**
Confirm by reading: with no `--automation-server` flag, the server/backend/dispatcher are never constructed (`GUI_App.cpp` `start_automation_server()` early-return), so `file.open` is unreachable. No new hot-path cost beyond the existing single bool check. (See `doc/automation.md` §Verification — disabled-path audit; this feature adds no new gating surface.)
---
## Backward compatibility
Additive only: one new method (`file.open`), one new error code (`1007`), one new capabilities entry, and one new backend interface method. No existing method, profile, project-file handling, or default behavior changes. The method is reachable only when `--automation-server` is passed.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
# Design — `file.open` automation method (runtime model loading)
**Date:** 2026-06-03
**Branch:** `feature/automation`
**Status:** Approved for implementation
---
## Problem
Today a model can be loaded into OrcaSlicer **only at process launch**: the model path
is passed as a CLI positional arg and OrcaSlicer's normal startup file-loading ingests
it. The JSON-RPC automation protocol has **no** load method, so swapping or adding a
model in an already-running instance requires a fresh process launch.
Driving the native File→Import dialog via `input.click` is not a viable substitute: the
OS file picker is not a `wxWindow`, so it never appears in the `tree.dump` hierarchy
(`WxUiBackend::dump_tree` walks `wxGetApp().mainframe` children only), and `input.click`
can only target nodes resolved from that tree (no raw-coordinate click). Blind typing via
`input.type` is mechanically possible but unobservable: the native picker is not a
`wxDialog`, so `app.state().modal_dialog` and `sync.wait_for` cannot gate on it, leaving
only sleep-and-hope timing. A direct API method is the clean fix.
## Goal
Add a `file.open` JSON-RPC method that loads one or more files into a running instance by
calling `Plater::load_files(...)` directly on the GUI thread. Out of scope: any
dialog-driving mechanism (intercept hook or true OS-level drive) — explicitly deferred.
---
## Protocol
- **Method:** `file.open`
- **Params:** `{ "paths": ["C:/abs/a.stl", ...] }`
- A bare string is also accepted: `{ "paths": "C:/abs/a.stl" }`.
- Paths must be **absolute**. The server reads them from the host filesystem
(client/server are localhost-only).
- **Result:** `{ "ok": true, "loaded": <count> }`
- `count` is `load_files(...).size()` — the number of objects added to the scene.
- **Errors:**
| Code | Constant | Condition |
|------|----------|-----------|
| -32602 | `kInvalidParams` | `paths` missing/empty, a non-string entry, or no non-empty path |
| 1004 | `kErrGuiBusy` | GUI-thread marshal timed out (`m_gui_timeout_ms`) |
| 1007 | `kErrLoadFailed` | `load_files` returned empty / threw (not found, parse error, unsupported format) — **new code** |
## Semantics — synchronous
`Plater::load_files` runs and completes on the GUI thread. The backend marshals via the
existing `run_on_gui(m_gui_timeout_ms, …)` helper and returns only after the load
finishes. Consequently, when `file.open` returns `ok:true`, `app.state().project_loaded`
is already `true` — there is no polling race.
Rejected alternative — async "fire-and-poll-`project_loaded`": adds client complexity and
loses a definitive per-call error result, with no benefit since loading is synchronous.
**Caveat:** an extremely large model could exceed `m_gui_timeout_ms` and surface as
`1004 kErrGuiBusy`. Documented; not mitigated in v1.
## Load strategy (v1 minimal)
Pass the default `LoadStrategy::LoadModel | LoadStrategy::LoadConfig` (identical to
drag-drop / `Plater::load_files`'s default) with `ask_multi = false`. This already routes
`.3mf` files as projects and meshes as models based on file content, so **no `as_project`
flag is needed in v1**. A future `{ "as_project": bool }` flag remains possible but is not
implemented now.
---
## Components / files to touch
Follows the existing `screenshot_window` / `app_state` method pattern.
1. **`src/slic3r/GUI/Automation/IUiBackend.hpp`** — add pure-virtual
`int open_files(const std::vector<std::string>& paths)` returning the loaded count,
throwing `AutomationError` on failure. Header stays wx-free (no `LoadStrategy` leak).
2. **`src/slic3r/GUI/Automation/WxUiBackend.{hpp,cpp}`** — implement `open_files`:
`run_on_gui(m_gui_timeout_ms, …)``wxGetApp().plater()->load_files(paths, default_strategy, false)`;
throw `kErrLoadFailed` if the returned vector is empty.
3. **`src/slic3r/GUI/Automation/JsonRpcDispatcher.{hpp,cpp}`** —
- add `constexpr int kErrLoadFailed = 1007;`
- declare + define `m_file_open(params)` (param parsing/validation; accept string or
array; require ≥1 non-empty string path)
- add dispatch route `if (method == "file.open") return make_result(id, m_file_open(params));`
- add `"file.open"` to the capabilities array in `m_version`.
4. **`tests/automation/MockUiBackend.hpp`** — `open_files` override recording the paths
vector + a configurable return-count (and a throw/fail knob).
5. **`tests/automation/test_dispatcher.cpp`** — Catch2 v2 tests:
- array of paths → routes to backend, returns `loaded` count
- bare-string path → normalized to one path
- missing/empty `paths``-32602` (`kInvalidParams`)
- backend load failure → `1007`
- `automation.version` capabilities array includes `"file.open"`
6. **`tools/automation/orca_automation.py`** — `open(self, paths)` wrapper (normalize
`str``[str]`, send `file.open`).
7. **`tools/automation/example_slice.py`** — launch **without** a model arg, then
`orca.open([model])`, then wait for `project_loaded`.
8. **`doc/automation.md`** — document method (params/result/errors), add to the
capabilities list, method index, and error table (`1007`).
---
## Testing / verification
- **Build (Windows):** `cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m`.
- **Unit:** `automation` Catch2 suite green including new tests (≈31 → ≈34 cases).
- **Manual:** launch with `--automation-server` and **no** model arg → call `file.open`
→ confirm `app.state().project_loaded` flips `true` and `screenshot.window` shows the
model.
- **Gating:** unchanged — the server only runs under `--automation-server`, so the method
is a no-op (unreachable) when automation is disabled.
## Backward compatibility
Additive only: a new method, a new error code, and a new capabilities entry. No change to
existing methods, profiles, project-file handling, or default behavior.

View File

@@ -0,0 +1,314 @@
# OrcaSlicer UI Automation — Design Spec
**Date:** 2026-06-03
**Status:** Approved design, pending implementation plan
**Topic:** Add an opt-in, externally-controllable UI automation interface to OrcaSlicer for automated GUI testing and future AI-agent control.
---
## 1. Overview
Add a localhost JSON-RPC server to a running OrcaSlicer GUI instance that lets an
**external** script (or AI agent) drive and observe the real GUI. "Driving" is done
the way a user would — simulated mouse/keyboard via `wxUIActionSimulator` — while
"observing" reads the live widget state and captures screenshots.
The interface must cover **three UI technologies** present in OrcaSlicer:
1. Native **wxWidgets** widgets (`wxWindow` hierarchy).
2. The **OpenGL 3D viewport** (`GLCanvas3D`) — screenshots via the existing
framebuffer/thumbnail path.
3. **Dear ImGui** immediate-mode controls (gizmo panels, in-canvas overlays,
notifications) — recorded as they are drawn, because there is no persistent tree.
## 2. Goals / Non-Goals
### Goals
- Let an external, language-agnostic client connect to a running instance and:
introspect the UI, locate widgets by stable name, perform input actions, wait on
conditions, and capture screenshots (including the 3D view as a separate image).
- Be safe by default: disabled unless explicitly enabled; bound to `127.0.0.1` only.
- Be testable in CI without a display (pure-logic units behind a mock backend).
- Ship a reference Python client, a runnable end-to-end example, protocol docs, and
C++ unit tests.
### Non-Goals (v1)
- No headless/offscreen automation — OS input injection needs a focused, visible
window (Linux CI requires a display, e.g. Xvfb).
- No auth token in v1 (documented future hardening; localhost-only is the boundary).
- No per-item coverage of raw-`ImGui::` gizmos (Emboss/SVG/Text). They get
window-level coverage; per-item is future work.
- No new scripting language embedded in the app; control is purely external over JSON-RPC.
- We do **not** modify the existing auth `HttpServer`.
## 3. Background — existing infrastructure (verified)
- **`wxUIActionSimulator`** is compiled into the wx build and already used
(`src/slic3r/GUI/GUI_ObjectList.cpp:211`). Cross-platform simulated input is available.
- **`GLCanvas3D::render_thumbnail()`** (`src/slic3r/GUI/GLCanvas3D.cpp:2210+`) →
`render_thumbnail_framebuffer()` (`:6352`) renders the 3D scene into a
`ThumbnailData` (RGBA) via an FBO + `glReadPixels`. `debug_output_thumbnail()`
(`:6099`) shows the `ThumbnailData → wxImage → PNG` conversion. This is the
separate-3D-screenshot path. `Plater::generate_thumbnail()`/`generate_thumbnails()`
wrap it.
- **`ImGuiWrapper`** (`src/slic3r/GUI/ImGuiWrapper.hpp`) is the chokepoint for nearly
all ImGui controls: `button`/`bbl_button`, `checkbox`/`bbl_checkbox`, `combo`,
`slider_float`, `input_double`, `radio_button`, `menu_item_with_icon`, plus
`begin`/`end` for windows. `imgui_internal.h` is in-tree, exposing
`ImGui::GetCurrentContext()->Windows`, item rects, and hovered/active id.
- **`HttpServer`** (`src/slic3r/GUI/HttpServer.{hpp,cpp}`, boost::beast, port 13618)
is used for cloud auth. **It cannot serve a POST body**`session::read_body()`
reads and discards the body and never replies (`HttpServer.cpp:57-65`). It is
effectively GET-only. We will **not** reuse or modify it.
- **`OtherInstanceMessageHandler`** (`src/slic3r/GUI/InstanceCheck.{hpp,cpp}`) is the
template for "start a localhost listener once the MainFrame exists, post events into
the GUI." Useful as a structural reference.
- **CLI args** are parsed in `src/OrcaSlicer.cpp` (`CLI::setup`/`CLI::run`) and flow
into the GUI run params consumed by `GUI_App::OnInit()`.
## 4. Architecture
```
External script / AI agent ──► Python client (orca_automation.py)
│ HTTP POST /jsonrpc (JSON-RPC 2.0) on 127.0.0.1:<port>
AutomationServer (dedicated boost::beast listener; own thread;
│ started only when --automation-server is set)
│ parse JSON-RPC envelope
JsonRpcDispatcher (pure logic — method registry; unit-testable)
│ marshal each call to the GUI thread via wxGetApp().CallAfter
│ + std::promise/future with a per-request timeout
IUiBackend (interface) ──► WxUiBackend (real, GUI thread) / MockBackend (tests)
├─ Introspection : walk wxWindow tree + read ImGui item table → unified JSON
├─ Locator : resolve automation-id / predicate → wx widget or ImGui item
├─ Actions : raise window, then wxUIActionSimulator click/type/key
├─ Sync : wait_for (poll condition) + app.state snapshot
└─ Screenshots : wx widget → wxDC→PNG ; 3D view → render_thumbnail()→PNG
```
### Components (new files unless noted)
All new code lives under `src/slic3r/GUI/Automation/`.
| Component | Responsibility |
|---|---|
| `AutomationServer.{hpp,cpp}` | Dedicated boost::beast HTTP listener with **POST + body** support; one `POST /jsonrpc` endpoint; returns `application/json`. Localhost-only. Own thread. |
| `JsonRpcDispatcher.{hpp,cpp}` | Parse JSON-RPC 2.0; route `method` → handler; build result/error. Depends only on `IUiBackend`. No wx/ImGui includes → unit-testable. |
| `IUiBackend.hpp` | Abstract interface: `dump_tree`, `find`, `get_widget`, `click`, `type`, `key`, `wait_for`, `app_state`, `screenshot_window`, `screenshot_viewport3d`. Uses plain structs (no wx types) so tests can mock it. |
| `WxUiBackend.{hpp,cpp}` | Real implementation. Runs on GUI thread. Walks `wxWindow` tree, reads the ImGui item table, drives `wxUIActionSimulator`, captures screenshots. |
| `MockUiBackend.{hpp,cpp}` (tests) | Deterministic fake tree + recorded actions for unit tests. |
| `AutomationRegistry.{hpp,cpp}` | Process-wide `wxWindow* → automation_id` map + reverse lookup; `set_automation_id(win, "id")` helper. Header is dependency-light so widget-construction code can call the helper unconditionally (it is a cheap no-op-safe registration). |
| `WidgetSerializer.{hpp,cpp}` | `wxWindow` → JSON node (name/id, class, label, screen-rect, enabled, shown, value via RTTI). |
| `ImGuiItemTable.{hpp,cpp}` | Per-frame recorder of ImGui items + live-window enumeration. Populated from `ImGuiWrapper`; read on GUI thread. |
### Touch points in existing files
- `src/slic3r/GUI/ImGuiWrapper.cpp` — add recording hooks inside the wrapped widget
methods and `begin`/`end`, **guarded by an `is_automation_enabled()` flag** so there
is zero overhead and zero behavior change when automation is off.
- `src/slic3r/GUI/GUI_App.{hpp,cpp}` — own the `AutomationServer`; start it in
`OnInit()` only when the flag is set; stop it on exit. Expose
`is_automation_enabled()`.
- `src/OrcaSlicer.cpp` — parse `--automation-server[=PORT]`; pass through GUI run params.
- A handful of widget-construction sites (Slice/Export buttons, preset combos, main
tabs, common dialog OK/Cancel, the 3D canvas) — add `set_automation_id(...)` calls
(~15-20 widgets in v1).
- CMake: add the new `Automation/` sources to the GUI target; add the unit-test target.
## 5. Transport & Protocol
- **Transport:** HTTP/1.1 on `127.0.0.1:<port>` (default **13619**, adjacent to the
auth server's 13618). Single endpoint: `POST /jsonrpc`, body is a JSON-RPC 2.0
request, response is a JSON-RPC 2.0 result/error. `GET /` returns a tiny health/version page.
- **Protocol:** JSON-RPC 2.0. `id`, `method`, `params`. Batch not required in v1.
### v1 methods
| Method | Params | Result |
|---|---|---|
| `automation.version` | — | `{version, protocol, capabilities[]}` |
| `tree.dump` | `{root?, max_depth?, visible_only?, include_imgui?}` | tree of nodes (wx + imgui) |
| `tree.find` | `{name?, class?, label?, value?, backend?}` | `[node...]` matches |
| `widget.get` | `{target}` | single node detail |
| `input.click` | `{target, button?=left, double?=false, modifiers?[]}` | `{ok}` |
| `input.type` | `{target?, text}` | `{ok}` |
| `input.key` | `{keys}` e.g. `"ctrl+s"` or `["ctrl","s"]` | `{ok}` |
| `sync.wait_for` | `{target, state: exists\|visible\|enabled\|value, value?, timeout_ms?=5000, poll_ms?=100}` | `{ok, elapsed_ms}` |
| `app.state` | — | `{active_tab, project_loaded, slicing, slice_progress, modal_dialog?, foreground}` |
| `screenshot.window` | `{target?}` (default main frame) | `{png_base64, width, height}` |
| `screenshot.viewport3d` | `{plate?, width?, height?}` | `{png_base64, width, height}` |
### Node shape (unified for wx and ImGui)
```json
{
"backend": "wx" | "imgui",
"id": "btn_slice", // automation id if set, else derived path id
"path": "MainFrame/.../btn_slice", // stable-ish positional path
"class": "Button", // wx class name or imgui item type
"label": "Slice plate",
"rect": { "x": 100, "y": 200, "w": 120, "h": 32 }, // screen coords
"enabled": true,
"visible": true,
"value": "PLA", // when applicable (text/choice/check/slider)
"children": [ ... ] // wx only; imgui items are flat under their window
}
```
### Error model (JSON-RPC `error.code`)
- `-32700` parse error, `-32601` method not found, `-32602` invalid params (standard).
- Application codes: `1001` widget/target not found, `1002` target not actionable
(disabled/hidden), `1003` wait timeout, `1004` GUI thread busy/timeout,
`1005` screenshot failed, `1006` automation feature disabled.
## 6. Threading model
- `AutomationServer` runs on its own thread and accepts connections; the dispatcher
parses on that thread.
- **Every** call touching wx/ImGui/GL is marshaled to the GUI thread with
`wxGetApp().CallAfter([...]{ ... })`; the server thread blocks on a `std::future`
with a per-request timeout (default 5 s; `wait_for` uses its own larger budget).
Timeout → error `1004`.
- This is mandatory: wx widgets, the ImGui context, and the GL context are not
thread-safe and are owned by the GUI thread.
- `CallAfter` is serviced even while modal dialogs run (nested event loop), so
automation can interact with dialogs.
## 7. Widget locator & automation IDs (wxWidgets)
- **Stable IDs:** `set_automation_id(window, "btn_slice")` registers the widget in
`AutomationRegistry`. Stored in a side map keyed by `wxWindow*` (not via `SetName`,
to avoid any coupling with wx's name-based lookups). Registration is removed on
widget destruction (bind to `wxEVT_DESTROY` or prune lazily on lookup).
- **Derived IDs:** for un-instrumented widgets, `WidgetSerializer` derives a positional
`path` (e.g. `MainFrame/Panel[2]/Button[0]`) so an AI agent can still target anything.
Named IDs are the preferred, stable path.
- **Locator resolution order:** exact automation id → exact path → predicate match
(name/class/label/value). Ambiguous matches return the list via `tree.find`; action
methods require a unique match or error `1001`.
- **v1 instrumented widgets (~15-20):** Slice/Export buttons, printer & filament preset
combos, the main tab buttons (`tp3DEditor`/`tpPreview`/`tpMonitor`/…), Add/Import,
common dialog OK/Cancel/Yes/No, the `GLCanvas3D` itself.
## 8. ImGui coverage (v1 = wrapper items + window introspection)
- **Item recording:** inside each `ImGuiWrapper` wrapped method, when
`is_automation_enabled()` is true, append the just-drawn item to a per-frame
`ImGuiItemTable` entry: `{window_name, label/id, type, rect, enabled, value}`.
Item rect comes from `ImGui::GetItemRectMin/Max()` (ImGui display coords) mapped to
**screen** coords via the `GLCanvas3D` client origin (`ClientToScreen`) and DPI scale.
- **Window enumeration:** via `imgui_internal.h`, enumerate `GetCurrentContext()->Windows`
for window name, rect, visibility, plus the global hovered/active item id.
- **Double-buffering:** the table is swapped at frame end (`ImGuiWrapper::render`) so
readers see a complete frame. Reads happen on the GUI thread (after marshaling), same
thread as rendering, so a simple front/back swap suffices.
- **Freshness:** because items exist only while drawn, before an ImGui tree read or
action the backend forces a canvas refresh and flushes events so the latest frame is
captured. `sync.wait_for` can poll for an ImGui item to appear (e.g. after opening a
gizmo).
- **Actions:** an ImGui target resolves to its recorded screen rect; `input.click`/
`input.type` use `wxUIActionSimulator` on that rect — identical action path to wx,
different rect source. Typing into an ImGui input works because simulated keystrokes
flow through the existing `ImGuiWrapper::update_key_data` bridge once the field is
focused by a click.
- **Limitation (documented):** raw-`ImGui::` gizmos (Emboss, SVG, Text) are covered at
the **window** level only in v1; per-item instrumentation is future work.
## 9. Screenshots
- **`screenshot.window`:** capture a `wxWindow` (default: main frame) via
`wxClientDC`/`wxWindowDC``wxBitmap``wxImage` → PNG → base64. Works for native
widgets but **not** for the GL canvas region (returns black there) — hence the
separate 3D method.
- **`screenshot.viewport3d`:** reuse `GLCanvas3D::render_thumbnail()` (FBO +
`glReadPixels`) → `ThumbnailData``wxImage` (per `debug_output_thumbnail`) → PNG →
base64. Optional `plate`, `width`, `height` params. Runs on the GUI thread with the GL
context current.
## 10. Activation & security
- **Off by default.** Enabled by CLI flag `--automation-server[=PORT]` (default port
13619). (An app-config/Preferences toggle may be added later; v1 is flag-only.)
- **Bind `127.0.0.1` only.** No external interface.
- **No token in v1** (per decision); documented as a recommended future hardening,
along with an optional `--automation-token`.
- When disabled: no listener, no thread, and all `ImGuiWrapper` recording hooks are
skipped — **zero** runtime overhead and **zero** behavior change. This satisfies the
project's "features gated by options must not affect existing behavior when disabled"
constraint.
## 11. Testability
- `JsonRpcDispatcher` depends only on `IUiBackend` and has **no** wx/ImGui/GL includes.
- **C++ unit tests (Catch2), display-free, run in CI:**
- JSON-RPC envelope parse/validate/dispatch (good + malformed input, error codes).
- Method routing and param validation for every v1 method against `MockUiBackend`.
- `WidgetSerializer` node shape (fed a synthetic node model, not real wx widgets).
- Locator resolution: exact id, path, predicate, ambiguity, not-found.
- The only piece needing a real GUI is `WxUiBackend`; it is exercised by the manual
end-to-end example, not by CI unit tests.
## 12. Deliverables
- **C++:** the `Automation/` components, `ImGuiWrapper` recording hooks, widget
instrumentation, CLI flag plumbing, `GUI_App` lifecycle, CMake wiring.
- **`tools/automation/orca_automation.py`:** reference Python client wrapping the
JSON-RPC calls (`connect`, `version`, `dump_tree`, `find`, `click`, `type`, `key`,
`wait_for`, `app_state`, `screenshot`, `screenshot_3d`).
- **`tools/automation/example_slice.py`:** runnable end-to-end flow — launch OrcaSlicer
with the flag, load a model, click Slice, `wait_for` completion, save a 3D-preview PNG.
Doubles as a manual smoke test.
- **`doc/automation.md`:** protocol reference (methods, params, results, error codes),
node shape, automation-id naming conventions, ImGui notes, platform/display caveats.
- **`tests/`:** Catch2 unit-test target for the dispatch/serialize/locator logic.
## 13. New / changed file inventory
**New**
- `src/slic3r/GUI/Automation/AutomationServer.{hpp,cpp}`
- `src/slic3r/GUI/Automation/JsonRpcDispatcher.{hpp,cpp}`
- `src/slic3r/GUI/Automation/IUiBackend.hpp`
- `src/slic3r/GUI/Automation/WxUiBackend.{hpp,cpp}`
- `src/slic3r/GUI/Automation/AutomationRegistry.{hpp,cpp}`
- `src/slic3r/GUI/Automation/WidgetSerializer.{hpp,cpp}`
- `src/slic3r/GUI/Automation/ImGuiItemTable.{hpp,cpp}`
- `tools/automation/orca_automation.py`
- `tools/automation/example_slice.py`
- `doc/automation.md`
- `tests/automation/` (Catch2 target) + `MockUiBackend.{hpp,cpp}`
**Changed**
- `src/slic3r/GUI/ImGuiWrapper.cpp` (guarded recording hooks)
- `src/slic3r/GUI/GUI_App.{hpp,cpp}` (server lifecycle, `is_automation_enabled()`)
- `src/OrcaSlicer.cpp` (CLI flag)
- ~15-20 widget-construction sites (`set_automation_id`)
- `src/slic3r/GUI/CMakeLists.txt` + `tests/CMakeLists.txt`
## 14. Known constraints & limitations
- OS input injection requires the OrcaSlicer window **focused and visible**; the backend
raises/focuses the main window before injecting. Linux CI needs a display (Xvfb).
- Input is asynchronous at the OS level; correctness relies on `sync.wait_for` rather
than fixed sleeps.
- ImGui items are only addressable while their host panel is drawn.
- Raw-`ImGui::` gizmos: window-level only in v1.
- Single-client assumption in v1 (serialized request handling); no concurrent sessions
contract.
## 15. Future work (out of scope for v1)
- Optional auth token + Preferences toggle.
- WebSocket channel for server-push events (slice progress, dialog-appeared).
- Per-item instrumentation for raw-`ImGui::` gizmos.
- An MCP server wrapping the JSON-RPC client for direct AI-agent integration.
- Optional integration of Dear ImGui Test Engine for deterministic ImGui interaction.
## 16. Verification plan
- **CI:** Catch2 unit tests (dispatch/serialize/locator) pass with no display.
- **Manual / e2e:** run `tools/automation/example_slice.py` against a built OrcaSlicer
launched with `--automation-server`; confirm model loads, Slice runs, `wait_for`
returns on completion, and both a wx-window PNG and a 3D-viewport PNG are produced.
- **Regression:** build and run with automation **off**; confirm no new threads, no
listener, and ImGui rendering is byte-for-byte unchanged (hooks compiled out of the
hot path via the disabled flag).

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -48,12 +48,9 @@ src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp
src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.h
src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.cpp
src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp
src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp
src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp
src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp
src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp
src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp
src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.hpp
src/slic3r/GUI/Gizmos/GLGizmoMove.cpp
src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp
src/slic3r/GUI/Gizmos/GLGizmoScale.cpp
@@ -61,7 +58,6 @@ src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp
src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp
src/slic3r/GUI/Gizmos/GLGizmoCut.cpp
src/slic3r/GUI/Gizmos/GLGizmoCut.hpp
src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp
src/slic3r/GUI/Gizmos/GLGizmoFaceDetector.cpp
src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp
@@ -71,9 +67,7 @@ src/slic3r/GUI/Gizmos/GLGizmoText.hpp
src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp
src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp
src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp
src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp
src/slic3r/GUI/Gizmos/GLGizmoAssembly.cpp
src/slic3r/GUI/Gizmos/GLGizmoAssembly.hpp
src/slic3r/GUI/GUI.cpp
src/slic3r/GUI/GUI_App.cpp
src/slic3r/GUI/GUI_AuxiliaryList.cpp
@@ -95,8 +89,6 @@ src/slic3r/GUI/Widgets/AMSControl.cpp
src/slic3r/GUI/Widgets/FanControl.cpp
src/slic3r/GUI/Widgets/FilamentLoad.cpp
src/slic3r/GUI/Widgets/TempInput.cpp
src/slic3r/GUI/Widgets/CheckList.cpp
src/slic3r/GUI/Widgets/SwitchButton.cpp
src/slic3r/GUI/ImGuiWrapper.cpp
src/slic3r/GUI/Jobs/ArrangeJob.cpp
src/slic3r/GUI/Jobs/OrientJob.cpp
@@ -106,7 +98,6 @@ src/slic3r/GUI/Jobs/BindJob.cpp
src/slic3r/GUI/Jobs/PrintJob.cpp
src/slic3r/GUI/Jobs/SendJob.cpp
src/slic3r/GUI/Jobs/EmbossJob.cpp
src/slic3r/GUI/Jobs/PlaterWorker.hpp
src/slic3r/GUI/ThermalPreconditioningDialog.cpp
src/slic3r/GUI/ThermalPreconditioningDialog.hpp
src/slic3r/GUI/Jobs/SLAImportJob.cpp
@@ -246,12 +237,8 @@ src/slic3r/GUI/FilamentMapPanel.cpp
src/slic3r/Utils/Obico.cpp
src/slic3r/Utils/SimplyPrint.cpp
src/slic3r/Utils/Flashforge.cpp
src/slic3r/Utils/ElegooLink.cpp
src/slic3r/Utils/CrealityPrint.cpp
src/slic3r/Utils/PrintHost.cpp
src/slic3r/GUI/Jobs/OAuthJob.cpp
src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp
src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp
src/slic3r/GUI/PartSkipDialog.cpp
src/slic3r/GUI/PartSkipDialog.hpp
src/slic3r/GUI/SkipPartCanvas.cpp
@@ -263,12 +250,4 @@ src/slic3r/GUI/NetworkPluginDialog.cpp
src/slic3r/GUI/RammingChart.cpp
src/slic3r/GUI/StepMeshDialog.cpp
src/slic3r/GUI/FilamentPickerDialog.hpp
src/slic3r/GUI/PresetBundleDialog.cpp
src/slic3r/GUI/ExportPresetBundleDialog.cpp
src/slic3r/GUI/DesktopIntegrationDialog.cpp
src/slic3r/GUI/Downloader.cpp
src/slic3r/GUI/DownloaderFileGet.cpp
src/slic3r/GUI/FileArchiveDialog.cpp
src/slic3r/GUI/PrinterCloudAuthDialog.cpp
src/slic3r/GUI/PrinterWebViewHandler.cpp
src/libslic3r/PresetBundle.cpp

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -146,16 +146,16 @@ text = Fine-tuning for flow rate\nDid you know that flow rate can be fine-tuned
text = Split your prints into plates\nDid you know that you can split a model that has a lot of parts into individual plates ready to print? This will simplify the process of keeping track of all the parts.
[hint:Speed up your print with Adaptive Layer Height]
text = Speed up your print with Adaptive Layer Height\nDid you know that you can print a model even faster by using the Adaptive Layer Height option? Check it out!
text = Speed up your print with Adaptive Layer Height\nDid you know that you can print a model even faster, by using the Adaptive Layer Height option? Check it out!
[hint:Support painting]
text = Support painting\nDid you know that you can paint the location of your supports? This feature makes it easy to place the support material only on the sections of the model that actually need it.
[hint:Different types of supports]
text = Different types of supports\nDid you know that you can choose from multiple types of supports? Tree supports work great for organic models while saving filament and improving print speed. Check them out!
text = Different types of supports\nDid you know that you can choose from multiple types of supports? Tree supports work great for organic models, while saving filament and improving print speed. Check them out!
[hint:Printing Silk Filament]
text = Printing Silk Filament\nDid you know that Silk filament needs special consideration to print successfully? A higher temperature and lower speed are always recommended for the best results.
text = Printing Silk Filament\nDid you know that Silk filament needs special consideration to print it successfully? Higher temperature and lower speed are always recommended for the best results.
[hint:Brim for better adhesion]
text = Brim for better adhesion\nDid you know that when printed models have a small contact interface with the printing surface, it's recommended to use a brim?
@@ -167,13 +167,13 @@ text = Set parameters for multiple objects\nDid you know that you can set slicin
text = Stack objects\nDid you know that you can stack objects as a whole one?
[hint:Flush into support/objects/infill]
text = Flush into support/objects/infill\nDid you know that you can reduce wasted filament by flushing it into support/objects/infill during filament changes?
text = Flush into support/objects/infill\nDid you know that you can reduce wasted filament by flushing it into support/objects/infill during filament change?
[hint:Improve strength]
text = Improve strength\nDid you know that you can use more wall loops and higher sparse infill density to improve the strength of the model?
[hint:When do you need to print with the printer door opened]
text = When do you need to print with the printer door opened?\nDid you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? There is more info about this in the Wiki.
text = When do you need to print with the printer door opened?\nDid you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? More info about this in the Wiki.
[hint:Avoid warping]
text = Avoid warping\nDid you know that when printing materials that are prone to warping such as ABS, appropriately increasing the heatbed temperature can reduce the probability of warping?

View File

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2992 1.42627C10.7774 1.42627 8.73315 3.47056 8.73315 5.99231C8.73315 8.51406 10.7774 10.5583 13.2992 10.5583C13.3609 10.5583 13.4223 10.5571 13.4834 10.5547C15.9197 10.4581 17.8652 8.45238 17.8652 5.99231C17.8652 3.47056 15.8209 1.42627 13.2992 1.42627ZM15.6185 11.0536C17.5346 10.1741 18.8652 8.23861 18.8652 5.99231C18.8652 2.91827 16.3732 0.42627 13.2992 0.42627C11.5793 0.42627 10.0417 1.2063 9.02067 2.43188H2.31853C1.93193 2.43188 1.61853 2.74528 1.61853 3.13188V12.4712C1.61853 12.6313 1.6723 12.7789 1.76278 12.8968C1.76671 13.0046 1.79621 13.1144 1.85576 13.2185L3.48408 16.0649C3.60875 16.2829 3.8406 16.4173 4.09168 16.4173H6.75859L7.97903 19.1586C8.22556 19.7123 9.01148 19.7123 9.25801 19.1586L10.4784 16.4173H13.1454C13.3965 16.4173 13.6283 16.2829 13.753 16.0649L15.3813 13.2185C15.4409 13.1144 15.4704 13.0046 15.4743 12.8968C15.5648 12.7789 15.6185 12.6313 15.6185 12.4712V11.0536ZM13.4429 11.5565C13.8469 11.5463 14.2403 11.493 14.6185 11.4011V12.1709H2.61853V3.43188H8.35573C7.95792 4.19837 7.73315 5.06909 7.73315 5.99231C7.73315 9.06635 10.2252 11.5583 13.2992 11.5583C13.3472 11.5583 13.3951 11.5577 13.4429 11.5565ZM2.98076 13.1712H14.2563L12.9714 15.4173H9.85194L9.84578 15.4173H7.39126L7.3851 15.4173H4.26569L2.98076 13.1712ZM9.38381 16.4173L8.61852 18.1363L7.85322 16.4173H9.38381ZM12.2021 3.36422C12.3973 3.55948 12.3973 3.87606 12.2021 4.07133L11.743 4.53041H16.0625C16.3386 4.53041 16.5625 4.75426 16.5625 5.03041C16.5625 5.30655 16.3386 5.53041 16.0625 5.53041H10.7773C10.2428 5.53041 9.97507 4.88412 10.353 4.50614L11.495 3.36422C11.6902 3.16896 12.0068 3.16896 12.2021 3.36422ZM14.3963 7.91305C14.2011 8.10831 14.2011 8.42489 14.3963 8.62015C14.5916 8.81542 14.9082 8.81542 15.1034 8.62015L16.2453 7.47823C16.6233 7.10026 16.3556 6.45397 15.8211 6.45397L10.5359 6.45397C10.2597 6.45397 10.0359 6.67783 10.0359 6.95397C10.0359 7.23011 10.2597 7.45397 10.5359 7.45397L14.8554 7.45397L14.3963 7.91305Z" fill="#262E30"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "Afinia",
"version": "02.04.00.02",
"version": "02.04.00.00",
"force_update": "0",
"description": "Afinia configurations",
"machine_model_list": [
@@ -124,6 +124,14 @@
"name": "fdm_filament_tpu",
"sub_path": "filament/fdm_filament_tpu.json"
},
{
"name": "Afinia ABS",
"sub_path": "filament/Afinia ABS.json"
},
{
"name": "Afinia ABS+",
"sub_path": "filament/Afinia ABS+.json"
},
{
"name": "Afinia ABS+@HS",
"sub_path": "filament/Afinia ABS+@HS.json"
@@ -132,18 +140,34 @@
"name": "Afinia ABS@HS",
"sub_path": "filament/Afinia ABS@HS.json"
},
{
"name": "Afinia Value ABS",
"sub_path": "filament/Afinia Value ABS.json"
},
{
"name": "Afinia Value ABS@HS",
"sub_path": "filament/Afinia Value ABS@HS.json"
},
{
"name": "Afinia PLA",
"sub_path": "filament/Afinia PLA.json"
},
{
"name": "Afinia PLA@HS",
"sub_path": "filament/Afinia PLA@HS.json"
},
{
"name": "Afinia Value PLA",
"sub_path": "filament/Afinia Value PLA.json"
},
{
"name": "Afinia Value PLA@HS",
"sub_path": "filament/Afinia Value PLA@HS.json"
},
{
"name": "Afinia TPU",
"sub_path": "filament/Afinia TPU.json"
},
{
"name": "Afinia TPU@HS",
"sub_path": "filament/Afinia TPU@HS.json"

View File

@@ -0,0 +1,37 @@
{
"type": "filament",
"filament_id": "GFB00",
"setting_id": "GFSB00",
"name": "Afinia ABS+",
"from": "system",
"instantiation": "true",
"inherits": "fdm_filament_abs",
"filament_flow_ratio": [
"0.95"
],
"filament_cost": [
"24.99"
],
"filament_vendor": [
"Afinia"
],
"fan_max_speed": [
"60"
],
"filament_max_volumetric_speed": [
"16"
],
"nozzle_temperature": [
"275"
],
"nozzle_temperature_initial_layer": [
"275"
],
"slow_down_layer_time": [
"12"
],
"compatible_printers": [
"Afinia H400 Pro 0.4 nozzle",
"Afinia H400 Pro 0.6 nozzle"
]
}

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"filament_id": "GFB00_01",
"setting_id": "wAJTMxtCY7EoavRi",
"setting_id": "GFSB00",
"name": "Afinia ABS+@HS",
"from": "system",
"instantiation": "true",

View File

@@ -0,0 +1,31 @@
{
"type": "filament",
"filament_id": "GFB00",
"setting_id": "GFSB00",
"name": "Afinia ABS",
"from": "system",
"instantiation": "true",
"inherits": "fdm_filament_abs",
"filament_flow_ratio": [
"0.95"
],
"filament_cost": [
"24.99"
],
"filament_vendor": [
"Afinia"
],
"fan_max_speed": [
"60"
],
"filament_max_volumetric_speed": [
"16"
],
"slow_down_layer_time": [
"12"
],
"compatible_printers": [
"Afinia H400 Pro 0.4 nozzle",
"Afinia H400 Pro 0.6 nozzle"
]
}

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"filament_id": "GFB00_01",
"setting_id": "qCDnb2iBaz4hd4vX",
"setting_id": "GFSB00",
"name": "Afinia ABS@HS",
"from": "system",
"instantiation": "true",

View File

@@ -0,0 +1,34 @@
{
"type": "filament",
"filament_id": "GFA00",
"setting_id": "GFSA00",
"name": "Afinia PLA",
"from": "system",
"instantiation": "true",
"inherits": "fdm_filament_pla",
"filament_cost": [
"24.99"
],
"filament_density": [
"1.26"
],
"filament_flow_ratio": [
"0.98"
],
"filament_max_volumetric_speed": [
"21"
],
"filament_vendor": [
"Afinia"
],
"filament_long_retractions_when_cut": [
"1"
],
"filament_retraction_distances_when_cut": [
"18"
],
"compatible_printers": [
"Afinia H400 Pro 0.4 nozzle",
"Afinia H400 Pro 0.6 nozzle"
]
}

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"filament_id": "GFA00_01",
"setting_id": "N3sCgjdjvp6FTtw9",
"setting_id": "GFSA00",
"name": "Afinia PLA@HS",
"from": "system",
"instantiation": "true",

View File

@@ -0,0 +1,27 @@
{
"type": "filament",
"name": "Afinia TPU",
"inherits": "fdm_filament_tpu",
"from": "system",
"filament_id": "GFU01",
"instantiation": "true",
"filament_vendor": [
"Afinia"
],
"filament_density": [
"1.22"
],
"nozzle_temperature_initial_layer": [
"230"
],
"filament_cost": [
"41.99"
],
"nozzle_temperature": [
"230"
],
"compatible_printers": [
"Afinia H400 Pro 0.4 nozzle",
"Afinia H400 Pro 0.6 nozzle"
]
}

View File

@@ -3,7 +3,6 @@
"name": "Afinia TPU@HS",
"inherits": "fdm_filament_tpu",
"from": "system",
"setting_id": "zUqTgAEbqTN1EdRl",
"filament_id": "GFU01_01",
"instantiation": "true",
"filament_vendor": [

View File

@@ -0,0 +1,37 @@
{
"type": "filament",
"filament_id": "GFB00",
"setting_id": "GFSB00",
"name": "Afinia Value ABS",
"from": "system",
"instantiation": "true",
"inherits": "fdm_filament_abs",
"filament_flow_ratio": [
"0.95"
],
"filament_cost": [
"24.99"
],
"filament_vendor": [
"Afinia"
],
"fan_max_speed": [
"60"
],
"filament_max_volumetric_speed": [
"16"
],
"nozzle_temperature": [
"245"
],
"nozzle_temperature_initial_layer": [
"245"
],
"slow_down_layer_time": [
"12"
],
"compatible_printers": [
"Afinia H400 Pro 0.4 nozzle",
"Afinia H400 Pro 0.6 nozzle"
]
}

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"filament_id": "GFB00_01",
"setting_id": "OxIiEYjbhEvSykaQ",
"setting_id": "GFSB00",
"name": "Afinia Value ABS@HS",
"from": "system",
"instantiation": "true",

View File

@@ -0,0 +1,40 @@
{
"type": "filament",
"filament_id": "GFA00",
"setting_id": "GFSA00",
"name": "Afinia Value PLA",
"from": "system",
"instantiation": "true",
"inherits": "fdm_filament_pla",
"filament_cost": [
"24.99"
],
"filament_density": [
"1.26"
],
"filament_flow_ratio": [
"0.98"
],
"filament_max_volumetric_speed": [
"21"
],
"filament_vendor": [
"Afinia"
],
"filament_long_retractions_when_cut": [
"1"
],
"filament_retraction_distances_when_cut": [
"18"
],
"nozzle_temperature": [
"190"
],
"nozzle_temperature_initial_layer": [
"190"
],
"compatible_printers": [
"Afinia H400 Pro 0.4 nozzle",
"Afinia H400 Pro 0.6 nozzle"
]
}

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"filament_id": "GFA00_01",
"setting_id": "BASsUdyvElEVJ9AA",
"setting_id": "GFSA00",
"name": "Afinia Value PLA@HS",
"from": "system",
"instantiation": "true",

View File

@@ -1,6 +1,6 @@
{
"type": "machine",
"setting_id": "iwkXfmEyz9RLZTvh",
"setting_id": "GM001",
"name": "Afinia H+1(HS) 0.4 nozzle",
"from": "system",
"instantiation": "true",

View File

@@ -1,6 +1,6 @@
{
"type": "machine",
"setting_id": "vngsrxKvEIsmr8tQ",
"setting_id": "GM001",
"name": "Afinia H+1(HS) 0.6 nozzle",
"from": "system",
"instantiation": "true",

View File

@@ -1,6 +1,6 @@
{
"type": "process",
"setting_id": "FueWYijuvLNWzNmL",
"setting_id": "GP004",
"name": "0.12mm Fine @Afinia H+1(HS)",
"from": "system",
"instantiation": "true",

View File

@@ -1,6 +1,6 @@
{
"type": "process",
"setting_id": "Ntf0ZMcETTPz7EY1",
"setting_id": "GP004",
"name": "0.16mm Optimal @Afinia H+1(HS)",
"from": "system",
"instantiation": "true",

View File

@@ -3,7 +3,7 @@
"name": "0.18mm Fine @Afinia H+1(HS) 0.6 nozzle",
"inherits": "fdm_process_afinia_0.18_nozzle_0.6_HS",
"from": "system",
"setting_id": "b7oGgrSCik70LqdL",
"setting_id": "GP021",
"instantiation": "true",
"description": "It has a smaller layer height and results in smoother surface and higher printing quality.",
"elefant_foot_compensation": "0.15",

View File

@@ -1,6 +1,6 @@
{
"type": "process",
"setting_id": "Lxid5Z2e0fvVDMMm",
"setting_id": "GP004",
"name": "0.20mm Standard @Afinia H+1(HS)",
"from": "system",
"inherits": "fdm_process_afinia_HS_common",

View File

@@ -1,6 +1,6 @@
{
"type": "process",
"setting_id": "APeSqnRZh6I9OTFr",
"setting_id": "GP004",
"name": "0.24mm Draft @Afinia H+1(HS)",
"from": "system",
"instantiation": "true",

View File

@@ -3,7 +3,7 @@
"name": "0.24mm Standard @Afinia H+1(HS) 0.6 nozzle",
"inherits": "fdm_process_afinia_0.24_nozzle_0.6_HS",
"from": "system",
"setting_id": "QlOnQEeUXkGaxZS5",
"setting_id": "GP022",
"instantiation": "true",
"description": "It has a balanced layer height for good quality and reasonable printing time.",
"elefant_foot_compensation": "0.15",

View File

@@ -1,6 +1,6 @@
{
"type": "process",
"setting_id": "VZ3NOVzdVWqZ37Ge",
"setting_id": "GP004",
"name": "0.28mm Extra Draft @Afinia H+1(HS)",
"from": "system",
"instantiation": "true",

View File

@@ -3,7 +3,7 @@
"name": "0.30mm Standard @Afinia H+1(HS) 0.6 nozzle",
"inherits": "fdm_process_afinia_0.30_nozzle_0.6_HS",
"from": "system",
"setting_id": "7PPJm0girx11EP80",
"setting_id": "GP023",
"instantiation": "true",
"description": "It has a big layer height, and results in apparent layer lines and ordinary printing quality and printing time.",
"elefant_foot_compensation": "0.15",

View File

@@ -3,7 +3,7 @@
"name": "0.30mm Strength @Afinia H+1(HS) 0.6 nozzle",
"inherits": "fdm_process_afinia_0.30_nozzle_0.6_HS",
"from": "system",
"setting_id": "BewLobRVuiw6HPuc",
"setting_id": "GP024",
"instantiation": "true",
"description": "It has a big layer height with optimized settings for stronger parts.",
"elefant_foot_compensation": "0.15",

View File

@@ -3,7 +3,7 @@
"name": "0.36mm Draft @Afinia H+1(HS) 0.6 nozzle",
"inherits": "fdm_process_afinia_0.36_nozzle_0.6_HS",
"from": "system",
"setting_id": "pBS3NpSZPjtySd3X",
"setting_id": "GP025",
"instantiation": "true",
"description": "It has a bigger layer height for faster printing but with more visible layer lines.",
"elefant_foot_compensation": "0.15",

View File

@@ -3,7 +3,7 @@
"name": "0.42mm Extra Draft @Afinia H+1(HS) 0.6 nozzle",
"inherits": "fdm_process_afinia_0.42_nozzle_0.6_HS",
"from": "system",
"setting_id": "kHUEpY5ZMtYrLKtO",
"setting_id": "GP026",
"instantiation": "true",
"description": "It has the biggest layer height for fastest printing but with very visible layer lines.",
"elefant_foot_compensation": "0.15",

View File

@@ -1,6 +1,6 @@
{
"name": "Anker",
"version": "02.04.00.01",
"version": "02.04.00.00",
"force_update": "0",
"description": "Anker configurations",
"machine_model_list": [

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic ABS 0.2 nozzle",
"inherits": "Anker Generic ABS @base",
"from": "system",
"setting_id": "BD5ODYVM90Ig44C5",
"setting_id": "GFSB99_20",
"instantiation": "true",
"filament_max_volumetric_speed": [
"2"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic ABS 0.25 nozzle",
"inherits": "Anker Generic ABS @base",
"from": "system",
"setting_id": "WS5wXckNuiQqSVwO",
"setting_id": "GFSB99_25",
"instantiation": "true",
"filament_max_volumetric_speed": [
"3"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic ABS",
"inherits": "Anker Generic ABS @base",
"from": "system",
"setting_id": "axEspGzFJnrnidm4",
"setting_id": "GFSB99",
"instantiation": "true",
"compatible_printers": [
"Anker M5 0.4 nozzle",

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic ASA 0.2 nozzle",
"inherits": "Anker Generic ASA @base",
"from": "system",
"setting_id": "G7pzEyyhPFDnae3R",
"setting_id": "GFSB98_20",
"instantiation": "true",
"filament_max_volumetric_speed": [
"2"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic ASA 0.25 nozzle",
"inherits": "Anker Generic ASA @base",
"from": "system",
"setting_id": "ErUUDTQ7h1wNHAKT",
"setting_id": "GFSB98_25",
"instantiation": "true",
"filament_max_volumetric_speed": [
"3"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic ASA",
"inherits": "Anker Generic ASA @base",
"from": "system",
"setting_id": "QiFoBW5WuDUJGmFZ",
"setting_id": "GFSB98",
"instantiation": "true",
"compatible_printers": [
"Anker M5 0.4 nozzle",

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PA 0.2 nozzle",
"inherits": "Anker Generic PA @base",
"from": "system",
"setting_id": "NZB26MSD9WbOeY2S",
"setting_id": "GFSN99_20",
"instantiation": "true",
"filament_max_volumetric_speed": [
"2"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PA 0.25 nozzle",
"inherits": "Anker Generic PA @base",
"from": "system",
"setting_id": "Wj7oXTGUxo8lie1B",
"setting_id": "GFSN99_25",
"instantiation": "true",
"filament_max_volumetric_speed": [
"3"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PA-CF",
"inherits": "Anker Generic PA-CF @base",
"from": "system",
"setting_id": "qdsEps11ugFHJkn7",
"setting_id": "GFSN98",
"instantiation": "true",
"compatible_printers": [
"Anker M5 All-Metal 0.4 nozzle",

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PA",
"inherits": "Anker Generic PA @base",
"from": "system",
"setting_id": "QbIiX554Yee6vb32",
"setting_id": "GFSN99",
"instantiation": "true",
"compatible_printers": [
"Anker M5 All-Metal 0.4 nozzle",

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PC 0.2 nozzle",
"inherits": "Anker Generic PC @base",
"from": "system",
"setting_id": "GhIOAidtgDkzh9Aj",
"setting_id": "GFSC99_20",
"instantiation": "true",
"filament_max_volumetric_speed": [
"2"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PC 0.25 nozzle",
"inherits": "Anker Generic PC @base",
"from": "system",
"setting_id": "TQm9EPdenyGJMvgP",
"setting_id": "GFSC99_25",
"instantiation": "true",
"filament_max_volumetric_speed": [
"3"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PC",
"inherits": "Anker Generic PC @base",
"from": "system",
"setting_id": "xY25QcIsR9pWai3n",
"setting_id": "GFSC99",
"instantiation": "true",
"compatible_printers": [
"Anker M5 All-Metal 0.4 nozzle",

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PETG 0.2 nozzle",
"inherits": "Anker Generic PETG @base",
"from": "system",
"setting_id": "VItvDP6zmenWXwPO",
"setting_id": "GFSG99_20",
"instantiation": "true",
"filament_max_volumetric_speed": [
"2"

Some files were not shown because too many files have changed in this diff Show More