mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 20:31:14 +00:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d80af50f23 | ||
|
|
3bfe40e17a | ||
|
|
62f05dad2b | ||
|
|
82aee1abdd | ||
|
|
4d3c61c02a | ||
|
|
cefdd9b609 | ||
|
|
caade80624 | ||
|
|
cbd8ce98d5 | ||
|
|
99714afc4c | ||
|
|
3d07663ab2 | ||
|
|
29b6bbb8c9 | ||
|
|
8b6c074c69 | ||
|
|
109d09e0b8 | ||
|
|
b00237807d | ||
|
|
4149401c53 | ||
|
|
f6cf18fb96 | ||
|
|
4ee1b9c6e9 | ||
|
|
bc8d0a7858 | ||
|
|
e0651ec31d | ||
|
|
0caefcf4aa | ||
|
|
dc9203126d | ||
|
|
8c2a1198d6 | ||
|
|
193cfc6dbf | ||
|
|
86905a8a20 | ||
|
|
d4fabbf7c8 | ||
|
|
51efb24752 | ||
|
|
cf9ec18df5 | ||
|
|
99de46b9d0 | ||
|
|
258f927a0f | ||
|
|
b8d40ed61f | ||
|
|
6aa613ccf0 | ||
|
|
54e6df0235 | ||
|
|
7599868797 | ||
|
|
81ec931990 | ||
|
|
8fd97b5120 | ||
|
|
aacc36b9d2 | ||
|
|
79ed246d26 | ||
|
|
92b5301bf6 | ||
|
|
20c157b0f4 | ||
|
|
4cf16febf6 | ||
|
|
70e52a105f | ||
|
|
b525cb79e1 | ||
|
|
86ab598899 |
@@ -569,12 +569,18 @@ on the **first** call. So:
|
||||
#endif
|
||||
```
|
||||
|
||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
|
||||
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
|
||||
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
||||
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
|
||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
|
||||
DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
|
||||
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
|
||||
`WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and
|
||||
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
|
||||
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
|
||||
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked
|
||||
WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case
|
||||
that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
|
||||
Wayland app inherits both). It calls `XInitThreads()`
|
||||
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in
|
||||
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
|
||||
|
||||
**wx's own detectors**, for reference:
|
||||
|
||||
@@ -61,7 +61,8 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
|
||||
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
|
||||
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
||||
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond the no-EGL default path where Orca forces
|
||||
`GDK_BACKEND=x11`, for WebKitGTK older than 2.42, and never
|
||||
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
|
||||
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
|
||||
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
|
||||
@@ -567,33 +568,69 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
|
||||
## WebKitGTK on Linux sessions
|
||||
|
||||
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
|
||||
branches on `GDK_BACKEND`:
|
||||
sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
|
||||
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
|
||||
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
|
||||
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
|
||||
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
|
||||
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
|
||||
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
|
||||
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
|
||||
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
||||
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
|
||||
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
|
||||
`DISPLAY` is set.
|
||||
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
|
||||
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
|
||||
(`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only
|
||||
default-path case that really runs through XWayland; `DISPLAY` and `WAYLAND_DISPLAY` alone do not
|
||||
identify it, because a native Wayland app inherits both. `XInitThreads()` runs only when `DISPLAY` is
|
||||
set.
|
||||
- **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when
|
||||
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
|
||||
compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving
|
||||
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
|
||||
compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
|
||||
|
||||
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
|
||||
(see `references/platforms.md`), not these environment variables.
|
||||
|
||||
**Pitfall.**
|
||||
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
|
||||
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
|
||||
initialises; never unconditionally. Keep JS-side fallbacks so a page never
|
||||
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
|
||||
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
|
||||
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
|
||||
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
|
||||
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
|
||||
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
|
||||
X11/Wayland fallback renderers were removed) and has a reported crash path. On Debian/Ubuntu and
|
||||
openSUSE the `isNVIDIA()` patch returns before the FORCE_SHM check, so `WEBKIT_FORCE_DMABUF_RENDERER=1`
|
||||
must clear it first; that variable exists only in those patches. WebKit reads all of these once per
|
||||
process, before the first WebView.
|
||||
The test is `/proc/driver/nvidia/version`, so a hybrid laptop that renders on its iGPU also takes the
|
||||
shared-memory path, which is a per-frame GPU readback rather than zero-copy; exporting
|
||||
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=0` in the environment restores the hardware path.
|
||||
```cpp
|
||||
// Wrong: kills compositing and threaded scrolling (and can crash on modern WebKit)
|
||||
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", false);
|
||||
// Right: keep compositing, skip only the hardware DMABUF path
|
||||
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
|
||||
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /*replace*/ false);
|
||||
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /*replace*/ false);
|
||||
}
|
||||
```
|
||||
Cite: WebKit 2.52.6 `Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp`
|
||||
(`rendererBufferTransportMode()`), Debian `disable-nvidia-dmabuf.patch`, WebKit bug 262607.
|
||||
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path, in the branch where wx
|
||||
lacks EGL and Orca forces `GDK_BACKEND=x11`, non-replacing (`setenv(..., false)`, so a value the user
|
||||
already exported wins), before GTK
|
||||
initialises; never unconditionally, and only for the WebKitGTK releases that need it (older than
|
||||
2.42, `WebView::NeedsCompositingWorkaround()`). Keep JS-side fallbacks so a page never
|
||||
waits forever for a C++→JS call.
|
||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views
|
||||
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware
|
||||
acceleration; without the JS timeout the user is stuck when the signal is lost.
|
||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
|
||||
web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
|
||||
so applying it on a newer WebKit needlessly drops both GPU compositing and async scrolling (the
|
||||
Device tab loses its smoothness); without the JS timeout the user is stuck when the signal is lost.
|
||||
```cpp
|
||||
// Wrong: unconditional, kills HW acceleration everywhere
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
|
||||
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
|
||||
if (display && *display && wayland && *wayland)
|
||||
// Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
|
||||
// and only in the branch that forces GDK_BACKEND=x11 (wx without EGL), the
|
||||
// case that runs through XWayland
|
||||
if (WebView::NeedsCompositingWorkaround() && wayland && *wayland)
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
|
||||
```
|
||||
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
|
||||
|
||||
@@ -288,6 +288,19 @@ if (APPLE)
|
||||
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
|
||||
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
|
||||
|
||||
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
|
||||
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
|
||||
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
|
||||
# both are set. The setting takes a single program path, not a CMake launcher list.
|
||||
# Xcode versions without it ignore it and build uncached.
|
||||
if (CMAKE_GENERATOR STREQUAL "Xcode")
|
||||
if (CMAKE_CXX_COMPILER_LAUNCHER)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
|
||||
elseif (CMAKE_C_COMPILER_LAUNCHER)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
|
||||
+15
-1
@@ -22,7 +22,7 @@ function usage() {
|
||||
echo " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner"
|
||||
echo " -h: prints this help text"
|
||||
echo " -i: build the Orca Slicer AppImage (optional)"
|
||||
echo " -p: boost ccache hit rate by disabling precompiled headers (default: ON)"
|
||||
echo " -p: disable precompiled headers (on by default): more ccache hits after header changes, slower misses"
|
||||
echo " -r: skip RAM and disk checks (low RAM compiling)"
|
||||
echo " -s: build the Orca Slicer (optional)"
|
||||
echo " -t: build tests (optional), requires -s flag"
|
||||
@@ -549,6 +549,20 @@ 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}")
|
||||
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
|
||||
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
|
||||
# a checkout has just written. It is added to the sloppiness already configured, and
|
||||
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
|
||||
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
|
||||
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
|
||||
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
|
||||
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
|
||||
fi
|
||||
# Hash the includes the compiler reports instead of preprocessing every miss.
|
||||
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
|
||||
export CCACHE_DEPEND=1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
||||
fi
|
||||
|
||||
@@ -111,6 +111,54 @@ else
|
||||
export CMAKE_POLICY_COMPAT=""
|
||||
fi
|
||||
|
||||
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
|
||||
# launcher already set in the environment (the CI build sets ccache there), else sccache or
|
||||
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
|
||||
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
|
||||
CMAKE_CCACHE_ARGS=()
|
||||
CCACHE_VAR=""
|
||||
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
|
||||
if [ -n "${!var:-}" ]; then
|
||||
CCACHE_VAR=$var
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$CCACHE_VAR" ]; then
|
||||
CCACHE_PROGRAM=${!CCACHE_VAR}
|
||||
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
|
||||
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
|
||||
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' 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..."
|
||||
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
|
||||
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
|
||||
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
|
||||
# a checkout has just written. It is added to the sloppiness already configured, and
|
||||
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
|
||||
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
|
||||
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
|
||||
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
|
||||
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
|
||||
fi
|
||||
# Hash the includes the compiler reports instead of preprocessing every miss.
|
||||
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
|
||||
export CCACHE_DEPEND=1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
||||
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
|
||||
# than removed: CMake fills a missing launcher entry from the environment.
|
||||
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
|
||||
fi
|
||||
|
||||
echo "Build params:"
|
||||
echo " - ARCH: $ARCH"
|
||||
echo " - BUILD_CONFIG: $BUILD_CONFIG"
|
||||
@@ -162,6 +210,7 @@ function build_deps() {
|
||||
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||
@@ -264,6 +313,7 @@ function build_slicer() {
|
||||
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
||||
|
||||
Vendored
+2
-17
@@ -1,22 +1,7 @@
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
if (JPEG_VERSION STREQUAL "6")
|
||||
message("Using Jpeg Lib 62")
|
||||
set(jpeg_flag "")
|
||||
elseif (JPEG_VERSION STREQUAL "7")
|
||||
message("Using Jpeg Lib 70")
|
||||
set(jpeg_flag "-DWITH_JPEG7=ON")
|
||||
else ()
|
||||
message("Using Jpeg Lib 80")
|
||||
set(jpeg_flag "-DWITH_JPEG8=ON")
|
||||
endif ()
|
||||
endif()
|
||||
|
||||
orcaslicer_add_cmake_project(JPEG
|
||||
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
||||
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
||||
DEPENDS ${ZLIB_PKG}
|
||||
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||
CMAKE_ARGS
|
||||
-DENABLE_SHARED=OFF
|
||||
-DENABLE_STATIC=ON
|
||||
${jpeg_flag}
|
||||
)
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
# Multi-extruder and multi-AMS device control
|
||||
|
||||
`DeviceManager`/`MachineObject` model the toolheads, AMS units, and filament slots of a connected
|
||||
printer and drive the device-control UI from that model. The model represents the counts as
|
||||
device-reported runtime values rather than compile-time caps, so a printer with an arbitrary number
|
||||
of toolheads, several AMS units, and a non-uniform number of slots per unit is handled by the same
|
||||
code paths. The printer agent is responsible for translating its printer's state into the shared
|
||||
JSON dialect this model consumes; the agent boundary is described in
|
||||
[printer-agent.md](printer-agent.md), and this document describes the dialect and the model.
|
||||
|
||||
## Model layout
|
||||
|
||||
`MachineObject` owns the device subsystems that this document covers:
|
||||
|
||||
- `DevExtderSystem` — the toolheads (extruders) and their per-toolhead filament/temperature state.
|
||||
- `DevFilaSystem` — the AMS units (`DevAms`) and their trays (`DevAmsTray`).
|
||||
- `DevNozzleSystem` — the nozzles and, on rack printers, `DevNozzleRack`.
|
||||
- `DevFilaSwitch` — a filament switch that can feed several AMS units to more than one extruder.
|
||||
|
||||
Each subsystem exposes its own count — `DevExtderSystem::GetTotalExtderCount()`,
|
||||
`DevFilaSystem::GetAmsCount()`, `DevAms::GetSlotCount()` — and the rest of the application derives
|
||||
its layout from those values rather than from fixed assumptions.
|
||||
|
||||
## Wire dialect
|
||||
|
||||
The dialect is the Bambu-shaped JSON that `DeviceManager` and `MachineObject` already consume. An
|
||||
agent whose printer speaks something else is expected to translate into it; the agent is not free
|
||||
to invent a new shape. Two representations coexist for extruders, and one for AMS/slots.
|
||||
|
||||
### Extruders
|
||||
|
||||
The generic representation is `print.device.extruder`, parsed by
|
||||
`ExtderSystemParser::ParseV2_0` (`DevExtruderSystem.cpp`). It has a packed `state` integer and an
|
||||
`info` array with one object per toolhead:
|
||||
|
||||
```text
|
||||
device.extruder.state packed integer
|
||||
bits 0..3 total extruder count
|
||||
bits 4..7 current extruder id
|
||||
bits 8..11 target extruder id
|
||||
bits 12..14 switch state
|
||||
bits 15..18 currently loading extruder id
|
||||
bit 19 busy for loading
|
||||
|
||||
device.extruder.info[] one entry per toolhead
|
||||
id extruder id
|
||||
filam_bak array of backup-group bits
|
||||
info bit 1 has filament, bit 2 buffer has filament, bit 3 nozzle present
|
||||
temp bits 0..15 current temperature, bits 16..31 target temperature
|
||||
spre slot_id bits 0..7, ams_id bits 8..15 (previous)
|
||||
snow same packing (current)
|
||||
star same packing (target)
|
||||
stat bit 0..15 AMS status, bits 16..31 RFID status
|
||||
hnow current nozzle id
|
||||
```
|
||||
|
||||
The legacy single/dual representation is the pair of scalar fields `nozzle_temper` /
|
||||
`nozzle_target_temper`, plus the `print.ams.tray_tar` / `tray_now` tray pointer, parsed by
|
||||
`ExtderSystemParser::ParseV1_0`. That parser is a fallback and only fills the main extruder when the
|
||||
reported toolhead count is one.
|
||||
|
||||
Physical extruder ids are fixed (`MAIN_EXTRUDER_ID = 0`, `DEPUTY_EXTRUDER_ID = 1`). Bambu printers
|
||||
present the two toolheads as a left/right pair and invert the physical-to-logical mapping; generic
|
||||
printers use the toolhead id itself as the logical id. `DevNozzle::GetLogicExtruderId()` /
|
||||
`GetExtruderId()` centralise that distinction.
|
||||
|
||||
### AMS units and slots
|
||||
|
||||
AMS units arrive as `print.ams.ams[]` and are parsed by `DevFilaSystemParser::ParseV1_0`. Each unit
|
||||
carries an `id` and an `info` bitfield:
|
||||
|
||||
```text
|
||||
info bits 0..3 type: 0 dummy, 1 AMS, 2 AMS-Lite, 3 N3F, 4 N3S, 5 AMS-Lite-mixed (N9)
|
||||
info bits 8..11 bound extruder id (0xE marks a switch-bound unit)
|
||||
info bits 24..27 switch inlet when the unit is switch-bound
|
||||
```
|
||||
|
||||
`DevAmsType` (`DevDefs.h`) enumerates the resulting unit kinds: `EXT_SPOOL`, `AMS`, `AMS_LITE`,
|
||||
`N3F`, `N3S`, and the `AMS_LITE_MIXED` N9 variant. `AMS_LITE_MIXED` reports itself as `AMS_LITE` to
|
||||
generic callers; code that needs the distinction uses `IsAmsLiteMixed()`.
|
||||
|
||||
A unit whose bound-extruder nibble is `0xE` is switch-bound: it is kept only when a `DevFilaSwitch`
|
||||
is installed, in which case its extruder id is pinned to the main extruder and its binding set
|
||||
becomes `{0, 1}`; otherwise the unit is dropped from the AMS list.
|
||||
|
||||
Each unit owns a map of `DevAmsTray` keyed by the tray id from the payload. The slot count is a
|
||||
property of the unit kind and the reported trays:
|
||||
|
||||
- `AMS`, `AMS_LITE`, and `N3F` report four slots on Bambu printers, matching the firmware contract;
|
||||
- `N3S` reports one slot;
|
||||
- `EXT_SPOOL` and any non-Bambu unit report the number of trays actually present.
|
||||
|
||||
The tray ids are the payload's slot indices; they are expected to be unique, 0-based, and contiguous
|
||||
within a unit, because the generic tray-index mapping below is cumulative.
|
||||
|
||||
### External spool slots
|
||||
|
||||
External/manual spools are virtual trays in `MachineObject::vt_slot`, separate from the trays owned
|
||||
by `DevFilaSystem`. Their stable id is `vt_id = VIRTUAL_TRAY_MAIN_ID - extruder_id`: 255 for
|
||||
extruder 0, 254 for extruder 1, then 253, 252, and so on. The index in `vt_slot` is therefore
|
||||
`VIRTUAL_TRAY_MAIN_ID - vt_id`, not the order in which a printer happens to report trays.
|
||||
|
||||
For non-Bambu printers, slot topology comes from the live `DevExtderSystem` extruder count, not from
|
||||
external-spool telemetry. `MachineObject::ensure_external_virtual_tray_slots()` grows `vt_slot` to
|
||||
one entry per extruder and enables virtual-tray support even when the agent reports no `vir_slot`,
|
||||
`vt_tray`, or external-spool data. It only adds missing entries, so reported tray metadata is kept.
|
||||
The shared ensure runs after push-status parsing has updated the extruder count and after the generic
|
||||
AMS/agent-filament parsers. Thus agents may report external-spool metadata, but do not have to invent
|
||||
empty slots just to make each toolhead's external-spool row visible.
|
||||
|
||||
When a payload does report virtual trays, `vir_slot` entries are placed by their id and missing
|
||||
indices are filled with placeholders. Generic agent `external` entries are likewise placed by
|
||||
extruder id, so sparse or out-of-order reports do not shift a spool onto another toolhead. An empty
|
||||
placeholder represents the slot's existence, not a claim that the printer reported a loaded spool.
|
||||
Bambu keeps its fixed 255/254 virtual-tray pair and does not use generic slot synthesis.
|
||||
|
||||
## Mapping
|
||||
|
||||
Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray
|
||||
index. Bambu mapping computes `ams_id * 4 + slot_id` for its fixed-width units; generic mapping in
|
||||
`DevFilaSystem` and the sidebar matcher uses the count of lanes in preceding units plus the slot id.
|
||||
The two agree for Bambu units and differ for a non-Bambu unit whose slot count is not four.
|
||||
|
||||
### AMS id to extruder id
|
||||
|
||||
`DevFilaSystem::GetExtruderIdByAmsId()` resolves an AMS id to the extruder it feeds:
|
||||
|
||||
- an AMS unit returns its bound extruder (`DevAms::GetExtruderId()`);
|
||||
- a virtual tray returns `VIRTUAL_TRAY_MAIN_ID - vt_id`;
|
||||
- Bambu's virtual AMS ids map to the main/deputy extruder.
|
||||
|
||||
A unit may be bound to more than one extruder when a `DevFilaSwitch` feeds it; the binding set
|
||||
(`DevAms::m_binded_extruder_set`) and switch inlet are read by switch-aware code while the
|
||||
single-extruder id stays pinned for legacy callers.
|
||||
|
||||
### Tray index
|
||||
|
||||
`DevFilaSystem::GetTrayIndexMap()` maps a global tray index to an `(ams_id, slot_id)` pair and is
|
||||
used for backup-slot translation, calibration addressing, and the mapping popups.
|
||||
|
||||
- Bambu AMS: `ams_id * 4 + slot_id`; N3S: the AMS id; AMS-Lite-mixed (N9): `24 + slot_id`.
|
||||
- Generic (non-Bambu): a running lane index — the count of trays in preceding units plus the slot
|
||||
id. This depends on each unit's tray ids being 0-based and contiguous.
|
||||
|
||||
The sidebar sync matcher uses the same identities. Bambu retains its fixed left/right AMS and
|
||||
external-spool choices. For a generic printer, a full sync considers AMS units according to their
|
||||
bound extruder ids and considers every external slot by `255 - vt_id`; a split left/right mapping
|
||||
still restricts itself to the corresponding legacy extruders 0 and 1. Generic AMS lanes use the
|
||||
cumulative tray index above, while their `ams_id`/`slot_id` pair remains the address used to update
|
||||
the matching sidebar entry. The runtime filament-list configs also carry a transient device-extruder
|
||||
association so generic entries are grouped under the correct toolhead after sync; that association
|
||||
is removed before the configs enter the slicing pipeline. Bambu keeps the existing key-based
|
||||
left/right grouping.
|
||||
|
||||
### Virtual-slot recognition
|
||||
|
||||
`devPrinterUtil::IsVirtualSlot()` decides whether an id denotes a virtual tray. On Bambu printers it
|
||||
is the fixed pair `{255, 254}`. On generic printers it is the descending range
|
||||
`[255 - count + 1, 255]`, where `count` is the selected printer preset's extruder count. Slot
|
||||
topology is grown from the live device extruder count, while this recognition range still uses the
|
||||
preset count; the preset is expected to match the connected device. A mismatch can therefore make
|
||||
the recognized virtual-id range disagree with `vt_slot`, and is reported by the existing count
|
||||
warning rather than silently accepted.
|
||||
|
||||
## Device-control UI
|
||||
|
||||
The device-control widgets follow the same device-reported counts.
|
||||
|
||||
- `AMSModel` (`AMSItem.hpp`) mirrors `DevAmsType` numerically: `EXT_AMS`, `GENERIC_AMS`, `AMS_LITE`,
|
||||
`N3F_AMS`, `N3S_AMS`. A plain `AMS` maps to `GENERIC_AMS`.
|
||||
- On non-Bambu printers `use_generic_ams_layout()` routes `GENERIC_AMS` units through a variable-lane
|
||||
rendering that draws one lane per reported tray; Bambu keeps its fixed four-slot rendering.
|
||||
- `AMSControl` picks its layout by toolhead count, not vendor: `CreateAmsSingleNozzle` for one
|
||||
toolhead, `CreateAmsDoubleNozzle` for two, and `CreateAmsMultiNozzle` (one preview and one AMS
|
||||
simplebook per toolhead) for three or more. A two-toolhead generic printer therefore keeps the
|
||||
legacy left/right presentation; that is deliberate.
|
||||
- The status panel builds external-spool entries from `MachineObject::vt_slot`; generic slot
|
||||
synthesis keeps one empty entry available for each live toolhead even when the printer does not
|
||||
report external-spool state.
|
||||
- The status panel's `ExtruderImage` renders one per-toolhead state per nozzle and is resized to
|
||||
`GetTotalExtderCount()`. Non-Bambu printers choose the active toolhead through
|
||||
`m_generic_nozzle_selector` (shown when more than one toolhead exists); Bambu keeps its left/right
|
||||
`m_nozzle_btn_panel`. Nozzle temperature controls are created on demand per toolhead.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Single dialect.** The agent translates its protocol into this JSON; the model does not learn
|
||||
vendor protocols. Extending the model with a new unit kind or field means extending the dialect.
|
||||
- **Runtime counts.** The agent reports toolhead count and AMS unit/tray data. The shared model derives
|
||||
generic external-spool slot count from the parsed live toolhead count; nothing caps the number of
|
||||
toolheads or generic slots except the id range and the Bambu contract described above.
|
||||
- **Extruder dialect invariants.** `device.extruder.state`'s count field must equal the length of
|
||||
`info[]`, and `info[]` must be ordered by extruder id with `id` equal to the array index: the
|
||||
parser places toolheads by array position while `GetExtderById()` indexes by id.
|
||||
- **One filament road per toolhead.** Each toolhead renders a single AMS filament road rather than an
|
||||
independent per-toolhead road state. A generic scaffold that allowed the latter was removed as
|
||||
unused; the one-extruder-per-toolhead road is the intended model.
|
||||
- **Preset/device agreement.** The generic layout and virtual-slot range derive their count from the
|
||||
selected printer preset. A printer preset must therefore declare the same number of
|
||||
`nozzle_diameter` entries as the device has toolheads. A mismatch is surfaced only as a log warning
|
||||
(`StatusPanel::check_extruder_count_mismatch()`), deliberately log-only, because the sidebar and
|
||||
extruder logic would otherwise silently disagree.
|
||||
- **Bambu compatibility.** Bambu-specific presentation (left/right pair, fixed 4-slot units, fixed
|
||||
virtual ids, physical/logical inversion) is preserved behind the vendor check. Generic paths do
|
||||
not alter it.
|
||||
- **Tray-index invariant.** The generic tray-index map is cumulative, so payload tray ids must be
|
||||
unique, 0-based, and contiguous per unit. A malformed id is a producer error.
|
||||
- **Protocol markers.** The dialect has no neutral equivalent for some Bambu state, so agents that
|
||||
lack it send empty `print.cfg`, `print.fun`, `print.aux`, and `print.stat`. Their presence selects
|
||||
the new-protocol parse path; they are compatibility markers, not feature switches.
|
||||
|
||||
## Main implementation locations
|
||||
|
||||
- [`DevExtruderSystem`](../../src/slic3r/GUI/DeviceCore/DevExtruderSystem.h) — toolhead model and the
|
||||
`device.extruder` parser
|
||||
- [`DevFilaSystem`](../../src/slic3r/GUI/DeviceCore/DevFilaSystem.h) — AMS/tray model, the AMS parser,
|
||||
and the mapping helpers
|
||||
- [`DevMapping.cpp`](../../src/slic3r/GUI/DeviceCore/DevMapping.cpp) — filament-to-device tray matching
|
||||
for Bambu and generic external/AMS slots
|
||||
- [`PresetBundle.cpp`](../../src/libslic3r/PresetBundle.cpp) — per-extruder grouping of the transient
|
||||
device filament list
|
||||
- [`DevDefs.h`](../../src/slic3r/GUI/DeviceCore/DevDefs.h) — `DevAmsType`, extruder ids, and virtual
|
||||
tray ids
|
||||
- [`DeviceManager.cpp`](../../src/slic3r/GUI/DeviceManager.cpp) — message dispatch, `vir_slot`
|
||||
parsing, external-slot topology, and device state assembly
|
||||
- [`AMSControl.cpp`](../../src/slic3r/GUI/Widgets/AMSControl.cpp) — per-toolhead AMS panes
|
||||
(`CreateAmsMultiNozzle`) and the generic layout
|
||||
- [`AMSItem.hpp`](../../src/slic3r/GUI/Widgets/AMSItem.hpp) — `AMSModel` and the variable-lane AMS
|
||||
rendering
|
||||
- [`StatusPanel.cpp`](../../src/slic3r/GUI/StatusPanel.cpp) — `ExtruderImage`, the toolhead selector,
|
||||
and per-toolhead temperature controls
|
||||
- [`MoonrakerPrinterAgent.cpp`](../../src/slic3r/Utils/MoonrakerPrinterAgent.cpp) — producer example
|
||||
(AMS payload and the `device.extruder` encoding)
|
||||
@@ -0,0 +1,255 @@
|
||||
# Preset Plugin API — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
The preset plugin API lets Python plugins update the presets OrcaSlicer is using,
|
||||
so users do not have to perform every routine setting change by hand. It covers
|
||||
process, printer, and filament presets. All three use one staged update lifecycle;
|
||||
the way a target is selected is specific to the preset type.
|
||||
|
||||
The API edits Orca's managed edited preset, not a detached copy or a saved preset
|
||||
behind the UI's back. It does not save to disk. Saving, discarding, and transferring
|
||||
unsaved edits remain under Orca's existing preset workflow.
|
||||
|
||||
The API does not create or delete presets or provide off-tab editing. For filament,
|
||||
choosing a different `preset_name` as the update target also selects that preset for
|
||||
the requested slot, following the normal UI flow. A standalone slot-assignment or
|
||||
preset-fork operation is outside this API's contract.
|
||||
|
||||
## Preset and slot model
|
||||
|
||||
Each preset collection maintains a managed edited copy for its selected preset.
|
||||
Settings tabs apply changes to that copy and own the normal dirty-state, validation,
|
||||
save/discard/transfer, and downstream refresh behavior. Plugin updates must use the
|
||||
same path so that the settings tab, dirty indicators, and effective print
|
||||
configuration observe a consistent state.
|
||||
|
||||
Filament selection adds a slot-to-preset mapping. `PresetBundle::filaments` is the
|
||||
catalog, while `PresetBundle::filament_presets` maps each slot to a preset name.
|
||||
Several slots may refer to the same preset. A filament update edits that shared
|
||||
preset, so all slots referring to it are affected. Slot numbers exposed by the API
|
||||
are 1-based, matching the UI.
|
||||
|
||||
## API shape
|
||||
|
||||
Each preset API exposes `begin_update()`, which selects and binds one target, and
|
||||
returns a transaction. The transaction stages setting changes using `set()` and
|
||||
applies them only on explicit `commit()`.
|
||||
|
||||
```python
|
||||
# Filament is targeted by its 1-based slot. Omitting preset_name keeps the
|
||||
# preset currently assigned to the slot; providing it selects that preset for
|
||||
# the slot before staging updates.
|
||||
tx = await orca.host.preset.filament.begin_update(
|
||||
slot_number=2,
|
||||
preset_name="Generic PLA"
|
||||
)
|
||||
tx.set("filament_flow_ratio", "0.98")
|
||||
tx.set("filament_retract_length", "0.8")
|
||||
result = tx.commit()
|
||||
|
||||
# Process and printer default to their active presets. A caller can request a
|
||||
# different preset that is selectable in the corresponding UI dropdown.
|
||||
tx = await orca.host.preset.process.begin_update(
|
||||
preset_name="0.20mm Standard"
|
||||
)
|
||||
tx.set("layer_height", "0.2")
|
||||
tx.commit()
|
||||
```
|
||||
|
||||
The example is the API contract, and the Python bridge provides it. `begin_update()`
|
||||
is awaitable: it suspends the plugin operation until the target selection and any
|
||||
unsaved-edit dialog settle, without blocking Orca's UI thread. `set()` and `commit()`
|
||||
run synchronously on the background Script worker once the awaitable resolves.
|
||||
Awaiting `begin_update()` requires the Script worker's running asyncio loop; the
|
||||
mechanism is described in [python-plugin-async-bridge.md](python-plugin-async-bridge.md).
|
||||
|
||||
### Target resolution
|
||||
|
||||
- **Filament:** `slot_number` identifies the slot. If `preset_name` is omitted,
|
||||
the transaction targets the preset currently assigned to that slot. If supplied,
|
||||
it requests that preset for the slot and selects it through the normal UI flow;
|
||||
the name must be among the presets offered by that slot's dropdown, with one
|
||||
exception: the preset currently assigned to the slot stays targetable even when the
|
||||
slot's dropdown hides it (for example an incompatible-but-selected preset). Selecting
|
||||
a different preset changes the slot-to-preset mapping. In both cases the transaction
|
||||
edits the shared preset, not an isolated slot override.
|
||||
- **Process and printer:** omitting `preset_name` targets the currently active
|
||||
preset. Supplying `preset_name` requests the preset by the name shown in its
|
||||
dropdown. It must be one of the choices the dropdown currently offers, rather
|
||||
than merely any preset present in the collection. This is particularly important
|
||||
for process presets, whose selectable choices can depend on the current context.
|
||||
- A transaction targets exactly one preset. Multi-preset transactions are not
|
||||
supported: applying them would require switching among targets while the
|
||||
application has only one managed edited copy per collection.
|
||||
|
||||
Targets are resolved through Orca's normal preset-selection model. The API should
|
||||
not accept raw preset pointers or filesystem paths as target identities. A missing,
|
||||
unavailable, or ambiguous target fails explicitly instead of silently selecting a
|
||||
different preset.
|
||||
|
||||
## Selection and user interaction
|
||||
|
||||
`begin_update()` resolves its target through shared binding helpers
|
||||
(`bind_preset_target` for process/printer, `bind_filament_target` for filament) and
|
||||
performs the selection only when the requested target differs from the active target;
|
||||
otherwise it returns a transaction without switching. The helpers use Orca's normal
|
||||
preset selection flow, `Tab::select_preset()`, including updating a filament slot
|
||||
assignment when requested. If the normal selection flow requires user input, it opens
|
||||
the relevant existing dialog(s), including unsaved-edit choices such as save, discard,
|
||||
or transfer. The operation is awaitable and remains pending until those interactions
|
||||
finish; it resolves with the bound transaction, or with a cancellation, without
|
||||
applying staged settings. A canceled selection does not change the requested target or
|
||||
filament slot assignment, but it does not roll back decisions the user already accepted
|
||||
in an earlier dialog. Cancellation of the awaiting coroutine is separate from a user
|
||||
cancel: a selection the user already accepted is not reversed, while the remaining slot
|
||||
assignment and the transaction creation are skipped when the cancellation is observed
|
||||
before the slot is reassigned (see [python-plugin-async-bridge.md](python-plugin-async-bridge.md)). The helpers then confirm that the requested preset actually
|
||||
became the edited preset; a successful selection request alone is not sufficient. There
|
||||
is no separate `switch_preset` plugin API.
|
||||
|
||||
If the requested target is already active, `begin_update()` returns without a
|
||||
selection prompt, apart from any UI-thread dispatch needed by the bridge. If a
|
||||
different preset is requested, Orca selects it through the normal UI flow. For
|
||||
filament, this also updates the requested slot's assignment when `preset_name`
|
||||
differs from its current assignment. When the current preset has unsaved changes,
|
||||
the plugin operation remains pending until the user chooses among Orca's existing
|
||||
options, such as transfer, discard, or cancel.
|
||||
|
||||
The API never suppresses that warning, chooses an option for the user, or switches
|
||||
away from dirty edits silently. Cancel aborts the operation before any staged target
|
||||
changes are applied. Process and printer updates follow this rule too when the
|
||||
plugin requests a different `preset_name`; they normally return immediately when
|
||||
the active preset is the target.
|
||||
|
||||
## Staging and commit semantics
|
||||
|
||||
`set(key, value)` stages a serialized config value and does not mutate the preset.
|
||||
The transaction is the boundary for a related group of changes, avoiding
|
||||
intermediate partially-updated states. `commit()` runs synchronously on the background
|
||||
Script worker. At commit, the API validates keys and values against the target preset
|
||||
type and applies the batch through `Tab::apply_preset_config_values`, the existing tab
|
||||
and settings-group update path. Existing setting-specific validation, correction, and
|
||||
callbacks remain authoritative; plugin updates do not create a parallel setter with
|
||||
different behavior.
|
||||
|
||||
A successful commit updates dirty state and downstream configuration/Plater state
|
||||
once for the batch. The edited preset remains unsaved; persistence continues through
|
||||
Orca's normal save action. A validation or commit failure restores the edited preset's
|
||||
config to its pre-batch state and recomputes the state derived from it (compatibility
|
||||
marks, the preset selector, dependent tabs, the Plater and the printer agent), so no
|
||||
staged setting is left partially applied. That guarantee is limited to the edited
|
||||
preset and its config-derived state: an `on_change` callback is a GUI method, not a pure
|
||||
function of the config, so the transaction does not reverse a preset or slot selection
|
||||
made while resolving the target, a dialog decision the user already accepted, an undo
|
||||
snapshot, a dependent preset's rewritten config, or anything already sent to a device.
|
||||
Exact all-or-nothing across those external effects is not achievable while the
|
||||
authoritative GUI callbacks run; the boundary is documented on
|
||||
`Tab::apply_preset_config_values`. A filament slot assignment made as part of target
|
||||
selection is likewise not a staged setting: after the user accepts the selection flow it
|
||||
may remain changed even if a later commit fails, and the API does not switch back or
|
||||
reverse the user's selection or any accepted transfer/discard decision for the previous
|
||||
preset.
|
||||
|
||||
## Outcomes and consistency
|
||||
|
||||
The API distinguishes target resolution failures, invalid setting/value, user
|
||||
cancellation, stale filament-slot mapping, and commit failure. A result identifies
|
||||
the target preset, whether preset selection changed, and (for filament) whether the
|
||||
slot assignment changed and which slots share the edited preset. A canceled
|
||||
selection applies neither the requested slot assignment nor staged target changes;
|
||||
a failed commit applies none of the staged target settings.
|
||||
|
||||
Filament slot assignments can change while a plugin operation is pending. The API
|
||||
must detect that and reject a stale operation instead of silently editing the new
|
||||
slot assignment. It must not hold raw preset pointers across reloads. Conflicting
|
||||
duplicate writes to one setting in a transaction are rejected rather than resolved
|
||||
by incidental call order.
|
||||
|
||||
The existing one-setting filament call,
|
||||
`orca.host.preset.filament.update(slot_number, key, value)`, remains available as a
|
||||
compatibility shorthand. It shares the staged API's target resolution, selection,
|
||||
validation, and commit path, and it is synchronous (it blocks the background Script
|
||||
worker) rather than awaitable.
|
||||
|
||||
## Design principles
|
||||
|
||||
- **Reduce routine interaction, preserve agency.** Clean switches should be
|
||||
seamless; unsaved user work must still receive the normal warning and decision.
|
||||
- **One source of truth.** UI and plugins share the managed edited copy and setting
|
||||
update path, including validation and callbacks.
|
||||
- **The UI workflow is authoritative.** The API must enter through the same
|
||||
selection and setting-change workflows as the UI, not reproduce their logic or
|
||||
write config values directly. This keeps existing and future side effects active:
|
||||
`on_change` handling for mutually exclusive, invalid, or out-of-range settings;
|
||||
compatibility and dependent-preset updates; and printer-agent changes when the
|
||||
selected printer changes.
|
||||
- **Explicit target and explicit commit.** Plugins know which preset they edit and
|
||||
when staged changes become visible.
|
||||
- **No hidden persistence.** Plugin edits remain unsaved until the user saves them
|
||||
through Orca.
|
||||
- **Shared means shared.** Editing a filament preset through one slot affects every
|
||||
slot assigned to it. Choosing another preset for a slot is explicit in
|
||||
`preset_name`; slot-specific divergence would require a separate fork operation.
|
||||
- **Consistent lifecycle, type-specific targeting.** Process, printer, and
|
||||
filament share transaction and error semantics while respecting their existing
|
||||
selection models.
|
||||
|
||||
## Implementation in the current codebase
|
||||
|
||||
The transaction API is implemented. The current plugin surface exposes `begin_update()`,
|
||||
`PresetTransaction` with `set()`/`commit()`, and the per-setting `update()` compatibility
|
||||
shorthand. For filament, `update_filament_slot()` resolves the preset already assigned
|
||||
to a slot, selects it through `Tab::select_preset()`, and applies one setting through
|
||||
`Tab::apply_preset_config_values()`; it does not change the slot assignment.
|
||||
|
||||
`begin_update()` is awaitable through a Future-backed UI continuation
|
||||
(`begin_preset_update_async` / `begin_filament_update_async` plus `AsyncFuture`; see
|
||||
[python-plugin-async-bridge.md](python-plugin-async-bridge.md)). It posts the target
|
||||
binding to the UI thread and resolves once the selection settles, so the wait does not
|
||||
block the UI thread. The `update()` shorthand keeps the synchronous, blocking form,
|
||||
which is valid only on the background Script worker.
|
||||
|
||||
`Tab::select_preset()` is the basis for switching to process, printer, or filament
|
||||
presets and for invoking the normal unsaved-edit flow. The flow is type-specific: a
|
||||
process selection can trigger compatibility handling for filaments, and a printer
|
||||
selection can involve technology changes and dependent preset compatibility. It may
|
||||
therefore show more than one dialog. Cancellation of a later step does not necessarily
|
||||
undo decisions the user already accepted in an earlier dialog. Target resolution
|
||||
preserves those side effects and then verifies the resulting edited target.
|
||||
|
||||
Filament `preset_name` is an additional integration on top of `Tab::select_preset()`:
|
||||
selecting a different name for a slot also updates the slot-to-preset mapping and its
|
||||
dependent UI/configuration state, mirroring the filament branch of the Plater's normal
|
||||
selection path (`PresetBundle::set_filament_preset()` plus the project/selection refresh,
|
||||
the dynamic filament list, the conditional support-filament flushing recalc, the slot
|
||||
combo refresh and its AMS sync badge). A mixed-color virtual slot is not targetable.
|
||||
|
||||
Process and printer dropdown choices are constrained by visibility and compatibility,
|
||||
and compatibility may depend on the active context. Target resolution checks the tab
|
||||
dropdown's actual items before the selection flow, so a target the user cannot pick
|
||||
fails without raising that flow's unsaved-edit prompt; a name two entries carry is
|
||||
rejected as ambiguous. A filament `preset_name` resolves against the requested slot's
|
||||
own sidebar selector rather than the tab's, so that slot's compatibility filter is the
|
||||
one that applies. A name the slot's dropdown does not show is rejected unless it names
|
||||
that slot's current assignment (a hidden or incompatible-but-selected preset), which
|
||||
stays targetable.
|
||||
|
||||
`commit()` applies its batch through `Tab::apply_preset_config_values`, which parses and
|
||||
validates the whole batch before touching the edited preset and restores the edited
|
||||
preset and its config-derived state on failure (see "Staging and commit semantics" for
|
||||
the limit of that rollback). Since `begin_update()` performs target selection before the
|
||||
caller stages values, invalid staged values can still follow a user-visible selection
|
||||
prompt; that is a consequence of this API shape and remains explicit in its behavior.
|
||||
|
||||
## Integration boundary
|
||||
|
||||
The plugin API coordinates target resolution, preset selection, staged values, and
|
||||
the settings commit path. It does not own or duplicate preset dirty-state logic,
|
||||
selection prompts, setting validation, or persistence. Target changes flow through
|
||||
the same UI selection path so dependent behavior—such as selecting the printer
|
||||
agent—still runs. Setting changes flow through the same UI change path so `on_change`
|
||||
and related validation/correction behavior still runs. The integration bridges UI
|
||||
prompts to the Python operation asynchronously (the awaitable `begin_update()`), reuses
|
||||
the normal Plater slot-selection behavior for explicit filament targets, and commits a
|
||||
batch without losing existing per-setting behavior.
|
||||
@@ -0,0 +1,398 @@
|
||||
# Python plugin async bridge — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
The embedded Python plugin runtime runs capability calls synchronously: a C++ call site acquires the
|
||||
GIL and the capability's Python method runs to completion on the calling thread. That model cannot
|
||||
express an operation that has to wait for the user — most visibly the staged preset update
|
||||
(`begin_update()` in [preset-plugin-api.md](preset-plugin-api.md)), whose target selection can open
|
||||
unsaved-edit dialogs and must not block Orca's UI thread while it waits.
|
||||
|
||||
This document describes the bridge that lets a Python plugin suspend and resume such an operation
|
||||
without blocking the UI thread. The bridge is deliberately narrow: only the Script capability gains
|
||||
an awaitable execution path. Every other capability — including the SlicingPipeline hooks — stays
|
||||
synchronous.
|
||||
|
||||
## Execution baseline
|
||||
|
||||
### How a capability call runs
|
||||
|
||||
A capability instance is a Python object owned through `Slic3r::PluginInstanceHandle`
|
||||
(`src/slic3r/plugin/PythonPluginBridge.cpp`) and handed to the rest of the program as a
|
||||
`std::shared_ptr<Slic3r::PluginCapabilityInterface>`. Its identity (`name()`, `type()`,
|
||||
`audit_plugin_key()`) is resolved once at materialization and cached in C++
|
||||
(`PluginCapabilityInterface` in `src/slic3r/plugin/PythonPluginInterface.hpp`).
|
||||
|
||||
Every C++→Python call crosses a trampoline method. `PyPluginCommonTrampoline` and
|
||||
`PyPluginInterfaceTrampoline` (`src/slic3r/plugin/PyPluginTrampoline.hpp`), and the per-type subclass
|
||||
`PyScriptPluginCapabilityTrampoline`
|
||||
(`src/slic3r/plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp`), wrap the call in the
|
||||
`ORCA_PY_OVERRIDE_AUDITED` macro. For one synchronous call that macro, in order:
|
||||
|
||||
1. constructs a `PluginCapabilityInterface::RefCounter`, which `increment()`s the capability's
|
||||
reference count for the call's duration;
|
||||
2. constructs a `PythonGILState`, which first acquires a `PythonRuntimeLease` and then
|
||||
`PyGILState_Ensure()`;
|
||||
3. opens a `ScopedPluginAuditContext(audit_plugin_key(), name())`;
|
||||
4. dispatches into Python via `PYBIND11_OVERRIDE`/`PYBIND11_OVERRIDE_PURE`;
|
||||
|
||||
and unwinds those guards when the call returns.
|
||||
|
||||
`PythonGILState` and `PythonRuntimeLease` live in `src/slic3r/plugin/PythonInterpreter.hpp`. A lease is
|
||||
a `shared_lock` on `PythonInterpreter::m_runtime_mutex`; `PythonInterpreter::shutdown()` takes the
|
||||
exclusive side, so a thread that holds a lease can safely touch Python objects, and interpreter
|
||||
finalization cannot run underneath it. `ScopedPluginAuditContext`
|
||||
(`src/slic3r/plugin/PluginAuditManager.hpp`) stores the previous thread-local plugin/capability pair
|
||||
and scoped filesystem roots, sets this call's, and restores them on destruction; the audit hook then
|
||||
attributes every filesystem/socket/process event to the plugin currently executing on that thread.
|
||||
|
||||
### The Script capability
|
||||
|
||||
`Slic3r::ScriptPluginCapability` (`src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp`)
|
||||
declares one pure virtual method, `ExecutionResult execute()`. The async runtime invokes it through
|
||||
`invoke_script_execute` (`src/slic3r/plugin/PyPluginTrampoline.hpp`), which replicates the audited
|
||||
trampoline call (ref counter, audit scope, traceback-preserving rethrow) but returns a
|
||||
`ScriptExecuteInvocation` instead of forcing a cast:
|
||||
|
||||
- a plain `def execute` runs to completion in `invoke_script_execute` and yields
|
||||
`ScriptExecuteInvocation::result`;
|
||||
- an `async def execute` (or any override that returns a coroutine, detected with
|
||||
`inspect.iscoroutine`) yields the coroutine object in `ScriptExecuteInvocation::awaitable` without
|
||||
running it, so the runtime can drive it.
|
||||
|
||||
`ScriptExecuteInvocation` is defined in `PyPluginTrampoline.hpp`; `py_is_coroutine` is the detection
|
||||
helper.
|
||||
|
||||
### UI marshalling
|
||||
|
||||
`PluginHostUi::run_on_ui_thread` (`src/slic3r/plugin/host/PluginHostUi.cpp`) forwards to the
|
||||
file-local `run_on_ui_blocking`:
|
||||
|
||||
- if the caller is already on the wx main thread, it runs the callable inline after a
|
||||
`py::gil_scoped_release`;
|
||||
- otherwise it sets up a `std::promise`/`std::future`, releases the GIL, posts the callable with
|
||||
`GUI::wxGetApp().CallAfter`, and blocks on `future.get()`.
|
||||
|
||||
That is a blocking handoff: the caller's stack is parked on a thread while the UI thread runs the
|
||||
callable. It is the correct primitive for the synchronous `update()`/`commit()` path and for
|
||||
`reload_local_bundle`/`list_bundle_ids`, but it cannot be used by an operation that must suspend.
|
||||
|
||||
### Slicing hooks
|
||||
|
||||
`Print::set_slicing_pipeline_hook_fn` is installed by `src/slic3r/plugin/PluginHooks.cpp`. The hook
|
||||
runs on the slicing worker thread and dispatches to `SlicingPipelinePluginCapability::execute(ctx)`
|
||||
per capability, acquiring a `PythonGILState` per capability. `SlicingPipelineContext`
|
||||
(`SlicingPipelinePluginCapability.hpp`) holds raw pointers into the live slicing graph
|
||||
(`ctx.print`, `ctx.object`) that are valid only for the duration of `execute(ctx)`. The UI thread can
|
||||
be blocked waiting on the slicing worker (`BackgroundSlicingProcess::stop()` parks it), so a
|
||||
marshalled UI call from a hook can deadlock; `PluginHostSlicing.cpp`, `SlicingPipelinePluginCapability.hpp`
|
||||
and `PluginHostUi.hpp` all document this.
|
||||
|
||||
## Script-only async execution
|
||||
|
||||
### Per-invocation worker and loop
|
||||
|
||||
`ScriptAsyncRuntime` (declared in `src/slic3r/plugin/PluginManager.hpp`, implemented in
|
||||
`PluginManager.cpp`) drives every Script invocation off the caller's thread. Its `start()` creates a
|
||||
`Run` and launches a detached worker thread that runs `Impl::run_invocation`. There is one worker
|
||||
thread per invocation, so no invocation ever blocks the UI thread and no invocation is queued behind
|
||||
another plugin's.
|
||||
|
||||
`run_invocation` runs detection and execution on the worker:
|
||||
|
||||
- it re-checks `cancel_requested` before anything (a cancellation that arrived before the run
|
||||
started is honored without running the plugin);
|
||||
- it calls `invoke_script_execute`; a plain `def execute` result is used directly, and an `async def`
|
||||
coroutine is driven on a loop created for that invocation:
|
||||
`asyncio.new_event_loop()` → `asyncio.set_event_loop(loop)` → `loop.create_task(coroutine)` →
|
||||
`loop.run_until_complete(task)` → `loop.close()`, then `asyncio.set_event_loop(None)`.
|
||||
|
||||
The loop is per invocation, not global, and no loop outlives its invocation. The loop for the
|
||||
calling thread is published in the thread-local `tl_script_loop`, exposed to host awaitables through
|
||||
`ScriptAsyncRuntime::current_loop()` (GIL held).
|
||||
|
||||
`PluginManager::launch_script_capability` is the entry point: it resolves the capability, hands it to
|
||||
`ScriptAsyncRuntime::start`, and sets `ScriptLaunchResult::launched_async`. The completion callback
|
||||
runs on the GUI thread (or inline when there is no wx app), and `launch_script_capability` records a
|
||||
run error through `set_plugin_error` before invoking the caller's `ScriptCompletionFn`.
|
||||
`PluginManager::run_script_capability` is kept as a compatibility wrapper: it waits for the
|
||||
completion callback, pumping `wxYield()` when it is itself on the GUI thread, and returns the
|
||||
`ExecutionResult`.
|
||||
|
||||
### Capability, runtime, GIL and audit lifetime across a suspension
|
||||
|
||||
For the whole invocation — including an `async def` suspended at an `await` — `run_invocation` holds:
|
||||
|
||||
- a `PluginCapabilityInterface::RefCounter` per invocation and a `shared_ptr` to the capability in the
|
||||
`Run`, so the capability and its module stay alive across awaits and unload cannot drop them;
|
||||
- a `PythonRuntimeLease` (acquired before the GIL). `PythonInterpreter::shutdown()` cannot finalize
|
||||
the interpreter while any invocation worker holds a lease;
|
||||
- the GIL, acquired with `pybind11::gil_scoped_acquire` on the worker;
|
||||
- a `ScopedPluginAuditContext(audit_plugin_key(), name())`, so filesystem/socket/process events
|
||||
raised by the coroutine are attributed to the plugin.
|
||||
|
||||
The capability reference is released inside the GIL scope (`run->capability.reset()`), because
|
||||
dropping the pybind holder from a non-Python thread would `DECREF` without the GIL. The loop is
|
||||
closed and `tl_script_loop` cleared before the worker exits.
|
||||
|
||||
A consequence of running Script on a non-main thread: the audit permission prompt is fail-closed off
|
||||
the main thread. `request_permission` in `src/slic3r/plugin/PluginAuditManager.cpp` posts the dialog
|
||||
with `CallAfter` and denies the current event, persisting the grant when the user accepts, so a
|
||||
Script sees a denial on the first attempt and can succeed on a later one. This matches the existing
|
||||
behavior for the plugin-load worker and the slicing worker.
|
||||
|
||||
### Awaitable UI continuation
|
||||
|
||||
`AsyncFuture` (`src/slic3r/plugin/host/PluginHostUi.hpp`, implemented in `PluginHostUi.cpp`) is the
|
||||
suspension primitive for a host call that must show a dialog before it can resolve. It carries no GUI
|
||||
or preset state — only the loop, the Future created on it, and a Python-side resolver:
|
||||
|
||||
- `AsyncFuture::create()` requires the GIL and a running asyncio loop. It binds
|
||||
`asyncio.get_running_loop()`, creates a Future with `create_future()`, and installs the resolver
|
||||
`make_future_resolver` and the cancellation watcher `make_cancellation_watcher` on the Future.
|
||||
- `complete(value)`/`fail(exc)` marshal the outcome with `loop.call_soon_threadsafe(m_bind, …)`, so
|
||||
the Future is resolved on its own loop thread, never from the UI thread. The resolver drops an
|
||||
outcome whose Future is already cancelled (`set_result`/`set_exception` on a cancelled Future would
|
||||
raise `InvalidStateError`).
|
||||
- Cancellation is observed two ways, so the UI thread can decide without touching the loop. The
|
||||
loop-thread done-callback `make_cancellation_watcher` sets an atomic (`abandon()`); `is_cancelled()`
|
||||
additionally reads the Python Future's own `cancelled()` under the GIL, so a cancellation that
|
||||
`Future.cancel()` applied synchronously is seen even before the loop runs the done-callback.
|
||||
`is_cancelled()` never mutates the Future or the loop and is safe from the UI thread. A queued
|
||||
selection therefore never starts, and one inside a modal stops when the modal returns.
|
||||
`~AsyncFuture` releases its Python references under the GIL, or intentionally leaks them if the
|
||||
interpreter is already gone (same rule as the UI callable holders).
|
||||
|
||||
`PluginHostUi::post_to_ui_thread(fn)` posts work to the UI thread without blocking (returns false when
|
||||
the application is not initialized); `require_ui_application()` fails synchronously so an awaitable
|
||||
entry point can reject before touching asyncio.
|
||||
|
||||
### Awaitable `begin_update()`
|
||||
|
||||
`begin_update()` is a real awaitable. `begin_preset_update_async` and `begin_filament_update_async`
|
||||
(`src/slic3r/plugin/host/PluginHostPresets.cpp`) run on the plugin worker with the GIL held and a
|
||||
running loop:
|
||||
|
||||
1. validate the application synchronously (`require_ui_application()`), so a call without a wx app
|
||||
fails exactly like `update()`;
|
||||
2. create an `AsyncFuture` on the running loop and register it with
|
||||
`PluginHostUi::track_pending_async(handle)`;
|
||||
3. post the selection to the UI thread with `PluginHostUi::post_to_ui_thread`;
|
||||
4. return the Future at once.
|
||||
|
||||
The UI callback first skips the selection entirely when `handle->is_cancelled()`, then runs the
|
||||
target binding with the GIL released — `bind_preset_target` for process/printer, `bind_filament_target`
|
||||
for filament — passing a `CancelProbe` that also reads `handle->is_cancelled()`. It reuses the normal
|
||||
selection flow `Tab::select_preset()`, including the unsaved-edit prompt. On success it resolves the
|
||||
Future with the name-based `PresetTransaction`; on failure it fails the Future with the mapped Python
|
||||
exception. Nothing is staged until the caller's later `set()`/`commit()`. An unavailable target is
|
||||
rejected before selection and changes no preset. If the user declines selection, decisions already
|
||||
accepted in an earlier dialog are not rolled back. Coroutine cancellation does not undo an accepted
|
||||
preset selection; for filament, slot reassignment is skipped when cancellation is observed before the
|
||||
mapping check, but may remain if cancellation arrives after that check, even when no transaction is
|
||||
delivered. No GUI or preset pointer crosses the suspension — the awaitable holds only copied
|
||||
category/name/slot values.
|
||||
|
||||
`set()` stages a serialized value without mutating the preset, and `commit()` applies the batch
|
||||
through `Tab::apply_preset_config_values` on the UI thread; both are synchronous and, per their
|
||||
bindings, valid only on the background Script worker (they call `PluginHostUi::run_on_ui_thread`,
|
||||
which blocks the worker, or runs inline if entered from the UI thread).
|
||||
|
||||
### Cancellation and accepted-selection boundary
|
||||
|
||||
Two distinct cancellations exist:
|
||||
|
||||
- **User declined the selection** — `Tab::select_preset()` returned false. `bind_*_target` throws
|
||||
`SelectionCancelled`, which the awaitable path maps to `orca.host.preset.PresetSelectionCancelled`
|
||||
(a `RuntimeError` subclass). The synchronous `update()` path keeps raising plain `RuntimeError`.
|
||||
- **The awaiting coroutine was cancelled** — task cancellation or plugin unload. `AsyncFuture`
|
||||
observes this two ways: the loop-thread done-callback sets an atomic, and `is_cancelled()` also
|
||||
reads the Future's own `cancelled()` under the GIL (see above). `bind_*_target` checks its
|
||||
`CancelProbe` after `select_preset()` returns and after the modal-free paths, throwing
|
||||
`SelectionAbandoned`; the outcome is dropped because the handle is cancelled/abandoned, so the
|
||||
coroutine sees `asyncio.CancelledError`. A queued selection never starts.
|
||||
|
||||
`ScriptAsyncRuntime::cancel_plugin(plugin_key)` sets `cancel_requested` on every active run for the
|
||||
plugin and, when a loop is live (`has_loop`), cancels the task with
|
||||
`loop.call_soon_threadsafe(task.cancel)` while holding the GIL. A synchronous `def execute`
|
||||
invocation has no loop, so only the flag is set (checked before the run starts).
|
||||
|
||||
The accepted-selection boundary is deliberate and cannot be fully enforced once the UI flow has
|
||||
begun:
|
||||
|
||||
- A selection the user already accepted — including any unsaved-edit dialog decision and the switch
|
||||
itself — is a UI decision and is **not** reversed. A cancellation that lands after the modal has
|
||||
been answered leaves that accepted selection in place.
|
||||
- Cancellation only skips the remaining work. In the filament path, the slot assignment and the
|
||||
transaction creation are skipped only when the cancellation is observed **before** the slot mapping
|
||||
(`bind_filament_target` checks the `CancelProbe` immediately before it reassigns the slot); the
|
||||
transaction is likewise skipped when cancellation is observed before it is created.
|
||||
- A narrow window remains where a cancellation and the operation's start race: if the cancel lands
|
||||
just as the UI callback begins, the selection may still start. Whichever side wins, the coroutine
|
||||
observes `asyncio.CancelledError`, never a plugin error.
|
||||
|
||||
A finished `commit()` is unaffected by a later cancellation.
|
||||
|
||||
A plugin's own `asyncio.CancelledError` is treated as a cancellation only when `cancel_requested` was
|
||||
set; otherwise it stays a genuine error (see `run_invocation`), so a plugin that cancels its own task
|
||||
does not masquerade as an unload cancellation.
|
||||
|
||||
### Unload and shutdown drain
|
||||
|
||||
Unloading or shutting down must guarantee that no plugin Python code runs after its module is
|
||||
unloaded, and that a posted UI selection cannot mutate presets after teardown.
|
||||
|
||||
- `PluginManager::unload_plugin` and `unload_all_plugins` call
|
||||
`PluginHostUi::abandon_pending_async(plugin_key)` first, so the earliest possible UI callback
|
||||
cannot resume the plugin's coroutine into a run that is being cancelled.
|
||||
- `PluginManager::cancel_and_wait_for_capabilities` calls `on_cancelled()` first (so the plugin can
|
||||
observe state while the run is intact), then `ScriptAsyncRuntime::cancel_plugin`, then drains.
|
||||
The drain is **unbounded while a Script invocation worker or a posted host UI callback for the
|
||||
plugin is outstanding** — its loop-exit condition is `all_released && !invocation_active &&
|
||||
!ui_pending`. `has_active_run()` distinguishes a run that was cancelled before it started from one
|
||||
whose worker is still live; `pending_async_count()` counts posted UI callbacks whose weak
|
||||
registration has not expired. On the GUI thread the drain pumps `wxYield()` so a posted callback
|
||||
(possibly a modal) can settle instead of being starved by the blocking wait. Only the non-Script
|
||||
path keeps the historical bounded 2-second grace: when neither an invocation nor a UI callback is
|
||||
pending, the drain stops waiting for capability references at the grace deadline and logs any
|
||||
remaining ones.
|
||||
- `PluginManager::shutdown` detaches the libslic3r hooks (`plugin_hooks::uninstall`), stops new
|
||||
loads, waits for in-flight loads, unloads every plugin (draining the async runs above), then calls
|
||||
`ScriptAsyncRuntime::instance().shutdown()`, which refuses new runs and waits without a deadline
|
||||
for every invocation worker to finish — nothing is torn down by force. `ScriptAsyncRuntime::reset()`
|
||||
re-arms the runtime so a later `initialize()` in the same process can start runs again.
|
||||
|
||||
Cancellation triggered by unload or shutdown is a separate, expected completion, not a plugin
|
||||
failure. `run_invocation` marks a run whose `cancel_requested` was set as `completion.cancelled` and
|
||||
clears any error, whatever the coroutine body did. `launch_script_capability` records a plugin error
|
||||
only when `!completion.cancelled && !completion.error.empty()`; `run_script_capability` reports an
|
||||
empty error for a cancelled completion; and the two GUI launchers treat `cancelled` as neither error
|
||||
nor success (the speed dial stays silent, the Plugins dialog updates no status). A plugin's own
|
||||
`CancelledError` is not counted as this expected termination unless `cancel_requested` is set, so a
|
||||
self-cancelled plugin still records its failure.
|
||||
|
||||
## Supported and unsupported capabilities
|
||||
|
||||
| Capability | Async support | Reason |
|
||||
| --- | --- | --- |
|
||||
| Script | `def execute` and `async def execute` both run on the per-invocation worker | The runtime owns the worker and, for a coroutine, a per-invocation loop; nothing about a Script invocation borrows C++ state. |
|
||||
| SlicingPipeline | Synchronous only | `execute(ctx)` borrows `Print`/`PrintObject` for the callback's duration, and the UI thread can be blocked on the slicing worker. `PluginHooks.cpp` rejects an async override: `cap->execute()` casts its Python result to `ExecutionResult`, and a coroutine object cannot be cast, so it surfaces as a `SlicingError` rather than being scheduled. |
|
||||
| PrinterConnection, Pages, Analysis, Importer, Exporter, Visualization, ResourceTexture | Unchanged | Not part of this bridge; they keep their existing synchronous contract. |
|
||||
|
||||
## Deadlock constraints
|
||||
|
||||
- **Never block the UI thread on plugin work.** Every Script launch path goes through
|
||||
`launch_script_capability`; the speed-dial `ActionRegistry` action and the Plugins dialog launcher
|
||||
return immediately and report the outcome later (see Examples).
|
||||
- **Never call `orca.host.ui.*` from a slicing hook.** The slicing worker runs while the UI thread
|
||||
can be parked on it.
|
||||
- **Resolve an awaitable's Future only with `loop.call_soon_threadsafe`.** `AsyncFuture::complete`/
|
||||
`fail` do this; the UI thread never touches the loop or the Future directly.
|
||||
- **Create host awaitables only on the worker with a running loop.** `AsyncFuture::create()` binds
|
||||
`asyncio.get_running_loop()`; `begin_update()`'s binding requires this.
|
||||
- **No pointer crosses a suspension.** Awaitables carry copied values; the `SlicingPipelineContext`
|
||||
graph is valid only within one synchronous `execute(ctx)`.
|
||||
|
||||
## File and symbol map
|
||||
|
||||
Script execution runtime:
|
||||
|
||||
- `src/slic3r/plugin/PluginManager.hpp` / `.cpp` — `ScriptAsyncRuntime` (`instance`, `start`,
|
||||
`cancel_plugin`, `has_active_run`, `shutdown`, `reset`, `current_loop`), `ScriptCompletion`,
|
||||
`ScriptCompletionFn`, `ScriptLaunchResult`, `launch_script_capability`, `run_script_capability`.
|
||||
- `src/slic3r/plugin/PyPluginTrampoline.hpp` — `invoke_script_execute`, `ScriptExecuteInvocation`,
|
||||
`py_is_coroutine`.
|
||||
- `src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp` / `ScriptPluginCapabilityTrampoline.hpp`
|
||||
— `ScriptPluginCapability::execute`, `PyScriptPluginCapabilityTrampoline`.
|
||||
|
||||
Lifetime and GIL primitives:
|
||||
|
||||
- `src/slic3r/plugin/PythonInterpreter.hpp` — `PythonInterpreter`, `PythonRuntimeLease`,
|
||||
`PythonGILState`.
|
||||
- `src/slic3r/plugin/PythonPluginInterface.hpp` — `PluginCapabilityInterface`, `RefCounter`,
|
||||
`increment`/`decrement`/`ref_count`.
|
||||
- `src/slic3r/plugin/PluginAuditManager.hpp` / `.cpp` — `ScopedPluginAuditContext`,
|
||||
`request_filesystem_read_permissions`, `request_permission` (fail-closed off the main thread).
|
||||
|
||||
UI suspension and lifecycle tracking:
|
||||
|
||||
- `src/slic3r/plugin/host/PluginHostUi.hpp` / `.cpp` — `AsyncFuture` (`create`, `complete`, `fail`,
|
||||
`abandon`, `is_cancelled`), `make_future_resolver`, `make_cancellation_watcher`,
|
||||
`PluginHostUi::post_to_ui_thread`, `require_ui_application`, `track_pending_async`,
|
||||
`abandon_pending_async`, `pending_async_count`, `run_on_ui_thread`/`run_on_ui_blocking`.
|
||||
- `GUI::wxGetApp().CallAfter` — the post-to-UI-thread primitive.
|
||||
|
||||
Preset targets:
|
||||
|
||||
- `src/slic3r/plugin/host/PluginHostPresets.cpp` — `PresetEditor`, `PresetTransaction`,
|
||||
`bind_preset_target`, `bind_filament_target`, `begin_preset_update_async`,
|
||||
`begin_filament_update_async`, `PresetSelectionCancelled`, `SelectionCancelled`,
|
||||
`SelectionAbandoned`, `CancelProbe`, `resolve_combo_preset`/`resolve_preset_in_combo`.
|
||||
- `src/slic3r/GUI/Tab.hpp` / `.cpp` — `Tab::select_preset`, `Tab::apply_preset_config_values`,
|
||||
`Tab::commit_preset_config_value`.
|
||||
|
||||
Launch paths:
|
||||
|
||||
- `src/slic3r/GUI/ActionRegistry.cpp` — `PluginScriptAction::run`, `notify_script_outcome`.
|
||||
- `src/slic3r/GUI/PluginsDialog.cpp` — `PluginsDialog::run_script_plugin_capability`.
|
||||
|
||||
Slicing (synchronous only):
|
||||
|
||||
- `src/slic3r/plugin/PluginHooks.cpp` — `install_slicing_pipeline_hook`.
|
||||
- `src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp` —
|
||||
`SlicingPipelineContext`, `execute`.
|
||||
|
||||
Unload/shutdown:
|
||||
|
||||
- `src/slic3r/plugin/PluginManager.cpp` — `unload_plugin`, `unload_all_plugins`,
|
||||
`cancel_and_wait_for_capabilities`, `shutdown`.
|
||||
|
||||
## Examples
|
||||
|
||||
An `async def execute` that awaits a preset update. `execute` runs on the background Script worker
|
||||
with its own asyncio loop; `begin_update()` suspends the coroutine until any selection / unsaved-edit
|
||||
dialog is answered, without blocking the UI thread; `set()`/`commit()` run synchronously on the
|
||||
worker. This is the shape used by `sandboxes/orca_preset_transaction_demo.py`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
import orca
|
||||
|
||||
|
||||
class AutoTune(orca.script.ScriptPluginCapabilityBase):
|
||||
def get_name(self):
|
||||
return "Auto tune"
|
||||
|
||||
async def execute(self):
|
||||
# begin_update() is awaitable: it resolves once the user answers any
|
||||
# selection / unsaved-edit dialog. The UI thread stays responsive.
|
||||
tx = await orca.host.preset.process.begin_update(preset_name="0.20mm Standard")
|
||||
tx.set("layer_height", "0.2")
|
||||
tx.commit()
|
||||
return orca.ExecutionResult.success("process preset updated")
|
||||
```
|
||||
|
||||
The window-driven demo shows the same pattern where the plugin's own UI must hand work to the worker:
|
||||
`on_message` fires on the UI thread and only enqueues onto the worker loop with
|
||||
`loop.call_soon_threadsafe`; `execute` consumes that queue and awaits `begin_update()` per command.
|
||||
|
||||
Continuation sequence for one `await begin_update(...)`:
|
||||
|
||||
```text
|
||||
Script worker (owned, per invocation) UI thread
|
||||
loop.run_until_complete(task)
|
||||
await begin_update() ─────────────────► post_to_ui_thread
|
||||
(suspended) bind_*_target →
|
||||
Tab::select_preset() … dialog
|
||||
settles
|
||||
loop.call_soon_threadsafe(fut.set_result) ◄──── AsyncFuture::complete
|
||||
coroutine resumes with PresetTransaction
|
||||
execute() returns ExecutionResult
|
||||
```
|
||||
|
||||
A plain `def execute` needs no loop and runs to completion on the same worker; both kinds are
|
||||
launched with `launch_script_capability`, so the caller (UI thread) never blocks. A launch failure is
|
||||
reported inline; a started run reports its outcome through the completion callback — the speed dial
|
||||
shows "started" and pushes a completion notification, and the Plugins dialog updates its status when
|
||||
`finish` runs.
|
||||
+1336
-56
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
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
+4922
-1909
File diff suppressed because it is too large
Load Diff
+6162
-2709
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="1.5" y="3.5" width="12" height="8" rx="1" ry="1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M3,5h1v1h-1Z M5,5h1v1h-1Z M7,5h1v1h-1Z M9,5h1v1h-1Z M11,5h1v1h-1Z M4,7h1v1h-1Z M6,7h1v1h-1Z M8,7h1v1h-1Z M10,7h1v1h-1Z M5,9h5v1h-5Z" style="fill:#fefefe;"/></svg>
|
||||
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 415 B |
@@ -592,6 +592,10 @@
|
||||
"name": "SUNLU PETG @base",
|
||||
"sub_path": "filament/SUNLU/SUNLU PETG @base.json"
|
||||
},
|
||||
{
|
||||
"name": "School House Spools PETG @base",
|
||||
"sub_path": "filament/SchoolHouseSpools/School House Spools PETG @base.json"
|
||||
},
|
||||
{
|
||||
"name": "eSUN PETG @base",
|
||||
"sub_path": "filament/eSUN/eSUN PETG @base.json"
|
||||
@@ -1096,6 +1100,10 @@
|
||||
"name": "SUNLU Wood PLA @base",
|
||||
"sub_path": "filament/SUNLU/SUNLU Wood PLA @base.json"
|
||||
},
|
||||
{
|
||||
"name": "School House Spools PLA @base",
|
||||
"sub_path": "filament/SchoolHouseSpools/School House Spools PLA @base.json"
|
||||
},
|
||||
{
|
||||
"name": "Valment PLA @base",
|
||||
"sub_path": "filament/Valment/Valment PLA @base.json"
|
||||
@@ -2260,6 +2268,10 @@
|
||||
"name": "SUNLU PETG @System",
|
||||
"sub_path": "filament/SUNLU/SUNLU PETG @System.json"
|
||||
},
|
||||
{
|
||||
"name": "School House Spools PETG @System",
|
||||
"sub_path": "filament/SchoolHouseSpools/School House Spools PETG @System.json"
|
||||
},
|
||||
{
|
||||
"name": "eSUN PETG @System",
|
||||
"sub_path": "filament/eSUN/eSUN PETG @System.json"
|
||||
@@ -3812,6 +3824,10 @@
|
||||
"name": "SUNLU Wood PLA @System",
|
||||
"sub_path": "filament/SUNLU/SUNLU Wood PLA @System.json"
|
||||
},
|
||||
{
|
||||
"name": "School House Spools PLA @System",
|
||||
"sub_path": "filament/SchoolHouseSpools/School House Spools PLA @System.json"
|
||||
},
|
||||
{
|
||||
"name": "Valment PLA @System",
|
||||
"sub_path": "filament/Valment/Valment PLA @System.json"
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "School House Spools PETG @System",
|
||||
"inherits": "School House Spools PETG @base",
|
||||
"from": "system",
|
||||
"setting_id": "RSu2orfhR8ktT36h",
|
||||
"instantiation": "true",
|
||||
"filament_max_volumetric_speed": [
|
||||
"13"
|
||||
],
|
||||
"filament_long_retractions_when_cut": [
|
||||
"1"
|
||||
],
|
||||
"filament_retraction_distances_when_cut": [
|
||||
"18"
|
||||
],
|
||||
"compatible_printers": []
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "School House Spools PETG @base",
|
||||
"inherits": "fdm_filament_pet",
|
||||
"from": "system",
|
||||
"filament_id": "OFnaCGQ9",
|
||||
"instantiation": "false",
|
||||
"cool_plate_temp": [
|
||||
"0"
|
||||
],
|
||||
"cool_plate_temp_initial_layer": [
|
||||
"0"
|
||||
],
|
||||
"eng_plate_temp": [
|
||||
"70"
|
||||
],
|
||||
"eng_plate_temp_initial_layer": [
|
||||
"70"
|
||||
],
|
||||
"fan_cooling_layer_time": [
|
||||
"30"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"40"
|
||||
],
|
||||
"fan_min_speed": [
|
||||
"10"
|
||||
],
|
||||
"filament_cost": [
|
||||
"16.00"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.25"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.95"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"School House Spools"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"70"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"70"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"252"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"252"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"270"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"230"
|
||||
],
|
||||
"overhang_fan_speed": [
|
||||
"90"
|
||||
],
|
||||
"overhang_fan_threshold": [
|
||||
"10%"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"12"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"70"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"70"
|
||||
]
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "School House Spools PLA @System",
|
||||
"inherits": "School House Spools PLA @base",
|
||||
"from": "system",
|
||||
"setting_id": "3rHJk6SVAvFzyHI0",
|
||||
"instantiation": "true",
|
||||
"filament_max_volumetric_speed": [
|
||||
"21"
|
||||
],
|
||||
"filament_long_retractions_when_cut": [
|
||||
"1"
|
||||
],
|
||||
"filament_retraction_distances_when_cut": [
|
||||
"18"
|
||||
],
|
||||
"compatible_printers": []
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "School House Spools PLA @base",
|
||||
"inherits": "fdm_filament_pla",
|
||||
"from": "system",
|
||||
"filament_id": "OFHzwmT9",
|
||||
"instantiation": "false",
|
||||
"filament_cost": [
|
||||
"17.00"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.26"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.98"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"School House Spools"
|
||||
],
|
||||
"filament_scarf_seam_type": [
|
||||
"none"
|
||||
],
|
||||
"filament_scarf_height": [
|
||||
"10%"
|
||||
],
|
||||
"filament_scarf_gap": [
|
||||
"0%"
|
||||
],
|
||||
"filament_scarf_length": [
|
||||
"10"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "RatRig",
|
||||
"version": "02.04.00.06",
|
||||
"version": "02.04.00.07",
|
||||
"force_update": "0",
|
||||
"description": "RatRig configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"0.2"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"bed_exclude_area": [
|
||||
"0x0"
|
||||
],
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single]",
|
||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single]",
|
||||
"machine_end_gcode": "END_PRINT",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<script src="../js/plugin-config-ui.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<script src="./plugin-sort.js"></script>
|
||||
<script src="../js/fuzzy-search.js"></script>
|
||||
<script src="../../js/fuzzy-search.js"></script>
|
||||
<script src="./plugin-search.js"></script>
|
||||
</head>
|
||||
<body onLoad="OnInit()">
|
||||
|
||||
@@ -156,12 +156,16 @@ function SetCloudMode( bOn )
|
||||
|
||||
m_CloudMode=bOn;
|
||||
$("body").toggleClass("cloud-mode", bOn);
|
||||
|
||||
if( bOn )
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
}
|
||||
}
|
||||
|
||||
function GotoCloud()
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
SetCloudMode(true);
|
||||
SendSimpleCommand("homepage_open_cloud");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# id = "preset-transaction-demo"
|
||||
# name = "Preset Transaction Demo"
|
||||
# description = "Manual test for the staged begin_update()/set()/commit() preset API."
|
||||
# author = "OrcaSlicer contributors"
|
||||
# version = "0.1.0"
|
||||
# type = "script"
|
||||
# ///
|
||||
"""Preset Transaction Demo — a manual test harness for the staged preset API.
|
||||
|
||||
Install this file in the local ``orca_plugins`` directory, enable the plugin, then run
|
||||
"Preset Transaction Demo" from the Plugins dialog. The window drives
|
||||
``orca.host.preset.<category>.begin_update(...)`` and shows the returned
|
||||
``PresetUpdateResult``.
|
||||
|
||||
This is an asynchronous Script capability: ``execute`` is ``async def`` and runs on the
|
||||
background Script worker's own asyncio event loop. ``begin_update()`` is awaitable — it
|
||||
suspends the coroutine until the normal preset-selection / unsaved-edit dialog is answered,
|
||||
without blocking Orca's UI thread. ``set()`` and ``commit()`` remain synchronous and are
|
||||
only valid on that worker.
|
||||
|
||||
The window's own messages arrive on the UI thread, so ``on_message`` cannot await directly.
|
||||
Instead the handler enqueues each message onto the worker loop (``call_soon_threadsafe``) and
|
||||
``execute`` consumes them, one coroutine at a time, so every command can ``await
|
||||
begin_update``. Closing the window enqueues a sentinel that ends ``execute``.
|
||||
|
||||
Categories
|
||||
* process / printer: target is the active preset, or the name typed into the target
|
||||
field, resolved through the normal preset-selection flow;
|
||||
* filament: target is the 1-based slot number, with an optional preset name that
|
||||
selects (and assigns) that preset for the slot.
|
||||
|
||||
Buttons exercise the documented outcomes:
|
||||
* "Commit" stages every key/value row and commits the batch;
|
||||
* "Duplicate key" stages the same key twice and shows the rejected conflict;
|
||||
* "Invalid key" stages a key that belongs to no preset and shows the commit error.
|
||||
|
||||
Nothing here saves to disk: an applied change leaves the edited preset dirty until the
|
||||
user saves it through Orca's normal preset workflow. Values use Orca's serialized config
|
||||
syntax (numbers plain, percentages with ``%``, booleans ``1``/``0``, lists ``;``-separated).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import orca
|
||||
|
||||
CATEGORIES = ("process", "printer", "filament")
|
||||
|
||||
PAGE = """<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<style>
|
||||
body { font: 13px/1.4 system-ui, sans-serif; margin: 16px; color: #222; }
|
||||
fieldset { border: 1px solid #ccc; border-radius: 6px; margin: 0 0 12px; padding: 10px 12px; }
|
||||
legend { font-weight: 600; padding: 0 4px; }
|
||||
label { display: inline-block; min-width: 90px; }
|
||||
input, select { font: inherit; padding: 3px 5px; margin: 2px 0; }
|
||||
input[type=text] { width: 260px; }
|
||||
.row { margin: 3px 0; }
|
||||
button { font: inherit; padding: 5px 10px; margin: 2px 6px 2px 0; cursor: pointer; }
|
||||
#out { white-space: pre-wrap; background: #f6f6f6; border: 1px solid #ddd; border-radius: 6px;
|
||||
padding: 10px; min-height: 70px; }
|
||||
.err { color: #b00020; }
|
||||
.ok { color: #0a7a2f; }
|
||||
</style></head>
|
||||
<body>
|
||||
<fieldset><legend>Target</legend>
|
||||
<div class="row">
|
||||
<label>Category</label>
|
||||
<select id="category">
|
||||
<option value="process">process</option>
|
||||
<option value="printer">printer</option>
|
||||
<option value="filament">filament</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" id="slotRow" style="display:none">
|
||||
<label>Slot (1-based)</label><input id="slot" type="text" value="1" style="width:80px">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Preset name</label>
|
||||
<input id="preset" type="text" placeholder="(blank = current / slot assignment)">
|
||||
</div>
|
||||
<div class="row" style="color:#666">Edited now: <span id="current">?</span></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset><legend>Staged settings (serialized values)</legend>
|
||||
<div class="row"><input id="k1" type="text" placeholder="key, e.g. layer_height"> =
|
||||
<input id="v1" type="text" placeholder="value, e.g. 0.25"></div>
|
||||
<div class="row"><input id="k2" type="text" placeholder="key (optional)"> =
|
||||
<input id="v2" type="text" placeholder="value"></div>
|
||||
</fieldset>
|
||||
|
||||
<div>
|
||||
<button onclick="send('commit')">Commit</button>
|
||||
<button onclick="send('duplicate')">Duplicate key</button>
|
||||
<button onclick="send('invalid')">Invalid key</button>
|
||||
<button onclick="send('refresh')">Refresh</button>
|
||||
</div>
|
||||
<p id="out">Ready.</p>
|
||||
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
function pairs() {
|
||||
const out = [];
|
||||
for (const n of [1, 2]) {
|
||||
const k = $('k' + n).value.trim();
|
||||
if (k) out.push({ key: k, value: $('v' + n).value });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function send(command) {
|
||||
const category = $('category').value;
|
||||
$('slotRow').style.display = category === 'filament' ? '' : 'none';
|
||||
const message = {
|
||||
command,
|
||||
category,
|
||||
preset_name: $('preset').value.trim(),
|
||||
slot: parseInt($('slot').value, 10) || 1,
|
||||
pairs: pairs(),
|
||||
};
|
||||
orca.postMessage(message);
|
||||
}
|
||||
function show(payload) {
|
||||
if (payload.error) { $('out').className = 'err'; $('out').textContent = payload.error; return; }
|
||||
if (payload.message) { $('out').className = 'ok'; $('out').textContent = payload.message; }
|
||||
if (payload.current !== undefined) $('current').textContent = payload.current;
|
||||
}
|
||||
orca.onMessage(show);
|
||||
send('refresh');
|
||||
</script>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
async def _begin(category, preset_name, slot):
|
||||
"""Create a transaction for one category, following the two begin_update arities.
|
||||
|
||||
begin_update() is awaitable: it suspends until any selection / unsaved-edit dialog is
|
||||
answered, without blocking Orca's UI thread.
|
||||
"""
|
||||
editor = getattr(orca.host.preset, category)
|
||||
# The binding takes a plain string, so an empty field means "" (keep the current / slot target).
|
||||
if category == "filament":
|
||||
return await editor.begin_update(slot, preset_name)
|
||||
return await editor.begin_update(preset_name)
|
||||
|
||||
|
||||
def _result(payload, category):
|
||||
return {
|
||||
"category": category,
|
||||
"target_name": payload.target_name,
|
||||
"selection_changed": payload.selection_changed,
|
||||
"slot_number": payload.slot_number,
|
||||
"slot_assignment_changed": payload.slot_assignment_changed,
|
||||
"shared_slots": list(payload.shared_slots),
|
||||
"keys": list(payload.keys),
|
||||
"dirty": payload.dirty,
|
||||
}
|
||||
|
||||
|
||||
def _current(bundle, category, slot):
|
||||
if category == "filament":
|
||||
presets = list(bundle.current_filament_presets())
|
||||
if 1 <= slot <= len(presets) and presets[slot - 1] is not None:
|
||||
return "%s (slot %d)" % (presets[slot - 1].name, slot)
|
||||
return "<slot %d has no preset>" % slot
|
||||
if category == "printer":
|
||||
return bundle.current_printer_preset().name
|
||||
return bundle.current_process_preset().name
|
||||
|
||||
|
||||
class PresetTransactionDemo(orca.script.ScriptPluginCapabilityBase):
|
||||
window = None
|
||||
category = "process"
|
||||
slot = 1
|
||||
loop = None
|
||||
|
||||
def get_name(self):
|
||||
return "Preset transaction demo"
|
||||
|
||||
async def execute(self):
|
||||
"""Open the window and drive its messages, awaiting begin_update per command.
|
||||
|
||||
Runs on the background Script worker's asyncio loop. The window's on_message fires on
|
||||
the UI thread, so it only enqueues; execute consumes the queue here, where awaits are
|
||||
legal, and keeps the loop alive until the window closes.
|
||||
"""
|
||||
try:
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self._messages = asyncio.Queue()
|
||||
self.category = "process"
|
||||
self.slot = 1
|
||||
self.window = orca.host.ui.create_window(
|
||||
title="Preset transaction demo", html=PAGE, width=760, height=520,
|
||||
on_message=self._enqueue, on_close=self._on_close,
|
||||
)
|
||||
try:
|
||||
while True:
|
||||
message = await self._messages.get()
|
||||
if message is None: # sentinel: the window closed
|
||||
break
|
||||
await self._handle(message)
|
||||
finally:
|
||||
self.window = None
|
||||
return orca.ExecutionResult.success("Preset transaction demo closed.")
|
||||
except Exception as error:
|
||||
text = "Preset transaction demo failed: %s" % error
|
||||
orca.host.ui.message(text, title="Preset transaction demo", buttons="ok", icon="error")
|
||||
return orca.ExecutionResult.failure(orca.PluginResult.RecoverableError, text)
|
||||
|
||||
# --- UI thread -> worker loop hand-off ------------------------------------------------
|
||||
def _enqueue(self, message):
|
||||
"""on_message: runs on the UI thread. Hand the message to the worker loop."""
|
||||
try:
|
||||
self.loop.call_soon_threadsafe(self._messages.put_nowait, message)
|
||||
except Exception:
|
||||
# The loop is already gone (plugin unloading); drop the message.
|
||||
pass
|
||||
|
||||
def _on_close(self):
|
||||
"""on_close: runs on the UI thread. Wake execute so it returns and the loop closes."""
|
||||
try:
|
||||
self.loop.call_soon_threadsafe(self._messages.put_nowait, None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- worker loop ----------------------------------------------------------------------
|
||||
async def _handle(self, message):
|
||||
message = message or {}
|
||||
category = message.get("category", "process")
|
||||
if category not in CATEGORIES:
|
||||
self._post(error="Unknown category %r" % (category,))
|
||||
return
|
||||
self.category = category
|
||||
self.slot = message.get("slot", 1) if isinstance(message.get("slot"), int) else 1
|
||||
preset_name = message.get("preset_name") or ""
|
||||
|
||||
try:
|
||||
command = message.get("command")
|
||||
if command == "refresh":
|
||||
self._post()
|
||||
return
|
||||
|
||||
if command == "duplicate":
|
||||
transaction = await _begin(category, preset_name, self.slot)
|
||||
transaction.set("layer_height", "0.2")
|
||||
transaction.set("layer_height", "0.25")
|
||||
self._post(error="Duplicate key was not rejected (unexpected)")
|
||||
return
|
||||
|
||||
if command == "invalid":
|
||||
transaction = await _begin(category, preset_name, self.slot)
|
||||
transaction.set("this_is_not_a_preset_setting", "1")
|
||||
transaction.commit()
|
||||
self._post(error="Invalid key was not rejected (unexpected)")
|
||||
return
|
||||
|
||||
if command == "commit":
|
||||
pairs = message.get("pairs") or []
|
||||
if not pairs:
|
||||
self._post(error="Stage at least one key/value row.")
|
||||
return
|
||||
transaction = await _begin(category, preset_name, self.slot)
|
||||
for entry in pairs:
|
||||
key = entry.get("key")
|
||||
if not isinstance(key, str) or not key:
|
||||
raise RuntimeError("A setting key is required in every row")
|
||||
value = entry.get("value")
|
||||
transaction.set(key, value if isinstance(value, str) else str(value))
|
||||
result = transaction.commit()
|
||||
self._post(
|
||||
payload=_result(result, category),
|
||||
message="Committed %d setting(s) to %s \"%s\"."
|
||||
% (len(result.keys), category, result.target_name),
|
||||
)
|
||||
return
|
||||
|
||||
self._post(error="Unknown command %r" % (command,))
|
||||
except Exception as error:
|
||||
self._post(error="%s: %s" % (type(error).__name__, error))
|
||||
|
||||
def _post(self, payload=None, message="", error=""):
|
||||
payload = dict(payload or {})
|
||||
if message:
|
||||
payload["message"] = message
|
||||
if error:
|
||||
payload["error"] = error
|
||||
try:
|
||||
payload["current"] = _current(orca.host.preset_bundle(), self.category, self.slot)
|
||||
except Exception as state_error:
|
||||
payload["current"] = "unavailable: %s" % state_error
|
||||
if self.window is not None and self.window.is_open():
|
||||
self.window.post(payload)
|
||||
|
||||
|
||||
@orca.plugin
|
||||
class PresetTransactionDemoPackage(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(PresetTransactionDemo)
|
||||
@@ -0,0 +1,957 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# id = "preset-update-demo"
|
||||
# name = "Preset Update Demo"
|
||||
# description = "Edits any setting of the currently edited process, printer, or filament preset through the preset host API."
|
||||
# author = "OrcaSlicer contributors"
|
||||
# version = "0.2.0"
|
||||
# type = "script"
|
||||
# ///
|
||||
"""Preset Update Demo — edit any setting of the current preset from a plugin window.
|
||||
|
||||
Install this file in the local ``orca_plugins`` directory, enable the plugin, then run
|
||||
"Preset Update Demo" from the Plugins dialog. The window lists every setting of the
|
||||
preset the matching settings tab is editing right now — process, printer, or filament —
|
||||
with its current serialized value, and writes a value back through
|
||||
``orca.host.preset.<type>.update(...)``.
|
||||
|
||||
Process and printer each have one edited preset, taken from
|
||||
``current_process_preset()`` / ``current_printer_preset()``. Filament presets are
|
||||
addressed by slot instead: the window reads one preset per slot from
|
||||
``current_filament_presets()`` (slot numbers are one-based, matching Orca's labels),
|
||||
shows the selected slot's preset, and writes through
|
||||
``orca.host.preset.filament.update(slot, key, value)``.
|
||||
|
||||
Any assigned slot can be edited: the backend selects that slot's preset through Orca's
|
||||
normal preset selection before it writes, so applying can switch the filament tab and Orca
|
||||
may ask about unsaved changes first. Only an empty slot, whose mount resolves to no preset,
|
||||
is unavailable — the window says so instead of offering an edit. A slot whose preset is not
|
||||
the one the filament settings already have open gets an informational note that applying
|
||||
will switch to it; it is never presented as a refusal. Two slots that mount the same preset
|
||||
are one preset, so editing through either of them changes both; the window marks those
|
||||
slots and says which they are.
|
||||
|
||||
Every row is one ``config_keys()`` entry carrying ``config_value(key)``.
|
||||
|
||||
What ``update()`` does, and what it does not:
|
||||
|
||||
* it runs the same validation and UI update path as editing the setting by hand, so
|
||||
the change lands in the preset the tab is editing and is never written to disk here
|
||||
— the preset shows as modified in OrcaSlicer once it differs from the saved one;
|
||||
* it accepts only settings of that preset category. A key that belongs to another
|
||||
category, or a value the setting would reject, raises an error and writes nothing;
|
||||
* it is refused when the selected printer's technology does not use that tab (the
|
||||
print and filament tabs do nothing under SLA), so this demo covers the three FFF
|
||||
categories only;
|
||||
* a setting whose page has not been built yet still runs its owning settings group's
|
||||
checks and callbacks; behavior that requires an actual widget (such as widget-specific
|
||||
parsing or clamping) only runs when the field exists;
|
||||
* on a filament slot it selects that slot's preset first, so Orca may prompt about
|
||||
unsaved changes in the filament tab before the value is written.
|
||||
|
||||
Values are OrcaSlicer's serialized config syntax — the same text a preset file stores —
|
||||
so numbers are plain, percentages keep their ``%``, booleans are ``1``/``0``, choices use
|
||||
the setting's internal key, and lists are semicolon-separated.
|
||||
"""
|
||||
|
||||
import orca
|
||||
|
||||
|
||||
# The three preset categories the main tabs edit. Each key is both the value the page
|
||||
# sends and the attribute name on orca.host.preset that carries the matching editor.
|
||||
KINDS = (
|
||||
{"kind": "process", "label": "Process"},
|
||||
{"kind": "printer", "label": "Printer"},
|
||||
{"kind": "filament", "label": "Filament"},
|
||||
)
|
||||
KIND_KEYS = {item["kind"] for item in KINDS}
|
||||
|
||||
|
||||
def _editor(kind):
|
||||
"""The host.PresetEditor for a category (orca.host.preset.process / printer).
|
||||
|
||||
Filament is not reached through this helper: it is addressed by slot, so it calls
|
||||
orca.host.preset.filament.update(slot, key, value) directly.
|
||||
"""
|
||||
return getattr(orca.host.preset, kind)
|
||||
|
||||
|
||||
def _current_preset(bundle, kind):
|
||||
"""The single preset the matching settings tab edits right now, as a host.Preset.
|
||||
|
||||
Only the process and printer categories have one such preset; filament presets are
|
||||
addressed by slot instead (see _slot_presets).
|
||||
"""
|
||||
if kind == "process":
|
||||
return bundle.current_process_preset()
|
||||
return bundle.current_printer_preset()
|
||||
|
||||
|
||||
def _slot_presets(bundle):
|
||||
"""The filament preset mounted in each slot, in slot order; a slot may be None."""
|
||||
return list(bundle.current_filament_presets())
|
||||
|
||||
|
||||
def _preset_is_open(preset, edited):
|
||||
"""Whether this slot's preset is the one the filament settings already have open.
|
||||
|
||||
A slot with an assigned preset can be edited either way: applying to another slot makes
|
||||
the backend select that slot's preset first. This only distinguishes the slot that
|
||||
switches nothing from the ones that will switch the filament tab.
|
||||
"""
|
||||
return preset is not None and preset.name == edited.name
|
||||
|
||||
|
||||
def _filament_slots(presets, edited):
|
||||
"""One entry per mounted slot: its one-based number, preset name, and open flag.
|
||||
|
||||
The number is the slot label Orca shows; the name is None for a slot that mounts no
|
||||
preset, which is the only kind that cannot be edited.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"slot": index + 1,
|
||||
"name": None if preset is None else preset.name,
|
||||
"open": _preset_is_open(preset, edited),
|
||||
}
|
||||
for index, preset in enumerate(presets)
|
||||
]
|
||||
|
||||
|
||||
def _as_slot(value):
|
||||
"""A requested slot as an int, or None when it is not a whole number."""
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, float) and value.is_integer():
|
||||
return int(value)
|
||||
return None
|
||||
|
||||
|
||||
def _clamp_slot(slot, count):
|
||||
"""The slot to display: the requested one, kept inside the available range."""
|
||||
if count <= 0:
|
||||
return 0
|
||||
number = _as_slot(slot)
|
||||
if number is None or number < 1:
|
||||
return 1
|
||||
return number if number <= count else count
|
||||
|
||||
|
||||
def _parse_slot(slot, count):
|
||||
"""Validate a requested filament slot against the mounted slots (one-based)."""
|
||||
if count == 0:
|
||||
raise RuntimeError("No filament slots are mounted")
|
||||
number = _as_slot(slot)
|
||||
if number is None:
|
||||
raise RuntimeError("A filament slot number is required")
|
||||
if number < 1 or number > count:
|
||||
raise RuntimeError("Filament slot %d is out of range (1-%d)" % (number, count))
|
||||
return number
|
||||
|
||||
|
||||
def _rows(preset):
|
||||
"""Every setting of the preset, sorted by key, as [{k, v}].
|
||||
|
||||
config_value() returns None for a key the preset does not carry; the page shows
|
||||
those as empty rather than "None".
|
||||
"""
|
||||
rows = []
|
||||
for key in sorted(preset.config_keys()):
|
||||
value = preset.config_value(key)
|
||||
rows.append({"k": key, "v": "" if value is None else str(value)})
|
||||
return rows
|
||||
|
||||
|
||||
def _state(bundle, kind, slot, message="", error=""):
|
||||
"""The payload the page renders: shown preset, rows, slots, result line."""
|
||||
payload = {"command": "state", "kind": kind, "message": message, "error": error}
|
||||
if kind == "filament":
|
||||
presets = _slot_presets(bundle)
|
||||
edited = bundle.filaments.edited_preset()
|
||||
effective = _clamp_slot(slot, len(presets))
|
||||
preset = None if effective == 0 else presets[effective - 1]
|
||||
payload["slots"] = _filament_slots(presets, edited)
|
||||
payload["slot"] = effective
|
||||
# An empty slot has nothing to edit; any assigned slot is editable, and one that is
|
||||
# not already open makes the backend select it first.
|
||||
payload["assigned"] = preset is not None
|
||||
payload["switches"] = preset is not None and not _preset_is_open(preset, edited)
|
||||
payload["edited_name"] = edited.name
|
||||
else:
|
||||
preset = _current_preset(bundle, kind)
|
||||
payload["preset"] = None if preset is None else {
|
||||
"name": preset.name,
|
||||
"is_dirty": bool(preset.is_dirty),
|
||||
"is_system": bool(preset.is_system),
|
||||
}
|
||||
payload["rows"] = [] if preset is None else _rows(preset)
|
||||
return payload
|
||||
|
||||
|
||||
def _apply(kind, key, value, slot=None):
|
||||
"""Set one setting of the preset the page is showing, through the host API.
|
||||
|
||||
A filament preset is addressed by slot number; process and printer each resolve
|
||||
their single edited preset themselves, so no slot is passed to them.
|
||||
"""
|
||||
if kind == "filament":
|
||||
orca.host.preset.filament.update(slot, key, value)
|
||||
return
|
||||
_editor(kind).update(key, value)
|
||||
|
||||
|
||||
PAGE = r"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
:root {
|
||||
--bg: var(--orca-bg, #fafafa);
|
||||
--fg: var(--orca-fg, #1b1e23);
|
||||
--muted: var(--orca-muted, #6b7280);
|
||||
--border: var(--orca-border, #dadee3);
|
||||
--accent: var(--orca-accent, #0e7c86);
|
||||
--accent-fg: var(--orca-accent-fg, #ffffff);
|
||||
--ui: var(--orca-font, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif);
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
--surface: rgba(127, 127, 127, .09);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: var(--orca-bg, #23262a);
|
||||
--fg: var(--orca-fg, #e6e8ea);
|
||||
--muted: var(--orca-muted, #9aa1a9);
|
||||
--border: var(--orca-border, #383c42);
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--fg);
|
||||
font: 13px/1.45 var(--ui);
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
button { font: inherit; cursor: pointer; }
|
||||
:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
|
||||
/* header ------------------------------------------------------------- */
|
||||
header {
|
||||
flex: none; display: flex; align-items: center; gap: 10px;
|
||||
padding: 9px 14px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.logo { color: var(--accent); font-size: 15px; line-height: 1; }
|
||||
h1 { margin: 0; font-size: 14px; font-weight: 650; letter-spacing: .01em; }
|
||||
|
||||
.seg {
|
||||
display: flex; gap: 2px; margin-left: auto; padding: 2px;
|
||||
border: 1px solid var(--border); border-radius: 9px; background: var(--surface);
|
||||
}
|
||||
.seg button {
|
||||
border: 0; background: transparent; color: var(--muted);
|
||||
padding: 4px 13px; border-radius: 7px;
|
||||
transition: background .15s ease, color .15s ease;
|
||||
}
|
||||
.seg button:hover { color: var(--fg); }
|
||||
.seg button[aria-pressed="true"] { background: var(--accent); color: var(--accent-fg); }
|
||||
|
||||
.iconbtn {
|
||||
width: 28px; height: 28px; border: 1px solid var(--border); border-radius: 7px;
|
||||
background: transparent; color: var(--fg); display: grid; place-items: center;
|
||||
font-size: 13px; transition: background .15s ease;
|
||||
}
|
||||
.iconbtn:hover:enabled { background: var(--surface); }
|
||||
.iconbtn:disabled { opacity: .5; }
|
||||
|
||||
/* preset bar --------------------------------------------------------- */
|
||||
.presetbar {
|
||||
flex: none; display: flex; align-items: center; gap: 8px;
|
||||
padding: 7px 14px; border-bottom: 1px solid var(--border); font-size: 12px;
|
||||
}
|
||||
.presetbar .name {
|
||||
font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
max-width: 60%;
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.tag {
|
||||
border: 1px solid var(--border); color: var(--muted); border-radius: 999px;
|
||||
padding: 1px 8px; font-size: 11px; white-space: nowrap;
|
||||
}
|
||||
.tag.on { border-color: transparent; background: var(--accent); color: var(--accent-fg); }
|
||||
|
||||
/* filament slots ----------------------------------------------------- */
|
||||
.slotbar { flex: none; padding: 7px 14px 8px; border-bottom: 1px solid var(--border); }
|
||||
.slotrow { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
||||
.slotlabel {
|
||||
flex: none; font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.slots { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: 6px;
|
||||
max-height: 84px; overflow-y: auto; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 6px; flex: none; max-width: 190px;
|
||||
padding: 2px 10px 2px 3px; border: 1px solid var(--border); border-radius: 999px;
|
||||
background: transparent; color: var(--fg);
|
||||
transition: background .15s ease, color .15s ease, border-color .15s ease;
|
||||
}
|
||||
.chip:hover { background: var(--surface); }
|
||||
.chip .n {
|
||||
flex: none; min-width: 19px; height: 19px; padding: 0 4px; border-radius: 999px;
|
||||
display: grid; place-items: center; background: var(--surface); color: var(--muted);
|
||||
font-size: 11px; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* the slot whose preset the filament settings already have open, so applying switches nothing */
|
||||
.chip.open .n { box-shadow: inset 0 0 0 1px var(--accent); color: var(--fg); }
|
||||
.chip .s {
|
||||
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
font-size: 11px; color: var(--muted);
|
||||
}
|
||||
.chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
|
||||
.chip[aria-pressed="true"] .n {
|
||||
background: rgba(255, 255, 255, .22); color: var(--accent-fg); box-shadow: none;
|
||||
}
|
||||
.chip[aria-pressed="true"] .s { color: var(--accent-fg); }
|
||||
/* a slot that mounts the preset the page is showing: editing either changes both */
|
||||
.chip.linked::after {
|
||||
content: ""; flex: none; width: 5px; height: 5px;
|
||||
border-radius: 50%; background: var(--accent);
|
||||
}
|
||||
.slotnote {
|
||||
margin-top: 7px; padding-left: 8px; border-left: 2px solid var(--accent);
|
||||
font-size: 11px; line-height: 1.5; color: var(--muted);
|
||||
}
|
||||
.slotnote:empty { display: none; }
|
||||
|
||||
/* panes -------------------------------------------------------------- */
|
||||
main { flex: 1; display: flex; min-height: 0; }
|
||||
|
||||
.list-pane {
|
||||
flex: none; width: 45%; min-width: 250px; min-height: 0;
|
||||
display: flex; flex-direction: column; border-right: 1px solid var(--border);
|
||||
}
|
||||
.search {
|
||||
flex: none; display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 10px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.search input {
|
||||
flex: 1; min-width: 0; font: inherit; color: var(--fg); background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: 7px; padding: 4px 9px;
|
||||
}
|
||||
.rows { flex: 1; min-height: 0; overflow-y: auto; padding: 3px 0; }
|
||||
|
||||
.row {
|
||||
display: flex; align-items: baseline; gap: 8px; width: 100%;
|
||||
padding: 5px 10px; text-align: left; border: 0; border-left: 3px solid transparent;
|
||||
background: transparent; color: var(--fg);
|
||||
}
|
||||
.row:hover { background: var(--surface); }
|
||||
.row[aria-selected="true"] { background: var(--surface); border-left-color: var(--accent); }
|
||||
.row .k {
|
||||
flex: 1; min-width: 0; font-family: var(--mono); font-size: 12px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.row .v {
|
||||
flex: none; max-width: 40%; font-family: var(--mono); font-size: 11px;
|
||||
color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.row .dot {
|
||||
flex: none; align-self: center; width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--accent); visibility: hidden;
|
||||
}
|
||||
.row.changed .dot { visibility: visible; }
|
||||
@keyframes flash {
|
||||
0% { box-shadow: inset 0 0 0 2px var(--accent); }
|
||||
100% { box-shadow: inset 0 0 0 2px transparent; }
|
||||
}
|
||||
.row.flash { animation: flash .7s ease-out; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.row.flash { animation: none; }
|
||||
}
|
||||
|
||||
.empty { color: var(--muted); padding: 14px 12px; }
|
||||
.nil { font-style: italic; opacity: .75; }
|
||||
|
||||
/* editor ------------------------------------------------------------- */
|
||||
.edit-pane {
|
||||
flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 9px;
|
||||
padding: 12px 14px; overflow-y: auto;
|
||||
}
|
||||
.edit-key { font-family: var(--mono); font-size: 13px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
|
||||
.current {
|
||||
margin: 0; max-height: 108px; overflow: auto;
|
||||
font-family: var(--mono); font-size: 12px;
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 7px;
|
||||
padding: 7px 10px; white-space: pre-wrap; overflow-wrap: anywhere;
|
||||
}
|
||||
textarea {
|
||||
width: 100%; min-height: 74px; resize: vertical;
|
||||
font-family: var(--mono); font-size: 12px; color: var(--fg); background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: 7px; padding: 7px 10px;
|
||||
}
|
||||
.actions { display: flex; gap: 8px; align-items: center; }
|
||||
button.primary {
|
||||
padding: 5px 14px; border-radius: 7px; border: 1px solid var(--accent);
|
||||
background: var(--accent); color: var(--accent-fg);
|
||||
}
|
||||
button.ghost {
|
||||
padding: 5px 12px; border-radius: 7px; border: 1px solid var(--border);
|
||||
background: transparent; color: var(--fg);
|
||||
}
|
||||
button.primary:disabled, button.ghost:disabled { opacity: .5; cursor: default; }
|
||||
button.primary:hover:enabled { filter: brightness(1.08); }
|
||||
button.ghost:hover:enabled { background: var(--surface); }
|
||||
|
||||
.notice {
|
||||
padding: 8px 11px; border: 1px solid var(--border); border-left: 3px solid var(--accent);
|
||||
border-radius: 7px; background: var(--surface);
|
||||
font-size: 12px; line-height: 1.5; color: var(--muted);
|
||||
}
|
||||
.notice b { color: var(--fg); font-weight: 600; }
|
||||
.notice:empty { display: none; }
|
||||
|
||||
.syntax {
|
||||
margin-top: 2px; padding: 9px 11px; border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--accent); border-radius: 7px;
|
||||
font-size: 12px; line-height: 1.55; color: var(--muted);
|
||||
}
|
||||
.syntax b { color: var(--fg); font-weight: 600; }
|
||||
.syntax code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }
|
||||
|
||||
/* status ------------------------------------------------------------- */
|
||||
footer {
|
||||
flex: none; min-height: 30px; padding: 7px 14px;
|
||||
border-top: 1px solid var(--border); font-size: 12px; color: var(--muted);
|
||||
}
|
||||
footer.ok { color: #0f8a5f; }
|
||||
footer.err { color: #c8382c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<span class="logo">◈</span>
|
||||
<h1>Preset update</h1>
|
||||
<div class="seg" id="seg"></div>
|
||||
<button class="iconbtn" id="refresh" title="Reload the current values">⟳</button>
|
||||
<button class="iconbtn" id="close" title="Close">✕</button>
|
||||
</header>
|
||||
|
||||
<div class="presetbar">
|
||||
<span class="name" id="pname">Loading…</span>
|
||||
<span id="ptag"></span>
|
||||
<span class="spacer"></span>
|
||||
<span class="tag" id="pcount">0 settings</span>
|
||||
</div>
|
||||
|
||||
<div class="slotbar" id="slotbar" hidden>
|
||||
<div class="slotrow">
|
||||
<span class="slotlabel">Filament slots</span>
|
||||
<div class="slots" id="slots"></div>
|
||||
</div>
|
||||
<div class="slotnote" id="slotnote"></div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<section class="list-pane">
|
||||
<div class="search">
|
||||
<input id="q" type="search" placeholder="Search settings…" autocomplete="off" spellcheck="false">
|
||||
<span class="tag" id="qcount">0</span>
|
||||
</div>
|
||||
<div class="rows" id="rows"></div>
|
||||
</section>
|
||||
|
||||
<section class="edit-pane">
|
||||
<div class="edit-key" id="ekey">Select a setting</div>
|
||||
<div class="lbl">Current value</div>
|
||||
<pre class="current" id="ecur">—</pre>
|
||||
<label class="lbl" for="evalue">New value</label>
|
||||
<textarea id="evalue" spellcheck="false" autocomplete="off"
|
||||
placeholder="Serialized value" disabled></textarea>
|
||||
<div class="notice" id="notice"></div>
|
||||
<div class="actions">
|
||||
<button class="primary" id="apply" disabled>Apply</button>
|
||||
<button class="ghost" id="reset" disabled>Reset to current</button>
|
||||
</div>
|
||||
<div class="syntax">
|
||||
<b>Values use OrcaSlicer's serialized config syntax</b> — the same text a preset
|
||||
file stores. Numbers are plain (<code>0.2</code>, <code>3</code>) and percentages keep
|
||||
their sign (<code>15%</code>). On/off is <code>1</code> or <code>0</code>. A choice uses
|
||||
the setting's own key, e.g. <code>grid</code>. Lists are semicolon-separated
|
||||
(<code>0.4;0.4;0.4</code>, or <code>"#FF0000";"#00FF00"</code>). Text keeps its
|
||||
escapes, so a line break stays <code>\n</code>. A key that does not belong to this
|
||||
preset, or a value the setting would reject, comes back as an error and writes
|
||||
nothing. The edit lands in the preset the tab is editing and stays unsaved.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer id="status"></footer>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
const SEG = [['process', 'Process'], ['printer', 'Printer'], ['filament', 'Filament']];
|
||||
const IDLE = 'Edits apply to the preset shown here and stay unsaved until you save '
|
||||
+ 'or export it.';
|
||||
|
||||
const S = {
|
||||
kind: 'process',
|
||||
slot: 1, // selected filament slot (one-based); ignored by the other kinds
|
||||
slots: [], // [{slot, name, open}] from current_filament_presets()
|
||||
preset: null,
|
||||
rows: [],
|
||||
selected: null,
|
||||
changed: {}, // "kind/preset/key" -> true, for edits made in this window
|
||||
pending: null, // {kind, slot, preset, key} of an apply awaiting its reply
|
||||
error: '', // the last error the plugin reported, if any
|
||||
assigned: true, // filament only: the shown slot mounts a preset, so it is editable
|
||||
switches: false, // filament only: applying will select that preset in the filament tab
|
||||
editedName: null, // filament only: the preset the filament settings have open
|
||||
busy: true,
|
||||
filter: '',
|
||||
statusText: '',
|
||||
statusCls: '',
|
||||
};
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const esc = s => String(s == null ? '' : s)
|
||||
.replace(/[&<>"']/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c]));
|
||||
|
||||
/* ------------------------------------------------------------------ paint */
|
||||
function paint() {
|
||||
paintSeg();
|
||||
const p = S.preset;
|
||||
const slotTag = S.kind === 'filament' && S.slot
|
||||
? '<span class="tag">slot ' + S.slot + '</span>' : '';
|
||||
$('pname').textContent = p ? p.name : (S.busy ? 'Loading…' : 'No preset');
|
||||
$('pname').title = p ? p.name : '';
|
||||
$('ptag').innerHTML = slotTag + (p
|
||||
? '<span class="tag">' + (p.is_system ? 'system' : 'user') + '</span>'
|
||||
+ (p.is_dirty ? '<span class="tag on">modified</span>' : '<span class="tag">saved</span>')
|
||||
: '');
|
||||
$('pcount').textContent = S.rows.length + (S.rows.length === 1 ? ' setting' : ' settings');
|
||||
$('refresh').disabled = S.busy;
|
||||
paintSlots();
|
||||
paintNotice();
|
||||
renderList();
|
||||
paintEditor();
|
||||
paintStatus();
|
||||
}
|
||||
|
||||
function paintSeg() {
|
||||
// Built once (see buildSeg) so a click never loses focus; only the state is repainted.
|
||||
document.querySelectorAll('#seg button').forEach(b =>
|
||||
b.setAttribute('aria-pressed', String(b.dataset.kind === S.kind)));
|
||||
}
|
||||
|
||||
/* the filament slot strip: one chip per mounted slot, shown for filament only */
|
||||
function paintSlots() {
|
||||
const show = S.kind === 'filament' && (S.slots.length > 0 || S.busy);
|
||||
$('slotbar').hidden = !show;
|
||||
if (!show) { $('slots').innerHTML = ''; $('slotnote').innerHTML = ''; return; }
|
||||
// Rebuilding the chips drops keyboard focus; put it back on the same slot's chip.
|
||||
const focused = document.activeElement && document.activeElement.dataset
|
||||
? document.activeElement.dataset.slot : null;
|
||||
$('slots').innerHTML = S.slots.map(s => {
|
||||
const selected = s.slot === S.slot;
|
||||
const linked = !selected && sharesSelected(s.name);
|
||||
const label = s.name == null ? '<span class="nil">empty slot</span>' : esc(s.name);
|
||||
const hint = s.name ? ': ' + s.name : ': no preset';
|
||||
return '<button type="button" class="chip' + (linked ? ' linked' : '')
|
||||
+ (s.open ? ' open' : '') + '"'
|
||||
+ ' data-slot="' + s.slot + '" aria-pressed="' + selected + '"'
|
||||
+ ' title="' + esc('Filament slot ' + s.slot + hint
|
||||
+ (s.open ? ' — open in the filament settings' : '')) + '">'
|
||||
+ '<span class="n">' + s.slot + '</span><span class="s">' + label + '</span></button>';
|
||||
}).join('');
|
||||
$('slotnote').innerHTML = shareNote();
|
||||
if (focused) {
|
||||
const chip = document.querySelector('#slots .chip[data-slot="' + focused + '"]');
|
||||
if (chip) chip.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function slotName(slot) {
|
||||
const found = S.slots.find(s => s.slot === slot);
|
||||
return found && found.name ? found.name : null;
|
||||
}
|
||||
|
||||
// Applying selects the slot's preset first, so say when that switches the filament tab. An
|
||||
// empty slot is the only unavailable one, and it says so instead of offering an edit.
|
||||
function paintNotice() {
|
||||
const el = $('notice');
|
||||
if (S.kind !== 'filament' || S.busy) { el.innerHTML = ''; return; }
|
||||
// With nothing mounted the list already says so; avoid naming a slot number here.
|
||||
if (!S.slots.length) { el.innerHTML = ''; return; }
|
||||
const name = S.preset ? S.preset.name : null;
|
||||
if (!name) {
|
||||
el.innerHTML = 'Slot ' + S.slot + ' holds no filament preset. Assign one in the '
|
||||
+ 'OrcaSlicer filament settings first.';
|
||||
return;
|
||||
}
|
||||
if (!S.switches) { el.innerHTML = ''; return; }
|
||||
const open = S.editedName
|
||||
? ', while the filament settings have <b>' + esc(S.editedName) + '</b> open'
|
||||
: '';
|
||||
el.innerHTML = 'Slot ' + S.slot + ' uses <b>' + esc(name) + '</b>' + open
|
||||
+ '. Applying switches the filament tab to <b>' + esc(name) + '</b> first — Orca may '
|
||||
+ 'ask about unsaved changes there — then sets the value.';
|
||||
}
|
||||
|
||||
function sharesSelected(name) {
|
||||
const current = slotName(S.slot);
|
||||
return !!name && !!current && name === current;
|
||||
}
|
||||
|
||||
function listSlots(numbers) {
|
||||
if (numbers.length === 1) return String(numbers[0]);
|
||||
return numbers.slice(0, -1).join(', ') + ' and ' + numbers[numbers.length - 1];
|
||||
}
|
||||
|
||||
// Slots mounting the same preset are one preset, so say which slots an edit reaches.
|
||||
function shareNote() {
|
||||
const name = slotName(S.slot);
|
||||
if (!name) return '';
|
||||
const sharing = S.slots.filter(s => s.name === name).map(s => s.slot);
|
||||
if (sharing.length < 2) return '';
|
||||
return 'Slots ' + listSlots(sharing) + ' mount the same filament preset, so an edit '
|
||||
+ 'here changes every one of them.';
|
||||
}
|
||||
|
||||
function changedKey(presetName, key) {
|
||||
return S.kind + '/' + (presetName || '?') + '/' + key;
|
||||
}
|
||||
|
||||
function paintStatus() {
|
||||
const el = $('status');
|
||||
if (S.statusText) {
|
||||
el.textContent = S.statusText;
|
||||
el.className = S.statusCls;
|
||||
} else if (S.busy) {
|
||||
el.textContent = 'Working…';
|
||||
el.className = '';
|
||||
} else {
|
||||
el.textContent = IDLE;
|
||||
el.className = '';
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(text, cls) {
|
||||
S.statusText = text || '';
|
||||
S.statusCls = cls || '';
|
||||
paintStatus();
|
||||
}
|
||||
|
||||
function visibleRows() {
|
||||
const q = S.filter.trim().toLowerCase();
|
||||
if (!q) return S.rows;
|
||||
return S.rows.filter(r => r.k.toLowerCase().includes(q) || r.v.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const scroller = $('rows');
|
||||
const top = scroller.scrollTop;
|
||||
$('qcount').textContent = String(visibleRows().length);
|
||||
|
||||
if (!S.rows.length) {
|
||||
let text = 'This preset reports no settings.';
|
||||
if (S.busy) text = 'Loading settings…';
|
||||
else if (S.error) text = 'These values could not be read — see the message below.';
|
||||
else if (S.kind === 'filament' && !S.slots.length) text = 'No filament slots are mounted.';
|
||||
else if (S.kind === 'filament' && !S.preset) text = 'This filament slot has no preset.';
|
||||
scroller.innerHTML = '<div class="empty">' + text + '</div>';
|
||||
return;
|
||||
}
|
||||
const rows = visibleRows();
|
||||
if (!rows.length) {
|
||||
scroller.innerHTML = '<div class="empty">No setting matches “' + esc(S.filter) + '”.</div>';
|
||||
return;
|
||||
}
|
||||
scroller.innerHTML = rows.map(r => {
|
||||
const value = r.v === '' ? '<span class="nil">empty</span>' : esc(r.v);
|
||||
return '<button type="button" class="row'
|
||||
+ (S.changed[changedKey(S.preset && S.preset.name, r.k)] ? ' changed' : '') + '"'
|
||||
+ ' data-key="' + esc(r.k) + '"'
|
||||
+ ' aria-selected="' + (r.k === S.selected) + '"'
|
||||
+ ' title="' + esc(r.k + ' = ' + r.v) + '">'
|
||||
+ '<span class="k">' + esc(r.k) + '</span>'
|
||||
+ '<span class="v">' + value + '</span>'
|
||||
+ '<span class="dot"></span></button>';
|
||||
}).join('');
|
||||
scroller.scrollTop = top;
|
||||
}
|
||||
|
||||
function paintEditor() {
|
||||
const row = S.rows.find(r => r.k === S.selected) || null;
|
||||
// Every assigned slot is editable now that applying selects the slot's preset first, so
|
||||
// Apply is off only for an empty slot, a missing selection, or a request in flight.
|
||||
const selected = !!row && !S.busy;
|
||||
$('ekey').textContent = row ? row.k : 'Select a setting';
|
||||
$('ecur').textContent = row ? (row.v === '' ? '(empty)' : row.v) : '—';
|
||||
$('evalue').disabled = !selected;
|
||||
$('apply').disabled = !selected || !S.assigned;
|
||||
$('reset').disabled = !selected;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- actions */
|
||||
function buildSeg() {
|
||||
$('seg').innerHTML = SEG.map(([kind, label]) =>
|
||||
'<button type="button" data-kind="' + kind + '">' + label + '</button>').join('');
|
||||
}
|
||||
|
||||
function selectKey(key) {
|
||||
S.selected = key;
|
||||
const row = S.rows.find(r => r.k === key);
|
||||
$('evalue').value = row ? row.v : '';
|
||||
// Repaint the rows in place so the scroll position of a long list is kept.
|
||||
document.querySelectorAll('#rows .row').forEach(el =>
|
||||
el.setAttribute('aria-selected', String(el.dataset.key === key)));
|
||||
paintEditor();
|
||||
}
|
||||
|
||||
function switchKind(kind) {
|
||||
S.kind = kind;
|
||||
S.selected = null;
|
||||
S.pending = null;
|
||||
S.preset = null;
|
||||
S.rows = [];
|
||||
S.busy = true;
|
||||
$('evalue').value = '';
|
||||
setStatus('', '');
|
||||
paint();
|
||||
orca.postMessage({command: 'state', kind: kind, slot: S.slot});
|
||||
}
|
||||
|
||||
// Two slots that mount the same preset show the same settings, so only a switch to a
|
||||
// different preset drops the current selection and the value being typed.
|
||||
function selectSlot(slot) {
|
||||
if (S.busy || slot === S.slot) return;
|
||||
const same = !!slotName(slot) && slotName(slot) === slotName(S.slot);
|
||||
S.slot = slot;
|
||||
S.pending = null;
|
||||
S.busy = true;
|
||||
if (!same) {
|
||||
S.preset = null;
|
||||
S.rows = [];
|
||||
S.selected = null;
|
||||
$('evalue').value = '';
|
||||
}
|
||||
setStatus('', '');
|
||||
paint();
|
||||
orca.postMessage({command: 'state', kind: S.kind, slot: slot});
|
||||
}
|
||||
|
||||
function reload() {
|
||||
if (S.busy) return;
|
||||
S.busy = true;
|
||||
S.pending = null;
|
||||
setStatus('', '');
|
||||
paint();
|
||||
orca.postMessage({command: 'state', kind: S.kind, slot: S.slot});
|
||||
}
|
||||
|
||||
function applyValue() {
|
||||
const row = S.rows.find(r => r.k === S.selected);
|
||||
// Mirror the Apply button: only a missing selection, a request in flight, or an empty
|
||||
// slot blocks an edit, so Enter cannot send one the button itself would refuse.
|
||||
if (!row || S.busy || !S.assigned) return;
|
||||
S.pending = {
|
||||
kind: S.kind, slot: S.slot,
|
||||
preset: S.preset ? S.preset.name : null, key: row.k,
|
||||
};
|
||||
S.busy = true;
|
||||
setStatus('', '');
|
||||
paint();
|
||||
orca.postMessage({command: 'apply', kind: S.kind, slot: S.slot, key: row.k,
|
||||
value: $('evalue').value});
|
||||
}
|
||||
|
||||
function flashKey(key) {
|
||||
document.querySelectorAll('#rows .row').forEach(el => {
|
||||
if (el.dataset.key !== key) return;
|
||||
el.classList.remove('flash');
|
||||
void el.offsetWidth; // restart the animation if it ran on this row already
|
||||
el.classList.add('flash');
|
||||
el.addEventListener('animationend', () => el.classList.remove('flash'), {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- wiring */
|
||||
buildSeg();
|
||||
|
||||
$('seg').addEventListener('click', e => {
|
||||
const button = e.target.closest('[data-kind]');
|
||||
if (!button || S.busy || button.dataset.kind === S.kind) return;
|
||||
switchKind(button.dataset.kind);
|
||||
});
|
||||
|
||||
$('rows').addEventListener('click', e => {
|
||||
const row = e.target.closest('.row');
|
||||
if (row && !S.busy) selectKey(row.dataset.key);
|
||||
});
|
||||
|
||||
$('slots').addEventListener('click', e => {
|
||||
const chip = e.target.closest('[data-slot]');
|
||||
if (chip && !S.busy) selectSlot(Number(chip.dataset.slot));
|
||||
});
|
||||
|
||||
$('q').addEventListener('input', e => {
|
||||
S.filter = e.target.value;
|
||||
renderList();
|
||||
});
|
||||
|
||||
$('apply').addEventListener('click', applyValue);
|
||||
|
||||
$('reset').addEventListener('click', () => {
|
||||
const row = S.rows.find(r => r.k === S.selected);
|
||||
if (row) $('evalue').value = row.v;
|
||||
});
|
||||
|
||||
$('evalue').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); applyValue(); }
|
||||
else if (e.key === 'Escape') { e.preventDefault(); $('reset').click(); }
|
||||
});
|
||||
|
||||
$('refresh').addEventListener('click', reload);
|
||||
$('close').addEventListener('click', () => orca.close());
|
||||
|
||||
/* -------------------------------------------------------------- messages */
|
||||
orca.onMessage(msg => {
|
||||
if (!msg || msg.command !== 'state') return;
|
||||
if (msg.kind !== S.kind) return; // a reply for a category we already left
|
||||
|
||||
const pending = S.pending;
|
||||
S.busy = false;
|
||||
S.pending = null;
|
||||
S.slots = msg.slots || [];
|
||||
// Adopt the slot the plugin actually used (it clamps a slot that no longer exists).
|
||||
if (S.kind === 'filament' && typeof msg.slot === 'number') S.slot = msg.slot;
|
||||
S.preset = msg.preset || null;
|
||||
S.rows = msg.rows || [];
|
||||
S.error = msg.error || '';
|
||||
// Filament editability is a slot concern; process and printer have a single edited preset.
|
||||
S.assigned = S.kind === 'filament' ? !!msg.assigned : true;
|
||||
S.switches = S.kind === 'filament' ? !!msg.switches : false;
|
||||
S.editedName = S.kind === 'filament' ? (msg.edited_name || null) : null;
|
||||
if (S.selected && !S.rows.some(r => r.k === S.selected)) S.selected = null;
|
||||
|
||||
if (msg.error) {
|
||||
// Keep the typed value so it can be corrected, and keep the current selection.
|
||||
setStatus('⚠ ' + msg.error, 'err');
|
||||
} else {
|
||||
if (pending) S.changed[changedKey(pending.preset, pending.key)] = true;
|
||||
setStatus(msg.message || '', msg.message ? 'ok' : '');
|
||||
}
|
||||
|
||||
paint();
|
||||
|
||||
if (!msg.error && pending) {
|
||||
const row = S.rows.find(r => r.k === pending.key);
|
||||
if (row) { // show the value as the preset now stores it
|
||||
$('evalue').value = row.v;
|
||||
flashKey(row.k);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
paint();
|
||||
orca.postMessage({command: 'state', kind: S.kind, slot: S.slot});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
class PresetUpdateDemo(orca.script.ScriptPluginCapabilityBase):
|
||||
window = None
|
||||
kind = "process" # the category the open window is showing, for a re-run refresh
|
||||
slot = 1 # the filament slot the open window is showing
|
||||
|
||||
def get_name(self):
|
||||
return "Preset update demo"
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
if self.window is not None and self.window.is_open():
|
||||
self._post_state(self.kind, self.slot)
|
||||
return orca.ExecutionResult.success("Preset update demo refreshed.")
|
||||
self.window = orca.host.ui.create_window(
|
||||
title="Preset update demo", html=PAGE, width=820, height=560,
|
||||
on_message=self.on_message, on_close=self.on_close,
|
||||
)
|
||||
return orca.ExecutionResult.success("Preset update demo opened.")
|
||||
except Exception as error:
|
||||
message = "Preset update demo failed: %s" % error
|
||||
orca.host.ui.message(message, title="Preset update demo", buttons="ok", icon="error")
|
||||
return orca.ExecutionResult.failure(orca.PluginResult.RecoverableError, message)
|
||||
|
||||
def on_close(self):
|
||||
self.window = None
|
||||
|
||||
def _post_state(self, kind="process", slot=1, message="", error=""):
|
||||
try:
|
||||
payload = _state(orca.host.preset_bundle(), kind, slot, message, error)
|
||||
except Exception as state_error:
|
||||
payload = {"command": "state", "kind": kind, "slot": slot, "slots": [],
|
||||
"assigned": False, "switches": False, "edited_name": None,
|
||||
"preset": None, "rows": [], "message": message,
|
||||
"error": str(state_error)}
|
||||
if self.window is not None and self.window.is_open():
|
||||
self.window.post(payload)
|
||||
|
||||
def on_message(self, message):
|
||||
kind = "process"
|
||||
slot = 1
|
||||
try:
|
||||
message = message or {}
|
||||
kind = message.get("kind", "process")
|
||||
if not isinstance(kind, str) or kind not in KIND_KEYS:
|
||||
raise RuntimeError("Unknown preset category %r" % (kind,))
|
||||
self.kind = kind
|
||||
slot = message.get("slot", 1)
|
||||
|
||||
command = message.get("command")
|
||||
if command == "state":
|
||||
self.slot = slot
|
||||
self._post_state(kind, slot)
|
||||
return
|
||||
if command == "apply":
|
||||
key = message.get("key")
|
||||
value = message.get("value")
|
||||
if not isinstance(key, str) or not key:
|
||||
raise RuntimeError("A setting key is required")
|
||||
if not isinstance(value, str):
|
||||
raise RuntimeError("A value string is required")
|
||||
where = ""
|
||||
if kind == "filament":
|
||||
presets = _slot_presets(orca.host.preset_bundle())
|
||||
# Validate the slot strictly: an out-of-range slot must fail loudly
|
||||
# rather than edit a different filament. A slot with no preset has
|
||||
# nothing to write, which is the only slot shape the backend refuses.
|
||||
slot = _parse_slot(slot, len(presets))
|
||||
if presets[slot - 1] is None:
|
||||
raise RuntimeError("Filament slot %d has no preset to edit" % slot)
|
||||
_apply(kind, key, value, slot)
|
||||
where = " on slot %d" % slot
|
||||
else:
|
||||
_apply(kind, key, value)
|
||||
shown = value if value else "(empty)"
|
||||
self.slot = slot
|
||||
self._post_state(
|
||||
kind, slot,
|
||||
message='Set %s = "%s"%s. The edit stays in the preset tab until you save it.'
|
||||
% (key, shown, where),
|
||||
)
|
||||
return
|
||||
raise RuntimeError("Unknown command %r" % (command,))
|
||||
except Exception as error:
|
||||
safe_kind = kind if isinstance(kind, str) and kind in KIND_KEYS else "process"
|
||||
self._post_state(safe_kind, slot, error=str(error))
|
||||
|
||||
|
||||
@orca.plugin
|
||||
class PresetUpdateDemoPackage(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(PresetUpdateDemo)
|
||||
@@ -336,10 +336,10 @@ modules:
|
||||
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
|
||||
dest: external-packages/PNG
|
||||
|
||||
# libjpeg-turbo 3.0.1
|
||||
# libjpeg-turbo 3.2.0
|
||||
- type: file
|
||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
||||
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||
dest: external-packages/JPEG
|
||||
|
||||
# Freetype 2.12.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
|
||||
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
# WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
|
||||
# binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
|
||||
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
|
||||
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
|
||||
|
||||
+21
-15
@@ -163,6 +163,7 @@ using json = nlohmann::json;
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef SLIC3R_GUI
|
||||
@@ -1156,8 +1157,10 @@ int CLI::run(int argc, char **argv)
|
||||
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
|
||||
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
|
||||
// unset preserves WebKit hardware acceleration on Device / Setup
|
||||
// Wizard / login / store. The default path still applies it on
|
||||
// XWayland sessions as a conservative fallback for older WebKit.
|
||||
// Wizard / login / store. The default path applies it only where it
|
||||
// forces GDK_BACKEND=x11 below (a build without EGL, so the app really
|
||||
// runs through XWayland), and only when the linked WebKitGTK predates
|
||||
// the fix (< 2.42), as a conservative fallback for older WebKit.
|
||||
// ------------------------------------------------------------------
|
||||
{
|
||||
const char* gdk_backend = ::getenv("GDK_BACKEND");
|
||||
@@ -1165,6 +1168,14 @@ int CLI::run(int argc, char **argv)
|
||||
// honours the first backend in the comma-separated list).
|
||||
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
|
||||
|
||||
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
|
||||
// Force the shared-memory transport, which keeps compositing and threaded
|
||||
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
|
||||
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
|
||||
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
|
||||
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
|
||||
}
|
||||
|
||||
if (x11_opt_in) {
|
||||
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
|
||||
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
|
||||
@@ -1204,23 +1215,18 @@ int CLI::run(int argc, char **argv)
|
||||
if (wayland_env && *wayland_env) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
|
||||
::setenv("GDK_BACKEND", "x11", true);
|
||||
|
||||
// GTK will use X11 in a Wayland session here, which is the
|
||||
// one default-path case that really runs through XWayland.
|
||||
// WebKit2GTK accelerated compositing could fail silently
|
||||
// there on releases before 2.42, so force it off. Disabling
|
||||
// it also turns off threaded scrolling, hence the gate.
|
||||
if (WebView::NeedsCompositingWorkaround())
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// WebKit2GTK compositing can fail under XWayland on older
|
||||
// WebKit releases. Disable it only when both DISPLAY and
|
||||
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
|
||||
// use). On pure X11 or native Wayland, compositing is left
|
||||
// enabled so WebKit retains HW acceleration.
|
||||
{
|
||||
const char* display_env_wk = ::getenv("DISPLAY");
|
||||
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
|
||||
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||
}
|
||||
}
|
||||
|
||||
// XInitThreads is needed before GStreamer may use Xlib. On
|
||||
// native Wayland without DISPLAY, GStreamer uses waylandsink
|
||||
// (no Xlib involved), so the call is skipped.
|
||||
|
||||
@@ -429,7 +429,8 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
|
||||
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
|
||||
export MESA_LOADER_DRIVER_OVERRIDE=zink
|
||||
export GALLIUM_DRIVER=zink
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
|
||||
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
+27
-18
@@ -432,27 +432,36 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|
||||
std::string OozePrevention::pre_toolchange(GCode& gcodegen)
|
||||
{
|
||||
std::string gcode;
|
||||
|
||||
unsigned int extruder_id = gcodegen.writer().filament()->id();
|
||||
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
|
||||
if (filament_idle_temp.get_at(extruder_id) == 0) {
|
||||
// There is no idle temperature defined in filament settings.
|
||||
// Use the delta value from print config.
|
||||
if (gcodegen.config().standby_temperature_delta.value != 0) {
|
||||
// we assume that heating is always slower than cooling, so no need to block
|
||||
gcode += gcodegen.writer().set_temperature
|
||||
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
}
|
||||
} else {
|
||||
// Use the value from filament settings. That one is absolute, not delta.
|
||||
gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
|
||||
// Check if this tool will ever be used again in this print.
|
||||
bool is_last_use = false;
|
||||
if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) {
|
||||
coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z();
|
||||
const auto& tool_ordering = gcodegen.m_print->tool_ordering();
|
||||
const auto& cfg = gcodegen.config();
|
||||
const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id));
|
||||
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id);
|
||||
for (unsigned int f : tool_ordering.all_extruders())
|
||||
if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater)
|
||||
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f);
|
||||
}
|
||||
|
||||
int temp = 0;
|
||||
if (!is_last_use) {
|
||||
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
|
||||
if (filament_idle_temp.get_at(extruder_id) == 0) {
|
||||
if (gcodegen.config().standby_temperature_delta.value == 0)
|
||||
return std::string();
|
||||
temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value;
|
||||
} else {
|
||||
temp = filament_idle_temp.get_at(extruder_id);
|
||||
}
|
||||
}
|
||||
|
||||
std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
return gcode;
|
||||
}
|
||||
|
||||
|
||||
@@ -1004,6 +1004,7 @@ protected:
|
||||
size_t num_objects,
|
||||
size_t num_islands);
|
||||
|
||||
friend class OozePrevention;
|
||||
friend class Wipe;
|
||||
friend class WipeTowerIntegration;
|
||||
friend class PressureEqualizer;
|
||||
|
||||
@@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
|
||||
sort_remove_duplicates(m_all_printing_extruders);
|
||||
}
|
||||
|
||||
m_last_layer_per_extruder.clear();
|
||||
for (const LayerTools& lt : m_layer_tools) {
|
||||
for (unsigned int ext : lt.extruders) {
|
||||
m_last_layer_per_extruder[ext] = lt.print_z;
|
||||
}
|
||||
}
|
||||
|
||||
if (prime_multi_material && ! m_all_printing_extruders.empty()) {
|
||||
// Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder.
|
||||
// Then set m_first_printing_extruder to the 1st extruder primed.
|
||||
@@ -3664,5 +3671,11 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const
|
||||
{
|
||||
auto it = m_last_layer_per_extruder.find(extruder_id);
|
||||
if (it == m_last_layer_per_extruder.end())
|
||||
return true;
|
||||
return print_z >= it->second - EPSILON;
|
||||
}
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -370,6 +370,7 @@ public:
|
||||
std::vector<int> & initial_filaments);
|
||||
|
||||
bool has_non_support_filament(const PrintConfig &config);
|
||||
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
|
||||
|
||||
private:
|
||||
void initialize_layers(std::vector<coordf_t> &zs);
|
||||
@@ -413,6 +414,8 @@ private:
|
||||
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
|
||||
// no-ops; downstream emission falls through to the per-object branch).
|
||||
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
|
||||
// Map: extruder_id -> print_z (z coordinate) of the last layer it is used on
|
||||
std::map<unsigned int, coordf_t> m_last_layer_per_extruder;
|
||||
const PrintObject* m_print_object_ptr = nullptr;
|
||||
Print* m_print;
|
||||
bool m_sorted = false;
|
||||
|
||||
@@ -211,6 +211,35 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
|
||||
return decode_colored_png(stream, out_img);
|
||||
}
|
||||
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
|
||||
{
|
||||
static const constexpr int PNG_SIG_BYTES = 8;
|
||||
|
||||
struct ReadBufStream stream{pngbuf};
|
||||
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
|
||||
stream.read(sig.data(), PNG_SIG_BYTES);
|
||||
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
PNGDescr dsc;
|
||||
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!dsc.png)
|
||||
return false;
|
||||
dsc.info = png_create_info_struct(dsc.png);
|
||||
if (!dsc.info)
|
||||
return false;
|
||||
|
||||
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
|
||||
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
|
||||
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
|
||||
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
|
||||
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
|
||||
bit_depth == 8 && (grey || colour);
|
||||
}
|
||||
|
||||
|
||||
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
|
||||
// Based on https://www.lemoda.net/c/write-png/
|
||||
|
||||
@@ -50,6 +50,9 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
|
||||
|
||||
bool is_png(const ReadBuf &pngbuf);
|
||||
|
||||
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
|
||||
|
||||
struct ReadBufStream: public IStream {
|
||||
const ReadBuf &rbuf_ref;
|
||||
size_t pos = 0;
|
||||
|
||||
+53
-162
@@ -4511,53 +4511,6 @@ static void warn_ambiguous_filament_id_match(const PresetCollection &filaments,
|
||||
<< "\", also matches " << others;
|
||||
}
|
||||
|
||||
// Orca: resolve a compatible system preset from a printer-reported filament type. Prefers an exact
|
||||
// "Generic <type>" match, then the longest generic whose filament_type appears as a whole word in
|
||||
// the reported type (e.g. "ASA" in "ASA Sparkle"). On success `generic_name_out` holds the
|
||||
// "Generic <...>" name used for compatibility messages. Shared by the known-filament_id lookup and
|
||||
// the data-bearing-but-unmapped tray path so both pick the same preset.
|
||||
static PresetCollection::Iterator find_generic_preset_for_type(PresetCollection &filaments, const std::string &filament_type, std::string &generic_name_out)
|
||||
{
|
||||
if (filament_type.empty())
|
||||
return filaments.end();
|
||||
const std::string generic_name = "Generic " + filament_type;
|
||||
auto iter = std::find_if(filaments.begin(), filaments.end(), [&generic_name](const Preset &f) {
|
||||
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, generic_name);
|
||||
});
|
||||
if (iter != filaments.end()) {
|
||||
generic_name_out = generic_name;
|
||||
return iter;
|
||||
}
|
||||
// Similarity fallback: find a generic preset whose filament_type
|
||||
// appears as a whole word in the reported type (e.g. "ASA" in "ASA Sparkle").
|
||||
auto upper_type = boost::to_upper_copy(filament_type);
|
||||
auto contains_word = [](const std::string& haystack, const std::string& needle) {
|
||||
auto pos = haystack.find(needle);
|
||||
while (pos != std::string::npos) {
|
||||
bool start_ok = (pos == 0 || !std::isalnum(static_cast<unsigned char>(haystack[pos - 1])));
|
||||
bool end_ok = (pos + needle.size() >= haystack.size() ||
|
||||
!std::isalnum(static_cast<unsigned char>(haystack[pos + needle.size()])));
|
||||
if (start_ok && end_ok)
|
||||
return true;
|
||||
pos = haystack.find(needle, pos + 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
|
||||
size_t best_len = 0;
|
||||
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
|
||||
if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
|
||||
continue;
|
||||
auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
|
||||
if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
|
||||
iter = it;
|
||||
best_len = preset_type.size();
|
||||
generic_name_out = "Generic " + it->config.opt_string("filament_type", 0u);
|
||||
}
|
||||
}
|
||||
return iter;
|
||||
}
|
||||
|
||||
void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
|
||||
{
|
||||
combox_info.clear();
|
||||
@@ -4613,9 +4566,7 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
|
||||
|
||||
unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfig *,std::string>> &unknowns, bool use_map, std::map<int, AMSMapInfo> &maps, bool enable_append, MergeFilamentInfo &merge_info, bool color_only)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": use_map %1% enable_append %2% color_only %3% start_presets %4% start_colors %5%")
|
||||
% use_map % enable_append % color_only % this->filament_presets.size()
|
||||
% project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "use_map:" << use_map << " enable_append:" << enable_append;
|
||||
std::vector<std::string> ams_filament_presets;
|
||||
std::vector<std::string> ams_filament_colors;
|
||||
std::vector<std::string> ams_filament_color_types;
|
||||
@@ -4627,10 +4578,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
bool valid{false};
|
||||
bool is_map{false};
|
||||
bool is_placeholder{false};
|
||||
// Orca: set for a tray that reports filament data (type and/or loaded flag) but carries
|
||||
// no preset id. It is not `valid` (no id to map through), yet it still holds a real spool
|
||||
// and is eligible for the same append step as a mapped tray.
|
||||
bool has_reported_data{false};
|
||||
std::string filament_color = "";
|
||||
std::string filament_color_type = "";
|
||||
std::string filament_preset = "";
|
||||
@@ -4644,32 +4591,12 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
auto filament_id = ams.opt_string("filament_id", 0u);
|
||||
auto filament_color = ams.opt_string("filament_colour", 0u);
|
||||
auto filament_color_type = ams.opt_string("filament_colour_type", 0u);
|
||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||
auto filament_exist = ams.has("filament_exist") && ams.opt_bool("filament_exist", 0u);
|
||||
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
|
||||
auto filament_multi_color = ams.opt<ConfigOptionStrings>("filament_multi_colour")->values;
|
||||
auto ams_id = ams.opt_string("ams_id", 0u);
|
||||
auto slot_id = ams.opt_string("slot_id", 0u);
|
||||
auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u);
|
||||
// Real reported filament data: the device named a type or flagged the slot loaded. A
|
||||
// topology-only synthetic virtual tray carries neither, so it must not become a filament.
|
||||
bool has_reported_data = !is_placeholder && filament_id.empty() && (!filament_type.empty() || filament_exist);
|
||||
AmsInfo ams_info;
|
||||
ams_info.valid = !filament_id.empty();
|
||||
ams_info.is_placeholder = is_placeholder;
|
||||
ams_info.has_reported_data = has_reported_data;
|
||||
ams_info.filament_color = filament_color;
|
||||
// Only the append path of a data-bearing, id-less tray reads these; the known-id path keeps
|
||||
// taking its color and color type from ams_filament_colors/ams_filament_color_types.
|
||||
if (has_reported_data) {
|
||||
if (ams_info.filament_color.empty())
|
||||
ams_info.filament_color = "#CECECE";
|
||||
ams_info.filament_color_type = filament_color_type.empty() ? "1" : filament_color_type;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% ams_id %2% slot_id %3% filament_id %4% type %5% color %6% valid %7% placeholder %8% reported_data %9% changed %10%")
|
||||
% index % ams_id % slot_id % filament_id % filament_type % filament_color
|
||||
% (!filament_id.empty()) % is_placeholder % has_reported_data % filament_changed;
|
||||
ams_infos.push_back(std::move(ams_info));
|
||||
ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color});
|
||||
AMSMapInfo temp = {ams_id, slot_id};
|
||||
ams_array_maps.push_back(temp);
|
||||
index++;
|
||||
@@ -4680,55 +4607,20 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
maps.erase(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_placeholder) {
|
||||
// Orca: an empty AMS slot is a placeholder, not a filament - never synthesize a
|
||||
// preset for it. Kept here only to keep index alignment with ams_infos; the sync
|
||||
// drops it so no filament slot is created or retained for an empty tray.
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
ams_filament_color_types.push_back("1");
|
||||
if (filament_multi_color.size() == 0) {
|
||||
filament_multi_color.push_back(default_unknown_color);
|
||||
}
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
} else if (is_placeholder) {
|
||||
// Orca: push placeholders to keep index alignment with ams_infos
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
} else if (use_map) {
|
||||
if (has_reported_data) {
|
||||
// Orca: the tray reports a filament but no preset id, so it never reaches the
|
||||
// filament_id lookup. Resolve the same generic preset the id path would pick
|
||||
// from the reported type and keep the reported color; the append step below (if
|
||||
// enabled) then turns it into a sidebar filament. Without a generic match the
|
||||
// slot is dropped rather than invented, exactly like an empty tray.
|
||||
std::string resolved_generic;
|
||||
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
|
||||
if (generic == filaments.end()) {
|
||||
// Reported loaded but no usable type: fall back to the first compatible
|
||||
// generic, matching the id path's fallback for an unknown model.
|
||||
generic = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
|
||||
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, "Generic ");
|
||||
});
|
||||
}
|
||||
if (generic != filaments.end()) {
|
||||
auto append_color = filament_color.empty() ? std::string("#CECECE") : filament_color;
|
||||
ams_filament_presets.push_back(generic->name);
|
||||
ams_filament_colors.push_back(append_color);
|
||||
ams_filament_color_types.push_back(filament_color_type.empty() ? "1" : filament_color_type);
|
||||
if (filament_multi_color.size() == 0)
|
||||
filament_multi_color.push_back(append_color);
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
continue;
|
||||
}
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
} else {
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
ams_filament_color_types.push_back("1");
|
||||
if (filament_multi_color.size() == 0) {
|
||||
filament_multi_color.push_back(default_unknown_color);
|
||||
}
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -4740,19 +4632,49 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
continue;
|
||||
}
|
||||
bool has_type = false;
|
||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
|
||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
||||
if (iter == filaments.end()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
||||
std::string resolved_generic;
|
||||
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
|
||||
if (!filament_type.empty())
|
||||
if (!filament_type.empty()) {
|
||||
auto original_type = filament_type;
|
||||
filament_type = "Generic " + filament_type;
|
||||
if (generic != filaments.end()) {
|
||||
iter = generic;
|
||||
filament_type = resolved_generic;
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
|
||||
return f.is_compatible && f.is_system
|
||||
&& boost::algorithm::starts_with(f.name, filament_type);
|
||||
});
|
||||
if (iter == filaments.end()) {
|
||||
// Similarity fallback: find a generic preset whose filament_type
|
||||
// appears as a whole word in the AMS type (e.g. "ASA" in "ASA Sparkle").
|
||||
auto upper_type = boost::to_upper_copy(original_type);
|
||||
auto contains_word = [](const std::string& haystack, const std::string& needle) {
|
||||
auto pos = haystack.find(needle);
|
||||
while (pos != std::string::npos) {
|
||||
bool start_ok = (pos == 0 || !std::isalnum(static_cast<unsigned char>(haystack[pos - 1])));
|
||||
bool end_ok = (pos + needle.size() >= haystack.size() ||
|
||||
!std::isalnum(static_cast<unsigned char>(haystack[pos + needle.size()])));
|
||||
if (start_ok && end_ok)
|
||||
return true;
|
||||
pos = haystack.find(needle, pos + 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
|
||||
size_t best_len = 0;
|
||||
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
|
||||
if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
|
||||
continue;
|
||||
auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
|
||||
if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
|
||||
iter = it;
|
||||
best_len = preset_type.size();
|
||||
filament_type = "Generic " + it->config.opt_string("filament_type", 0u);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (iter == filaments.end()) {
|
||||
// Prefer old selection
|
||||
@@ -4912,8 +4834,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": color_only mapped_any %1% maps %2% entries %3%")
|
||||
% mapped_any % maps.size() % ams_infos.size();
|
||||
filament_color->values = exist_colors;
|
||||
ams_multi_color_filment = exist_multi_color_filment;
|
||||
merge_info.merges.clear();
|
||||
@@ -4973,18 +4893,13 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < ams_infos.size(); i++) {// check append
|
||||
// A mapped tray (valid id) and a data-bearing tray without an id both append the same
|
||||
// way; only a tray with neither contributes nothing.
|
||||
if (ams_infos[i].valid || ams_infos[i].has_reported_data) {
|
||||
if (ams_infos[i].valid) {
|
||||
if (i >= ams_filament_presets.size()) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)";
|
||||
continue;
|
||||
}
|
||||
ams_infos[i].filament_preset = ams_filament_presets[i];
|
||||
ams_infos[i].mutli_filament_color = ams_multi_color_filment[i];
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% mapped %2% preset %3% color %4% valid %5% reported_data %6%")
|
||||
% i % ams_infos[i].is_map % ams_infos[i].filament_preset % ams_infos[i].filament_color
|
||||
% ams_infos[i].valid % ams_infos[i].has_reported_data;
|
||||
if (!ams_infos[i].is_map) {
|
||||
need_append_colors.emplace_back(ams_infos[i]);
|
||||
ams_filament_colors[i] = "";
|
||||
@@ -4994,7 +4909,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% dropped (no valid id or reported data)") % i;
|
||||
ams_filament_colors[i] = "";
|
||||
ams_filament_color_types[i] = "";
|
||||
ams_filament_presets[i] = "";
|
||||
@@ -5012,7 +4926,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
[](std::vector<std::string> &value) { return value.empty(); }),
|
||||
ams_multi_color_filment.end());
|
||||
if (need_append_colors.size() > 0 && enable_append) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append candidates %1% (start existing %2%)") % need_append_colors.size() % exist_filament_presets.size();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "need_append_colors.size() > 0 && enable_append";
|
||||
auto get_idx_in_array = [](std::vector<std::string> &presets, std::vector<std::string> &colors, const std::string &preset, const std::string &color) -> int {
|
||||
for (size_t i = 0; i < presets.size(); i++) {
|
||||
if (presets[i] == preset && colors[i] == color) {
|
||||
@@ -5023,22 +4937,17 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
};
|
||||
for (size_t i = 0; i < need_append_colors.size(); i++){
|
||||
if (exist_filament_presets.size() >= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER){
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append capped at %1%, %2% candidate(s) left") % MAXIMUM_AMS_SYNC_FILAMENT_NUMBER % (need_append_colors.size() - i);
|
||||
break;
|
||||
}
|
||||
auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color);
|
||||
if (idx >= 0) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append dedup preset %1% color %2% already at %3%") % need_append_colors[i].filament_preset % need_append_colors[i].filament_color % idx;
|
||||
continue;
|
||||
}
|
||||
exist_filament_presets.push_back(need_append_colors[i].filament_preset);
|
||||
exist_colors.push_back(need_append_colors[i].filament_color);
|
||||
exist_color_types.push_back(need_append_colors[i].filament_color_type);
|
||||
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": appended preset %1% color %2% type %3% -> slot %4%") % need_append_colors[i].filament_preset % need_append_colors[i].filament_color % need_append_colors[i].filament_color_type % (exist_filament_presets.size() - 1);
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append skipped candidates %1% enable_append %2%") % need_append_colors.size() % enable_append;
|
||||
}
|
||||
filament_color->values = exist_colors;
|
||||
filament_color_type->values = exist_color_types;
|
||||
@@ -5075,10 +4984,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
result_multi_colors.push_back(
|
||||
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
|
||||
: std::vector<std::string>{ams_filament_colors[i]});
|
||||
} else if (i < ams_infos.size() && ams_infos[i].is_placeholder) {
|
||||
// Orca: an empty AMS slot is a placeholder, not a filament, so drop it instead of
|
||||
// keeping or filling a slot for it. The device AMS panel still shows the empty tray.
|
||||
continue;
|
||||
} else if (i < exist_presets.size()) {
|
||||
// Empty tray or beyond tray count: keep existing filament
|
||||
result_colors.push_back(exist_colors[i]);
|
||||
@@ -5103,8 +5008,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
filament_color_type->values = result_color_types;
|
||||
this->filament_presets = result_presets;
|
||||
ams_multi_color_filment = result_multi_colors;
|
||||
filament_map->values.resize(result_presets.size(), 1);
|
||||
filament_volume_map->values.resize(result_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
filament_map->values.resize(total, 1);
|
||||
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
} else {
|
||||
// BBL: existing wholesale replace
|
||||
filament_color->values = ams_filament_colors;
|
||||
@@ -5154,7 +5059,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
// Update ams_multi_color_filment
|
||||
update_filament_multi_color();
|
||||
update_multi_material_filament_presets();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finish sync ams list, end_presets %1% end_colors %2%") % this->filament_presets.size() % filament_color->values.size();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish sync ams list";
|
||||
return this->filament_presets.size();
|
||||
}
|
||||
|
||||
@@ -5233,20 +5138,6 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
|
||||
if (extruder_nums > 1) {
|
||||
filament_infos.resize(extruder_nums, std::vector<DynamicPrintConfig>());
|
||||
for (auto ams_item : filament_ams_list) {
|
||||
// A generic slot carries the extruder it actually belongs to, so it lands in that
|
||||
// extruder's bucket instead of the legacy left/right pair. Bambu entries (and anything
|
||||
// without the field) keep the existing 0x10000 key convention.
|
||||
const bool has_extruder_id = ams_item.second.has("device_tray_extruder_id");
|
||||
const int entry_extruder = has_extruder_id ? ams_item.second.opt_int("device_tray_extruder_id") : -1;
|
||||
if (has_extruder_id) {
|
||||
if (entry_extruder >= 0 && entry_extruder < extruder_nums) {
|
||||
// Drop the transient association before the config reaches the slicing pipeline.
|
||||
DynamicPrintConfig entry = ams_item.second;
|
||||
entry.erase("device_tray_extruder_id");
|
||||
filament_infos[entry_extruder].push_back(std::move(entry));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ams_item.first & 0x10000) { // right
|
||||
filament_infos[1].push_back(ams_item.second);
|
||||
} else { // left
|
||||
|
||||
@@ -858,11 +858,10 @@ void PrintObject::contour_z()
|
||||
BOOST_LOG_TRIVIAL(debug) << "Contouring in parallel - start";
|
||||
|
||||
TriangleMesh mesh = this->m_model_object->raw_mesh();
|
||||
if (m_model_object->instances.size() != 1) {
|
||||
throw RuntimeError("ContourZ: unexpected number of instances");
|
||||
}
|
||||
|
||||
ModelInstance *inst = m_model_object->instances.front();
|
||||
// A PrintObject groups the instances that share their whole transformation but the XY shift: an
|
||||
// instance rotated, scaled or mirrored differently is a PrintObject of its own. So the first
|
||||
// instance stands for all of them.
|
||||
const ModelInstance *inst = m_instances.front().model_instance;
|
||||
Point center_offset = this->center_offset();
|
||||
Geometry::Transformation trans = inst->get_transformation();
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
|
||||
return decode_height_texture(layer).has_color();
|
||||
}
|
||||
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
|
||||
{
|
||||
DecodedHeightTexture result;
|
||||
if (layer.empty())
|
||||
@@ -351,13 +351,15 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||
auto &entries = g_decoded_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
if (keep_decoded) {
|
||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||
auto &entries = g_decoded_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
}
|
||||
}
|
||||
|
||||
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
||||
|
||||
@@ -599,8 +599,9 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
|
||||
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
|
||||
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
|
||||
// as it did when the importer flattened it to grey on the way in. Returns an empty
|
||||
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
|
||||
// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
|
||||
// result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true);
|
||||
|
||||
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
|
||||
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
|
||||
|
||||
@@ -842,6 +842,13 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
|
||||
|
||||
bool check_layer_id_pattern(const std::string& pattern, int layer_id);
|
||||
|
||||
// On Windows, return an equivalent path carrying the extended-length ("\\?\" / "\\?\UNC\")
|
||||
// prefix so the Win32 APIs accept it past MAX_PATH even when LongPathsEnabled is off. The path
|
||||
// is made absolute and lexically normalized first, because the prefix disables normalisation.
|
||||
// A path that is already extended (or a device path) is returned unchanged; on other platforms,
|
||||
// or when an absolute path cannot be formed, the input is returned unchanged.
|
||||
boost::filesystem::path extended_path(const boost::filesystem::path& path);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#if WIN32
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
#include "boost/nowide/cstdio.hpp"
|
||||
#include "boost/nowide/convert.hpp"
|
||||
#endif
|
||||
|
||||
#include "I18N.hpp"
|
||||
@@ -62,7 +63,11 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
|
||||
|
||||
FILE *f = nullptr;
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
f = boost::nowide::fopen(fname, mode);
|
||||
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
|
||||
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
|
||||
const std::wstring wide_path = boost::nowide::widen(fname);
|
||||
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
|
||||
f = boost::nowide::fopen(extended.c_str(), mode);
|
||||
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
||||
f = fopen64(fname, mode);
|
||||
#else
|
||||
@@ -101,6 +106,14 @@ bool close_zip(mz_zip_archive *zip, bool isread)
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
// mz_file_write_func writing an extracted entry into an already-open FILE*.
|
||||
size_t mz_write_to_cfile(void *opaque, mz_uint64 /*file_ofs*/, const void *buffer, size_t size)
|
||||
{
|
||||
return std::fwrite(buffer, 1, size, static_cast<FILE *>(opaque));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
|
||||
@@ -116,6 +129,36 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
|
||||
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
|
||||
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
|
||||
|
||||
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path)
|
||||
{
|
||||
if (!zip)
|
||||
return false;
|
||||
#ifdef WIN32
|
||||
// Open the destination ourselves with a Windows extended-length path: miniz's narrow
|
||||
// extract_to_file cannot be relied on past MAX_PATH with LongPathsEnabled off, and its wide
|
||||
// variant takes a plain (unprefixed) path and a text-mode stream. This keeps the write binary.
|
||||
FILE *file = ::_wfopen(Slic3r::extended_path(dest_path).wstring().c_str(), L"wb");
|
||||
if (!file) {
|
||||
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool written = mz_zip_reader_extract_to_callback(zip, file_index, mz_write_to_cfile, file, 0) != 0;
|
||||
const bool closed = std::fclose(file) == 0;
|
||||
if (!written || !closed) {
|
||||
if (written && !closed)
|
||||
zip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
|
||||
// Drop the partial entry rather than leave a truncated file behind.
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(Slic3r::extended_path(dest_path), ec);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return mz_zip_reader_extract_to_file(zip, file_index, dest_path.string().c_str(), 0) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
|
||||
{
|
||||
if (stat.m_is_utf8)
|
||||
@@ -156,10 +199,13 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
||||
}
|
||||
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
||||
try {
|
||||
// Use the extended-length form for every filesystem call so entries under a destination
|
||||
// past MAX_PATH are created even with LongPathsEnabled off.
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
const boost::filesystem::path io_dest = extended_path(dest_path);
|
||||
if (stat.m_is_directory) {
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (!boost::filesystem::exists(dest_path))
|
||||
boost::filesystem::create_directories(dest_path);
|
||||
if (!boost::filesystem::exists(io_dest))
|
||||
boost::filesystem::create_directories(io_dest);
|
||||
continue;
|
||||
}
|
||||
if (stat.m_uncomp_size == 0) {
|
||||
@@ -167,10 +213,9 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
||||
continue;
|
||||
}
|
||||
// Replace a symlink at the destination rather than writing through it.
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
||||
boost::filesystem::remove(dest_path);
|
||||
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
|
||||
boost::filesystem::remove(io_dest);
|
||||
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
||||
close_zip_reader(&archive);
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MINIZ_EXTENSION_HPP
|
||||
#define MINIZ_EXTENSION_HPP
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <string>
|
||||
#include <miniz.h>
|
||||
|
||||
@@ -13,6 +14,10 @@ bool close_zip_writer(mz_zip_archive *zip);
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
||||
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
|
||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
|
||||
// Extracts one entry into dest_path, binary-safe and overwriting. On Windows the destination is
|
||||
// opened through an extended-length path so it works past MAX_PATH even with LongPathsEnabled off;
|
||||
// a partial file is removed if the write fails. False for a directory/unsupported entry or on error.
|
||||
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path);
|
||||
|
||||
class MZ_Archive {
|
||||
public:
|
||||
|
||||
@@ -2254,5 +2254,45 @@ bool check_layer_id_pattern(const std::string& pattern, int layer_id){
|
||||
return mod >= 0 && mod < count;
|
||||
}
|
||||
|
||||
boost::filesystem::path extended_path(const boost::filesystem::path& path) {
|
||||
#ifdef WIN32
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
// Already extended, or an explicit device path (which is not subject to MAX_PATH and must not
|
||||
// be absolutized or normalized): leave it alone.
|
||||
{
|
||||
fs::path prefix_candidate = path;
|
||||
const std::wstring native = prefix_candidate.make_preferred().wstring();
|
||||
if (native.rfind(L"\\\\?\\", 0) == 0 || native.rfind(L"\\\\.\\", 0) == 0)
|
||||
return fs::path(native);
|
||||
}
|
||||
|
||||
fs::path absolute = path;
|
||||
if (!absolute.is_absolute()) {
|
||||
boost::system::error_code ec;
|
||||
absolute = fs::absolute(absolute, ec);
|
||||
if (ec)
|
||||
return path; // can't form a valid prefixed path from a relative one
|
||||
}
|
||||
|
||||
// A trailing separator is redundant to the Win32 APIs, but lexically_normal() would rewrite it
|
||||
// as a "." element, which the "\\?\" prefix (normalisation off) takes literally. Drop it first.
|
||||
while (absolute.has_relative_path() && absolute.filename() == ".")
|
||||
absolute = absolute.parent_path();
|
||||
|
||||
// The "\\?\" prefix turns off Win32 normalisation, so do it here first.
|
||||
absolute = absolute.lexically_normal();
|
||||
|
||||
// Extended paths only recognize backslash separators.
|
||||
const std::wstring native = absolute.make_preferred().wstring();
|
||||
if (native.empty())
|
||||
return path;
|
||||
if (native.rfind(L"\\\\", 0) == 0)
|
||||
return fs::path(L"\\\\?\\UNC\\" + native.substr(2)); // \\server\share\... -> \\?\UNC\server\share\...
|
||||
return fs::path(L"\\\\?\\" + native); // C:\dir\... -> \\?\C:\dir\...
|
||||
#else
|
||||
return path;
|
||||
#endif
|
||||
}
|
||||
|
||||
}; // namespace Slic3r
|
||||
|
||||
@@ -222,6 +222,7 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Gizmos/GLGizmoUtils.hpp
|
||||
GUI/GLCanvas3D.cpp
|
||||
GUI/GLCanvas3D.hpp
|
||||
GUI/SequentialPrintClearance.hpp
|
||||
GUI/GLModel.cpp
|
||||
GUI/GLModel.hpp
|
||||
GUI/GLSelectionRectangle.cpp
|
||||
@@ -689,6 +690,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/host/PluginHostUi.hpp
|
||||
plugin/host/PluginPages.cpp
|
||||
plugin/host/PluginPages.hpp
|
||||
plugin/host/PluginResourceTextures.cpp
|
||||
plugin/host/PluginResourceTextures.hpp
|
||||
plugin/CloudPluginService.cpp
|
||||
plugin/CloudPluginService.hpp
|
||||
plugin/PluginFsUtils.cpp
|
||||
@@ -712,6 +715,9 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/pluginTypes/pages/PagesPluginCapability.hpp
|
||||
plugin/pluginTypes/pages/PagesPluginCapability.cpp
|
||||
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
|
||||
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp
|
||||
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.cpp
|
||||
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapabilityTrampoline.hpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapability.hpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapability.cpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
|
||||
|
||||
@@ -85,6 +85,96 @@ static std::string float_to_string_with_precision(float value, int precision = 3
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
static std::string colour_to_ams_string(const wxColour& color)
|
||||
{
|
||||
char col_buf[10];
|
||||
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
|
||||
return col_buf;
|
||||
}
|
||||
|
||||
static wxColour mix_colour(const wxColour& left, const wxColour& right, double ratio)
|
||||
{
|
||||
ratio = std::max(0.0, std::min(1.0, ratio));
|
||||
auto mix_channel = [ratio](unsigned char l, unsigned char r) {
|
||||
return static_cast<unsigned char>(std::round(l + (r - l) * ratio));
|
||||
};
|
||||
return wxColour(mix_channel(left.Red(), right.Red()),
|
||||
mix_channel(left.Green(), right.Green()),
|
||||
mix_channel(left.Blue(), right.Blue()),
|
||||
mix_channel(left.Alpha(), right.Alpha()));
|
||||
}
|
||||
|
||||
static std::vector<ColorPickerPopup::ColorItem> collect_ams_color_items(DevFilaSystem* fila_system)
|
||||
{
|
||||
std::vector<ColorPickerPopup::ColorItem> items;
|
||||
if (!fila_system) return items;
|
||||
|
||||
for (const auto& ams_pair : fila_system->GetAmsList()) {
|
||||
DevAms* ams = ams_pair.second;
|
||||
if (!ams) continue;
|
||||
|
||||
for (const auto& tray_pair : ams->GetTrays()) {
|
||||
DevAmsTray* tray = tray_pair.second;
|
||||
if (!tray || !tray->is_tray_info_ready()) continue;
|
||||
|
||||
ColorPickerPopup::ColorItem item;
|
||||
for (const std::string& col : tray->cols) {
|
||||
item.colors.emplace_back(DevAmsTray::decode_color(col));
|
||||
}
|
||||
if (item.colors.empty() && !tray->color.empty()) {
|
||||
item.colors.emplace_back(DevAmsTray::decode_color(tray->color));
|
||||
}
|
||||
if (item.colors.empty()) continue;
|
||||
|
||||
item.ctype = item.colors.size() > 1 ? tray->ctype : 2;
|
||||
items.emplace_back(std::move(item));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
static std::vector<ColorPickerPopup::ColorItem> collect_preset_color_items(const std::string& filament_id)
|
||||
{
|
||||
std::vector<ColorPickerPopup::ColorItem> items;
|
||||
if (filament_id.empty()) return items;
|
||||
|
||||
auto* clr_query = GUI::wxGetApp().get_filament_color_code_query();
|
||||
if (!clr_query) return items;
|
||||
|
||||
auto* agent = wxGetApp().getAgent();
|
||||
const std::string catalog_id = agent ? agent->from_orca_filament_id(filament_id) : filament_id;
|
||||
FilamentColorCodes* color_codes = clr_query->GetFilaInfoMap(wxString::FromUTF8(catalog_id));
|
||||
if (!color_codes || !color_codes->GetFilamentColor2CodeMap()) return items;
|
||||
|
||||
std::set<std::string> seen;
|
||||
for (const auto& color_pair : *color_codes->GetFilamentColor2CodeMap()) {
|
||||
const FilamentColor& fila_color = color_pair.first;
|
||||
FilamentColorCode* color_code = color_pair.second;
|
||||
if (!color_code || fila_color.GetColors().empty()) continue;
|
||||
|
||||
ColorPickerPopup::ColorItem item;
|
||||
item.ctype = fila_color.ColorCount() < 2 ? 2 :
|
||||
(fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR ? 0 : 1);
|
||||
item.name = color_code->GetFilaColorName();
|
||||
|
||||
std::string key = std::to_string(item.ctype);
|
||||
for (const wxColour& color : fila_color.GetColors()) {
|
||||
item.colors.emplace_back(color);
|
||||
char rgba[9];
|
||||
std::snprintf(rgba, sizeof(rgba), "%02X%02X%02X%02X",
|
||||
static_cast<unsigned>(color.Red()), static_cast<unsigned>(color.Green()),
|
||||
static_cast<unsigned>(color.Blue()), static_cast<unsigned>(color.Alpha()));
|
||||
key += "|" + std::string(rgba);
|
||||
}
|
||||
|
||||
if (item.colors.empty() || !seen.insert(key).second) continue;
|
||||
items.emplace_back(std::move(item));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
AMSMaterialsSetting::AMSMaterialsSetting(wxWindow *parent, wxWindowID id)
|
||||
: DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
|
||||
, m_color_picker_popup(ColorPickerPopup(this))
|
||||
@@ -740,8 +830,19 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
nozzle_temp_min.ToLong(&nozzle_temp_min_int);
|
||||
nozzle_temp_max.ToLong(&nozzle_temp_max_int);
|
||||
wxColour color = m_clr_picker->m_colour;
|
||||
char col_buf[10];
|
||||
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
|
||||
std::string tray_color = colour_to_ams_string(color);
|
||||
std::vector<std::string> tray_colors;
|
||||
int tray_ctype = 2;
|
||||
// Only firmware advertising fun2[23] receives the manual multi-color fields.
|
||||
if (obj->is_support_filament_manual_multi_color) {
|
||||
for (const wxColour& selected_color : m_clr_picker->m_cols) {
|
||||
tray_colors.emplace_back(colour_to_ams_string(selected_color));
|
||||
}
|
||||
if (tray_colors.empty()) {
|
||||
tray_colors.emplace_back(tray_color);
|
||||
}
|
||||
tray_ctype = tray_colors.size() > 1 ? m_clr_picker->ctype : 2;
|
||||
}
|
||||
|
||||
if (ams_filament_id.empty() || nozzle_temp_min.empty() || nozzle_temp_max.empty() || m_filament_type.empty()) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id";
|
||||
@@ -760,13 +861,33 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
<< ", tray_info_idx (filament_id) = " << ams_filament_id
|
||||
<< ", setting_id = " << ams_setting_id
|
||||
<< ", tray_type = " << m_filament_type
|
||||
<< ", tray_color = " << col_buf
|
||||
<< ", tray_color = " << tray_color
|
||||
<< ", nozzle_temp_min = " << nozzle_temp_min_int
|
||||
<< ", nozzle_temp_max = " << nozzle_temp_max_int;
|
||||
|
||||
// set filament
|
||||
if (m_is_third) {
|
||||
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int);
|
||||
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, tray_color, m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int,
|
||||
tray_colors, tray_ctype);
|
||||
|
||||
// Refresh the edited tray immediately while its MQTT acknowledgement is pending.
|
||||
DevAmsTray* tray = nullptr;
|
||||
if (is_virtual_tray()) {
|
||||
for (auto& virtual_tray : obj->vt_slot) {
|
||||
if (virtual_tray.id == std::to_string(ams_id)) {
|
||||
tray = &virtual_tray;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tray = obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
|
||||
}
|
||||
if (tray) {
|
||||
tray->color = tray_color;
|
||||
tray->cols = tray_colors;
|
||||
tray->ctype = tray_ctype;
|
||||
tray->set_hold_count();
|
||||
}
|
||||
}
|
||||
|
||||
//reset param
|
||||
@@ -923,8 +1044,15 @@ void AMSMaterialsSetting::set_ctype(int ctype)
|
||||
|
||||
void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event)
|
||||
{
|
||||
unsigned int color_num = event.GetInt();
|
||||
set_color(wxColour(color_num>>24&0xFF, color_num>>16&0xFF, color_num>>8&0xFF, color_num&0xFF));
|
||||
std::vector<wxColour> colors = m_color_picker_popup.get_selected_colours();
|
||||
if (colors.empty()) {
|
||||
unsigned int color_num = event.GetInt();
|
||||
colors.emplace_back(color_num >> 24 & 0xFF, color_num >> 16 & 0xFF, color_num >> 8 & 0xFF, color_num & 0xFF);
|
||||
}
|
||||
|
||||
set_ctype(m_color_picker_popup.get_selected_ctype());
|
||||
set_color(colors.front());
|
||||
set_colors(colors);
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
|
||||
@@ -939,14 +1067,12 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<wxColour> ams_colors;
|
||||
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
|
||||
|
||||
m_color_picker_popup.set_ams_colours(collect_ams_color_items(obj->GetFilaSystem().get()));
|
||||
m_color_picker_popup.set_preset_colours(collect_preset_color_items(ams_filament_id));
|
||||
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour, m_clr_picker->m_cols, m_clr_picker->ctype);
|
||||
wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0));
|
||||
wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65));
|
||||
m_color_picker_popup.Position(popup_pos, wxSize(0, 0));
|
||||
m_color_picker_popup.set_ams_colours(ams_colors);
|
||||
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
|
||||
m_color_picker_popup.Popup();
|
||||
}
|
||||
|
||||
@@ -1735,7 +1861,46 @@ void ColorPicker::doRender(wxDC& dc)
|
||||
auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2;
|
||||
if (m_selected) radius -= FromDIP(1);
|
||||
|
||||
if (alpha == 0) {
|
||||
if (m_show_full && m_cols.size() > 1) {
|
||||
if (ctype == 0) {
|
||||
const double center_x = size.x / 2.0;
|
||||
const double center_y = size.y / 2.0;
|
||||
const double draw_radius = radius;
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
for (int x = 0; x < size.x; ++x) {
|
||||
const double dx = x + 0.5 - center_x;
|
||||
if (std::abs(dx) > draw_radius) continue;
|
||||
|
||||
const double half_height = std::sqrt(std::max(0.0, draw_radius * draw_radius - dx * dx));
|
||||
const int top = std::max(0, static_cast<int>(std::ceil(center_y - half_height)));
|
||||
const int bottom = std::min(size.y - 1, static_cast<int>(std::floor(center_y + half_height)));
|
||||
const double pos = size.x > 1 ? static_cast<double>(x) / (size.x - 1) : 0.0;
|
||||
const double scaled = pos * (m_cols.size() - 1);
|
||||
const size_t idx = std::min(static_cast<size_t>(scaled), m_cols.size() - 2);
|
||||
const double ratio = scaled - idx;
|
||||
|
||||
if (top <= bottom) {
|
||||
dc.SetBrush(wxBrush(mix_colour(m_cols[idx], m_cols[idx + 1], ratio)));
|
||||
dc.DrawRectangle(x, top, 1, bottom - top + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
float ev_angle = 360.0 / m_cols.size();
|
||||
const float overlap = 2.0f;
|
||||
wxPoint center(size.x / 2, size.y / 2);
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(m_cols.front()));
|
||||
dc.DrawCircle(center.x, center.y, radius);
|
||||
for (int i = 0; i < m_cols.size(); i++) {
|
||||
dc.SetBrush(m_cols[i]);
|
||||
float startAngle = 270.0f + i * ev_angle;
|
||||
float endAngle = startAngle + ev_angle;
|
||||
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle - overlap, endAngle + overlap);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (alpha == 0) {
|
||||
wxSize bmp_size = m_bitmap_transparent_def.GetBmpSize();
|
||||
int center_x = (size.x - bmp_size.x) / 2;
|
||||
int center_y = (size.y - bmp_size.y) / 2;
|
||||
@@ -1777,52 +1942,6 @@ void ColorPicker::doRender(wxDC& dc)
|
||||
dc.SetPen(wxPen(wxColour("#6B6B6B")));
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
dc.DrawCircle(size.x / 2, size.y / 2, radius);
|
||||
|
||||
if (m_cols.size() > 1) {
|
||||
if (ctype == 0) {
|
||||
int left = FromDIP(0);
|
||||
float total_width = size.x;
|
||||
int gwidth = std::round(total_width / (m_cols.size() - 1));
|
||||
|
||||
for (int i = 0; i < m_cols.size() - 1; i++) {
|
||||
|
||||
if ((left + gwidth) > (size.x)) {
|
||||
gwidth = size.x - left;
|
||||
}
|
||||
|
||||
auto rect = wxRect(left, 0, gwidth, size.y);
|
||||
dc.GradientFillLinear(rect, m_cols[i], m_cols[i + 1], wxEAST);
|
||||
left += gwidth;
|
||||
}
|
||||
if (wxGetApp().dark_mode()) {
|
||||
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
|
||||
}
|
||||
else {
|
||||
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
|
||||
}
|
||||
}
|
||||
else {
|
||||
float ev_angle = 360.0 / m_cols.size();
|
||||
float startAngle = 270.0;
|
||||
float endAngle = 270.0;
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
for (int i = 0; i < m_cols.size(); i++) {
|
||||
dc.SetBrush(m_cols[i]);
|
||||
endAngle += ev_angle;
|
||||
endAngle = endAngle > 360.0 ? endAngle - 360.0 : endAngle;
|
||||
wxPoint center(size.x / 2, size.y / 2);
|
||||
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle, endAngle);
|
||||
startAngle += ev_angle;
|
||||
startAngle = startAngle > 360.0 ? startAngle - 360.0 : startAngle;
|
||||
}
|
||||
if (wxGetApp().dark_mode()) {
|
||||
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
|
||||
}
|
||||
else {
|
||||
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_is_empty) {
|
||||
@@ -1891,19 +2010,21 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
|
||||
//other
|
||||
wxFlexGridSizer* fg_sizer;
|
||||
fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
|
||||
fg_sizer->SetFlexibleDirection(wxBOTH);
|
||||
fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
m_other_fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
|
||||
m_other_fg_sizer->SetFlexibleDirection(wxBOTH);
|
||||
m_other_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
|
||||
|
||||
for (wxColour col : m_def_colors) {
|
||||
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
cp->set_color(col);
|
||||
cp->set_colors({col});
|
||||
cp->ctype = 2;
|
||||
cp->set_selected(false);
|
||||
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
|
||||
m_color_pickers.push_back(cp);
|
||||
fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
m_default_color_pickers.push_back(cp);
|
||||
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour);
|
||||
|
||||
@@ -1970,7 +2091,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_sizer_other, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_other_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_sizer_custom, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16));
|
||||
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
|
||||
@@ -1981,7 +2102,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
m_def_color_box->Fit();
|
||||
|
||||
m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10);
|
||||
SetSizer(m_sizer_main);
|
||||
SetSizerAndFit(m_sizer_main);
|
||||
Layout();
|
||||
Fit();
|
||||
|
||||
@@ -2019,13 +2140,15 @@ void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
|
||||
{
|
||||
m_ams_fg_sizer->Clear(false);
|
||||
if (m_ams_color_pickers.size() > 0) {
|
||||
for (ColorPicker* col_pick:m_ams_color_pickers) {
|
||||
|
||||
std::vector<ColorPicker*>::iterator iter = find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
|
||||
if (iter != m_color_pickers.end()) {
|
||||
col_pick->Hide();
|
||||
col_pick->Destroy();
|
||||
m_color_pickers.erase(iter);
|
||||
}
|
||||
@@ -2035,17 +2158,22 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
}
|
||||
|
||||
|
||||
m_ams_colors = ams;
|
||||
for (wxColour col : m_ams_colors) {
|
||||
m_ams_color_items = ams;
|
||||
for (const ColorItem& item : m_ams_color_items) {
|
||||
if (item.colors.empty()) continue;
|
||||
|
||||
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
cp->set_color(col);
|
||||
cp->set_color(item.colors.front());
|
||||
cp->set_colors(item.colors);
|
||||
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
|
||||
cp->set_show_full(true);
|
||||
cp->set_selected(false);
|
||||
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
|
||||
m_color_pickers.push_back(cp);
|
||||
m_ams_color_pickers.push_back(cp);
|
||||
m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour);
|
||||
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
|
||||
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
|
||||
@@ -2058,9 +2186,68 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_def_colour(wxColour col)
|
||||
void ColorPickerPopup::set_preset_colours(const std::vector<ColorItem>& preset_colors)
|
||||
{
|
||||
m_other_fg_sizer->Clear(false);
|
||||
if (!m_preset_color_pickers.empty()) {
|
||||
for (ColorPicker* col_pick : m_preset_color_pickers) {
|
||||
auto iter = std::find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
|
||||
if (iter != m_color_pickers.end()) {
|
||||
col_pick->Hide();
|
||||
col_pick->Destroy();
|
||||
m_color_pickers.erase(iter);
|
||||
}
|
||||
}
|
||||
m_preset_color_pickers.clear();
|
||||
}
|
||||
|
||||
for (const ColorItem& item : preset_colors) {
|
||||
if (item.colors.empty()) continue;
|
||||
|
||||
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
cp->set_color(item.colors.front());
|
||||
cp->set_colors(item.colors);
|
||||
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
|
||||
cp->set_show_full(true);
|
||||
cp->set_selected(false);
|
||||
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
|
||||
if (!item.name.empty()) {
|
||||
cp->SetToolTip(item.name);
|
||||
}
|
||||
|
||||
m_color_pickers.push_back(cp);
|
||||
m_preset_color_pickers.push_back(cp);
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
|
||||
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
|
||||
evt.SetInt(g_col);
|
||||
wxPostEvent(GetParent(), evt);
|
||||
});
|
||||
}
|
||||
|
||||
const bool show_default_colours = m_preset_color_pickers.empty();
|
||||
for (ColorPicker* cp : m_default_color_pickers) {
|
||||
cp->Show(show_default_colours);
|
||||
}
|
||||
for (ColorPicker* cp : (show_default_colours ? m_default_color_pickers : m_preset_color_pickers)) {
|
||||
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
}
|
||||
|
||||
m_other_fg_sizer->Layout();
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols, int ctype)
|
||||
{
|
||||
m_def_col = col;
|
||||
if (cols.empty()) {
|
||||
cols.push_back(col);
|
||||
}
|
||||
m_def_cols = cols;
|
||||
m_def_ctype = m_def_cols.size() > 1 ? ctype : 2;
|
||||
|
||||
for (ColorPicker* cp : m_color_pickers) {
|
||||
if (cp->m_selected) {
|
||||
@@ -2069,7 +2256,8 @@ void ColorPickerPopup::set_def_colour(wxColour col)
|
||||
}
|
||||
|
||||
for (ColorPicker* cp : m_color_pickers) {
|
||||
if (cp->m_colour == m_def_col) {
|
||||
if (!cp->IsShown()) continue;
|
||||
if (cp->ctype == m_def_ctype && cp->m_cols == m_def_cols) {
|
||||
cp->set_selected(true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -82,14 +82,22 @@ public:
|
||||
class ColorPickerPopup : public PopupWindow
|
||||
{
|
||||
public:
|
||||
struct ColorItem
|
||||
{
|
||||
std::vector<wxColour> colors;
|
||||
int ctype = 2;
|
||||
wxString name;
|
||||
};
|
||||
|
||||
ScalableBitmap m_ts_bitmap_custom;
|
||||
wxStaticBitmap* m_ts_stbitmap_custom;
|
||||
StaticBox* m_custom_cp;
|
||||
StaticBox* m_def_color_box;
|
||||
wxFlexGridSizer* m_ams_fg_sizer;
|
||||
wxFlexGridSizer* m_other_fg_sizer;
|
||||
wxColour m_def_col;
|
||||
std::vector<wxColour> m_def_colors;
|
||||
std::vector<wxColour> m_ams_colors;
|
||||
std::vector<ColorItem> m_ams_color_items;
|
||||
std::vector<ColorPicker*> m_color_pickers;
|
||||
std::vector<ColorPicker*> m_ams_color_pickers;
|
||||
|
||||
@@ -97,13 +105,20 @@ public:
|
||||
ColorPickerPopup(wxWindow* parent);
|
||||
~ColorPickerPopup() {};
|
||||
void on_custom_clr_picker(wxMouseEvent& event);
|
||||
void set_ams_colours(std::vector<wxColour> ams);
|
||||
void set_def_colour(wxColour col);
|
||||
void set_ams_colours(const std::vector<ColorItem>& ams);
|
||||
void set_preset_colours(const std::vector<ColorItem>& preset_colors);
|
||||
void set_def_colour(wxColour col, std::vector<wxColour> cols = {}, int ctype = 2);
|
||||
const std::vector<wxColour>& get_selected_colours() const { return m_def_cols; }
|
||||
int get_selected_ctype() const { return m_def_ctype; }
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
virtual void OnDismiss() wxOVERRIDE;
|
||||
virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE;
|
||||
|
||||
public:
|
||||
private:
|
||||
std::vector<ColorPicker*> m_default_color_pickers;
|
||||
std::vector<ColorPicker*> m_preset_color_pickers;
|
||||
std::vector<wxColour> m_def_cols;
|
||||
int m_def_ctype = 2;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "ImGuiWrapper.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "NativeCommands.hpp"
|
||||
#include "Notebook.hpp"
|
||||
#include "NotificationManager.hpp"
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "SettingsIndex.hpp"
|
||||
@@ -99,6 +101,45 @@ std::string find_loaded_source_name(PluginManager& manager, const std::string& p
|
||||
return plugin_key;
|
||||
}
|
||||
|
||||
// Translates a Script capability outcome into the action layer's result vocabulary. Shared by
|
||||
// the synchronous run path and the async completion notification.
|
||||
AppActionRunResult script_outcome_to_action_result(const ScriptCompletion& completion)
|
||||
{
|
||||
// Cancelled by PluginManager (unload/shutdown): expected termination, nothing to show.
|
||||
if (completion.cancelled)
|
||||
return {};
|
||||
|
||||
if (!completion.error.empty())
|
||||
return {AppActionRunResult::Level::Error, from_u8(completion.error)};
|
||||
|
||||
const ExecutionResult& result = completion.result;
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
|
||||
return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success,
|
||||
result.message.empty() ? fallback : from_u8(result.message)};
|
||||
}
|
||||
|
||||
// Surfaces the eventual outcome of an async script launched from the speed dial. That launch
|
||||
// already returned (the UI was not blocked), so the finish is reported as a notification. A
|
||||
// cancellation is silent.
|
||||
void notify_script_outcome(const ScriptCompletion& completion)
|
||||
{
|
||||
if (completion.cancelled)
|
||||
return;
|
||||
if (wxTheApp == nullptr || wxGetApp().is_closing())
|
||||
return;
|
||||
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr || wxGetApp().imgui() == nullptr || !wxGetApp().imgui()->display_initialized())
|
||||
return;
|
||||
|
||||
const AppActionRunResult action_result = script_outcome_to_action_result(completion);
|
||||
const auto level = action_result.level == AppActionRunResult::Level::Error
|
||||
? NotificationManager::NotificationLevel::ErrorNotificationLevel
|
||||
: NotificationManager::NotificationLevel::RegularNotificationLevel;
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, level, into_u8(action_result.message));
|
||||
}
|
||||
|
||||
// A runnable script capability exposed as a speed-dial action. source_key = plugin_key
|
||||
// (identity), so a plugin display-name change does not re-key the action.
|
||||
struct PluginScriptAction : AppAction
|
||||
@@ -124,15 +165,36 @@ struct PluginScriptAction : AppAction
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{
|
||||
std::string error;
|
||||
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
|
||||
if (!error.empty())
|
||||
return {AppActionRunResult::Level::Error, from_u8(error)};
|
||||
struct Outcome
|
||||
{
|
||||
bool async = false;
|
||||
bool completed = false;
|
||||
AppActionRunResult result;
|
||||
};
|
||||
auto outcome = std::make_shared<Outcome>();
|
||||
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
|
||||
return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success,
|
||||
result.message.empty() ? fallback : from_u8(result.message)};
|
||||
auto on_done = [outcome](const ScriptCompletion& completion) {
|
||||
outcome->result = script_outcome_to_action_result(completion);
|
||||
outcome->completed = true;
|
||||
if (outcome->async)
|
||||
notify_script_outcome(completion);
|
||||
};
|
||||
|
||||
const PluginManager::ScriptLaunchResult launch = PluginManager::instance().launch_script_capability(plugin_key, capability, on_done);
|
||||
if (launch.launched_async) {
|
||||
// The completion is delivered on this (GUI) thread once the (sync or async) invocation
|
||||
// finishes, so marking `async` here cannot race with on_done. The speed dial has already
|
||||
// shown a result by then; the final outcome arrives as a notification (a cancellation is
|
||||
// silent).
|
||||
outcome->async = true;
|
||||
if (outcome->completed)
|
||||
return outcome->result;
|
||||
return {AppActionRunResult::Level::Info, _L("Script plugin started.")};
|
||||
}
|
||||
|
||||
// The launch failed inline; on_done produced the (error) result.
|
||||
on_done(launch.outcome);
|
||||
return outcome->result;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -747,7 +747,9 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model& model, const Dyn
|
||||
|
||||
// Orca: prevent resetting under gcode viewer mode
|
||||
if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) {
|
||||
const auto plater = GUI::wxGetApp().mainframe->m_plater;
|
||||
// Use the app’s plater accessor instead of going through mainframe, otherwise anything during startup that reaches apply()
|
||||
// (loading presets, config or input files, e.g. from the CLI) therefore dereferences a null `mainframe` and crashes.
|
||||
const auto plater = GUI::wxGetApp().plater();
|
||||
if (plater && plater->only_gcode_mode()) {
|
||||
invalidated = PrintBase::APPLY_STATUS_UNCHANGED;
|
||||
}
|
||||
|
||||
@@ -163,8 +163,8 @@ public:
|
||||
~devPrinterUtil() = delete;
|
||||
|
||||
public:
|
||||
static bool IsVirtualSlot(int ams_id);
|
||||
static bool IsVirtualSlot(const std::string& ams_id);
|
||||
static bool IsVirtualSlot(int ams_id) { return (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID);}
|
||||
static bool IsVirtualSlot(const std::string& ams_id) { return (ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR); }
|
||||
};
|
||||
|
||||
namespace GUI
|
||||
@@ -198,6 +198,3 @@ template<> struct std::hash<NozzleDef>
|
||||
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
|
||||
};
|
||||
};
|
||||
|
||||
// key(extruder_id) -> { key1(nozzle type info), val1( number of the nozzle type)}
|
||||
using ExtruderNozzleInfos = std::unordered_map<int, std::unordered_map<NozzleDef, int>>;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
@@ -34,36 +33,6 @@
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool devPrinterUtil::IsVirtualSlot(int ams_id)
|
||||
{
|
||||
// When the app or preset bundle is unavailable we cannot know the vendor's
|
||||
// extruder count, so fall back deterministically to the fixed Bambu pair
|
||||
// {255, 254} rather than guessing from an unknown generic layout.
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr || GUI::wxGetApp().preset_bundle->is_bbl_vendor())
|
||||
return ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID;
|
||||
|
||||
const int extruder_count = GUI::wxGetApp().preset_bundle->get_printer_extruder_count();
|
||||
return ams_id >= VIRTUAL_TRAY_MAIN_ID - extruder_count + 1 && ams_id <= VIRTUAL_TRAY_MAIN_ID;
|
||||
}
|
||||
|
||||
bool devPrinterUtil::IsVirtualSlot(const std::string& ams_id)
|
||||
{
|
||||
// Strict: only a fully numeric string is a valid slot id. std::stoi alone
|
||||
// would accept trailing garbage (e.g. "255abc" -> 255).
|
||||
if (ams_id.empty())
|
||||
return false;
|
||||
for (char c : ams_id) {
|
||||
if (c < '0' || c > '9')
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return IsVirtualSlot(std::stoi(ams_id));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c - '0' : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1; }
|
||||
|
||||
wxColour DevAmsTray::decode_color(const std::string &color)
|
||||
@@ -231,22 +200,18 @@ wxString DevAms::GetDisplayName() const
|
||||
|
||||
int DevAms::GetSlotCount() const
|
||||
{
|
||||
// N3S is a 1-slot Bambu contract, so report 1 regardless of whether the
|
||||
// preset bundle is momentarily unavailable (m_trays.size() can be 0).
|
||||
if (GetAmsType() == N3S)
|
||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
||||
auto ams_type = GetAmsType();
|
||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
else if (ams_type == N3S)
|
||||
{
|
||||
return 1;
|
||||
|
||||
if (wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
||||
auto ams_type = GetAmsType();
|
||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<int>(m_trays.size());
|
||||
return 1;
|
||||
}
|
||||
|
||||
DevAmsTray* DevAms::GetTray(const std::string& tray_id) const
|
||||
@@ -337,21 +302,13 @@ int DevFilaSystem::GetExtruderIdByAmsId(const std::string& ams_id) const
|
||||
{
|
||||
return it->second->GetExtruderId();
|
||||
}
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (!is_bbl_vendor && GetOwner()) {
|
||||
for (const auto& tray : GetOwner()->vt_slot) {
|
||||
if (tray.id == ams_id)
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_MAIN_ID_STR)
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_MAIN_ID)
|
||||
{
|
||||
return MAIN_EXTRUDER_ID;
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
}
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
{
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
if (!is_bbl_vendor && devPrinterUtil::IsVirtualSlot(ams_id)) {
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
|
||||
assert(false && __FUNCTION__);
|
||||
@@ -368,22 +325,9 @@ std::string DevFilaSystem::GetNozzleFlowStringByAmsId(const std::string& ams_id)
|
||||
std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
{
|
||||
std::map<int, DevAmsSlotId> tray_id_map;
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
||||
} else if (GetOwner()) {
|
||||
for (const auto& tray : GetOwner()->vt_slot) {
|
||||
try {
|
||||
const int tray_id = std::stoi(tray.id);
|
||||
tray_id_map[tray_id] = DevAmsSlotId{tray_id, 0};
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid virtual tray id: " << tray.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
||||
|
||||
int generic_tray_index = 0;
|
||||
for (auto& [ams_id, ams_item] : GetAmsList()) {
|
||||
for (auto &[slot_id, slot_item] : ams_item->GetTrays()) {
|
||||
if (ams_item && slot_item) {
|
||||
@@ -391,11 +335,9 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
int ams_id_int = stoi(ams_id);
|
||||
int slot_id_int = stoi(slot_id);
|
||||
int tray_index = -1;
|
||||
if (!is_bbl_vendor) {
|
||||
tray_index = generic_tray_index + slot_id_int;
|
||||
} else if (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
if (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
tray_index = ams_id_int;
|
||||
} else if (ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
|
||||
} else if(ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
|
||||
tray_index = 24 + slot_id_int;
|
||||
} else {
|
||||
tray_index = (ams_id_int * 4 + slot_id_int);
|
||||
@@ -406,8 +348,6 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor)
|
||||
generic_tray_index += static_cast<int>(ams_item->GetTrays().size());
|
||||
}
|
||||
|
||||
return tray_id_map;
|
||||
@@ -516,13 +456,6 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
ams_id_set.insert(it->first);
|
||||
}
|
||||
|
||||
// Bambu trays index their exist bit as ams_id*4+tray_id; a generic (non-Bambu) unit
|
||||
// reports its own lane count, so its trays carry a cumulative lane index instead.
|
||||
// Same index DevFilaSystem::GetTrayIndexMap() builds and the agent sets bits with.
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
int generic_tray_base = 0;
|
||||
|
||||
for (auto it = j_ams.begin(); it != j_ams.end(); it++)
|
||||
{
|
||||
if (!it->contains("id")) continue;
|
||||
@@ -862,18 +795,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
|
||||
if (type_id < 4)
|
||||
{
|
||||
if (is_bbl_vendor)
|
||||
{
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A generic unit is not four slots wide, so its trays carry a
|
||||
// cumulative lane index across units instead of ams_id*4+tray_id.
|
||||
const int tray_bit = generic_tray_base + tray_id_int;
|
||||
curr_tray->is_exists = tray_bit >= 0 && tray_bit < 32 &&
|
||||
(obj->tray_exist_bits & (1UL << tray_bit)) != 0;
|
||||
}
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
}
|
||||
else if (type_id == DevAms::AMS_LITE_MIXED)
|
||||
{
|
||||
@@ -929,9 +851,6 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor && it->contains("tray") && (*it)["tray"].is_array()) {
|
||||
generic_tray_base += static_cast<int>((*it)["tray"].size());
|
||||
}
|
||||
}
|
||||
// remove not in amsList
|
||||
for (auto it = ams_id_set.begin(); it != ams_id_set.end(); it++)
|
||||
@@ -947,21 +866,6 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give every live device extruder an external virtual tray slot (non-Bambu) so a plugin agent
|
||||
// that reaches the model only through this parser also gets the shared external-slot topology.
|
||||
if (obj)
|
||||
obj->ensure_external_virtual_tray_slots();
|
||||
|
||||
// Diagnostic: summarize the AMS state this production parse produced, so the agent boundary
|
||||
// (submit_ams_payload) box/tray counts can be correlated with what actually landed in the model.
|
||||
if (obj && system) {
|
||||
size_t parsed_trays = 0;
|
||||
for (const auto& kv : system->amsList)
|
||||
parsed_trays += kv.second->GetTrays().size();
|
||||
BOOST_LOG_TRIVIAL(info) << "DevFilaSystemParser::ParseV1_0: parsed units " << system->amsList.size()
|
||||
<< " trays " << parsed_trays;
|
||||
}
|
||||
}
|
||||
|
||||
static DevAms::AmsType ams_type_from_string(const std::string& s)
|
||||
@@ -972,23 +876,6 @@ static DevAms::AmsType ams_type_from_string(const std::string& s)
|
||||
return DevAms::AMS; // default
|
||||
}
|
||||
|
||||
void grow_virtual_tray_slots(std::vector<DevAmsTray>& slots, std::size_t tray_id)
|
||||
{
|
||||
// Address a slot by its virtual tray id, never by a raw vector index: the slot for `tray_id`
|
||||
// lives at index VIRTUAL_TRAY_MAIN_ID - tray_id and placeholder entries carry
|
||||
// id = VIRTUAL_TRAY_MAIN_ID - index. Append-only, so a slot that already holds reported
|
||||
// metadata keeps it and a sparse/out-of-order report cannot shift the id <-> extruder mapping.
|
||||
if (tray_id > static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID))
|
||||
return;
|
||||
const std::size_t slot_index = static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID) - tray_id;
|
||||
while (slots.size() <= slot_index) {
|
||||
slots.emplace_back(std::to_string(VIRTUAL_TRAY_MAIN_ID - static_cast<int>(slots.size())));
|
||||
// A filler only makes the slot addressable; it is topology, not a reported spool. Callers
|
||||
// overwrite the target entry with reported metadata, which clears this marker.
|
||||
slots.back().is_slot_placeholder = true;
|
||||
}
|
||||
}
|
||||
|
||||
void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system)
|
||||
{
|
||||
if (!system || !data.is_object())
|
||||
@@ -1026,8 +913,6 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
ams->m_exist = true;
|
||||
ams->m_binded_extruder_set = {ext_id};
|
||||
ams->m_binded_switcher_pos.reset();
|
||||
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
|
||||
ams->m_humidity_percent = u.value("humidity_percent", -1);
|
||||
ams->m_left_dry_time = u.value("dry_time_min", 0);
|
||||
@@ -1110,21 +995,16 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
// --- external / direct spools -> obj->vt_slot ---
|
||||
// Keep one virtual slot per extruder. The legacy two-extruder mapping uses
|
||||
// 255 for extruder 0 and 254 for extruder 1; continue that sequence for
|
||||
// additional extruders (253, 252, ...).
|
||||
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
|
||||
if (obj && data.contains("external") && data["external"].is_array())
|
||||
{
|
||||
std::map<int, DevAmsTray> external_slots;
|
||||
obj->vt_slot.clear();
|
||||
for (const auto& e : data["external"])
|
||||
{
|
||||
if (!e.is_object())
|
||||
continue;
|
||||
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
|
||||
if (ext < MAIN_EXTRUDER_ID || ext >= VIRTUAL_TRAY_MAIN_ID)
|
||||
continue;
|
||||
|
||||
const int vt_id = VIRTUAL_TRAY_MAIN_ID - ext;
|
||||
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
|
||||
DevAmsTray tray(std::to_string(vt_id));
|
||||
tray.is_exists = e.value("loaded", false);
|
||||
tray.m_fila_type = e.value("material", std::string());
|
||||
@@ -1133,26 +1013,9 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
|
||||
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
|
||||
tray.remain = e.value("remain_percent", -1);
|
||||
external_slots.insert_or_assign(ext, std::move(tray));
|
||||
}
|
||||
|
||||
// Place each reported spool at its own index (VIRTUAL_TRAY_MAIN_ID - extruder_id) instead of
|
||||
// appending in report order: a sparse or out-of-order report must not compact the vector and
|
||||
// shift the slot -> extruder association.
|
||||
obj->vt_slot.clear();
|
||||
for (const auto& entry : external_slots)
|
||||
{
|
||||
// entry.first is the extruder id; its virtual tray id is VIRTUAL_TRAY_MAIN_ID - extruder_id
|
||||
// and the slot lives at vector index extruder_id (the id <-> index mapping grow applies).
|
||||
grow_virtual_tray_slots(obj->vt_slot, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - entry.first));
|
||||
obj->vt_slot[entry.first] = entry.second;
|
||||
obj->vt_slot.push_back(tray);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill any extruder still missing a slot so the model always has one external slot per live
|
||||
// extruder, even when the agent reported no external data at all. Preserves reported metadata.
|
||||
if (obj)
|
||||
obj->ensure_external_virtual_tray_slots();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "DevFilaSwitch.h"
|
||||
#include "DevUtil.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -83,7 +82,7 @@ public:
|
||||
wxColour wx_color;
|
||||
bool is_bbl;
|
||||
bool is_exists = false;
|
||||
bool is_slot_placeholder = false; // Orca: True for synthetic topology slots or empty reported trays
|
||||
bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents
|
||||
int hold_count = 0;
|
||||
int remain = 0; // filament remain: 0 ~ 100
|
||||
|
||||
@@ -115,16 +114,6 @@ public:
|
||||
std::optional<DevFilamentDryingPreset> get_ams_drying_preset() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure the virtual-tray vector can address the slot for a virtual tray id. The slot's index is
|
||||
* `VIRTUAL_TRAY_MAIN_ID - tray_id` and placeholder entries carry id = VIRTUAL_TRAY_MAIN_ID - index.
|
||||
* Existing entries are left untouched, so a slot already populated with reported filament data
|
||||
* keeps it; callers then assign the reported tray at index `VIRTUAL_TRAY_MAIN_ID - tray_id`.
|
||||
* Appended placeholders are marked `is_slot_placeholder`, so consumers can tell a synthetic
|
||||
* topology filler from a spool the printer actually reported (overwriting the entry clears it).
|
||||
*/
|
||||
void grow_virtual_tray_slots(std::vector<DevAmsTray>& slots, std::size_t tray_id);
|
||||
|
||||
/**
|
||||
* DevAms - Represents a single AMS (Automatic Material System) unit.
|
||||
*
|
||||
@@ -359,7 +348,7 @@ public:
|
||||
DevAmsTray* GetAmsTray(const std::string& ams_id, const std::string& tray_id) const;
|
||||
void CollectAmsColors(std::vector<wxColour>& ams_colors) const;
|
||||
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes virtual external-spool trays,
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes the two virtual (external-spool) trays,
|
||||
// N3S single-slot units, and the A2L/N9 AMS-Lite-mixed layout (trays 24-27).
|
||||
std::map<int, DevAmsSlotId> GetTrayIndexMap();
|
||||
|
||||
@@ -444,4 +433,4 @@ struct DevFilamentDryingPreset
|
||||
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
|
||||
};
|
||||
|
||||
}// namespace Slic3r
|
||||
}// namespace Slic3r
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <set>
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
@@ -19,80 +18,14 @@
|
||||
|
||||
// TODO: remove this include
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/GuiColor.hpp"
|
||||
|
||||
namespace Slic3r
|
||||
{
|
||||
// Bambu keeps its fixed external pair (255 main/right, 254 deputy/left) and its two
|
||||
// left/right flags. Every other printer addresses one external virtual tray per live
|
||||
// extruder, so ids 255, 254, 253, ... are extruders 0, 1, 2, ... .
|
||||
static bool _is_bbl_vendor_printer()
|
||||
{
|
||||
return wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
}
|
||||
|
||||
// Extruder a virtual (external spool) tray id feeds: VIRTUAL_TRAY_MAIN_ID - tray_id.
|
||||
// Returns -1 for an empty, non-numeric or out-of-range id.
|
||||
static int _virtual_tray_extruder_id(const std::string& tray_id)
|
||||
{
|
||||
if (tray_id.empty())
|
||||
return -1;
|
||||
for (char c : tray_id)
|
||||
{
|
||||
if (c < '0' || c > '9')
|
||||
return -1;
|
||||
}
|
||||
try
|
||||
{
|
||||
const int vt_id = std::stoi(tray_id);
|
||||
if (vt_id < 0 || vt_id > VIRTUAL_TRAY_MAIN_ID)
|
||||
return -1;
|
||||
return VIRTUAL_TRAY_MAIN_ID - vt_id;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// True when obj->vt_slot actually carries this virtual tray id as a reported spool. A synthetic
|
||||
// topology filler (is_slot_placeholder) only makes the slot addressable and does not count.
|
||||
static bool _has_virtual_tray_slot(const MachineObject* obj, const std::string& tray_id)
|
||||
{
|
||||
if (obj == nullptr || tray_id.empty())
|
||||
return false;
|
||||
for (const auto& tray : obj->vt_slot)
|
||||
{
|
||||
if (!tray.is_slot_placeholder && tray.id == tray_id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A generic printer may expose one AMS unit / external slot per extruder. When both legacy
|
||||
// flags of a category are enabled (the sidebar sync call uses both AMS and both external flags)
|
||||
// every extruder participates; when only one side is enabled (SelectMachine's left/right split)
|
||||
// keep to that side's legacy extruder (right = main = 0, left = deputy = 1), so the N slots do
|
||||
// not leak across sides.
|
||||
static bool _generic_extruder_in_selected_side(int extruder_id, bool use_right, bool use_left)
|
||||
{
|
||||
if (use_right && use_left)
|
||||
return true;
|
||||
if (use_right)
|
||||
return extruder_id == MAIN_EXTRUDER_ID;
|
||||
if (use_left)
|
||||
return extruder_id == DEPUTY_EXTRUDER_ID;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DevMappingUtil::is_valid_mapping_result(const MachineObject* obj, std::vector<FilamentInfo>& result, bool check_empty_slot)
|
||||
{
|
||||
if (result.empty()) return false;
|
||||
|
||||
const bool is_bbl_vendor = _is_bbl_vendor_printer();
|
||||
|
||||
for (int i = 0; i < result.size(); i++)
|
||||
{
|
||||
// invalid mapping result
|
||||
@@ -109,13 +42,8 @@ namespace Slic3r
|
||||
auto ams_item = obj->GetFilaSystem()->GetAmsById(result[i].ams_id);
|
||||
if (ams_item == nullptr)
|
||||
{
|
||||
// A virtual (external spool) tray is not an AMS unit. Bambu keeps its fixed pair;
|
||||
// a generic printer accepts whichever per-extruder slots it actually reported.
|
||||
const bool is_virtual_tray = is_bbl_vendor
|
||||
? (result[i].ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) ||
|
||||
result[i].ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
|
||||
: _has_virtual_tray_slot(obj, result[i].ams_id);
|
||||
if (!is_virtual_tray)
|
||||
if ((result[i].ams_id != std::to_string(VIRTUAL_TRAY_MAIN_ID)) &&
|
||||
(result[i].ams_id != std::to_string(VIRTUAL_TRAY_DEPUTY_ID)))
|
||||
{
|
||||
result[i].tray_id = -1;
|
||||
return false;
|
||||
@@ -166,11 +94,8 @@ namespace Slic3r
|
||||
result.ams_id = std::to_string(ams_id);
|
||||
result.slot_id = std::to_string(slot_id);
|
||||
|
||||
if (type == DevAms::EXT_SPOOL || ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
{
|
||||
// An external spool is addressed by its virtual tray id (255 -> extruder 0, 254 -> 1,
|
||||
// ...); the id is the tray's identity, so it must not fall through to the AMS
|
||||
// ams_id * 4 + slot_id indexing.
|
||||
result.tray_id = atoi(tray.id.c_str());
|
||||
result.id = atoi(tray.id.c_str());
|
||||
}
|
||||
@@ -201,25 +126,11 @@ namespace Slic3r
|
||||
std::map<int, FilamentInfo> tray_filaments; // tray_index : tray_color
|
||||
bool left_nozzle_has_ams = false, right_nozzle_has_ams = false;
|
||||
|
||||
const bool is_bbl_vendor = _is_bbl_vendor_printer();
|
||||
// Extruders whose AMS filament participates in this mapping. On a generic printer this
|
||||
// carries the per-extruder "nozzle already has AMS" state that the legacy right/left flags
|
||||
// express for Bambu, so the matching external spool can be suppressed.
|
||||
std::set<int> generic_extruders_with_ams;
|
||||
|
||||
const auto& ams_list = obj->GetFilaSystem()->GetAmsList();
|
||||
for (auto ams = ams_list.begin(); ams != ams_list.end(); ams++)
|
||||
{
|
||||
std::string ams_id = ams->second->GetAmsId();
|
||||
auto ams_type = ams->second->GetAmsType();
|
||||
const int ams_id_int = atoi(ams->first.c_str());
|
||||
int generic_tray_base = 0;
|
||||
if (!is_bbl_vendor) {
|
||||
for (const auto& other : ams_list) {
|
||||
if (atoi(other.first.c_str()) < ams_id_int)
|
||||
generic_tray_base += static_cast<int>(other.second->GetTrays().size());
|
||||
}
|
||||
}
|
||||
// GetAmsType() maps mixed -> AMS_LITE; recover the mixed type so N9 trays index at 24+slot.
|
||||
if (ams_type == DevAms::AMS_LITE && ams->second->IsAmsLiteMixed())
|
||||
{
|
||||
@@ -227,16 +138,10 @@ namespace Slic3r
|
||||
}
|
||||
for (auto tray = ams->second->GetTrays().begin(); tray != ams->second->GetTrays().end(); tray++)
|
||||
{
|
||||
int ams_id = ams_id_int;
|
||||
int ams_id = atoi(ams->first.c_str());
|
||||
int tray_id = atoi(tray->first.c_str());
|
||||
int tray_index = 0;
|
||||
if (!is_bbl_vendor)
|
||||
{
|
||||
// Generic units can have different lane counts; index all lanes cumulatively,
|
||||
// matching the shared tray-index map and sidebar list builder.
|
||||
tray_index = generic_tray_base + tray_id;
|
||||
}
|
||||
else if (ams_type == DevAms::AMS || ams_type == DevAms::AMS_LITE || ams_type == DevAms::N3F)
|
||||
if (ams_type == DevAms::AMS || ams_type == DevAms::AMS_LITE || ams_type == DevAms::N3F)
|
||||
{
|
||||
tray_index = ams_id * 4 + tray_id;
|
||||
}
|
||||
@@ -264,18 +169,14 @@ namespace Slic3r
|
||||
if (tray->second->is_tray_info_ready())
|
||||
{
|
||||
_parse_tray_info(ams_id, tray_id, ams_type, *(tray->second), info);
|
||||
// Generic unit lanes use one cumulative index across variable-width units;
|
||||
// ams_id * 4 + slot_id is only valid for Bambu's fixed four-slot units.
|
||||
if (!is_bbl_vendor)
|
||||
info.id = tray_index;
|
||||
}
|
||||
|
||||
bool ams_valid = false;
|
||||
if (is_bbl_vendor)
|
||||
//first: left,nozzle=1,map=1 second: right,nozzle=0,map=2
|
||||
bool right_ams_valid = (ams->second->GetBindedExtruderSet().count(MAIN_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_RIGHT_AMS];
|
||||
bool left_ams_valid = (ams->second->GetBindedExtruderSet().count(DEPUTY_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_LEFT_AMS];
|
||||
if (right_ams_valid || left_ams_valid)
|
||||
{
|
||||
//first: left,nozzle=1,map=1 second: right,nozzle=0,map=2
|
||||
bool right_ams_valid = (ams->second->GetBindedExtruderSet().count(MAIN_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_RIGHT_AMS];
|
||||
bool left_ams_valid = (ams->second->GetBindedExtruderSet().count(DEPUTY_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_LEFT_AMS];
|
||||
tray_filaments.emplace(std::make_pair(tray_index, info));
|
||||
if (right_ams_valid)
|
||||
{
|
||||
right_nozzle_has_ams = true;
|
||||
@@ -284,25 +185,6 @@ namespace Slic3r
|
||||
{
|
||||
left_nozzle_has_ams = true;
|
||||
}
|
||||
ams_valid = right_ams_valid || left_ams_valid;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A generic unit is bound to its own extruder, so it takes part whenever the
|
||||
// request covers that extruder.
|
||||
for (int extruder_id : ams->second->GetBindedExtruderSet())
|
||||
{
|
||||
if (_generic_extruder_in_selected_side(extruder_id, map_opt[MappingOption::USE_RIGHT_AMS], map_opt[MappingOption::USE_LEFT_AMS]))
|
||||
{
|
||||
ams_valid = true;
|
||||
generic_extruders_with_ams.insert(extruder_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ams_valid)
|
||||
{
|
||||
tray_filaments.emplace(std::make_pair(tray_index, info));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,21 +193,10 @@ namespace Slic3r
|
||||
{
|
||||
for (auto tray : obj->vt_slot)
|
||||
{
|
||||
// A synthetic topology filler is not a reported external spool, so it is never a
|
||||
// mapping candidate. Bambu keeps its fixed reported pair.
|
||||
if (!is_bbl_vendor && tray.is_slot_placeholder)
|
||||
bool right_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID) && map_opt[MappingOption::USE_RIGHT_EXT]);
|
||||
bool left_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID) && map_opt[MappingOption::USE_LEFT_EXT]);
|
||||
if (right_ext_valid || left_ext_valid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (is_bbl_vendor)
|
||||
{
|
||||
// Bambu keeps its fixed pair and the two left/right external flags.
|
||||
const bool right_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID) && map_opt[MappingOption::USE_RIGHT_EXT]);
|
||||
const bool left_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID) && map_opt[MappingOption::USE_LEFT_EXT]);
|
||||
if (!right_ext_valid && !left_ext_valid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (nozzle_has_ams_then_ignore_ext)
|
||||
{
|
||||
if (right_ext_valid && right_nozzle_has_ams)
|
||||
@@ -337,31 +208,10 @@ namespace Slic3r
|
||||
continue;
|
||||
}
|
||||
}
|
||||
FilamentInfo info;
|
||||
_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info);
|
||||
tray_filaments.emplace(std::make_pair(info.tray_id, info));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generic: every present virtual tray is the external spool of the extruder it
|
||||
// addresses (VIRTUAL_TRAY_MAIN_ID - tray.id): 255 -> 0, 254 -> 1, ... . Keep to
|
||||
// the selected side when only one legacy flag is enabled, and drop the spool when
|
||||
// its extruder already has AMS filament in this mapping and suppression is on.
|
||||
const int extruder_id = _virtual_tray_extruder_id(tray.id);
|
||||
if (extruder_id < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!_generic_extruder_in_selected_side(extruder_id, map_opt[MappingOption::USE_RIGHT_EXT], map_opt[MappingOption::USE_LEFT_EXT]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (nozzle_has_ams_then_ignore_ext && generic_extruders_with_ams.count(extruder_id) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
FilamentInfo info;
|
||||
_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info);
|
||||
tray_filaments.emplace(std::make_pair(info.tray_id, info));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,4 +412,4 @@ namespace Slic3r
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/string.h>
|
||||
#include <string>
|
||||
#include "libslic3r/CommonDefs.hpp"
|
||||
@@ -28,7 +27,6 @@
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -178,52 +176,34 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
|
||||
int DevNozzle::GetLogicExtruderId() const
|
||||
{
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
if (total_ext_count == 1) {
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtLeftExtruder()) {
|
||||
return LOGIC_L_EXTRUDER_ID;
|
||||
} else if (AtRightExtruder()) {
|
||||
return LOGIC_R_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
assert(0);
|
||||
if (total_ext_count == 1) {
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtLeftExtruder()) {
|
||||
return LOGIC_L_EXTRUDER_ID;
|
||||
} else if (AtRightExtruder()) {
|
||||
return LOGIC_R_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
// For some reason, BBL's logical extruder ID is inverted:
|
||||
// physical extruder id = 0 (MAIN_EXTRUDER_ID) vs logical extruder id = 1 (LOGIC_R_EXTRUDER_ID)
|
||||
// Likely because logical id reads from left to right (left = 0, right = 1)
|
||||
// For generic N extruders, this inversion does not apply.
|
||||
if (IsOnRack()) return INVALID_EXTRUDER_ID;
|
||||
return m_nozzle_id;
|
||||
assert(0);
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
int DevNozzle::GetExtruderId() const
|
||||
{
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
return m_nozzle_id;
|
||||
return MAIN_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
bool DevNozzle::AtLeftExtruder() const
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <tuple>
|
||||
#include <wx/app.h>
|
||||
@@ -807,12 +806,8 @@ bool MachineObject::is_extrusion_cali_finished()
|
||||
DevAmsTray *MachineObject::get_curr_tray()
|
||||
{
|
||||
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
|
||||
if (devPrinterUtil::IsVirtualSlot(cur_ams_id)) {
|
||||
for (auto& tray : vt_slot) {
|
||||
if (tray.id == cur_ams_id)
|
||||
return &tray;
|
||||
}
|
||||
return nullptr;
|
||||
if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) {
|
||||
return &vt_slot[0];
|
||||
}
|
||||
|
||||
DevAms* curr_ams = get_curr_Ams();
|
||||
@@ -1278,24 +1273,19 @@ int MachineObject::get_bed_temperature_limit()
|
||||
|
||||
bool MachineObject::is_filament_installed()
|
||||
{
|
||||
// if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||
// // right//or single
|
||||
// auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||
// if (ext.m_ext_has_filament) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// /*left*/
|
||||
// if (m_extder_system->GetTotalExtderCount() > 1) {
|
||||
// auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
|
||||
// if (ext.m_ext_has_filament) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
for (auto& ext : m_extder_system->m_extders) {
|
||||
if (ext.m_ext_has_filament)
|
||||
if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||
// right//or single
|
||||
auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||
if (ext.m_ext_has_filament) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/*left*/
|
||||
if (m_extder_system->GetTotalExtderCount() > 1) {
|
||||
auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
|
||||
if (ext.m_ext_has_filament) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1808,7 +1798,8 @@ int MachineObject::command_ams_calibrate(int ams_id)
|
||||
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
|
||||
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type,
|
||||
int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors, int tray_ctype)
|
||||
{
|
||||
int tag_tray_id = 0;
|
||||
int tag_ams_id = ams_id;
|
||||
@@ -1837,6 +1828,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
|
||||
j["print"]["nozzle_temp_min"] = nozzle_temp_min;
|
||||
j["print"]["nozzle_temp_max"] = nozzle_temp_max;
|
||||
j["print"]["tray_type"] = tray_type;
|
||||
if (!tray_colors.empty()) {
|
||||
j["print"]["cols"] = tray_colors;
|
||||
j["print"]["ctype"] = tray_ctype;
|
||||
}
|
||||
|
||||
return this->publish_json(j);
|
||||
}
|
||||
@@ -2637,10 +2632,13 @@ void MachineObject::reset()
|
||||
json empty_j;
|
||||
print_json.diff2all_base_reset(empty_j);
|
||||
|
||||
for (auto& tray : vt_slot)
|
||||
tray.reset();
|
||||
if (vt_slot.size() > 1)
|
||||
vt_slot.erase(vt_slot.begin() + 1, vt_slot.end());
|
||||
for (auto i = 0; i < vt_slot.size(); i++) {
|
||||
vt_slot[i].reset();
|
||||
|
||||
if (i == 1) {
|
||||
vt_slot.erase(vt_slot.begin() + 1);
|
||||
}
|
||||
}
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
@@ -4121,25 +4119,25 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||
|
||||
for (auto it = jj["vir_slot"].begin(); it != jj["vir_slot"].end(); it++) {
|
||||
try {
|
||||
const auto tray_json = it.value().get<json>();
|
||||
if (!tray_json.is_object() || !tray_json.contains("id") || !tray_json["id"].is_string())
|
||||
continue;
|
||||
auto vslot = parse_vt_tray(it.value().get<json>());
|
||||
|
||||
const auto tray_id_str = tray_json["id"].get<std::string>();
|
||||
int tray_id = 0;
|
||||
const auto result = std::from_chars(tray_id_str.data(), tray_id_str.data() + tray_id_str.size(), tray_id);
|
||||
if (result.ec != std::errc{} || result.ptr != tray_id_str.data() + tray_id_str.size()
|
||||
|| tray_id < 0 || tray_id > VIRTUAL_TRAY_MAIN_ID)
|
||||
continue;
|
||||
|
||||
auto vslot = parse_vt_tray(tray_json);
|
||||
grow_virtual_tray_slots(vt_slot, static_cast<std::size_t>(tray_id));
|
||||
const auto index = static_cast<std::vector<DevAmsTray>::size_type>(VIRTUAL_TRAY_MAIN_ID - tray_id);
|
||||
vt_slot[index] = vslot;
|
||||
if (vslot.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 0);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[0] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
continue;
|
||||
else if (vslot.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 1);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[1] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4170,13 +4168,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
try {
|
||||
parse_new_info(jj);
|
||||
} catch (...) {}
|
||||
|
||||
// Give every live device extruder an external virtual tray even when the payload
|
||||
// carried no vir_slot/vt_tray data (non-Bambu), so the UI always has a slot per
|
||||
// extruder. Runs after parse_new_info(), which refreshes the extruder count, and
|
||||
// after the vir_slot/vt_tray handling above, which may have cleared the support
|
||||
// flag and would otherwise hide these synthetic slots.
|
||||
ensure_external_virtual_tray_slots();
|
||||
#pragma endregion
|
||||
} else if (jj["command"].get<std::string>() == "gcode_line") {
|
||||
//ack of gcode_line
|
||||
@@ -5282,50 +5273,18 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
return vt_tray;
|
||||
}
|
||||
|
||||
void MachineObject::ensure_external_virtual_tray_slots()
|
||||
{
|
||||
// Without a preset bundle the vendor layout is unknown, and Bambu owns its own fixed pair
|
||||
// (255, 254) reported through vir_slot/vt_tray; in both cases leave vt_slot untouched.
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr ||
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int extruder_count = m_extder_system != nullptr ? m_extder_system->GetTotalExtderCount() : 0;
|
||||
if (extruder_count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// One slot per live extruder, addressed by VIRTUAL_TRAY_MAIN_ID - extruder_id. Grow with
|
||||
// placeholders and only fill slots that do not exist yet, so a slot already populated from a
|
||||
// reported tray keeps its metadata and the slot -> extruder mapping never shifts.
|
||||
for (int extruder_id = 0; extruder_id < extruder_count; ++extruder_id) {
|
||||
const int tray_id = VIRTUAL_TRAY_MAIN_ID - extruder_id;
|
||||
grow_virtual_tray_slots(vt_slot, static_cast<std::vector<DevAmsTray>::size_type>(tray_id));
|
||||
auto& tray = vt_slot[extruder_id];
|
||||
// The constructor seeds id 255 without a placeholder flag. Mark any empty generic slot as
|
||||
// synthetic, but leave a slot carrying reported filament data eligible for sync/mapping.
|
||||
tray.is_slot_placeholder = !tray.is_exists && tray.setting_id.empty() && tray.m_fila_type.empty();
|
||||
}
|
||||
|
||||
// A printer with an external-slot layout supports virtual trays even when the payload carried
|
||||
// no vir_slot/vt_tray data, which would otherwise clear this flag and hide the synthetic slots.
|
||||
ams_support_virtual_tray = true;
|
||||
}
|
||||
|
||||
bool MachineObject::contains_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
{
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
return m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
|
||||
if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
return m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
} else {
|
||||
for (const auto& tray : vt_slot) {
|
||||
if (tray.id == ams_id) return true;
|
||||
if (tray.id == ams_id) { return true; }
|
||||
}
|
||||
}
|
||||
|
||||
return !is_bbl_vendor && m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
@@ -5335,19 +5294,13 @@ DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string
|
||||
return DevAmsTray(tray_id);
|
||||
}
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) return *tray;
|
||||
if (tray) { return *tray;};
|
||||
}
|
||||
else if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
else {
|
||||
for (const auto &tray : vt_slot) {
|
||||
if (tray.id == ams_id) return tray;
|
||||
}
|
||||
|
||||
if (!is_bbl_vendor) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) return *tray;
|
||||
if (tray.id == ams_id) { return tray; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5530,6 +5483,7 @@ void MachineObject::parse_new_info(json print)
|
||||
is_support_pa_mode = (get_flag_bits_no_border(fun2, 3) == 1);
|
||||
is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1);
|
||||
is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1;
|
||||
is_support_filament_manual_multi_color = (get_flag_bits_no_border(fun2, 23) == 1);
|
||||
|
||||
if (DevPrinterConfigUtil::support_print_check_firmware_for_tpu_left(printer_type)) {
|
||||
m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1;
|
||||
|
||||
@@ -674,6 +674,7 @@ public:
|
||||
bool is_support_remote_dry = false;
|
||||
bool is_support_check_track_switch_match_slice_printer{false};
|
||||
bool is_support_pa_mode{false};
|
||||
bool is_support_filament_manual_multi_color{false};
|
||||
std::optional<bool> m_firmware_support_print_tpu_left;
|
||||
|
||||
bool installed_upgrade_kit{false};
|
||||
@@ -804,7 +805,7 @@ public:
|
||||
int command_ams_switch_filament(bool switch_filament);
|
||||
int command_ams_air_print_detect(bool air_print_detect);
|
||||
int command_ams_calibrate(int ams_id);
|
||||
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max);
|
||||
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors = {}, int tray_ctype = 2);
|
||||
int command_ams_select_tray(std::string tray_id);
|
||||
int command_ams_refresh_rfid(int ams_id, int slot_id);
|
||||
int command_ams_control(std::string action);
|
||||
@@ -950,16 +951,6 @@ public:
|
||||
std::vector<DevAmsTray> vt_slot;
|
||||
DevAmsTray parse_vt_tray(nlohmann::json vtray);
|
||||
|
||||
/**
|
||||
* Give every live device extruder an external virtual tray slot (non-Bambu printers only).
|
||||
*
|
||||
* Slots are addressed by VIRTUAL_TRAY_MAIN_ID - extruder_id, so a slot already reported for an
|
||||
* extruder keeps its tray metadata and sparse/out-of-order reports never shift the slot ->
|
||||
* extruder association. Only missing topology slots are added, so the call is idempotent across
|
||||
* refreshes. Bambu keeps its existing fixed pair reported through vir_slot/vt_tray.
|
||||
*/
|
||||
void ensure_external_virtual_tray_slots();
|
||||
|
||||
/*get ams slot info*/
|
||||
bool contains_tray(const std::string &ams_id, const std::string &tray_id) const;
|
||||
DevAmsTray get_tray(const std::string &ams_id, const std::string &tray_id) const;/*use contains_tray() check first*/
|
||||
|
||||
@@ -96,7 +96,7 @@ void FilamentColorCodeQuery::LoadFromLocal()
|
||||
const auto& fila_color_strs = json_data_item["fila_color"].get<std::vector<wxString>>();
|
||||
for (const auto& color_str : fila_color_strs) {
|
||||
if (color_str.size() > 3) /* Skip the value like "#0"*/{
|
||||
fila_color.m_colors.emplace(wxColour(color_str));
|
||||
fila_color.AddColor(wxColour(color_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user