mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 20:01:31 +00:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed4095b20 | ||
|
|
ee1cc1b227 | ||
|
|
46096416cd | ||
|
|
26f5fd6251 | ||
|
|
74fe2b7996 | ||
|
|
38f649fc00 | ||
|
|
5d73410281 | ||
|
|
b75af485ce | ||
|
|
6a7763f08d | ||
|
|
70e5b330be | ||
|
|
5e94368863 | ||
|
|
0df9b782bb | ||
|
|
d5403ccc94 | ||
|
|
32d690ecee | ||
|
|
303d7a1a07 | ||
|
|
c0e6db227d | ||
|
|
a078155890 | ||
|
|
3b6e27d725 | ||
|
|
6254ab643a | ||
|
|
9ab0178dd0 | ||
|
|
23f6771dcb | ||
|
|
aebc0a2979 | ||
|
|
6f0ec60d21 | ||
|
|
8e43a2ac13 | ||
|
|
6b366ad8a9 | ||
|
|
7d8261668e | ||
|
|
00127beffa | ||
|
|
927eb9922c | ||
|
|
3d62769664 | ||
|
|
e5e30ee0e8 | ||
|
|
7515fb480f | ||
|
|
acc1d6fd21 | ||
|
|
6ca0fc5ef1 | ||
|
|
bcf7d078cd | ||
|
|
15fc96fca7 | ||
|
|
1fffda0533 | ||
|
|
33e26a127e | ||
|
|
6dd0cb9eba | ||
|
|
9bf0fc2a8e | ||
|
|
5bf6db95b3 | ||
|
|
0feb87b417 |
@@ -8,6 +8,7 @@ on:
|
||||
paths:
|
||||
- 'deps/**'
|
||||
- 'deps_src/**'
|
||||
- 'cmake/**'
|
||||
- 'src/**'
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
@@ -31,6 +32,7 @@ on:
|
||||
paths:
|
||||
- 'deps/**'
|
||||
- 'deps_src/**'
|
||||
- 'cmake/**'
|
||||
- 'src/**'
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
|
||||
+2
-1
@@ -1345,7 +1345,8 @@ set (CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})
|
||||
set (CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/images\\\\OrcaSlicer.ico")
|
||||
set (CPACK_NSIS_MUI_ICON "${CPACK_PACKAGE_ICON}")
|
||||
set (CPACK_NSIS_MUI_UNIICON "${CPACK_PACKAGE_ICON}")
|
||||
set (CPACK_NSIS_INSTALLED_ICON_NAME "$INSTDIR\\\\orca-slicer.exe")
|
||||
# Relative to the install dir: CPack's NSIS template prefixes $INSTDIR itself.
|
||||
set (CPACK_NSIS_INSTALLED_ICON_NAME "orca-slicer.exe")
|
||||
set (CPACK_PACKAGE_CHECKSUM SHA256)
|
||||
set (CPACK_PACKAGE_INSTALL_REGISTRY_KEY "OrcaSlicer")
|
||||
set (CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
|
||||
|
||||
@@ -86,7 +86,8 @@ The [wiki](https://www.orcaslicer.com/wiki) aims to provide a detailed explanati
|
||||
## Stable Release
|
||||
|
||||
📥 **[Download the Latest Stable Release](https://github.com/OrcaSlicer/OrcaSlicer/releases/latest)**
|
||||
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
|
||||
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
|
||||
Windows release binaries are code signed; see our [Code signing policy](#code-signing-policy).
|
||||
|
||||
## Nightly Builds
|
||||
|
||||
@@ -244,6 +245,16 @@ OrcaSlicer began in that same spirit, drawing from BambuStudio, PrusaSlicer, and
|
||||
|
||||
The OrcaSlicer logo was designed by community member [Justin Levine](https://github.com/jal-co).
|
||||
|
||||
# Code Signing Policy
|
||||
|
||||
Free code signing provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org).
|
||||
|
||||
Windows release binaries are built by GitHub Actions from this repository and signed through the SignPath Foundation program. Every signing request is manually approved.
|
||||
|
||||
**Team roles:** [SoftFever](https://github.com/SoftFever) is the committer, reviewer and approver for all signed releases.
|
||||
|
||||
**Privacy policy:** See the [OrcaSlicer privacy policy](https://www.orcaslicer.com/privacy-policy/).
|
||||
|
||||
# License
|
||||
|
||||
- **OrcaSlicer** is licensed under the GNU Affero General Public License, version 3.
|
||||
|
||||
@@ -42,6 +42,7 @@ call :add_arg build_deps bool d deps "Download and build the dependencies, neede
|
||||
call :add_arg build_slicer bool s slicer "Build OrcaSlicer"
|
||||
call :add_arg build_tests bool "" tests "Build the unit tests"
|
||||
call :add_arg run_tests bool "" run-tests "Build the unit tests and run them"
|
||||
call :add_arg make_installer bool "" installer "Build OrcaSlicer and package it into an NSIS installer"
|
||||
call :add_arg pack_deps bool p pack "Bundle the built dependencies into a zip file"
|
||||
call :add_arg install_deps bool u install-deps "Install or update CMake, Perl and Git with WinGet"
|
||||
call :add_arg install_vs string "" install-vs "Also install Visual Studio: buildtools or ide"
|
||||
@@ -185,6 +186,9 @@ if "%build_deps%%build_slicer%%pack_deps%%install_deps%%install_vs%" == "" (
|
||||
if "%run_tests%" == "ON" set "build_slicer=ON"
|
||||
)
|
||||
|
||||
REM The installer packages the slicer build, so it always needs one.
|
||||
if "%make_installer%" == "ON" set "build_slicer=ON"
|
||||
|
||||
REM Options like --config or -j only shape a build. Without one of the actions
|
||||
REM there is nothing for them to shape, so say so rather than resolving a whole
|
||||
REM build and reporting it took no time. The block above may have added one.
|
||||
@@ -222,6 +226,30 @@ if "%build_slicer%" == "ON" if "%install_slicer%" == "ON" if not "%slicer_target
|
||||
echo --install needs the executable, but --slicer-target names "%slicer_target%".
|
||||
exit /b 1
|
||||
)
|
||||
REM cpack runs the same install, so it has the same need.
|
||||
if "%make_installer%" == "ON" if not "%slicer_target%" == "" if /I not "%slicer_target%" == "OrcaSlicer" (
|
||||
echo --installer needs the executable, but --slicer-target names "%slicer_target%".
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM cpack finds makensis on PATH or through the registry key the NSIS installer
|
||||
REM writes, so look in the same places, and before the build rather than after
|
||||
REM it. The value is read past its REG_SZ marker because the name column is
|
||||
REM localised and may hold spaces.
|
||||
if not "%make_installer%" == "ON" goto :nsis_ready
|
||||
if "%dry_run%" == "ON" goto :nsis_ready
|
||||
where makensis >nul 2>nul && goto :nsis_ready
|
||||
for %%v in (32 64) do (
|
||||
for /f "delims=" %%l in ('reg query "HKLM\SOFTWARE\NSIS" /ve /reg:%%v 2^>nul ^| findstr REG_SZ') do (
|
||||
set "nsis_line=%%l"
|
||||
set "nsis_line=!nsis_line:*REG_SZ=!"
|
||||
for /f "tokens=*" %%d in ("!nsis_line!") do if exist "%%d\makensis.exe" goto :nsis_ready
|
||||
)
|
||||
)
|
||||
echo NSIS was not found, and --installer needs it to build the installer.
|
||||
echo Install it with "winget install NSIS.NSIS", then run this again.
|
||||
exit /b 1
|
||||
:nsis_ready
|
||||
if not "%vs_version%" == "" if "!vs_gen_%vs_version%!" == "" (
|
||||
echo Unknown Visual Studio release "%vs_version%". Known releases: 2019, 2022, 2026.
|
||||
exit /b 1
|
||||
@@ -734,6 +762,14 @@ if "%build_slicer%" == "ON" (
|
||||
call :print_and_run cmake --build "%build_dir%" --target install --config %build_type%
|
||||
%error_check%
|
||||
)
|
||||
|
||||
REM cpack installs into its own staging tree, so this does not need -i.
|
||||
REM Unlike CI it skips the preset cache, whose packaging step prunes the
|
||||
REM checkout's preset JSONs.
|
||||
if "%make_installer%" == "ON" (
|
||||
call :print_and_run cpack -G NSIS -C %build_type% --config "%build_dir%\CPackConfig.cmake" -B "%build_dir%"
|
||||
%error_check%
|
||||
)
|
||||
)
|
||||
|
||||
REM Elapsed wall clock. The 1%%a-100 trick strips a leading zero, which set /A
|
||||
@@ -820,6 +856,16 @@ REM worked out the same way in either run.
|
||||
if "%build_slicer%" == "ON" if not "%linked%" == "ON" echo Target %slicer_target%
|
||||
if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\!solution!
|
||||
if "%pack_deps%" == "ON" if defined bundle echo Bundle !bundle!
|
||||
if "%make_installer%" == "ON" (
|
||||
REM The file name carries the version, which only CMake knows, so name
|
||||
REM the newest match, or the pattern when there is none yet.
|
||||
set "installer="
|
||||
for /f "delims=" %%f in ('dir /b /o-d "!build_full!\OrcaSlicer_Windows_Installer_*.exe" 2^>nul') do (
|
||||
if not defined installer set "installer=!build_full!\%%f"
|
||||
)
|
||||
if not defined installer set "installer=!build_full!\OrcaSlicer_Windows_Installer_*.exe"
|
||||
echo Installer !installer!
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Next
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
# Find the wxWidgets module based on the information provided by the Perl Alien::wxWidgets module.
|
||||
|
||||
# Check for the Perl & PerlLib modules
|
||||
include(LibFindMacros)
|
||||
libfind_package(AlienWx Perl)
|
||||
libfind_package(AlienWx PerlLibs)
|
||||
|
||||
if (AlienWx_DEBUG)
|
||||
message(STATUS " AlienWx_FIND_COMPONENTS=${AlienWx_FIND_COMPONENTS}")
|
||||
endif()
|
||||
|
||||
# Execute an Alien::Wx module to find the relevant information regarding
|
||||
# the wxWidgets used by the Perl interpreter.
|
||||
# Perl specific stuff
|
||||
set(AlienWx_TEMP_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/AlienWx_TEMP_INCLUDE.txt)
|
||||
execute_process(
|
||||
COMMAND ${PERL_EXECUTABLE} -e "
|
||||
# Import Perl modules.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Text::ParseWords;
|
||||
|
||||
BEGIN {
|
||||
# CMake sets the environment variables CC and CXX to the detected C compiler.
|
||||
# There is an issue with the Perl ExtUtils::CBuilder, which does not handle whitespaces
|
||||
# in the paths correctly on Windows, so we rather drop the CMake auto-detected paths.
|
||||
delete \$ENV{CC};
|
||||
delete \$ENV{CXX};
|
||||
}
|
||||
|
||||
use Alien::wxWidgets;
|
||||
use ExtUtils::CppGuess;
|
||||
|
||||
# Test for a Visual Studio compiler
|
||||
my \$cpp_guess = ExtUtils::CppGuess->new;
|
||||
my \$mswin = \$^O eq 'MSWin32';
|
||||
my \$msvc = \$cpp_guess->is_msvc;
|
||||
|
||||
# List of wxWidgets components to be used.
|
||||
my @components = split /;/, '${AlienWx_FIND_COMPONENTS}';
|
||||
|
||||
# Query the available data from Alien::wxWidgets.
|
||||
my \$version = Alien::wxWidgets->version;
|
||||
my \$config = Alien::wxWidgets->config;
|
||||
my \$compiler = Alien::wxWidgets->compiler;
|
||||
my \$linker = Alien::wxWidgets->linker;
|
||||
my \$include_path = ' ' . Alien::wxWidgets->include_path;
|
||||
my \$defines = ' ' . Alien::wxWidgets->defines;
|
||||
my \$cflags = Alien::wxWidgets->c_flags;
|
||||
my \$linkflags = Alien::wxWidgets->link_flags;
|
||||
my \$libraries = ' ' . Alien::wxWidgets->libraries(@components);
|
||||
my \$gui_toolkit = Alien::wxWidgets->config->{toolkit};
|
||||
#my @libraries = Alien::wxWidgets->link_libraries(@components);
|
||||
#my @implib = Alien::wxWidgets->import_libraries(@components);
|
||||
#my @shrlib = Alien::wxWidgets->shared_libraries(@components);
|
||||
#my @keys = Alien::wxWidgets->library_keys; # 'gl', 'adv', ...
|
||||
#my \$library_path = Alien::wxWidgets->shared_library_path;
|
||||
#my \$key = Alien::wxWidgets->key;
|
||||
#my \$prefix = Alien::wxWidgets->prefix;
|
||||
|
||||
my \$filename = '${AlienWx_TEMP_INCLUDE}';
|
||||
open(my $fh, '>', \$filename) or die \"Could not open file '\$filename' \$!\";
|
||||
|
||||
# Convert a space separated lists to CMake semicolon separated lists,
|
||||
# escape the backslashes,
|
||||
# export the resulting list to a temp file.
|
||||
sub cmake_set_var {
|
||||
my (\$varname, \$content) = @_;
|
||||
# Remove line separators.
|
||||
\$content =~ s/\\r|\\n//g;
|
||||
# Escape the path separators.
|
||||
\$content =~ s/\\\\/\\\\\\\\\\\\\\\\/g;
|
||||
my @words = shellwords(\$content);
|
||||
print \$fh \"set(AlienWx_\$varname \\\"\" . join(';', @words) . \"\\\")\\n\";
|
||||
}
|
||||
cmake_set_var('VERSION', \$version);
|
||||
\$include_path =~ s/ -I/ /g;
|
||||
cmake_set_var('INCLUDE_DIRS', \$include_path);
|
||||
\$libraries =~ s/ -L/ -LIBPATH:/g if \$msvc;
|
||||
cmake_set_var('LIBRARIES', \$libraries);
|
||||
#cmake_set_var('LIBRARY_DIRS', );
|
||||
#\$defines =~ s/ -D/ /g;
|
||||
cmake_set_var('DEFINITIONS', \$defines);
|
||||
#cmake_set_var('DEFINITIONS_DEBUG', );
|
||||
cmake_set_var('CXX_FLAGS', \$cflags);
|
||||
cmake_set_var('GUI_TOOLKIT', \$gui_toolkit);
|
||||
close \$fh;
|
||||
")
|
||||
include(${AlienWx_TEMP_INCLUDE})
|
||||
file(REMOVE ${AlienWx_TEMP_INCLUDE})
|
||||
unset(AlienWx_TEMP_INCLUDE)
|
||||
|
||||
if (AlienWx_DEBUG)
|
||||
message(STATUS " AlienWx_VERSION = ${AlienWx_VERSION}")
|
||||
message(STATUS " AlienWx_INCLUDE_DIRS = ${AlienWx_INCLUDE_DIRS}")
|
||||
message(STATUS " AlienWx_LIBRARIES = ${AlienWx_LIBRARIES}")
|
||||
message(STATUS " AlienWx_LIBRARY_DIRS = ${AlienWx_LIBRARY_DIRS}")
|
||||
message(STATUS " AlienWx_DEFINITIONS = ${AlienWx_DEFINITIONS}")
|
||||
message(STATUS " AlienWx_DEFINITIONS_DEBUG = ${AlienWx_DEFINITIONS_DEBUG}")
|
||||
message(STATUS " AlienWx_CXX_FLAGS = ${AlienWx_CXX_FLAGS}")
|
||||
message(STATUS " AlienWx_GUI_TOOLKIT = ${AlienWx_GUI_TOOLKIT}")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(AlienWx
|
||||
REQUIRED_VARS AlienWx_INCLUDE_DIRS AlienWx_LIBRARIES
|
||||
# HANDLE_COMPONENTS
|
||||
VERSION_VAR AlienWx_VERSION)
|
||||
@@ -1,88 +0,0 @@
|
||||
# Find the dependencies for linking with the Perl runtime library.
|
||||
|
||||
# Check for the Perl & PerlLib modules
|
||||
include(LibFindMacros)
|
||||
libfind_package(PerlEmbed Perl)
|
||||
libfind_package(PerlEmbed PerlLibs)
|
||||
|
||||
# Execute an Alien::Wx module to find the relevant information regarding
|
||||
# the wxWidgets used by the Perl interpreter.
|
||||
# Perl specific stuff
|
||||
set(PerlEmbed_TEMP_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/PerlEmbed_TEMP_INCLUDE.txt)
|
||||
execute_process(
|
||||
COMMAND ${PERL_EXECUTABLE} -MExtUtils::Embed -e "
|
||||
# Import Perl modules.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Config;
|
||||
use Text::ParseWords;
|
||||
use ExtUtils::CppGuess;
|
||||
|
||||
# Test for a Visual Studio compiler
|
||||
my \$cpp_guess = ExtUtils::CppGuess->new;
|
||||
my \$mswin = \$^O eq 'MSWin32';
|
||||
my \$msvc = \$cpp_guess->is_msvc;
|
||||
|
||||
# Query the available data from Alien::wxWidgets.
|
||||
my \$ccflags;
|
||||
my \$ldflags;
|
||||
{ local *STDOUT; open STDOUT, '>', \\\$ccflags; ccflags; }
|
||||
{ local *STDOUT; open STDOUT, '>', \\\$ldflags; ldopts; }
|
||||
\$ccflags = ' ' . \$ccflags;
|
||||
\$ldflags = ' ' . \$ldflags;
|
||||
|
||||
my \$filename = '${PerlEmbed_TEMP_INCLUDE}';
|
||||
open(my $fh, '>', \$filename) or die \"Could not open file '\$filename' \$!\";
|
||||
|
||||
# Convert a space separated lists to CMake semicolon separated lists,
|
||||
# escape the backslashes,
|
||||
# export the resulting list to a temp file.
|
||||
sub cmake_set_var {
|
||||
my (\$varname, \$content) = @_;
|
||||
# Remove line separators.
|
||||
\$content =~ s/\\r|\\n//g;
|
||||
# Escape the path separators.
|
||||
\$content =~ s/\\\\/\\\\\\\\\\\\\\\\/g;
|
||||
my @words = shellwords(\$content);
|
||||
print \$fh \"set(PerlEmbed_\$varname \\\"\" . join(';', @words) . \"\\\")\\n\";
|
||||
}
|
||||
cmake_set_var('ARCHNAME', \$Config{archname});
|
||||
cmake_set_var('CCFLAGS', \$ccflags);
|
||||
\$ldflags =~ s/ -L/ -LIBPATH:/g if \$msvc;
|
||||
cmake_set_var('LD', \$Config{ld});
|
||||
cmake_set_var('LDFLAGS', \$ldflags);
|
||||
cmake_set_var('CCCDLFLAGS', \$Config{cccdlflags});
|
||||
cmake_set_var('LDDLFLAGS', \$Config{lddlflags});
|
||||
cmake_set_var('DLEXT', \$Config{dlext});
|
||||
close \$fh;
|
||||
")
|
||||
include(${PerlEmbed_TEMP_INCLUDE})
|
||||
file(REMOVE ${PerlEmbed_TEMP_INCLUDE})
|
||||
unset(PerlEmbed_TEMP_INCLUDE)
|
||||
|
||||
if (PerlEmbed_DEBUG)
|
||||
# First show the configuration extracted by FindPerl & FindPerlLibs:
|
||||
message(STATUS " PERL_INCLUDE_PATH = ${PERL_INCLUDE_PATH}")
|
||||
message(STATUS " PERL_LIBRARY = ${PERL_LIBRARY}")
|
||||
message(STATUS " PERL_EXECUTABLE = ${PERL_EXECUTABLE}")
|
||||
message(STATUS " PERL_SITESEARCH = ${PERL_SITESEARCH}")
|
||||
message(STATUS " PERL_SITELIB = ${PERL_SITELIB}")
|
||||
message(STATUS " PERL_VENDORARCH = ${PERL_VENDORARCH}")
|
||||
message(STATUS " PERL_VENDORLIB = ${PERL_VENDORLIB}")
|
||||
message(STATUS " PERL_ARCHLIB = ${PERL_ARCHLIB}")
|
||||
message(STATUS " PERL_PRIVLIB = ${PERL_PRIVLIB}")
|
||||
message(STATUS " PERL_EXTRA_C_FLAGS = ${PERL_EXTRA_C_FLAGS}")
|
||||
# Second show the configuration extracted by this module (FindPerlEmbed):
|
||||
message(STATUS " PerlEmbed_ARCHNAME = ${PerlEmbed_ARCHNAME}")
|
||||
message(STATUS " PerlEmbed_CCFLAGS = ${PerlEmbed_CCFLAGS}")
|
||||
message(STATUS " PerlEmbed_CCCDLFLAGS = ${PerlEmbed_CCCDLFLAGS}")
|
||||
message(STATUS " LD = ${PerlEmbed_LD}")
|
||||
message(STATUS " PerlEmbed_LDFLAGS = ${PerlEmbed_LDFLAGS}")
|
||||
message(STATUS " PerlEmbed_LDDLFLAGS = ${PerlEmbed_LDDLFLAGS}")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(PerlEmbed
|
||||
REQUIRED_VARS PerlEmbed_CCFLAGS PerlEmbed_LDFLAGS
|
||||
VERSION_VAR PERL_VERSION)
|
||||
@@ -1,265 +0,0 @@
|
||||
# Version 2.2
|
||||
# Public Domain, originally written by Lasse Kärkkäinen <tronic>
|
||||
# Maintained at https://github.com/Tronic/cmake-modules
|
||||
# Please send your improvements as pull requests on Github.
|
||||
|
||||
# Find another package and make it a dependency of the current package.
|
||||
# This also automatically forwards the "REQUIRED" argument.
|
||||
# Usage: libfind_package(<prefix> <another package> [extra args to find_package])
|
||||
macro (libfind_package PREFIX PKG)
|
||||
set(${PREFIX}_args ${PKG} ${ARGN})
|
||||
if (${PREFIX}_FIND_REQUIRED)
|
||||
set(${PREFIX}_args ${${PREFIX}_args} REQUIRED)
|
||||
endif()
|
||||
find_package(${${PREFIX}_args})
|
||||
set(${PREFIX}_DEPENDENCIES ${${PREFIX}_DEPENDENCIES};${PKG})
|
||||
unset(${PREFIX}_args)
|
||||
endmacro()
|
||||
|
||||
# A simple wrapper to make pkg-config searches a bit easier.
|
||||
# Works the same as CMake's internal pkg_check_modules but is always quiet.
|
||||
macro (libfind_pkg_check_modules)
|
||||
find_package(PkgConfig QUIET)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(${ARGN} QUIET)
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Avoid useless copy&pasta by doing what most simple libraries do anyway:
|
||||
# pkg-config, find headers, find library.
|
||||
# Usage: libfind_pkg_detect(<prefix> <pkg-config args> FIND_PATH <name> [other args] FIND_LIBRARY <name> [other args])
|
||||
# E.g. libfind_pkg_detect(SDL2 sdl2 FIND_PATH SDL.h PATH_SUFFIXES SDL2 FIND_LIBRARY SDL2)
|
||||
function (libfind_pkg_detect PREFIX)
|
||||
# Parse arguments
|
||||
set(argname pkgargs)
|
||||
foreach (i ${ARGN})
|
||||
if ("${i}" STREQUAL "FIND_PATH")
|
||||
set(argname pathargs)
|
||||
elseif ("${i}" STREQUAL "FIND_LIBRARY")
|
||||
set(argname libraryargs)
|
||||
else()
|
||||
set(${argname} ${${argname}} ${i})
|
||||
endif()
|
||||
endforeach()
|
||||
if (NOT pkgargs)
|
||||
message(FATAL_ERROR "libfind_pkg_detect requires at least a pkg_config package name to be passed.")
|
||||
endif()
|
||||
# Find library
|
||||
libfind_pkg_check_modules(${PREFIX}_PKGCONF ${pkgargs})
|
||||
if (pathargs)
|
||||
find_path(${PREFIX}_INCLUDE_DIR NAMES ${pathargs} HINTS ${${PREFIX}_PKGCONF_INCLUDE_DIRS})
|
||||
endif()
|
||||
if (libraryargs)
|
||||
find_library(${PREFIX}_LIBRARY NAMES ${libraryargs} HINTS ${${PREFIX}_PKGCONF_LIBRARY_DIRS})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Extracts a version #define from a version.h file, output stored to <PREFIX>_VERSION.
|
||||
# Usage: libfind_version_header(Foobar foobar/version.h FOOBAR_VERSION_STR)
|
||||
# Fourth argument "QUIET" may be used for silently testing different define names.
|
||||
# This function does nothing if the version variable is already defined.
|
||||
function (libfind_version_header PREFIX VERSION_H DEFINE_NAME)
|
||||
# Skip processing if we already have a version or if the include dir was not found
|
||||
if (${PREFIX}_VERSION OR NOT ${PREFIX}_INCLUDE_DIR)
|
||||
return()
|
||||
endif()
|
||||
set(quiet ${${PREFIX}_FIND_QUIETLY})
|
||||
# Process optional arguments
|
||||
foreach(arg ${ARGN})
|
||||
if (arg STREQUAL "QUIET")
|
||||
set(quiet TRUE)
|
||||
else()
|
||||
message(AUTHOR_WARNING "Unknown argument ${arg} to libfind_version_header ignored.")
|
||||
endif()
|
||||
endforeach()
|
||||
# Read the header and parse for version number
|
||||
set(filename "${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
if (NOT EXISTS ${filename})
|
||||
if (NOT quiet)
|
||||
message(AUTHOR_WARNING "Unable to find ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
file(READ "${filename}" header)
|
||||
string(REGEX REPLACE ".*#[ \t]*define[ \t]*${DEFINE_NAME}[ \t]*\"([^\n]*)\".*" "\\1" match "${header}")
|
||||
# No regex match?
|
||||
if (match STREQUAL header)
|
||||
if (NOT quiet)
|
||||
message(AUTHOR_WARNING "Unable to find \#define ${DEFINE_NAME} \"<version>\" from ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
# Export the version string
|
||||
set(${PREFIX}_VERSION "${match}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Do the final processing once the paths have been detected.
|
||||
# If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain
|
||||
# all the variables, each of which contain one include directory.
|
||||
# Ditto for ${PREFIX}_PROCESS_LIBS and library files.
|
||||
# Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES.
|
||||
# Also handles errors in case library detection was required, etc.
|
||||
function (libfind_process PREFIX)
|
||||
# Skip processing if already processed during this configuration run
|
||||
if (${PREFIX}_FOUND)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(found TRUE) # Start with the assumption that the package was found
|
||||
|
||||
# Did we find any files? Did we miss includes? These are for formatting better error messages.
|
||||
set(some_files FALSE)
|
||||
set(missing_headers FALSE)
|
||||
|
||||
# Shorthands for some variables that we need often
|
||||
set(quiet ${${PREFIX}_FIND_QUIETLY})
|
||||
set(required ${${PREFIX}_FIND_REQUIRED})
|
||||
set(exactver ${${PREFIX}_FIND_VERSION_EXACT})
|
||||
set(findver "${${PREFIX}_FIND_VERSION}")
|
||||
set(version "${${PREFIX}_VERSION}")
|
||||
|
||||
# Lists of config option names (all, includes, libs)
|
||||
unset(configopts)
|
||||
set(includeopts ${${PREFIX}_PROCESS_INCLUDES})
|
||||
set(libraryopts ${${PREFIX}_PROCESS_LIBS})
|
||||
|
||||
# Process deps to add to
|
||||
foreach (i ${PREFIX} ${${PREFIX}_DEPENDENCIES})
|
||||
if (DEFINED ${i}_INCLUDE_OPTS OR DEFINED ${i}_LIBRARY_OPTS)
|
||||
# The package seems to export option lists that we can use, woohoo!
|
||||
list(APPEND includeopts ${${i}_INCLUDE_OPTS})
|
||||
list(APPEND libraryopts ${${i}_LIBRARY_OPTS})
|
||||
else()
|
||||
# If plural forms don't exist or they equal singular forms
|
||||
if ((NOT DEFINED ${i}_INCLUDE_DIRS AND NOT DEFINED ${i}_LIBRARIES) OR
|
||||
({i}_INCLUDE_DIR STREQUAL ${i}_INCLUDE_DIRS AND ${i}_LIBRARY STREQUAL ${i}_LIBRARIES))
|
||||
# Singular forms can be used
|
||||
if (DEFINED ${i}_INCLUDE_DIR)
|
||||
list(APPEND includeopts ${i}_INCLUDE_DIR)
|
||||
endif()
|
||||
if (DEFINED ${i}_LIBRARY)
|
||||
list(APPEND libraryopts ${i}_LIBRARY)
|
||||
endif()
|
||||
else()
|
||||
# Oh no, we don't know the option names
|
||||
message(FATAL_ERROR "We couldn't determine config variable names for ${i} includes and libs. Aieeh!")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (includeopts)
|
||||
list(REMOVE_DUPLICATES includeopts)
|
||||
endif()
|
||||
|
||||
if (libraryopts)
|
||||
list(REMOVE_DUPLICATES libraryopts)
|
||||
endif()
|
||||
|
||||
string(REGEX REPLACE ".*[ ;]([^ ;]*(_INCLUDE_DIRS|_LIBRARIES))" "\\1" tmp "${includeopts} ${libraryopts}")
|
||||
if (NOT tmp STREQUAL "${includeopts} ${libraryopts}")
|
||||
message(AUTHOR_WARNING "Plural form ${tmp} found in config options of ${PREFIX}. This works as before but is now deprecated. Please only use singular forms INCLUDE_DIR and LIBRARY, and update your find scripts for LibFindMacros > 2.0 automatic dependency system (most often you can simply remove the PROCESS variables entirely).")
|
||||
endif()
|
||||
|
||||
# Include/library names separated by spaces (notice: not CMake lists)
|
||||
unset(includes)
|
||||
unset(libs)
|
||||
|
||||
# Process all includes and set found false if any are missing
|
||||
foreach (i ${includeopts})
|
||||
list(APPEND configopts ${i})
|
||||
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
|
||||
list(APPEND includes "${${i}}")
|
||||
else()
|
||||
set(found FALSE)
|
||||
set(missing_headers TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Process all libraries and set found false if any are missing
|
||||
foreach (i ${libraryopts})
|
||||
list(APPEND configopts ${i})
|
||||
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
|
||||
list(APPEND libs "${${i}}")
|
||||
else()
|
||||
set (found FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Version checks
|
||||
if (found AND findver)
|
||||
if (NOT version)
|
||||
message(WARNING "The find module for ${PREFIX} does not provide version information, so we'll just assume that it is OK. Please fix the module or remove package version requirements to get rid of this warning.")
|
||||
elseif (version VERSION_LESS findver OR (exactver AND NOT version VERSION_EQUAL findver))
|
||||
set(found FALSE)
|
||||
set(version_unsuitable TRUE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# If all-OK, hide all config options, export variables, print status and exit
|
||||
if (found)
|
||||
foreach (i ${configopts})
|
||||
mark_as_advanced(${i})
|
||||
endforeach()
|
||||
if (NOT quiet)
|
||||
message(STATUS "Found ${PREFIX} ${${PREFIX}_VERSION}")
|
||||
if (LIBFIND_DEBUG)
|
||||
message(STATUS " ${PREFIX}_DEPENDENCIES=${${PREFIX}_DEPENDENCIES}")
|
||||
message(STATUS " ${PREFIX}_INCLUDE_OPTS=${includeopts}")
|
||||
message(STATUS " ${PREFIX}_INCLUDE_DIRS=${includes}")
|
||||
message(STATUS " ${PREFIX}_LIBRARY_OPTS=${libraryopts}")
|
||||
message(STATUS " ${PREFIX}_LIBRARIES=${libs}")
|
||||
endif()
|
||||
set (${PREFIX}_INCLUDE_OPTS ${includeopts} PARENT_SCOPE)
|
||||
set (${PREFIX}_LIBRARY_OPTS ${libraryopts} PARENT_SCOPE)
|
||||
set (${PREFIX}_INCLUDE_DIRS ${includes} PARENT_SCOPE)
|
||||
set (${PREFIX}_LIBRARIES ${libs} PARENT_SCOPE)
|
||||
set (${PREFIX}_FOUND TRUE PARENT_SCOPE)
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Format messages for debug info and the type of error
|
||||
set(vars "Relevant CMake configuration variables:\n")
|
||||
foreach (i ${configopts})
|
||||
mark_as_advanced(CLEAR ${i})
|
||||
set(val ${${i}})
|
||||
if ("${val}" STREQUAL "${i}-NOTFOUND")
|
||||
set (val "<not found>")
|
||||
elseif (val AND NOT EXISTS ${val})
|
||||
set (val "${val} (does not exist)")
|
||||
else()
|
||||
set(some_files TRUE)
|
||||
endif()
|
||||
set(vars "${vars} ${i}=${val}\n")
|
||||
endforeach()
|
||||
set(vars "${vars}You may use CMake GUI, cmake -D or ccmake to modify the values. Delete CMakeCache.txt to discard all values and force full re-detection if necessary.\n")
|
||||
if (version_unsuitable)
|
||||
set(msg "${PREFIX} ${${PREFIX}_VERSION} was found but")
|
||||
if (exactver)
|
||||
set(msg "${msg} only version ${findver} is acceptable.")
|
||||
else()
|
||||
set(msg "${msg} version ${findver} is the minimum requirement.")
|
||||
endif()
|
||||
else()
|
||||
if (missing_headers)
|
||||
set(msg "We could not find development headers for ${PREFIX}. Do you have the necessary dev package installed?")
|
||||
elseif (some_files)
|
||||
set(msg "We only found some files of ${PREFIX}, not all of them. Perhaps your installation is incomplete or maybe we just didn't look in the right place?")
|
||||
if(findver)
|
||||
set(msg "${msg} This could also be caused by incompatible version (if it helps, at least ${PREFIX} ${findver} should work).")
|
||||
endif()
|
||||
else()
|
||||
set(msg "We were unable to find package ${PREFIX}.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Fatal error out if REQUIRED
|
||||
if (required)
|
||||
set(msg "REQUIRED PACKAGE NOT FOUND\n${msg} This package is REQUIRED and you need to install it or adjust CMake configuration in order to continue building ${CMAKE_PROJECT_NAME}.")
|
||||
message(FATAL_ERROR "${msg}\n${vars}")
|
||||
endif()
|
||||
# Otherwise just print a nasty warning
|
||||
if (NOT quiet)
|
||||
message(WARNING "WARNING: MISSING PACKAGE\n${msg} This package is NOT REQUIRED and you may ignore this warning but by doing so you may miss some functionality of ${CMAKE_PROJECT_NAME}. \n${vars}")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -1,89 +0,0 @@
|
||||
# Source: https://gist.github.com/sivachandran/3a0de157dccef822a230#file-bin2h-cmake
|
||||
# Added modifications to suit prusaslicer
|
||||
include(CMakeParseArguments)
|
||||
|
||||
# Function to wrap a given string into multiple lines at the given column position.
|
||||
# Parameters:
|
||||
# VARIABLE - The name of the CMake variable holding the string.
|
||||
# AT_COLUMN - The column position at which string will be wrapped.
|
||||
function(WRAP_STRING)
|
||||
set(oneValueArgs VARIABLE AT_COLUMN)
|
||||
cmake_parse_arguments(WRAP_STRING "${options}" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
string(LENGTH ${${WRAP_STRING_VARIABLE}} stringLength)
|
||||
math(EXPR offset "0")
|
||||
|
||||
while(stringLength GREATER 0)
|
||||
|
||||
if(stringLength GREATER ${WRAP_STRING_AT_COLUMN})
|
||||
math(EXPR length "${WRAP_STRING_AT_COLUMN}")
|
||||
else()
|
||||
math(EXPR length "${stringLength}")
|
||||
endif()
|
||||
|
||||
string(SUBSTRING ${${WRAP_STRING_VARIABLE}} ${offset} ${length} line)
|
||||
set(lines "${lines}\n${line}")
|
||||
|
||||
math(EXPR stringLength "${stringLength} - ${length}")
|
||||
math(EXPR offset "${offset} + ${length}")
|
||||
endwhile()
|
||||
|
||||
set(${WRAP_STRING_VARIABLE} "${lines}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Function to embed contents of a file as byte array in C/C++ header file(.h). The header file
|
||||
# will contain a byte array and integer variable holding the size of the array.
|
||||
# Parameters
|
||||
# SOURCE_FILE - The path of source file whose contents will be embedded in the header file.
|
||||
# VARIABLE_NAME - The name of the variable for the byte array. The string "_SIZE" will be append
|
||||
# to this name and will be used a variable name for size variable.
|
||||
# HEADER_FILE - The path of header file.
|
||||
# APPEND - If specified appends to the header file instead of overwriting it
|
||||
# NULL_TERMINATE - If specified a null byte(zero) will be append to the byte array. This will be
|
||||
# useful if the source file is a text file and we want to use the file contents
|
||||
# as string. But the size variable holds size of the byte array without this
|
||||
# null byte.
|
||||
# Usage:
|
||||
# bin2h(SOURCE_FILE "Logo.png" HEADER_FILE "Logo.h" VARIABLE_NAME "LOGO_PNG")
|
||||
function(BIN2H)
|
||||
set(options APPEND NULL_TERMINATE ADD_WARNING_TEXT)
|
||||
set(oneValueArgs SOURCE_FILE VARIABLE_NAME HEADER_FILE)
|
||||
cmake_parse_arguments(BIN2H "${options}" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
# reads source file contents as hex string
|
||||
file(READ ${BIN2H_SOURCE_FILE} hexString HEX)
|
||||
string(LENGTH ${hexString} hexStringLength)
|
||||
|
||||
# appends null byte if asked
|
||||
if(BIN2H_NULL_TERMINATE)
|
||||
set(hexString "${hexString}00")
|
||||
endif()
|
||||
|
||||
# wraps the hex string into multiple lines at column 32(i.e. 16 bytes per line)
|
||||
wrap_string(VARIABLE hexString AT_COLUMN 32)
|
||||
math(EXPR arraySize "${hexStringLength} / 2")
|
||||
|
||||
# adds '0x' prefix and comma suffix before and after every byte respectively
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1, " arrayValues ${hexString})
|
||||
# removes trailing comma
|
||||
string(REGEX REPLACE ", $" "" arrayValues ${arrayValues})
|
||||
|
||||
# converts the variable name into proper C identifier
|
||||
string(MAKE_C_IDENTIFIER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME)
|
||||
# string(TOUPPER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME)
|
||||
|
||||
# declares byte array and the length variables
|
||||
set(arrayDefinition "const unsigned char ${BIN2H_VARIABLE_NAME}[] = { ${arrayValues} };")
|
||||
set(arraySizeDefinition "const size_t ${BIN2H_VARIABLE_NAME}_SIZE = ${arraySize};")
|
||||
set(warnTxt "")
|
||||
if (BIN2H_ADD_WARNING_TEXT)
|
||||
set(warnTxt "/* WARN: This file is auto-generated from ${BIN2H_SOURCE_FILE} */\n")
|
||||
endif ()
|
||||
|
||||
set(declarations "${warnTxt}${arrayDefinition}\n\n${arraySizeDefinition}\n\n")
|
||||
if(BIN2H_APPEND)
|
||||
file(APPEND ${BIN2H_HEADER_FILE} "${declarations}")
|
||||
else()
|
||||
file(WRITE ${BIN2H_HEADER_FILE} "${declarations}")
|
||||
endif()
|
||||
endfunction()
|
||||
Vendored
-71
@@ -1,71 +0,0 @@
|
||||
From b32e63f75046d186d93ee7d627c978f2f7c892f3 Mon Sep 17 00:00:00 2001
|
||||
From: Steve Dower <steve.dower@python.org>
|
||||
Date: Fri, 10 Jul 2026 14:04:00 +0100
|
||||
Subject: [PATCH] [3.13] gh-153438: Update NuGet download URL (GH-153482)
|
||||
(GH-153515)
|
||||
|
||||
gh-153438: Update NuGet download URL (GH-153482)
|
||||
(cherry picked from commit 106eb532ea3b243423e62a702719e9d3c0e40c16)
|
||||
(cherry picked from commit 62eb50d65d538dda5164f9c4a097a896454a0d1c)
|
||||
|
||||
Co-authored-by: Steve Dower <steve.dower@python.org>
|
||||
Co-authored-by: Harjoth Khara <harjoth.khara@gmail.com>
|
||||
---
|
||||
Doc/using/windows.rst | 6 +++---
|
||||
.../Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst | 2 ++
|
||||
PCbuild/find_python.bat | 2 +-
|
||||
Tools/msi/get_externals.bat | 2 +-
|
||||
4 files changed, 7 insertions(+), 5 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
|
||||
diff --git a/Doc/using/windows.rst b/Doc/using/windows.rst
|
||||
index 2a507675044666..4800789ca2d6cc 100644
|
||||
--- a/Doc/using/windows.rst
|
||||
+++ b/Doc/using/windows.rst
|
||||
@@ -414,9 +414,9 @@ on using nuget. What follows is a summary that is sufficient for Python
|
||||
developers.
|
||||
|
||||
The ``nuget.exe`` command line tool may be downloaded directly from
|
||||
-``https://aka.ms/nugetclidl``, for example, using curl or PowerShell. With the
|
||||
-tool, the latest version of Python for 64-bit or 32-bit machines is installed
|
||||
-using::
|
||||
+``https://dist.nuget.org/win-x86-commandline/latest/nuget.exe``, for example,
|
||||
+using curl or PowerShell. With the tool, the latest version of Python for
|
||||
+64-bit or 32-bit machines is installed using::
|
||||
|
||||
nuget.exe install python -ExcludeVersion -OutputDirectory .
|
||||
nuget.exe install pythonx86 -ExcludeVersion -OutputDirectory .
|
||||
diff --git a/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst b/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
new file mode 100644
|
||||
index 00000000000000..edab8e6ba7b259
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+Update Windows build and installer tooling and documentation to use the
|
||||
+current download URL for ``nuget.exe``.
|
||||
diff --git a/PCbuild/find_python.bat b/PCbuild/find_python.bat
|
||||
index 0af367a3efafad..9612a2860ef9c3 100644
|
||||
--- a/PCbuild/find_python.bat
|
||||
+++ b/PCbuild/find_python.bat
|
||||
@@ -52,7 +52,7 @@
|
||||
@set _Py_HOST_PYTHON=%HOST_PYTHON%
|
||||
@if "%_Py_HOST_PYTHON%"=="" set _Py_HOST_PYTHON=py
|
||||
@if "%_Py_NUGET%"=="" (set _Py_NUGET=%_Py_EXTERNALS_DIR%\nuget.exe)
|
||||
-@if "%_Py_NUGET_URL%"=="" (set _Py_NUGET_URL=https://aka.ms/nugetclidl)
|
||||
+@if "%_Py_NUGET_URL%"=="" (set _Py_NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)
|
||||
@if NOT exist "%_Py_NUGET%" (
|
||||
@if not "%_Py_Quiet%"=="1" @echo Downloading nuget...
|
||||
@rem NB: Must use single quotes around NUGET here, NOT double!
|
||||
diff --git a/Tools/msi/get_externals.bat b/Tools/msi/get_externals.bat
|
||||
index f6602ce9588ff4..c7c7e9f470bc25 100644
|
||||
--- a/Tools/msi/get_externals.bat
|
||||
+++ b/Tools/msi/get_externals.bat
|
||||
@@ -6,7 +6,7 @@ set HERE=%~dp0
|
||||
if "%PCBUILD%"=="" (set PCBUILD=%HERE%..\..\PCbuild\)
|
||||
if "%EXTERNALS_DIR%"=="" (set EXTERNALS_DIR=%HERE%..\..\externals\windows-installer)
|
||||
if "%NUGET%"=="" (set NUGET=%EXTERNALS_DIR%\..\nuget.exe)
|
||||
-if "%NUGET_URL%"=="" (set NUGET_URL=https://aka.ms/nugetclidl)
|
||||
+if "%NUGET_URL%"=="" (set NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)
|
||||
|
||||
set DO_FETCH=true
|
||||
set DO_CLEAN=false
|
||||
Vendored
+80
-15
@@ -8,20 +8,45 @@ set(_python_url "https://www.python.org/ftp/python/${_python_version}/Python-${_
|
||||
set(_python_sha256 "c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684")
|
||||
|
||||
|
||||
set(_patch_cmd "")
|
||||
if(WIN32)
|
||||
|
||||
# Fix python build failure on Windows if python is not available, due to wrong nuget download URL
|
||||
# See https://github.com/python/cpython/issues/153438
|
||||
# Patch from https://github.com/python/cpython/pull/153608
|
||||
# This patch has not been merged to 3.12 yet so we need to apply it manually
|
||||
#
|
||||
# Without core.autocrlf=false the patched find_python.bat comes out LF and
|
||||
# cmd.exe cannot find its goto labels.
|
||||
set(_patch_cmd git init
|
||||
&& ${GIT_EXECUTABLE} -c core.autocrlf=false apply --verbose
|
||||
--ignore-space-change --whitespace=fix
|
||||
${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch)
|
||||
# Build-time Python for PCbuild's scripts, pinned because 3.15 cannot run the 3.12 scripts.
|
||||
set(DEP_PYTHON_HOST "" CACHE FILEPATH
|
||||
"Python 3.10-3.13 that runs CPython's build scripts; empty downloads a pinned one from nuget.org")
|
||||
if(DEP_PYTHON_HOST)
|
||||
execute_process(
|
||||
COMMAND "${DEP_PYTHON_HOST}" -E -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
||||
OUTPUT_VARIABLE _python_host_minor
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE _python_host_result
|
||||
)
|
||||
if(NOT _python_host_result EQUAL 0 OR _python_host_minor VERSION_LESS 3.10 OR _python_host_minor VERSION_GREATER 3.13)
|
||||
message(FATAL_ERROR "DEP_PYTHON_HOST must be Python 3.10 to 3.13, got '${_python_host_minor}' from ${DEP_PYTHON_HOST}")
|
||||
endif()
|
||||
file(TO_NATIVE_PATH "${DEP_PYTHON_HOST}" _python_host)
|
||||
else()
|
||||
set(_python_host_version "3.13.16")
|
||||
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64")
|
||||
set(_python_host_package pythonarm64)
|
||||
set(_python_host_sha256 bfd77aa5b0edff4a7936a3220e8b79e7c78f864ce13e8fa2c835d9946e11694a)
|
||||
else()
|
||||
set(_python_host_package pythonx86)
|
||||
set(_python_host_sha256 c65cf47664b33286dfc5f2a17303668dd59e057869815ed9b270fb3866c1dd15)
|
||||
endif()
|
||||
set(_python_host_dir "${CMAKE_CURRENT_BINARY_DIR}/dep_python3_host-prefix/src/dep_python3_host")
|
||||
ExternalProject_Add(dep_python3_host
|
||||
URL "https://api.nuget.org/v3-flatcontainer/${_python_host_package}/${_python_host_version}/${_python_host_package}.${_python_host_version}.nupkg"
|
||||
URL_HASH SHA256=${_python_host_sha256}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
# Renames the .nupkg to .zip so ExternalProject extracts it.
|
||||
DOWNLOAD_NAME ${_python_host_package}.${_python_host_version}.zip
|
||||
SOURCE_DIR "${_python_host_dir}"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
)
|
||||
file(TO_NATIVE_PATH "${_python_host_dir}/tools/python.exe" _python_host)
|
||||
set(_python_depends dep_python3_host)
|
||||
endif()
|
||||
|
||||
if(MSVC_VERSION EQUAL 1800)
|
||||
set(_python_platform_toolset v120)
|
||||
@@ -63,6 +88,47 @@ if(WIN32)
|
||||
# release python3xx.lib; Py_DEBUG could not load release plugin modules.
|
||||
set(_python_pcbuild_config Release)
|
||||
|
||||
# CPython's own libraries, fetched here instead of by get_externals.bat, which ignores failed
|
||||
# downloads. Must match PCbuild/get_externals.bat for _python_version.
|
||||
set(_python_externals
|
||||
# repo folder commit sha256 of the archive
|
||||
source-deps bzip2-1.0.8 05301997b2f9590f49c672cf3dfd3d3dfa7ad521 44b15bede52b32f8673a2976621a847258b041243c003e30f4d9a05214785fe5
|
||||
source-deps sqlite-3.49.1.0 cb7a3997059739fe9c63b2819aeda7c6907510ef 586fe469463f5b27b749f9eef1938baa8e8a12030341e96160a371356c2d6759
|
||||
source-deps xz-5.2.5 c6bc0c612605622aaef101a33a751f9de2ecc193 d99882a8f9fd48be0e605e27e3c077e5eaab370f6f52ebcc8ff754480ffdf0bc
|
||||
source-deps zlib-1.3.1 4dc98e1909830e2bdc2a9cc2236e3c5d5037335b 39d867c083ebc37c77cf03a442f87c0f83db006eb64343bd4a19f1c84c26f50c
|
||||
bin-deps libffi-3.4.4 94cb9a1c7feb608adf2b9f8fe2dbd6925ffbf90d 6b0f53490e0dd958708bff32df689cf2163c214147fcb8edb76a403fa6a877ca
|
||||
bin-deps openssl-bin-3.0.16.2 1174fd43d858425c345b16d631716fb98ba492ea b741fd1411a36cb899fee2c39b70a4492f4bc1c6e2301d67df32e9754118a63b
|
||||
)
|
||||
set(_python_externals_dir "${CMAKE_CURRENT_BINARY_DIR}/dep_python3_externals")
|
||||
list(LENGTH _python_externals _python_externals_len)
|
||||
math(EXPR _python_externals_last "${_python_externals_len} - 1")
|
||||
foreach(_i RANGE 0 ${_python_externals_last} 4)
|
||||
math(EXPR _j "${_i} + 1")
|
||||
math(EXPR _k "${_i} + 2")
|
||||
math(EXPR _l "${_i} + 3")
|
||||
list(GET _python_externals ${_i} _ext_repo)
|
||||
list(GET _python_externals ${_j} _ext_name)
|
||||
list(GET _python_externals ${_k} _ext_commit)
|
||||
list(GET _python_externals ${_l} _ext_sha256)
|
||||
ExternalProject_Add(dep_python3_ext_${_ext_name}
|
||||
URL "https://github.com/python/cpython-${_ext_repo}/archive/${_ext_commit}.zip"
|
||||
URL_HASH SHA256=${_ext_sha256}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
DOWNLOAD_NAME cpython-${_ext_repo}-${_ext_name}.zip
|
||||
SOURCE_DIR "${_python_externals_dir}/${_ext_name}"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
)
|
||||
list(APPEND _python_depends dep_python3_ext_${_ext_name})
|
||||
endforeach()
|
||||
file(TO_NATIVE_PATH "${_python_externals_dir}" _python_externals_native)
|
||||
|
||||
# PCbuild runs PYTHON without -E, so PYTHONHOME and PYTHONPATH are cleared.
|
||||
set(_python_env_args --unset=PYTHONHOME --unset=PYTHONPATH "PYTHON=${_python_host}"
|
||||
"EXTERNALS_DIR=${_python_externals_native}"
|
||||
"GIT_CEILING_DIRECTORIES=<SOURCE_DIR>/..")
|
||||
|
||||
# CPython's PCbuild needs a 64-bit-hosted toolchain: find_msbuild.bat picks the
|
||||
# 32-bit Bin\MSBuild.exe, whose x86 cl.exe/link.exe run out of address space
|
||||
# (fatal C1002 "out of heap space") building the LTCG-optimized pythoncore.
|
||||
@@ -71,7 +137,6 @@ if(WIN32)
|
||||
# then hosts arm64 tools on its own; the amd64 MSBuild still defaults to x86, so
|
||||
# PreferredToolArchitecture pins it to x64. Scoped to this build step, so
|
||||
# CPython's own sources stay untouched.
|
||||
set(_python_env_args "GIT_CEILING_DIRECTORIES=<SOURCE_DIR>/..")
|
||||
if(CMAKE_GENERATOR_INSTANCE) # empty for non-VS generators (e.g. Ninja)
|
||||
set(_python_msbuild "${CMAKE_GENERATOR_INSTANCE}/MSBuild/Current/Bin/${_python_msbuild_host}/MSBuild.exe")
|
||||
if(EXISTS "${_python_msbuild}")
|
||||
@@ -284,12 +349,12 @@ endif()
|
||||
ExternalProject_Add(dep_python3
|
||||
URL "${_python_url}"
|
||||
URL_HASH SHA256=${_python_sha256}
|
||||
PATCH_COMMAND ${_patch_cmd}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
BUILD_IN_SOURCE ON
|
||||
CONFIGURE_COMMAND ${_conf_cmd}
|
||||
BUILD_COMMAND ${_build_cmd}
|
||||
INSTALL_COMMAND ${_install_cmd}
|
||||
DEPENDS ${_python_depends}
|
||||
)
|
||||
|
||||
# Python depends on OpenSSL and ZLIB
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Color picker
|
||||
|
||||
The color picker is a local HTML dialog hosted by `ColorPickerDialog`, a
|
||||
`WebViewHostDialog` subclass. Its header and implementation own the ordered
|
||||
selection contract and shared filament conversion. Bridge validation, favorites
|
||||
persistence, placement and gettext strings remain private to the implementation.
|
||||
Its colors use the existing `ColorRGBA` utility.
|
||||
It uses a native title bar and fixed window border, allowing dragging but no
|
||||
manual resizing or maximization. Native title-bar colors use the application's
|
||||
shared dialog dark-theme update on creation and theme changes.
|
||||
Like SpeedDial, the host disables browser zoom accelerators, restores native
|
||||
WebView focus on activation and readiness, and synchronizes layout and the macOS
|
||||
viewport before repainting the backend. Activation never dismisses this modal
|
||||
dialog; focus restoration preserves an active editor and its partial input.
|
||||
The initialized page reports
|
||||
its intrinsic content height; native code validates the current page identifier
|
||||
and bounded numeric height, converts CSS pixels to device-independent window
|
||||
size, and fits within the display's work area. Identical sizes are ignored. The
|
||||
page observes content rather than viewport height, so resizing does not create
|
||||
a feedback loop; oversized content remains scrollable.
|
||||
The panel snapshots the triggering color or More Colors button's screen rectangle.
|
||||
It aligns below the button, flips above when needed, and clamps to that button's
|
||||
display work area on initial display. Later content and DPI changes update only
|
||||
the client size, preserving the position chosen by dragging the title bar.
|
||||
wxGTK clears the dialog's default
|
||||
parent-centering constraint and reapplies its native position after showing,
|
||||
bypassing wxGTK's requested-position cache. Absolute top-level placement remains
|
||||
subject to the window manager; Wayland compositors control it. The official filament palette's
|
||||
existing placement is independent of this custom panel.
|
||||
A solid selection contains one color; a gradient contains exactly two ordered
|
||||
endpoints. The bridge represents these as `{"type":"solid|gradient",
|
||||
"colors":["#RRGGBBAA", ...]}`. RGB input is also accepted and becomes opaque.
|
||||
Native validation rejects malformed hex and incorrect cardinality.
|
||||
|
||||
The project filament color action and the official filament picker's More Colors
|
||||
action enable two-endpoint gradients and independent endpoint alpha. The preset
|
||||
combo box reads project colors in their stored vector order locally and applies
|
||||
changes through `sync_colour_config`,
|
||||
retaining the existing RGB/RGBA profile/project formats and dirty marking.
|
||||
Sidebar gradient and segmented multicolor swatches draw colors in the supplied
|
||||
order without sorting them.
|
||||
Opaque colors use `#RRGGBB`; nonopaque colors use `#RRGGBBAA`, including fully
|
||||
transparent alpha `00`. Initial selections, alpha-only edits and cancellation
|
||||
retain stored alpha. Ordered
|
||||
custom results bypass the official palette's `FilamentColor` set. Choosing an
|
||||
official swatch retains its existing palette behavior.
|
||||
The Linux sidebar's Change extruder color menu uses this same custom editor and
|
||||
the filament swatch anchor, including alpha, ordered gradients and configuration syncing.
|
||||
|
||||
Striped colors and gradients with more than two endpoints cannot be represented
|
||||
by this editor. A notice explains that the current selection is retained; the
|
||||
editor starts from the first color, and unchanged confirmation is a no-op.
|
||||
Cancellation is also a no-op. If the WebView backend cannot initialize, a local
|
||||
unavailable panel offers Cancel and Escape, without opening a system color picker.
|
||||
|
||||
Generic color option fields use an ordinary color button on every platform and
|
||||
open the same panel with gradients and alpha disabled. The panel is anchored to
|
||||
the field button. Empty or invalid field values remain undefined (the empty
|
||||
string), distinct from opaque black. Right-click resets to undefined. Cancel does
|
||||
not notify the field; confirmation and reset notify only when its RGB string
|
||||
changes. Programmatic assignments remain silent, and reading a field never
|
||||
writes legacy favorites.
|
||||
|
||||
AMS material settings retain their fixed/AMS swatches and first-level popup. Only
|
||||
its custom-color action opens the solid, opaque editor, anchored to that custom
|
||||
control. The transient popup releases its grab while the modal is open and returns
|
||||
to the same selection on cancellation. Confirmation uses the existing default-color
|
||||
and packed RGBA event path. Device permission/status checks remain at the first-level
|
||||
entry.
|
||||
|
||||
Texture import's color-mapping Add Material action opens a solid, opaque editor
|
||||
anchored to the mapping row. The existing filament limit is checked before opening.
|
||||
The first-level material popup closes as an action; its callback is copied before
|
||||
dismissal, so popup destruction does not invalidate the modal's completion path.
|
||||
Confirmation invokes the existing virtual-filament callback once; cancellation
|
||||
adds no material.
|
||||
|
||||
Native gettext strings are installed at document start. Static page labels and
|
||||
palette names use stable English keys; RAL codes, channel symbols, and numeric
|
||||
labels remain data. The palette color values do not depend on the UI language.
|
||||
The application locale also sets the document language. Numeric editor validation
|
||||
uses the application's gettext messages, rather than browser language defaults;
|
||||
missing translations fall back to the stable English keys.
|
||||
|
||||
The pure color/state regressions run with
|
||||
`node --test resources/web/dialog/ColorPickerDialog/colorpicker.test.js`.
|
||||
Optional real-browser regressions live beside them in
|
||||
`colorpicker.browser.test.js`. They use an existing Playwright installation
|
||||
(`ORCA_PLAYWRIGHT_MODULE` can name its module path) and an installed browser
|
||||
(`ORCA_BROWSER_EXECUTABLE` can name its executable), without adding a production
|
||||
dependency. They cover bridge initialization, capability combinations, invalid
|
||||
editor recovery, ordered endpoint alpha, favorites, gettext keys, and live theme
|
||||
changes. These browser checks complement native contract/persistence tests; they
|
||||
do not establish the full native modal confirmation and favorites journey.
|
||||
|
||||
Gradient and alpha capabilities are independent and default off. Unsupported
|
||||
gradients are rejected; disabled alpha normalizes the selected result to opaque.
|
||||
Favorites retain both capabilities independently of the current caller. The page
|
||||
stores alpha as a byte and only converts to percentage when displaying or editing
|
||||
opacity, so switching endpoints does not change alpha precision.
|
||||
|
||||
The page starts disabled. Each document supplies a page identifier in its ready
|
||||
message, receives native initialization, and acknowledges it before native code
|
||||
accepts confirmation or favorite updates. Repeated initialization does not reset
|
||||
edits, and messages from an obsolete document are ignored. Native WebView message
|
||||
handling is deferred and guarded by a lifetime token. Only a validated confirmation
|
||||
sets the optional result. Cancel, Escape and native window close leave it empty.
|
||||
|
||||
Favorites belong to application configuration, separately from the selected result,
|
||||
in the editor configuration returned by `AppConfig::config_path()` under `data_dir()`.
|
||||
The `color_picker` section has string `version` equal to `1` and a `favorites`
|
||||
string containing a JSON array of typed selections. Native helpers normalize and
|
||||
deduplicate this array in order and limit it to 24 entries. Updates replace the
|
||||
entire section and immediately call `AppConfig::save()`, so accepted favorite
|
||||
changes survive cancellation. Empty arrays are saved explicitly. Core `AppConfig`
|
||||
only stores the raw section; color validation belongs to the GUI boundary.
|
||||
|
||||
When the section is absent, valid solid colors from `custom_color_list` are imported
|
||||
once, without changing that legacy section. The legacy native picker saves decimal
|
||||
`r,g,b,a` byte strings; hexadecimal RGB and RGBA strings are also supported. Migration
|
||||
strictly validates all four decimal channels and preserves alpha. Gradient strings
|
||||
and malformed values are ignored. The new version and an empty array are
|
||||
saved even when there is nothing to import. A present section with an unknown or
|
||||
missing version is left untouched and disables favorite writes. Malformed version
|
||||
1 data is displayed as empty and remains on disk until the user explicitly saves
|
||||
a valid replacement. Other configuration sections and project/profile color
|
||||
formats are independent of this storage.
|
||||
|
||||
Interface colors consume the host's `data-orca-theme` and `--orca-*` contract through
|
||||
shared styles. The picker opts into scoped global control roles. Application theme
|
||||
changes update UI surfaces without reloading the page or changing color data.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Focus mode (hiding inactive plates) — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
"Focus mode" is an entry of the canvas menu in the bottom left corner of Prepare. Turned on (eye
|
||||
lit), the 3D view hides the inactive plates and shows the current plate alone, so a project with
|
||||
many plates can be worked on one plate at a time. Turned off, every plate is shown. It is a view
|
||||
setting: it changes nothing in the model, the slice or the project file, and it does not reach
|
||||
plate thumbnails. The choice is kept in the app config as `focus_mode`, off by default.
|
||||
|
||||
Preview needs no such option, as it already draws the current plate alone and has its own plate
|
||||
toolbar. The assembly view and the Design tab do not lay objects out on plates, so the entry is
|
||||
disabled there.
|
||||
|
||||
## What is shown
|
||||
|
||||
The current plate is drawn with its bed, label and icons; the other plates are not drawn at all. The
|
||||
world axes, which Prepare draws at the corner of the first plate, move to the corner of the current
|
||||
plate, where Preview draws them.
|
||||
|
||||
An object instance is shown when its footprint, the XY bounding box of the convex hulls of its
|
||||
volumes, overlaps the current plate, or when it is selected. The test is made per instance, so the
|
||||
parts of an object are shown or hidden together, and a wipe tower follows its plate the same way.
|
||||
It is geometric rather than the plate membership `PartPlateList` keeps, because an instance stays on
|
||||
the plate it was first placed on: an object of another plate that reaches over this one, or one too
|
||||
large for any plate, would otherwise be invisible while it takes room on the current plate. A
|
||||
selected instance stays visible so that what a selection or a gizmo acts on is never hidden, for
|
||||
instance an object picked in the object list on another plate.
|
||||
|
||||
## Hiding
|
||||
|
||||
A hidden object is neither drawn nor picked. `GLCanvas3D::_update_inactive_plates_visibility()`
|
||||
runs at the start of each frame of Prepare. It clears `GLVolume::is_active` on the volumes of the
|
||||
hidden instances and deactivates their raycasters, which every render pass, the hover and rectangle
|
||||
selection, the shadow and center of mass markers already respect. It also deactivates the raycasters
|
||||
of the other plates, whose icons would otherwise still take clicks where nothing is drawn. The state
|
||||
persists between frames, so the raycasts made while handling the mouse, such as the anchor of a pan,
|
||||
see the same scene as the frame.
|
||||
|
||||
Other features clear `is_active` too: gizmos hide the objects they do not edit and show them all
|
||||
again when they close. `GLVolume::on_hidden_plate` records which volumes this update hid, so it only
|
||||
shows again what it hid itself, and it waits for a gizmo that hides objects to close before doing
|
||||
so. A volume another feature made active again is hidden again on the next frame. While Focus mode
|
||||
is off and nothing is left hidden, the update returns at once.
|
||||
|
||||
## Switching plates
|
||||
|
||||
The other plates cannot be clicked while Focus mode hides them, so a compact counterpart of the Preview
|
||||
plate toolbar takes their place: a column of plate thumbnails in the top left corner, below the top
|
||||
toolbar, shown when the project has more than one plate and no gizmo is open. The thumbnails are the
|
||||
ones the Preview toolbar shows (`PartPlate::thumbnail_data`); the canvas rebuilds its textures when
|
||||
`Plater::plate_toolbar_image_version()` changes, a counter each consumer tracks on its own so that the
|
||||
Preview toolbar's dirty flag is left alone. That counter moves after almost every edit, so the
|
||||
thumbnail of the current plate, the only one edited while the others are hidden, is drawn again each
|
||||
time, and the other plates' only once an edit to them has invalidated theirs. The current plate has an
|
||||
accent border, a hovered one the gray border of the Preview toolbar, empty plates are dimmed and the
|
||||
plate number sits in the corner; the tooltip gives the plate name. A click selects the plate and
|
||||
clears the selection, as a click on a plate row of the object list does. The column scrolls when it
|
||||
does not fit above the 3D navigator or the canvas toolbar. Its scrollbar works as the Preview
|
||||
toolbar's, with which it shares its style: it shows only while the mouse is over the column or drags
|
||||
the bar, and the mouse wheel scrolls the column either way.
|
||||
|
||||
Whatever selects a plate while the others are hidden, the camera moves its target over the new plate
|
||||
and keeps its angle, distance and height (`PartPlateList::select_plate()`), so the plate comes into
|
||||
view where the previous one was. A zoom to all plates, as after loading a project or adding a plate,
|
||||
frames the current plate instead. Turning Focus mode on does the same, so the current plate fills the view.
|
||||
@@ -0,0 +1,199 @@
|
||||
# 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.
|
||||
|
||||
Bulk/extended spools are carried by `print.vir_slot`, which `DeviceManager` parses into
|
||||
`MachineObject::vt_slot`. Two ids are fixed: `VIRTUAL_TRAY_MAIN_ID = 255` (extruder 0) and
|
||||
`VIRTUAL_TRAY_DEPUTY_ID = 254` (extruder 1); the id convention is `vt_id = 255 - extruder_id`, so
|
||||
further extruders use 253, 252, … . The `vir_slot` parser recognises the 255/254 pair; the agent
|
||||
filament descriptor path `DevFilaSystemParser::ParseAgentFilament` is the entry that stores a fuller
|
||||
descending set in `vt_slot`.
|
||||
|
||||
## Mapping
|
||||
|
||||
Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray
|
||||
index. The Bambu mapping path (`DevMappingUtil`) computes `ams_id * 4 + slot_id` and knows only the
|
||||
Bambu unit kinds; `DevFilaSystem` provides the device model's own mapping below. The two agree for
|
||||
Bambu units and diverge 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.
|
||||
|
||||
### 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. The preset
|
||||
is expected to match the connected device; when the two disagree the virtual range is wrong, which
|
||||
is why a mismatch is reported 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'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.
|
||||
- **Agent-produced counts.** Toolhead, AMS, and slot counts come from the device payload. Nothing
|
||||
caps them at two toolheads or four slots except 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
|
||||
- [`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, 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)
|
||||
@@ -215,6 +215,7 @@ src/libslic3r/GCode.cpp
|
||||
src/libslic3r/GCodeWriter.cpp
|
||||
src/libslic3r/GCode/ToolOrdering.cpp
|
||||
src/libslic3r/GCode/SeamPlacer.cpp
|
||||
src/libslic3r/GCode/GCodeStatistics.cpp
|
||||
src/libslic3r/ExtrusionEntity.cpp
|
||||
src/libslic3r/Flow.cpp
|
||||
src/libslic3r/Format/AMF.cpp
|
||||
@@ -324,6 +325,7 @@ src/slic3r/GUI/BaseTransparentDPIFrame.cpp
|
||||
src/slic3r/GUI/CloneDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeSupport.cpp
|
||||
src/slic3r/GUI/ColorPickerDialog.cpp
|
||||
src/slic3r/GUI/ConfigWizard.cpp
|
||||
src/slic3r/GUI/DeviceCore/DevCalib.cpp
|
||||
src/slic3r/GUI/DeviceCore/DevUpgrade.cpp
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/* Pure color/state helpers shared by the page and its node:test contract tests. */
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
else root.ColorPickerModel = api;
|
||||
})(typeof globalThis === "object" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
function parseHex(hex) {
|
||||
if (typeof hex !== "string" || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)) return null;
|
||||
return [1, 3, 5, 7].map((offset) => offset === 7 && hex.length === 7 ? 255 : parseInt(hex.slice(offset, offset + 2), 16));
|
||||
}
|
||||
|
||||
function toHex(color) {
|
||||
return "#" + color.map((byte) => Math.round(byte).toString(16).padStart(2, "0")).join("").toUpperCase();
|
||||
}
|
||||
|
||||
const MAX_GRADIENT_COLORS = 7; // Most colors a gradient may have. A caller allows 2 (the default) up to this many.
|
||||
const NEW_GRADIENT_HUE_SHIFT = 75; // Hue step used for every newly created gradient color.
|
||||
const gradientLimit = (value) => Math.max(2, Math.min(MAX_GRADIENT_COLORS, Math.floor(Number(value)) || 2));
|
||||
|
||||
// `maxGradientColors` caps how many colors a gradient may have (2 when omitted).
|
||||
function normalizeSelection(value, options = {}, maxGradientColors = 2) {
|
||||
if (!value || !["solid", "gradient"].includes(value.type) || !Array.isArray(value.colors)) return null;
|
||||
if (value.type === "gradient" && !options.allow_gradient) return null;
|
||||
const count = value.colors.length;
|
||||
if (value.type === "solid" ? count !== 1 : count < 2 || count > gradientLimit(maxGradientColors)) return null;
|
||||
const colors = value.colors.map(parseHex);
|
||||
if (colors.some((color) => color === null)) return null;
|
||||
if (!options.allow_alpha) colors.forEach((color) => { color[3] = 255; });
|
||||
return {type: value.type, colors: colors.map(toHex)};
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
|
||||
let h = 0, s = 0;
|
||||
const l = (max + min) / 2;
|
||||
if (delta) {
|
||||
s = delta / (1 - Math.abs(2 * l - 1));
|
||||
if (max === r) h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
|
||||
else if (max === g) h = ((b - r) / delta + 2) / 6;
|
||||
else h = ((r - g) / delta + 4) / 6;
|
||||
}
|
||||
return [h * 360, s * 100, l * 100];
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) {
|
||||
h = ((h % 360) + 360) % 360 / 60; s /= 100; l /= 100;
|
||||
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
const x = c * (1 - Math.abs(h % 2 - 1)), m = l - c / 2;
|
||||
const sectors = [[c,x,0], [x,c,0], [0,c,x], [0,x,c], [x,0,c], [c,0,x]];
|
||||
return sectors[Math.floor(h)].map((channel) => Math.round((channel + m) * 255));
|
||||
}
|
||||
|
||||
function contrastColor(color, hueShift = 90) {
|
||||
if (!Number.isFinite(hueShift)) hueShift = 90;
|
||||
const degrees = Math.max(-180, Math.min(180, hueShift));
|
||||
const amount = Math.abs(degrees) / 180;
|
||||
const [h, s, l] = rgbToHsl(color[0], color[1], color[2]);
|
||||
const grey = s < 10;
|
||||
const target = grey ? (l < 50 ? 100 : 0) : 100 - l;
|
||||
const rgb = hslToRgb(h + degrees, grey ? 0 : s, l + (target - l) * amount);
|
||||
return [...rgb, color[3]];
|
||||
}
|
||||
|
||||
// options.max_gradient_colors (2..7, default 2) is how many colors this page lets a gradient have.
|
||||
function createState(options = {}, selection = {type: "solid", colors: ["#808080"]}) {
|
||||
const capabilities = {allow_gradient: options.allow_gradient === true, allow_alpha: options.allow_alpha === true};
|
||||
// gradient stays null until a selection provides one or the user switches to gradient mode.
|
||||
const state = {options: capabilities, maxGradientColors: gradientLimit(options.max_gradient_colors),
|
||||
mode: "solid", active: 0, solid: [128,128,128,255], gradient: null};
|
||||
if (!applySelection(state, selection)) return null;
|
||||
return state;
|
||||
}
|
||||
|
||||
function applySelection(state, value) {
|
||||
const selection = normalizeSelection(value, state.options, state.maxGradientColors);
|
||||
if (!selection) return false;
|
||||
state.mode = selection.type;
|
||||
const colors = selection.colors.map(parseHex);
|
||||
if (state.mode === "solid") state.solid = colors[0];
|
||||
else {
|
||||
state.gradient = colors;
|
||||
state.active = Math.min(state.active, colors.length - 1); // A gradient with fewer colors must not leave the selected stop past its end.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function activeColor(state) { return state.mode === "solid" ? state.solid : state.gradient[state.active]; }
|
||||
|
||||
function setMode(state, mode) {
|
||||
if (mode !== "solid" && (mode !== "gradient" || !state.options.allow_gradient)) return false;
|
||||
// A new gradient starts at the current solid color and ends at its contrast color.
|
||||
if (mode === "gradient" && !state.gradient) state.gradient = [[...state.solid], contrastColor(state.solid, NEW_GRADIENT_HUE_SHIFT)];
|
||||
state.mode = mode;
|
||||
return true;
|
||||
}
|
||||
|
||||
function addGradientColor(state) {
|
||||
if (state.mode !== "gradient" || state.gradient.length >= state.maxGradientColors) return false;
|
||||
state.gradient.splice(state.active + 1, 0, contrastColor(state.gradient[state.active], NEW_GRADIENT_HUE_SHIFT));
|
||||
state.active += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeGradientColor(state) {
|
||||
if (state.mode !== "gradient" || state.gradient.length <= 2) return false;
|
||||
state.gradient.splice(state.active, 1);
|
||||
state.active = Math.min(state.active, state.gradient.length - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setRgb(state, rgb) {
|
||||
if (rgb.length !== 3 || rgb.some((v) => !Number.isFinite(v) || v < 0 || v > 255)) return false;
|
||||
const color = activeColor(state);
|
||||
rgb.forEach((v, i) => { color[i] = Math.round(v); });
|
||||
return true;
|
||||
}
|
||||
|
||||
function setAlphaPercent(state, value) {
|
||||
if (!state.options.allow_alpha || !Number.isFinite(value) || value < 0 || value > 100) return false;
|
||||
activeColor(state)[3] = Math.round(value * 255 / 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setHex(state, hex) {
|
||||
const color = parseHex(hex);
|
||||
if (!color) return false;
|
||||
if (hex.length === 7) color[3] = activeColor(state)[3];
|
||||
if (!state.options.allow_alpha) color[3] = 255;
|
||||
if (state.mode === "solid") state.solid = color;
|
||||
else state.gradient[state.active] = color;
|
||||
return true;
|
||||
}
|
||||
|
||||
function exportSelection(state) {
|
||||
const colors = state.mode === "solid" ? [state.solid] : state.gradient;
|
||||
return normalizeSelection({type: state.mode, colors: colors.map(toHex)}, state.options, state.maxGradientColors);
|
||||
}
|
||||
|
||||
// Favorites retain full capabilities regardless of what this caller can select.
|
||||
function normalizeFavorites(values) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
return values.slice(0, 24).map((value) => normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, MAX_GRADIENT_COLORS)).filter(Boolean);
|
||||
}
|
||||
|
||||
function selectionBackground(value) {
|
||||
return value.type === "gradient" ? `linear-gradient(to right, ${value.colors.join(", ")})` : `linear-gradient(${value.colors[0]}, ${value.colors[0]})`;
|
||||
}
|
||||
|
||||
return {MAX_GRADIENT_COLORS, NEW_GRADIENT_HUE_SHIFT, contrastColor, parseHex, toHex, normalizeSelection, rgbToHsl, hslToRgb, createState, applySelection, activeColor, setMode,
|
||||
addGradientColor, removeGradientColor, setRgb, setAlphaPercent, setHex, exportSelection, normalizeFavorites, selectionBackground};
|
||||
});
|
||||
@@ -0,0 +1,314 @@
|
||||
// Optional real-DOM regressions. Use an existing Playwright installation:
|
||||
// ORCA_PLAYWRIGHT_MODULE=<module path> ORCA_BROWSER_EXECUTABLE=<browser path>
|
||||
// ORCA_BROWSER_ENGINE=webkit exercises the macOS WebView layout engine.
|
||||
// node --test resources/web/dialog/ColorPickerDialog/colorpicker.browser.test.js
|
||||
const {test, before, after} = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {readFileSync} = require("node:fs");
|
||||
const {resolve} = require("node:path");
|
||||
const {pathToFileURL} = require("node:url");
|
||||
const engineName = process.env.ORCA_BROWSER_ENGINE || "chromium";
|
||||
let engine;
|
||||
try { engine = require(process.env.ORCA_PLAYWRIGHT_MODULE || "playwright")[engineName]; }
|
||||
catch (error) { if (error.code !== "MODULE_NOT_FOUND") throw error; }
|
||||
const options = {skip: engine ? false : "Use an existing Playwright installation via ORCA_PLAYWRIGHT_MODULE"};
|
||||
const url = pathToFileURL(resolve(__dirname, "index.html")).href;
|
||||
let browser;
|
||||
before(async () => { if (engine) browser = await engine.launch({headless: true, ...(process.env.ORCA_BROWSER_EXECUTABLE ? {executablePath: process.env.ORCA_BROWSER_EXECUTABLE} : {})}); });
|
||||
after(async () => { if (browser) await browser.close(); });
|
||||
async function open(t, strings = {}, language = "") {
|
||||
const page = await browser.newPage({viewport: {width: 600, height: 660}}), errors = [];
|
||||
page.on("pageerror", error => errors.push(error.message));
|
||||
t.after(async () => { await page.close(); assert.deepEqual(errors, []); });
|
||||
await page.addInitScript(({table, language}) => {
|
||||
window.ORCA_UI_STRINGS = table;
|
||||
window.ORCA_COLOR_PICKER_LANGUAGE = language;
|
||||
window.messages = [];
|
||||
window.wx = {postMessage: text => messages.push(JSON.parse(text))};
|
||||
}, {table: strings, language});
|
||||
await page.goto(url);
|
||||
const ready = await page.evaluate(() => messages[0]);
|
||||
assert.equal(ready.command, "ready");
|
||||
return {page, ready, send: payload => page.evaluate(value => ColorPickerDialog.handleMessage(value), payload)};
|
||||
}
|
||||
function init(ready, gradient = false, alpha = false) {
|
||||
return {command: "init", page_id: ready.page_id, options: {allow_gradient: gradient, allow_alpha: alpha},
|
||||
selection: {type: "solid", colors: ["#01234567"]},
|
||||
ui: {sidebar_open: true, palette: "basic"},
|
||||
favorites: [{type: "gradient", colors: ["#FF000040", "#0000FF90"]}, {type: "solid", colors: ["#ABCDEF40"]}]};
|
||||
}
|
||||
async function count(page, command) { return page.evaluate(value => messages.filter(message => message.command === value).length, command); }
|
||||
|
||||
for (const gradient of [false, true]) for (const alpha of [false, true]) {
|
||||
test(`Native capabilities gate gradient=${gradient} and alpha=${alpha}`, options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
|
||||
assert.equal(await send({...init(ready), options: {allow_gradient: "yes", allow_alpha: alpha}}), false);
|
||||
assert.equal(await send({...init(ready), page_id: "stale"}), false);
|
||||
assert.equal(await send(init(ready, gradient, alpha)), true);
|
||||
assert.equal(await page.locator("#modeControls").isVisible(), gradient);
|
||||
assert.equal(await page.locator("#alphaControls").isVisible(), alpha);
|
||||
assert.equal(await page.locator("#userColors button:not(.empty)").count(), gradient ? 2 : 1);
|
||||
await page.locator("#userColors button:not(.empty)").last().click();
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
assert.deepEqual(selection, {type: "solid", colors: [alpha ? "#ABCDEF40" : "#ABCDEFFF"]});
|
||||
assert.equal(await send(init(ready, gradient, alpha)), true);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection, "repeated ready/init retains edits");
|
||||
await page.locator("#colorPreview").click();
|
||||
const favorites = await page.evaluate(() => messages.at(-1).favorites);
|
||||
assert(favorites.some(favorite => favorite.colors[0] === "#ABCDEF40"), "original favorites retain their alpha");
|
||||
assert(favorites.some(favorite => favorite.type === "gradient"), "filtered gradients remain stored");
|
||||
await page.locator("#confirmBtn").click();
|
||||
assert.deepEqual(await page.evaluate(() => messages.at(-1).selection), selection);
|
||||
});
|
||||
}
|
||||
|
||||
test("Visible incomplete editors block confirmation and favorites until recovery", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send(init(ready, true, true));
|
||||
for (const [id, value, valid] of [["rValue", "", "21"], ["oValue", "", "50"], ["hexInput", "GG", "12345680"]]) {
|
||||
await page.locator("#" + id).fill(value);
|
||||
const confirms = await count(page, "confirm"), saves = await count(page, "update_favorites");
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click()); await page.locator("#colorPreview").click();
|
||||
assert.equal(await count(page, "confirm"), confirms); assert.equal(await count(page, "update_favorites"), saves);
|
||||
await page.locator("#" + id).fill(valid); await page.locator("#" + id).press("Enter");
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
}
|
||||
await page.locator("#hValue").fill(""); // HSL is now visible beside RGB, so incomplete HSL must block.
|
||||
const hslConfirms = await count(page, "confirm");
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click());
|
||||
assert.equal(await count(page, "confirm"), hslConfirms);
|
||||
await page.locator("#hValue").fill("100");
|
||||
const confirms = await count(page, "confirm"); await page.locator("#confirmBtn").click();
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
await page.locator("#rValue").fill(""); await page.locator("#rValue").press("Escape");
|
||||
assert.equal(await page.evaluate(() => messages.at(-1).command), "cancel");
|
||||
});
|
||||
|
||||
test("Numeric channel and opacity edits clamp to their field bounds", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send(init(ready, true, true));
|
||||
for (const [id, input, expected] of [["rValue", "999", "255"], ["rValue", "-1", "0"],
|
||||
["hValue", "361", "360"], ["oValue", "101", "100"], ["oValue", "-1", "0"]]) {
|
||||
await page.locator("#" + id).fill(input);
|
||||
assert.equal(await page.locator("#" + id).inputValue(), expected);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.checkValidity()), true);
|
||||
}
|
||||
assert.equal((await page.evaluate(() => ColorPickerDialog.exportSelection())).colors[0].slice(-2), "00");
|
||||
});
|
||||
|
||||
test("Endpoint edits and favorites retain ordered byte alpha", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
|
||||
await page.locator(".gradient-thumb[data-index='1']").click();
|
||||
await page.locator('.color-item[title="Red"]').first().click();
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "gradient", colors: ["#FF000001", "#FF000080"]});
|
||||
await page.locator("#hexInput").fill("0000FF80"); await page.locator(".gradient-thumb[data-index='0']").click();
|
||||
assert.equal(await page.locator("#hexInput").inputValue(), "FF000001");
|
||||
await page.locator("#colorPreview").click();
|
||||
assert.deepEqual(await page.evaluate(() => messages.at(-1).favorites[0]), {type: "gradient", colors: ["#FF000001", "#0000FF80"]});
|
||||
});
|
||||
|
||||
test("Read-only favorites remain selectable and late documents cannot initialize", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send({...init(ready), favorites_writable: false});
|
||||
assert.equal(await page.locator("#colorPreview").isDisabled(), true);
|
||||
await page.locator("#userColors button:not(.empty)").first().click();
|
||||
assert.equal(await page.locator("#hexInput").inputValue(), "ABCDEF");
|
||||
await page.reload();
|
||||
assert.equal(await send(init(ready)), false); assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
|
||||
});
|
||||
|
||||
test("Document strings translate palette names and editor errors while retaining RAL codes and numeric labels", options, async t => {
|
||||
const source = readFileSync(resolve(__dirname, "../../../../src/slic3r/GUI/ColorPickerDialog.cpp"), "utf8");
|
||||
const keys = [...source.matchAll(/_u8L\("([^"\n]+)"\)/g)].map(match => match[1]);
|
||||
const strings = Object.fromEntries(keys.map(key => [key, "Translated: " + key]));
|
||||
const {page, ready, send} = await open(t, strings); await send({...init(ready, true, true), options: {allow_gradient: true, allow_alpha: true, max_gradient_colors: 7}, preserve_multi_color: true});
|
||||
assert.equal(await page.title(), strings["Color Picker"]);
|
||||
for (const element of await page.locator("[data-i18n]").all()) assert.equal(await element.textContent(), strings[await element.getAttribute("data-i18n")]);
|
||||
for (const attribute of ["aria-label", "title"]) for (const element of await page.locator(`[data-i18n-${attribute}]`).all())
|
||||
assert.equal((await element.getAttribute(attribute)).split("\n")[0], strings[await element.getAttribute(`data-i18n-${attribute}`)]);
|
||||
assert.equal(await page.locator("#selectionNotice").textContent(), strings["The current multi-color selection is kept until you choose a different color."]);
|
||||
for (const palette of ["basic", "orca", "ral", "gradient"]) {
|
||||
await page.selectOption("#paletteCombo", palette);
|
||||
const names = await page.evaluate(value => ColorPickerPalettes[value].map(entry => entry.name), palette);
|
||||
assert.deepEqual(await page.locator(".color-name").allTextContents(), names.map(name => {
|
||||
const ral = name.match(/^(\d{4}) (.+)$/); return ral ? `${ral[1]} ${strings[ral[2]]}` : /^%\d+$/.test(name) ? name : strings[name];
|
||||
}));
|
||||
}
|
||||
await page.locator("#hexInput").fill("bad");
|
||||
assert.equal(await page.locator("#hexInput").evaluate(element => element.validationMessage), strings["Enter a complete hexadecimal color"]);
|
||||
await page.locator("#hexInput").fill("12345680");
|
||||
for (const [id, invalid, valid, key] of [["rValue", "", "18", "Invalid input"],
|
||||
["oValue", "", "50", "Invalid input"]]) {
|
||||
const confirms = await count(page, "confirm");
|
||||
await page.locator("#" + id).fill(invalid);
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click());
|
||||
await page.locator("#colorPreview").click(); // Saving invokes the same translated validation while OK is disabled.
|
||||
assert.equal(await count(page, "confirm"), confirms);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), strings[key]);
|
||||
await page.locator("#" + id).fill(valid);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), "");
|
||||
await page.locator("#confirmBtn").click();
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
}
|
||||
await page.locator("#hValue").fill("361");
|
||||
await page.locator("#colorPreview").click();
|
||||
assert.equal(await page.locator("#hValue").inputValue(), "360", "out-of-range hue clamps at its boundary");
|
||||
await page.locator(".color-item").first().click();
|
||||
assert.equal(await page.locator("#hValue").evaluate(element => element.validationMessage), "", "a new palette selection clears the previous editor error");
|
||||
});
|
||||
|
||||
test("Document language and escaped labels preserve untranslated English fallback", options, async t => {
|
||||
const cancel = '取消 "Cancel" \\ /\n下一行';
|
||||
const {page, ready, send} = await open(t, {Cancel: cancel}, "zh-CN");
|
||||
await send(init(ready));
|
||||
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN");
|
||||
assert.equal(await page.locator("#cancelBtn").textContent(), cancel);
|
||||
assert.equal(await page.title(), "Color Picker");
|
||||
assert.equal(await page.locator(".color-name").first().textContent(), "Red");
|
||||
assert.deepEqual(await page.locator("label[for=hValue],label[for=sValue],label[for=lValue],label[for=rValue],label[for=gValue],label[for=bValue]").allTextContents(), ["H", "S", "L", "R", "G", "B"]);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "solid", colors: ["#012345FF"]});
|
||||
});
|
||||
|
||||
test("Global control colors follow the system while host semantic colors follow the app", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
|
||||
const initial = await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()}));
|
||||
for (const theme of ["light", "dark"]) for (const system of ["light", "dark"]) {
|
||||
await page.emulateMedia({colorScheme: system});
|
||||
await page.evaluate(value => {
|
||||
document.documentElement.dataset.orcaTheme = value;
|
||||
const colors = value === "light" ? {bg: "#ffffff", fg: "#262e30", muted: "#363636", border: "#dbdbdb", accent: "#009688"} : {bg: "#2d2d31", fg: "#efeff0", muted: "#b2b3b5", border: "#36363b", accent: "#00675b"};
|
||||
for (const [key, color] of Object.entries(colors)) document.documentElement.style.setProperty("--orca-" + key, color);
|
||||
document.documentElement.style.setProperty("--orca-accent-fg", "#ffffff");
|
||||
}, theme);
|
||||
await page.evaluate(async () => {
|
||||
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
|
||||
});
|
||||
const colors = await page.evaluate(() => {
|
||||
const body = getComputedStyle(document.body);
|
||||
const tokenColor = token => {
|
||||
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
|
||||
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
|
||||
};
|
||||
const mode = document.querySelector("label[for=modeSwitch]");
|
||||
return {background: body.backgroundColor, foreground: body.color,
|
||||
modeBackground: getComputedStyle(mode).backgroundColor,
|
||||
accent: getComputedStyle(mode, "::before").backgroundColor,
|
||||
hostPanel: tokenColor("--panel"), hostText: tokenColor("--text")};
|
||||
});
|
||||
assert.equal(colors.background, system === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)", `${theme} app / ${system} system body`);
|
||||
assert.equal(colors.foreground, system === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
|
||||
assert.equal(colors.modeBackground, system === "light" ? "rgb(240, 240, 240)" : "rgb(36, 36, 40)");
|
||||
assert.equal(colors.accent, theme === "light" ? "rgb(0, 150, 136)" : "rgb(0, 103, 91)", "shared accent follows the injected app theme");
|
||||
assert.equal(colors.hostPanel, theme === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)");
|
||||
assert.equal(colors.hostText, theme === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
|
||||
assert.deepEqual(await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()})), initial);
|
||||
for (const id of ["modeSwitch"]) for (const checked of [false, true]) {
|
||||
if (await page.locator("#" + id).isChecked() !== checked) await page.locator(`label[for=${id}]`).click();
|
||||
await page.evaluate(async () => {
|
||||
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
|
||||
});
|
||||
const result = await page.locator(`label[for=${id}]`).evaluate((element, selected) => {
|
||||
const tokenColor = token => {
|
||||
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
|
||||
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
|
||||
};
|
||||
const labels = element.querySelectorAll(".switch-label");
|
||||
return {selected: getComputedStyle(labels[selected ? 1 : 0]).color,
|
||||
unselected: getComputedStyle(labels[selected ? 0 : 1]).color,
|
||||
accentForeground: tokenColor("--button-fg-light"), mutedForeground: tokenColor("--fg-color-label")};
|
||||
}, checked);
|
||||
assert.equal(result.selected, result.accentForeground, `${theme} app / ${system} system ${id} selected text uses the accent foreground`);
|
||||
assert.equal(result.selected, "rgb(254, 254, 254)", "global selected text stays light in both system schemes");
|
||||
assert.equal(result.unselected, result.mutedForeground, `${theme} app / ${system} system ${id} unselected text stays muted`);
|
||||
assert.equal(result.unselected, system === "light" ? "rgb(54, 54, 54)" : "rgb(178, 179, 181)");
|
||||
const selected = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
assert.equal(selected.type, checked ? "gradient" : "solid");
|
||||
if (checked) assert.deepEqual(selected, initial.selection, "returning to gradient keeps ordered endpoint byte alpha");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Sidebar resizes from a compact viewport and shrinks after every collapse", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await page.setViewportSize({width: 340, height: 380});
|
||||
await send({...init(ready, true, true), ui: {sidebar_open: false, palette: "basic"}});
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
for (const [index, sidebarOpen] of [false, true, false, true, false].entries()) {
|
||||
const width = sidebarOpen ? 510 : 340;
|
||||
await page.waitForFunction(expected => messages.filter(message => message.command === "resize").at(-1)?.width === expected, width);
|
||||
const size = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1));
|
||||
assert.equal(size.height, 380);
|
||||
await page.setViewportSize({width: size.width, height: size.height});
|
||||
await page.waitForTimeout(100);
|
||||
assert(await page.evaluate(() => {
|
||||
const body = document.body.getBoundingClientRect();
|
||||
const picker = document.querySelector(".right-panel").getBoundingClientRect();
|
||||
return body.width === innerWidth && picker.right <= body.right && picker.bottom <= body.bottom;
|
||||
}), "the host viewport contains the full picker");
|
||||
const resizes = await count(page, "resize");
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(await count(page, "resize"), resizes, "fitting the host does not cause a resize loop");
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
|
||||
if (index < 4) await page.locator("#paletteToggle").click();
|
||||
}
|
||||
});
|
||||
|
||||
test("Floating panel fits capabilities and wrapped translations without a resize feedback loop", options, async t => {
|
||||
const strings = {Color: "A long translated color label", Gradient: "A long translated gradient label", Cancel: "A longer translated cancel action", OK: "A longer translated confirmation action",
|
||||
"The current multi-color selection is kept until you choose a different color.": "A translated explanation that wraps across the compact floating panel and keeps the original multi-color selection until another color is chosen."};
|
||||
for (const gradient of [false, true]) for (const alpha of [false, true]) {
|
||||
const {page, ready, send} = await open(t, strings);
|
||||
await page.setViewportSize({width: 550, height: 520});
|
||||
await send({...init(ready, gradient, alpha), preserve_multi_color: true});
|
||||
await page.waitForFunction(() => messages.some(message => message.command === "resize"));
|
||||
const height = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1).height);
|
||||
assert(height >= 300 && height <= 900);
|
||||
assert.equal(height, await page.locator("body").evaluate(element => Math.ceil(element.getBoundingClientRect().height)));
|
||||
assert(await page.evaluate(() => messages.findIndex(message => message.command === "initialized") < messages.findIndex(message => message.command === "resize")));
|
||||
await page.setViewportSize({width: 550, height});
|
||||
await page.waitForTimeout(100);
|
||||
const resizes = await count(page, "resize");
|
||||
await page.setViewportSize({width: 550, height: height + 100});
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(await count(page, "resize"), resizes, "extra viewport height never changes intrinsic content height");
|
||||
const controls = await page.evaluate(() => {
|
||||
const body = document.body.getBoundingClientRect();
|
||||
return ["confirmBtn", "cancelBtn", ...(document.getElementById("alphaControls").classList.contains("hidden") ? [] : ["oValue"])].map(id => {
|
||||
const rect = document.getElementById(id).getBoundingClientRect();
|
||||
return rect.left >= body.left && rect.right <= body.right && rect.bottom <= body.bottom;
|
||||
});
|
||||
});
|
||||
assert(controls.every(Boolean), "wrapped buttons and opacity fit inside the panel");
|
||||
const edited = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
await page.evaluate(() => {
|
||||
document.getElementById("selectionNotice").textContent += " Additional wrapped text. ".repeat(15);
|
||||
});
|
||||
await page.waitForFunction(previous => messages.filter(message => message.command === "resize").length > previous, resizes);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), edited, "content resize leaves the selection untouched");
|
||||
// The compact layout now puts saved swatches to the right of both previews.
|
||||
assert(await page.evaluate(() => {
|
||||
const favorites = document.getElementById("userColors").getBoundingClientRect();
|
||||
const preview = document.getElementById("colorPreview").getBoundingClientRect();
|
||||
return favorites.left >= preview.right;
|
||||
}), "favorites do not overlap the preview");
|
||||
await page.locator("#confirmBtn").scrollIntoViewIfNeeded();
|
||||
assert.equal(await page.locator("#confirmBtn").isVisible(), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("Native focus hook waits for initialization and preserves active partial edits", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.notEqual(await page.evaluate(() => document.activeElement.id), "hexInput");
|
||||
await send(init(ready, true, true));
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.equal(await page.evaluate(() => document.activeElement.id), "hexInput");
|
||||
await page.locator("#rValue").fill("");
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.equal(await page.evaluate(() => document.activeElement.id), "rValue");
|
||||
assert.equal(await page.locator("#rValue").inputValue(), "");
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
|
||||
assert.equal(await count(page, "confirm"), 0);
|
||||
await page.locator("#rValue").press("Escape");
|
||||
assert.equal(await count(page, "cancel"), 1);
|
||||
});
|
||||
@@ -0,0 +1,464 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
if (window.ORCA_COLOR_PICKER_LANGUAGE) document.documentElement.lang = window.ORCA_COLOR_PICKER_LANGUAGE;
|
||||
const T = (key) => window.ORCA_UI_STRINGS?.[key] || key;
|
||||
document.title = T("Color Picker");
|
||||
for (const element of document.querySelectorAll("[data-i18n]")) element.textContent = T(element.dataset.i18n);
|
||||
for (const attribute of ["aria-label", "title", "placeholder"])
|
||||
for (const element of document.querySelectorAll(`[data-i18n-${attribute}]`))
|
||||
element.setAttribute(attribute, T(element.getAttribute(`data-i18n-${attribute}`)));
|
||||
function paletteName(name) {
|
||||
const ral = name.match(/^(\d{4}) (.+)$/);
|
||||
return ral ? `${ral[1]} ${T(ral[2])}` : /^%\d+$/.test(name) ? name : T(name);
|
||||
}
|
||||
|
||||
const FAVORITE_SLOTS = 16;
|
||||
const SPECTRUM_FOLLOWS_SATURATION = true; // true: the spectrum is drawn at the current saturation, false: it always shows full saturation
|
||||
const MULTI_COLOR_GRADIENTS = true; // true: gradients may have more than 2 colors (− / + buttons and the multi-color palette entries), false: exactly 2 colors.
|
||||
// up to the native limit the host sends as options.max_gradient_colors.
|
||||
let maxGradientColors = 2; // Stays 2 until init, and with a host that does not send a limit.
|
||||
|
||||
const model = window.ColorPickerModel;
|
||||
const palettes = window.ColorPickerPalettes;
|
||||
const byId = (id) => document.getElementById(id);
|
||||
const canvas = byId("spectrum"), context = canvas.getContext("2d");
|
||||
const paletteCombo = byId("paletteCombo"), palette = byId("palette"), preview = byId("colorPreview"), confirmBtn = byId("confirmBtn");
|
||||
const modeSwitch = byId("modeSwitch"), satSlider = byId("satSlider"), opacitySlider = byId("opacitySlider");
|
||||
const gradientBar = byId("gradientCtrlBar"), gradientStops = byId("gradientStops"), gradientAdd = byId("gradientAdd"), gradientRemove = byId("gradientRemove");
|
||||
const hostGradientLimit = (options) => options.max_gradient_colors === undefined ? 2 : options.max_gradient_colors;
|
||||
const originalPreview = byId("originalPreview"), palettePanel = byId("palettePanel"), paletteToggle = byId("paletteToggle");
|
||||
const paletteSelect = byId("paletteSelect"), paletteFilter = byId("paletteFilter"), paletteSearchToggle = byId("paletteSearchToggle");
|
||||
const rgbInputs = ["r", "g", "b"].map((name) => byId(name + "Value"));
|
||||
const hslInputs = ["h", "s", "l"].map((name) => byId(name + "Value"));
|
||||
const hexInput = byId("hexInput"), opacityInput = byId("oValue");
|
||||
const pageId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
||||
let state = model.createState(), favorites = [], initialized = false, favoritesWritable = true;
|
||||
// The selection the dialog opened with; the original swatch shows it and restores it on click.
|
||||
let originalSelection = model.exportSelection(state);
|
||||
let lastContentHeight = 0, lastContentWidth = 0, resizePending = false;
|
||||
// Last HSL shown or set. Kept while it still produces the current color: hue 360 and 0 are
|
||||
// both red, and greys, black and white have no hue or saturation in RGB.
|
||||
let hslMemory = [0, 0, 50];
|
||||
function displayHsl(color) {
|
||||
const rgb = model.hslToRgb(...hslMemory);
|
||||
if (!rgb.every((value, index) => value === color[index])) hslMemory = model.rgbToHsl(...color.slice(0, 3));
|
||||
return hslMemory;
|
||||
}
|
||||
// Copy of `values` with one entry replaced.
|
||||
const replaced = (values, index, value) => values.map((current, i) => i === index ? value : current);
|
||||
function setHslChannel(index, value) {
|
||||
hslMemory = replaced(displayHsl(model.activeColor(state)), index, value);
|
||||
model.setRgb(state, model.hslToRgb(...hslMemory));
|
||||
}
|
||||
const round1 = (value) => Math.round(value * 10) / 10;
|
||||
|
||||
function emit(command, data = {}) {
|
||||
window.dispatchEvent(new CustomEvent("color-picker-message", {detail: {command, ...data}}));
|
||||
if (window.wx && typeof window.wx.postMessage === "function")
|
||||
window.wx.postMessage(JSON.stringify({command, page_id: pageId, ...data}));
|
||||
}
|
||||
|
||||
function scheduleResize() {
|
||||
if (!initialized || resizePending) return;
|
||||
resizePending = true;
|
||||
requestAnimationFrame(() => {
|
||||
resizePending = false;
|
||||
const rect = document.body.getBoundingClientRect();
|
||||
const height = Math.min(900 , Math.max(300, Math.ceil(rect.height)));
|
||||
const width = Math.min(1200, Math.max(300, Math.ceil(rect.width )));
|
||||
if (height !== lastContentHeight || width !== lastContentWidth) {
|
||||
lastContentHeight = height; lastContentWidth = width;
|
||||
emit("resize", {width, height});
|
||||
}
|
||||
});
|
||||
}
|
||||
new ResizeObserver(scheduleResize).observe(document.body);
|
||||
|
||||
function setSwatch(element, selection) {
|
||||
element.style.setProperty("--swatch-color", model.selectionBackground(selection));
|
||||
}
|
||||
|
||||
// `editing` is the control the user is typing in. Its text is left alone so the caret,
|
||||
// partial input like "05" and fine steps that round-trip through bytes aren't overwritten.
|
||||
function render(resetEditors = false, editing = null) {
|
||||
const color = model.activeColor(state), hsl = displayHsl(color);
|
||||
const show = (input, value) => {
|
||||
if (input === editing) return;
|
||||
if (resetEditors) input.setCustomValidity("");
|
||||
if (resetEditors || input.checkValidity()) input.value = value;
|
||||
};
|
||||
show(hexInput, model.toHex(color).slice(1, state.options.allow_alpha ? 9 : 7));
|
||||
rgbInputs.forEach((input, index) => show(input, color[index]));
|
||||
// Shown in whole numbers; decimals can still be typed, and hslMemory keeps exact values.
|
||||
hslInputs.forEach((input, index) => show(input, Math.round(hsl[index])));
|
||||
// Percentage is a display only until explicitly edited. Never round it back
|
||||
// into the alpha byte on endpoint switches, RGB edits, or confirmation.
|
||||
show(opacityInput, Math.round(color[3] / 255 * 100));
|
||||
if (opacitySlider !== editing) opacitySlider.value = color[3] / 255 * 100;
|
||||
const rgb = color.slice(0, 3).join(",");
|
||||
opacitySlider.style.setProperty("--track-gradient", `linear-gradient(to right, rgba(${rgb},0), rgb(${rgb}))`);
|
||||
opacitySlider.style.setProperty("--thumb-color", `rgba(${rgb},${color[3] / 255})`);
|
||||
if (satSlider !== editing) satSlider.value = hsl[1];
|
||||
satSlider.style.setProperty("--track-gradient", `linear-gradient(to right, hsl(${hsl[0]},0%,${hsl[2]}%), hsl(${hsl[0]},100%,${hsl[2]}%))`);
|
||||
satSlider.style.setProperty("--thumb-color", `hsl(${hsl[0]},${hsl[1]}%,${hsl[2]}%)`);
|
||||
modeSwitch.checked = state.mode === "gradient";
|
||||
const selection = model.exportSelection(state);
|
||||
// Kept in the layout (visibility only) so the grid doesn't shift between Solid and Gradient.
|
||||
gradientBar.classList.toggle("invisible", state.mode !== "gradient");
|
||||
if (state.mode === "gradient") renderGradient(selection);
|
||||
setSwatch(preview, selection);
|
||||
preview.title = `${T("Save current color")}\n${selection.colors.join(" → ")}`;
|
||||
if (SPECTRUM_FOLLOWS_SATURATION && hsl[1] !== spectrumSaturation) { spectrumSaturation = hsl[1]; paintSpectrum(); }
|
||||
updateIndicator(hsl);
|
||||
updateConfirm();
|
||||
}
|
||||
|
||||
// One thumb per gradient color, evenly spaced; rebuilt only when the number of colors changes.
|
||||
function renderGradient(selection) {
|
||||
const count = selection.colors.length;
|
||||
setSwatch(gradientStops.querySelector(".gradient-track"), selection);
|
||||
let thumbs = [...gradientStops.querySelectorAll(".gradient-thumb")];
|
||||
if (thumbs.length !== count) {
|
||||
thumbs.forEach((thumb) => thumb.remove());
|
||||
thumbs = selection.colors.map((_, index) => {
|
||||
const thumb = document.createElement("button");
|
||||
thumb.type = "button"; thumb.className = "gradient-thumb"; thumb.dataset.index = index;
|
||||
thumb.style.setProperty("--stop", index / (count - 1));
|
||||
thumb.setAttribute("aria-label", count === 2 ? T(index ? "Gradient end" : "Gradient start") : `${T("Gradient color")} ${index + 1}`);
|
||||
gradientStops.appendChild(thumb);
|
||||
return thumb;
|
||||
});
|
||||
}
|
||||
thumbs.forEach((thumb, index) => {
|
||||
thumb.style.setProperty("--thumb-color", selection.colors[index]);
|
||||
thumb.setAttribute("aria-pressed", String(state.active === index));
|
||||
});
|
||||
|
||||
const multi = maxGradientColors > 2; // −/+ buttons only exist when more than 2 colors are allowed; − also needs a color to spare.
|
||||
gradientAdd.parentElement.classList.toggle("hidden", !multi);
|
||||
gradientRemove.classList.toggle("hidden", !multi || count <= 2);
|
||||
gradientAdd.classList.toggle("hidden", !multi);
|
||||
// aria-disabled instead of disabled: a disabled button gets no hover, so its tooltip would never show.
|
||||
const full = count >= maxGradientColors;
|
||||
gradientAdd.setAttribute("aria-disabled", String(full));
|
||||
gradientAdd.title = full ? T("Up to %d colors supported.").replace("%d", maxGradientColors) : T("Add color");
|
||||
}
|
||||
|
||||
// Every editor the user can see; alpha only when this caller allows it.
|
||||
function editors() {
|
||||
const inputs = [hexInput, ...rgbInputs, ...hslInputs];
|
||||
if (state.options.allow_alpha) inputs.push(opacityInput);
|
||||
return inputs;
|
||||
}
|
||||
// OK stays disabled while any visible editor holds an invalid or incomplete value.
|
||||
function updateConfirm() {
|
||||
confirmBtn.disabled = !editors().every((input) => input.checkValidity());
|
||||
}
|
||||
|
||||
// Matches the filter against the shown (translated) name, the English name and the hex digits.
|
||||
function paletteMatches(entry, query) {
|
||||
if (!query) return true;
|
||||
const haystack = [paletteName(entry.name), entry.name, entry.hex || "", entry.grad || ""].join(" ").toLowerCase();
|
||||
return query.split(/\s+/).every((word) => haystack.includes(word.replace(/^#/, "")));
|
||||
}
|
||||
|
||||
function populatePalette() {
|
||||
palette.replaceChildren();
|
||||
const query = paletteSearchToggle.getAttribute("aria-pressed") === "true" ? paletteFilter.value.trim().toLowerCase() : "";
|
||||
for (const entry of palettes[paletteCombo.value] || []) {
|
||||
const selection = {type: entry.grad ? "gradient" : "solid", colors: entry.grad ? entry.grad.split("-") : [entry.hex]};
|
||||
if (!model.normalizeSelection(selection, state.options, maxGradientColors) || !paletteMatches(entry, query)) continue;
|
||||
const row = document.createElement("button"); row.type = "button"; row.className = "color-item";
|
||||
const swatch = document.createElement("span"); swatch.className = "color-swatch";
|
||||
const name = document.createElement("span"); name.className = "color-name"; name.textContent = paletteName(entry.name);
|
||||
setSwatch(swatch, model.normalizeSelection(selection, {allow_gradient: true, allow_alpha: true}, maxGradientColors));
|
||||
row.append(swatch, name); row.title = paletteName(entry.name);
|
||||
row.addEventListener("click", () => {
|
||||
// Solid palette colors edit the active endpoint while gradient mode is on.
|
||||
const changed = entry.grad ? model.applySelection(state, selection) : model.setHex(state, entry.hex);
|
||||
if (changed) render(true);
|
||||
});
|
||||
palette.appendChild(row);
|
||||
}
|
||||
byId("paletteEmpty").classList.toggle("hidden", palette.childElementCount > 0);
|
||||
palette.classList.toggle("hidden", palette.childElementCount === 0);
|
||||
}
|
||||
|
||||
// Sidebar state the host keeps in the app config: open/closed and the chosen palette.
|
||||
const uiState = () => ({sidebar_open: palettePanel.classList.contains("open"), palette: paletteCombo.value});
|
||||
function setSidebarOpen(open) {
|
||||
palettePanel.classList.toggle("open", open);
|
||||
paletteToggle.setAttribute("aria-expanded", String(open));
|
||||
}
|
||||
// The search button swaps the combo box for a filter box; closing it clears the filter.
|
||||
function setPaletteSearch(open) {
|
||||
paletteSearchToggle.setAttribute("aria-pressed", String(open));
|
||||
paletteSelect.classList.toggle("hidden", open);
|
||||
paletteFilter.classList.toggle("hidden", !open);
|
||||
if (open) paletteFilter.focus();
|
||||
else paletteFilter.value = "";
|
||||
populatePalette();
|
||||
}
|
||||
|
||||
function drawFavorites() {
|
||||
const grid = byId("userColors"); grid.replaceChildren();
|
||||
// Favorites keep every color, but only those this page can select are shown.
|
||||
const visible = favorites.filter((favorite) => model.normalizeSelection(favorite, {...state.options, allow_alpha: true}, maxGradientColors));
|
||||
for (let index = 0; index < FAVORITE_SLOTS; ++index) {
|
||||
const swatch = document.createElement("button"); swatch.type = "button"; swatch.className = "saved-swatch";
|
||||
const favorite = visible[index];
|
||||
if (favorite) {
|
||||
setSwatch(swatch, favorite); swatch.title = favorite.colors.join(" → ");
|
||||
swatch.addEventListener("click", () => { if (model.applySelection(state, favorite)) render(true); });
|
||||
} else { swatch.classList.add("empty"); swatch.disabled = true; }
|
||||
grid.appendChild(swatch);
|
||||
}
|
||||
}
|
||||
|
||||
function init(payload) {
|
||||
if (!payload || typeof payload !== "object" || (payload.options !== undefined && (!payload.options || typeof payload.options !== "object"))) return false;
|
||||
maxGradientColors = Math.min(MULTI_COLOR_GRADIENTS ? model.MAX_GRADIENT_COLORS : 2, hostGradientLimit(payload.options || {}));
|
||||
// A gradient the host allows but this page does not (switch off) opens as its first color, like
|
||||
// other multi-color selections the picker cannot edit; OK then replaces it, Cancel keeps it.
|
||||
let selection = payload.selection, preserve = payload.preserve_multi_color === true;
|
||||
if (selection?.type === "gradient" && selection.colors?.length > maxGradientColors) {
|
||||
selection = {type: "solid", colors: [selection.colors[0]]};
|
||||
preserve = true;
|
||||
}
|
||||
const next = model.createState({...payload.options, max_gradient_colors: maxGradientColors}, selection);
|
||||
if (!next) return false;
|
||||
byId("selectionNotice").hidden = !preserve;
|
||||
state = next; favorites = model.normalizeFavorites(payload.favorites || []);
|
||||
originalSelection = model.exportSelection(state);
|
||||
setSwatch(originalPreview, originalSelection);
|
||||
originalPreview.title = `${T("Original color")}\n${originalSelection.colors.join(" → ")}`;
|
||||
favoritesWritable = payload.favorites_writable !== false;
|
||||
preview.disabled = !favoritesWritable;
|
||||
byId("modeControls").classList.toggle("hidden", !state.options.allow_gradient);
|
||||
for (const option of paletteCombo.options) option.hidden = option.value === "gradient" && !state.options.allow_gradient;
|
||||
const ui = payload.ui || {}; // Restore the sidebar the way it was left; a palette this caller can't use falls back to basic.
|
||||
const saved = [...paletteCombo.options].find((option) => option.value === ui.palette && !option.hidden);
|
||||
if (saved) paletteCombo.value = saved.value;
|
||||
if (!state.options.allow_gradient && paletteCombo.value === "gradient") paletteCombo.value = "basic";
|
||||
setSidebarOpen(ui.sidebar_open === true);
|
||||
byId("alphaControls").classList.toggle("hidden", !state.options.allow_alpha);
|
||||
hexInput.maxLength = state.options.allow_alpha ? 9 : 7;
|
||||
populatePalette(); drawFavorites(); render(true);
|
||||
initialized = true;
|
||||
byId("pickerEditors").disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
if (!initialized) return;
|
||||
// Restore initial keyboard focus without replacing an editor's in-progress input.
|
||||
const active = document.activeElement;
|
||||
if (!active || active === document.body || active === document.documentElement)
|
||||
hexInput.focus({preventScroll: true});
|
||||
}
|
||||
|
||||
function handleMessage(payload) {
|
||||
if (!payload || typeof payload !== "object" || payload.page_id !== pageId) return false;
|
||||
if (payload.command === "init") {
|
||||
const options = payload.options;
|
||||
const hostLimit = options && hostGradientLimit(options);
|
||||
if (!options || typeof options.allow_gradient !== "boolean" || typeof options.allow_alpha !== "boolean" ||
|
||||
!Number.isInteger(hostLimit) || hostLimit < 2 || hostLimit > model.MAX_GRADIENT_COLORS ||
|
||||
!model.normalizeSelection(payload.selection, options, hostLimit) || !validFavorites(payload.favorites) ||
|
||||
(payload.favorites_writable !== undefined && typeof payload.favorites_writable !== "boolean") ||
|
||||
(payload.preserve_multi_color !== undefined && typeof payload.preserve_multi_color !== "boolean") ||
|
||||
(payload.ui !== undefined && (!payload.ui || typeof payload.ui !== "object" ||
|
||||
(payload.ui.sidebar_open !== undefined && typeof payload.ui.sidebar_open !== "boolean") ||
|
||||
(payload.ui.palette !== undefined && typeof payload.ui.palette !== "string")))) return false;
|
||||
if (initialized) return true;
|
||||
if (!init(payload)) return false;
|
||||
emit("initialized");
|
||||
scheduleResize();
|
||||
return true;
|
||||
}
|
||||
if (payload.command === "favorites" && initialized && validFavorites(payload.favorites)) {
|
||||
favorites = model.normalizeFavorites(payload.favorites); drawFavorites(); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function validFavorites(values) {
|
||||
return Array.isArray(values) && values.length <= 24 && values.every((value) => model.normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, model.MAX_GRADIENT_COLORS));
|
||||
}
|
||||
|
||||
// Saturation the spectrum is currently painted at (always 100 when the option is off).
|
||||
let spectrumSaturation = 100;
|
||||
function drawSpectrum() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.max(1, Math.round(rect.width)); canvas.height = Math.max(1, Math.round(rect.height));
|
||||
paintSpectrum();
|
||||
}
|
||||
function paintSpectrum() {
|
||||
const hue = context.createLinearGradient(0, 0, canvas.width, 0);
|
||||
for (let i = 0; i <= 6; ++i) hue.addColorStop(i / 6, `hsl(${i * 60},100%,50%)`);
|
||||
context.fillStyle = hue; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const shade = context.createLinearGradient(0, 0, 0, canvas.height);
|
||||
shade.addColorStop(0, "#FFFFFF"); shade.addColorStop(0.5, "#FFFFFF00"); shade.addColorStop(0.5, "#00000000"); shade.addColorStop(1, "#000000");
|
||||
context.fillStyle = shade; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
// HSL desaturation blends each color toward the grey of the same lightness, and on this
|
||||
// canvas that grey only depends on the row (white at the top, black at the bottom).
|
||||
if (spectrumSaturation < 100) {
|
||||
const grey = context.createLinearGradient(0, 0, 0, canvas.height);
|
||||
grey.addColorStop(0, "#FFFFFF"); grey.addColorStop(1, "#000000");
|
||||
context.globalAlpha = 1 - spectrumSaturation / 100;
|
||||
context.fillStyle = grey; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const indicator = byId("colorIndicator");
|
||||
function updateIndicator(hsl) {
|
||||
indicator.style.left = hsl[0] / 360 * (canvas.width - 1) + "px";
|
||||
indicator.style.top = (1 - hsl[2] / 100) * (canvas.height - 1) + "px";
|
||||
}
|
||||
|
||||
// Spectrum position maps straight to hue (x, 0..360) and lightness (y, 100..0); saturation is
|
||||
// the third dimension and comes from the slider below. hslMemory keeps these exact values,
|
||||
// including hue 360 at the right edge and the hue of the white and black corners.
|
||||
function sampleSpectrum(event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const fraction = (client, start, size, pixels) =>
|
||||
Math.max(0, Math.min(pixels - 1, Math.floor((client - start) * pixels / size))) / Math.max(1, pixels - 1);
|
||||
const saturation = displayHsl(model.activeColor(state))[1];
|
||||
hslMemory = [round1(fraction(event.clientX, rect.left, rect.width, canvas.width) * 360), saturation,
|
||||
round1((1 - fraction(event.clientY, rect.top, rect.height, canvas.height)) * 100)];
|
||||
model.setRgb(state, model.hslToRgb(...hslMemory));
|
||||
// A pick replaces every editor's value, so clear any invalid state left from typing.
|
||||
render(true);
|
||||
}
|
||||
|
||||
const clamp = (control, value) => Math.min(Number(control.max), Math.max(Number(control.min), value));
|
||||
function bindValue(control, apply) {
|
||||
// Number fields are required, so an empty field is invalid; a range input always has a value.
|
||||
if (control.type === "number") control.required = true;
|
||||
const commit = (value) => { apply(value); render(false, control); };
|
||||
control.addEventListener("input", () => {
|
||||
control.setCustomValidity("");
|
||||
// Out-of-range numbers snap to the field's min/max instead of becoming invalid.
|
||||
if (control.value !== "") {
|
||||
const clamped = clamp(control, Number(control.value));
|
||||
if (clamped !== Number(control.value)) control.value = clamped;
|
||||
}
|
||||
if (control.checkValidity()) commit(Number(control.value));
|
||||
});
|
||||
control.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
const step = control.type === "range" ? 5 : 1;
|
||||
control.value = clamp(control, (Number(control.value) || 0) + (event.deltaY > 0 ? -step : step));
|
||||
control.setCustomValidity("");
|
||||
commit(Number(control.value));
|
||||
}, {passive: false});
|
||||
// Normalize the text (e.g. "05" → "5") once the user leaves the field.
|
||||
control.addEventListener("blur", () => { if (control.checkValidity()) render(); });
|
||||
}
|
||||
|
||||
rgbInputs.forEach((input, index) => bindValue(input, (value) => model.setRgb(state, replaced(model.activeColor(state).slice(0, 3), index, value))));
|
||||
hslInputs.forEach((input, index) => bindValue(input, (value) => setHslChannel(index, value)));
|
||||
bindValue(satSlider, (value) => setHslChannel(1, value));
|
||||
bindValue(opacityInput, (value) => model.setAlphaPercent(state, value));
|
||||
bindValue(opacitySlider, (value) => model.setAlphaPercent(state, value));
|
||||
// The opacity slider takes the gradient bar's row while the opacity field or the slider is in
|
||||
// use: it opens when the field is clicked or focused, and closes on a click or focus elsewhere.
|
||||
const thirdRow = byId("thirdRow"), alphaControls = byId("alphaControls");
|
||||
const inOpacityControls = (target) => target instanceof Element && target.closest("#alphaControls, #opacitySliderBar");
|
||||
const setOpacitySliderOpen = (open) => thirdRow.classList.toggle("opacity-open", open);
|
||||
for (const type of ["focusin", "click"]) alphaControls.addEventListener(type, () => setOpacitySliderOpen(true));
|
||||
for (const type of ["pointerdown", "focusin"])
|
||||
document.addEventListener(type, (event) => { if (!inOpacityControls(event.target)) setOpacitySliderOpen(false); });
|
||||
hexInput.addEventListener("input", () => {
|
||||
const text = hexInput.value;
|
||||
const valid = model.setHex(state, text.startsWith("#") ? text : "#" + text);
|
||||
hexInput.setCustomValidity(valid ? "" : T("Enter a complete hexadecimal color"));
|
||||
// render() leaves the typed text alone, so alpha isn't appended after the sixth digit.
|
||||
if (valid) render(false, hexInput);
|
||||
});
|
||||
hexInput.addEventListener("blur", () => { if (hexInput.checkValidity()) render(); });
|
||||
modeSwitch.addEventListener("change", () => {
|
||||
|
||||
setOpacitySliderOpen(false); // Close the opacity slider before the row changes, so it doesn't linger over the gradient bar.
|
||||
model.setMode(state, modeSwitch.checked ? "gradient" : "solid");
|
||||
render(true);
|
||||
});
|
||||
// Click on the track selects the nearest thumb. Keyboard users tab to a thumb and press Enter or Space.
|
||||
gradientStops.addEventListener("click", (event) => {
|
||||
if (state.mode !== "gradient") return;
|
||||
// A thumb is read from its index: a keyboard-triggered click has no pointer position.
|
||||
const thumb = event.target.closest(".gradient-thumb"), rect = gradientStops.getBoundingClientRect();
|
||||
const last = state.gradient.length - 1;
|
||||
// Thumbs sit at 10px + (width - 20px) * i / last, matching the CSS.
|
||||
const fraction = Math.max(0, Math.min(1, (event.clientX - rect.left - 10) / Math.max(1, rect.width - 20)));
|
||||
const index = thumb ? Number(thumb.dataset.index) : Math.round(fraction * last);
|
||||
if (index !== state.active) { state.active = index; render(true); }
|
||||
});
|
||||
gradientAdd.addEventListener("click", () => { if (gradientAdd.getAttribute("aria-disabled") !== "true" && model.addGradientColor(state)) render(true); });
|
||||
gradientRemove.addEventListener("click", () => { if (model.removeGradientColor(state)) render(true); });
|
||||
originalPreview.addEventListener("click", () => { if (model.applySelection(state, originalSelection)) render(true); });
|
||||
paletteToggle.addEventListener("click", () => {
|
||||
setSidebarOpen(!palettePanel.classList.contains("open"));
|
||||
if (initialized) emit("update_ui", uiState());
|
||||
});
|
||||
paletteCombo.addEventListener("change", () => {
|
||||
populatePalette();
|
||||
if (initialized) emit("update_ui", uiState());
|
||||
});
|
||||
paletteSearchToggle.addEventListener("click", () => setPaletteSearch(paletteSearchToggle.getAttribute("aria-pressed") !== "true"));
|
||||
paletteFilter.addEventListener("input", populatePalette);
|
||||
// Pointer capture ends by itself on pointerup/pointercancel, so it doubles as the drag state.
|
||||
canvas.addEventListener("pointerdown", (event) => { canvas.setPointerCapture(event.pointerId); sampleSpectrum(event); });
|
||||
canvas.addEventListener("pointermove", (event) => { if (canvas.hasPointerCapture(event.pointerId)) sampleSpectrum(event); });
|
||||
// The spectrum takes the panel's free height, which also changes with the notice, alpha and
|
||||
// gradient controls, so redraw on any size change rather than on window resize only.
|
||||
new ResizeObserver(() => { drawSpectrum(); updateIndicator(displayHsl(model.activeColor(state))); }).observe(canvas.parentElement);
|
||||
// Bubble phase on document runs after each editor's own handler has set its validity.
|
||||
document.addEventListener("input", updateConfirm);
|
||||
// Hex keeps its own message from its input handler; number fields get a translated one.
|
||||
function validateEditors() {
|
||||
const inputs = editors();
|
||||
for (const input of inputs.filter((input) => input !== hexInput)) {
|
||||
input.setCustomValidity("");
|
||||
const {valid, rangeUnderflow, rangeOverflow} = input.validity;
|
||||
if (!valid) input.setCustomValidity(T(rangeUnderflow || rangeOverflow ? "Value is out of range." : "Invalid input"));
|
||||
}
|
||||
return inputs.every((input) => input.reportValidity());
|
||||
}
|
||||
function confirm() {
|
||||
if (initialized && validateEditors()) emit("confirm", {selection: model.exportSelection(state)});
|
||||
}
|
||||
preview.addEventListener("click", () => {
|
||||
if (!initialized || !favoritesWritable || !validateEditors()) return;
|
||||
const selection = model.exportSelection(state);
|
||||
const key = JSON.stringify(selection);
|
||||
favorites = [selection, ...favorites.filter((favorite) => JSON.stringify(favorite) !== key)].slice(0, FAVORITE_SLOTS);
|
||||
drawFavorites(); emit("update_favorites", {favorites});
|
||||
});
|
||||
confirmBtn.addEventListener("click", confirm);
|
||||
byId("cancelBtn").addEventListener("click", () => emit("cancel"));
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// Escape in the filter box closes the search first; a second Escape cancels the dialog.
|
||||
if (event.key === "Escape" && event.target === paletteFilter) { event.preventDefault(); setPaletteSearch(false); paletteSearchToggle.focus(); }
|
||||
else if (event.key === "Escape") { event.preventDefault(); emit("cancel"); }
|
||||
else if (event.key === "Enter" && event.target instanceof HTMLInputElement && ["text", "number"].includes(event.target.type)) {
|
||||
event.preventDefault(); confirm();
|
||||
}
|
||||
});
|
||||
|
||||
window.ColorPickerDialog = {
|
||||
init, handleMessage, focusInput, exportSelection: () => model.exportSelection(state),
|
||||
setFavorites: (values) => { favorites = model.normalizeFavorites(values); drawFavorites(); }
|
||||
};
|
||||
// The host passes the saved sidebar state before the page loads (the same values come again in
|
||||
// init), so the first paint already matches the window size the host opened with.
|
||||
const earlyUi = window.ORCA_COLOR_PICKER_UI;
|
||||
if (earlyUi && typeof earlyUi === "object") {
|
||||
if (typeof earlyUi.palette === "string" && [...paletteCombo.options].some((option) => option.value === earlyUi.palette))
|
||||
paletteCombo.value = earlyUi.palette;
|
||||
if (typeof earlyUi.sidebar_open === "boolean") setSidebarOpen(earlyUi.sidebar_open);
|
||||
}
|
||||
setSwatch(originalPreview, originalSelection);
|
||||
drawSpectrum(); populatePalette(); drawFavorites(); render(true); emit("ready");
|
||||
})();
|
||||
@@ -0,0 +1,103 @@
|
||||
"use strict";
|
||||
const {test} = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const model = require("./colorpicker-model.js");
|
||||
|
||||
test("complete RGB/RGBA hex accepts mixed case and rejects partial or unbounded input", () => {
|
||||
assert.deepEqual(model.parseHex("#Ab12cD80"), [171,18,205,128]);
|
||||
for (const hex of ["", "123456", "#123", "#1234567", "#123456789", "#12345Z", "#123456GG", " #123456", null]) assert.equal(model.parseHex(hex), null);
|
||||
for (let byte = 0; byte <= 255; ++byte) {
|
||||
const hex = model.toHex([byte, byte, byte, byte]);
|
||||
assert.deepEqual(model.parseHex(hex), [byte,byte,byte,byte]);
|
||||
}
|
||||
});
|
||||
|
||||
test("HSL conversion preserves primary channels and hue wraps at 360", () => {
|
||||
for (const rgb of [[255,0,0], [0,255,0], [0,0,255], [128,128,128], [37,129,240]]) assert.deepEqual(model.hslToRgb(...model.rgbToHsl(...rgb)), rgb);
|
||||
assert.deepEqual(model.hslToRgb(360,100,50), [255,0,0]);
|
||||
assert.deepEqual(model.hslToRgb(240,100,50), [0,0,255]);
|
||||
});
|
||||
|
||||
test("capabilities default off and reject gradient through every model entry", () => {
|
||||
const gradient = {type: "gradient", colors: ["#FF000080", "#0000FF20"]};
|
||||
const state = model.createState();
|
||||
assert.deepEqual(state.options, {allow_gradient: false, allow_alpha: false});
|
||||
assert.equal(model.createState({}, gradient), null);
|
||||
assert.equal(model.applySelection(state, gradient), false);
|
||||
assert.equal(model.setMode(state, "gradient"), false);
|
||||
assert.equal(model.setAlphaPercent(state, 20), false);
|
||||
assert.equal(model.createState({allow_alpha: true}, gradient), null);
|
||||
const opaque = model.createState({allow_gradient: true}, gradient);
|
||||
assert.deepEqual(model.exportSelection(opaque), {type: "gradient", colors: ["#FF0000FF", "#0000FFFF"]});
|
||||
});
|
||||
|
||||
test("gradient endpoint edits keep order and preserve unedited byte alpha", () => {
|
||||
const initial = {type: "gradient", colors: ["#FF000001", "#0000FF80"]};
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true}, initial);
|
||||
state.active = 0; model.setRgb(state, [0,255,0]);
|
||||
state.active = 1; model.setHex(state, "#112233");
|
||||
assert.deepEqual(model.exportSelection(state), {type: "gradient", colors: ["#00FF0001", "#11223380"]});
|
||||
model.setAlphaPercent(state, 0);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#00FF0001", "#11223300"]);
|
||||
model.setAlphaPercent(state, 100);
|
||||
assert.equal(model.exportSelection(state).colors[1], "#112233FF");
|
||||
assert.deepEqual(initial.colors, ["#FF000001", "#0000FF80"]);
|
||||
});
|
||||
|
||||
test("favorites retain alpha and gradient even when the selection disables them", () => {
|
||||
const favorite = {type: "solid", colors: ["#ABCDEF80"]};
|
||||
const gradient = {type: "gradient", colors: ["#FFFFFF00", "#00000010"]};
|
||||
const favorites = model.normalizeFavorites([favorite, gradient]);
|
||||
const state = model.createState();
|
||||
assert.equal(model.applySelection(state, favorites[0]), true);
|
||||
assert.deepEqual(model.exportSelection(state), {type: "solid", colors: ["#ABCDEFFF"]});
|
||||
assert.deepEqual(favorites, [favorite, gradient]);
|
||||
assert.equal(model.applySelection(state, favorites[1]), false);
|
||||
assert.equal(model.normalizeFavorites(Array(25).fill(favorite)).length, 24);
|
||||
});
|
||||
|
||||
test("invalid cardinality and channel edits cannot change a selection", () => {
|
||||
const state = model.createState({allow_alpha: true});
|
||||
const before = model.exportSelection(state);
|
||||
for (const value of [{type: "solid", colors: []}, {type: "solid", colors: ["#112233", "#445566"]}, {type: "gradient", colors: ["#112233"]}, {type: "other", colors: ["#112233"]}]) assert.equal(model.applySelection(state, value), false);
|
||||
assert.equal(model.setHex(state, "#GG2233"), false);
|
||||
for (const value of [-1, 101, NaN, Infinity]) assert.equal(model.setAlphaPercent(state, value), false);
|
||||
assert.equal(model.setRgb(state, [256,0,0]), false);
|
||||
assert.deepEqual(model.exportSelection(state), before);
|
||||
});
|
||||
|
||||
test("multi-color gradients stay at 2 colors unless the caller raises the limit", () => {
|
||||
const five = {type: "gradient", colors: ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF"]};
|
||||
assert.equal(model.normalizeSelection(five, {allow_gradient: true}), null);
|
||||
assert.equal(model.createState({allow_gradient: true}, five), null);
|
||||
const state = model.createState({allow_gradient: true, max_gradient_colors: 7}, five);
|
||||
assert.equal(state.gradient.length, 5);
|
||||
assert.equal(model.normalizeSelection({...five, colors: Array(8).fill("#000000")}, {allow_gradient: true}, 7), null);
|
||||
assert.equal(model.createState({allow_gradient: true, max_gradient_colors: 99}).maxGradientColors, model.MAX_GRADIENT_COLORS);
|
||||
assert.equal(model.normalizeFavorites([five]).length, 1, "favorites keep every color");
|
||||
});
|
||||
|
||||
test("adding and removing gradient colors keeps order, selection and the 2..max limits", () => {
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true, max_gradient_colors: 4}, {type: "gradient", colors: ["#FF000080", "#0000FFFF"]});
|
||||
assert.equal(model.removeGradientColor(state), false, "never fewer than 2");
|
||||
assert.equal(model.addGradientColor(state), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080", "#0000FFFF"], "selected color turned 75° in hue, alpha kept");
|
||||
assert.equal(model.NEW_GRADIENT_HUE_SHIFT, 75);
|
||||
assert.equal(state.active, 1);
|
||||
state.active = 2; assert.equal(model.addGradientColor(state), true);
|
||||
assert.equal(state.active, 3); assert.equal(state.gradient.length, 4);
|
||||
assert.equal(model.addGradientColor(state), false, "limit reached");
|
||||
state.active = 1; assert.equal(model.removeGradientColor(state), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors.slice(0, 2), ["#FF000080", "#0000FFFF"]);
|
||||
assert.equal(state.active, 1);
|
||||
assert.equal(model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]}), true);
|
||||
state.active = 1; model.addGradientColor(state); model.addGradientColor(state);
|
||||
model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]});
|
||||
assert.equal(state.active, 1, "a shorter gradient clamps the selected color");
|
||||
});
|
||||
|
||||
test("a new gradient starts at the solid color and its 75° hue neighbour", () => {
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true}, {type: "solid", colors: ["#FF000080"]});
|
||||
assert.equal(model.setMode(state, "gradient"), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080"]);
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-orca-page="color-picker">
|
||||
<head><title data-i18n="Color Picker">Color Picker</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../include/global.css">
|
||||
<link rel="stylesheet" href="../css/theme.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<p id="selectionNotice" hidden data-i18n="The current multi-color selection is kept until you choose a different color.">The current multi-color selection is kept until you choose a different color.</p>
|
||||
<fieldset id="pickerEditors" disabled>
|
||||
<div class="main-wrapper">
|
||||
|
||||
<div class="left-panel" id="palettePanel">
|
||||
<div class="palette thin-scroll" id="palette" tabindex="-1"></div>
|
||||
<p class="palette-empty hidden" id="paletteEmpty" data-i18n="No matching colors">No matching colors</p>
|
||||
<div class="palette-toolbar">
|
||||
<div class="palette-select ComboBox" id="paletteSelect">
|
||||
<div class="arrow-icon"></div>
|
||||
<select id="paletteCombo" tabindex="-1">
|
||||
<option value="basic" data-i18n="Basic Colors">Basic Colors</option>
|
||||
<option value="orca" data-i18n="Orca Palette">Orca Palette</option>
|
||||
<option value="ral" data-i18n="RAL Classic" >RAL Classic</option>
|
||||
<option value="gradient" hidden data-i18n="Gradients" >Gradients</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="search" id="paletteFilter" class="palette-filter hidden" autocomplete="off" spellcheck="false"
|
||||
placeholder="Filter colors" data-i18n-placeholder="Filter colors" aria-label="Filter colors" data-i18n-aria-label="Filter colors">
|
||||
<button type="button" id="paletteSearchToggle" class="palette-search-button" aria-pressed="false" title="Search colors" data-i18n-title="Search colors">
|
||||
<div class="icon-search icon16"></div>
|
||||
<div class="icon-close icon16"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="spectrum-row">
|
||||
<div class="saturation-slider">
|
||||
<input class="valueSlider" type="range" id="satSlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Saturation" data-i18n-aria-label="Saturation">
|
||||
</div>
|
||||
<div class="spectrum-container">
|
||||
<canvas id="spectrum"></canvas>
|
||||
<div id="colorIndicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-grid">
|
||||
<div class="editor-column">
|
||||
<div id="modeControls" class="hidden">
|
||||
<input class="switch-check" type="checkbox" id="modeSwitch">
|
||||
<label class="switch-slider" for="modeSwitch">
|
||||
<div class="switch-label" data-i18n="Solid">Solid</div>
|
||||
<div class="switch-label" data-i18n="Gradient">Gradient</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="hex-row">
|
||||
<div class="inline-field field-hex">
|
||||
<label for="hexInput">#</label>
|
||||
<input type="text" id="hexInput" required value="808080" maxlength="8" autocomplete="off" spellcheck="false" aria-label="Hex color" data-i18n-aria-label="Hex color">
|
||||
</div>
|
||||
<div class="inline-field value-field hidden" id="alphaControls" title="Opacity percent">
|
||||
<label for="oValue">
|
||||
<svg viewBox="0 0 12 12" width="12" height="12">
|
||||
<path d="M0,1v10c0,.55.45,1,1,1h10c.55,0,1-.45,1-1V1c0-.55-.45-1-1-1H1C.45,0,0,.45,0,1ZM6,6v5H1v-5h5V1h5v5h-5Z" style="fill:#999"/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="number" id="oValue" min="0" max="100" aria-label="Opacity percent" data-i18n-aria-label="Opacity percent">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Third row: gradient endpoints, replaced by the opacity slider while the opacity field is in use. -->
|
||||
<div id="thirdRow" class="third-row">
|
||||
<!-- Gradient colors: evenly spaced thumbs (built by colorpicker.js); a click selects the nearest. -->
|
||||
<div id="gradientCtrlBar" class="invisible" role="group" aria-label="Gradient" data-i18n-aria-label="Gradient">
|
||||
<div id="gradientStops" class="gradient-stops">
|
||||
<div class="gradient-track"></div>
|
||||
</div>
|
||||
<div class="gradient-step-buttons hidden">
|
||||
<button type="button" id="gradientRemove" class="gradient-step-button hidden" title="Remove color" data-i18n-title="Remove color">
|
||||
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button type="button" id="gradientAdd" class="gradient-step-button hidden" title="Add color" data-i18n-title="Add color">
|
||||
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/><rect x="4" y="0" width="2" height="10" fill="currentColor"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opacitySliderBar" hidden>
|
||||
<input class="valueSlider" type="range" id="opacitySlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Opacity" data-i18n-aria-label="Opacity">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-column">
|
||||
<div class="inline-field value-field" title="Hue">
|
||||
<label for="hValue">H</label>
|
||||
<input type="number" id="hValue" min="0" max="360" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Saturation">
|
||||
<label for="sValue">S</label>
|
||||
<input type="number" id="sValue" min="0" max="100" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Lightness">
|
||||
<label for="lValue">L</label>
|
||||
<input type="number" id="lValue" min="0" max="100" step="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-column">
|
||||
<div class="inline-field value-field" title="Red">
|
||||
<label for="rValue">R</label>
|
||||
<input type="number" id="rValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Green">
|
||||
<label for="gValue">G</label>
|
||||
<input type="number" id="gValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Blue">
|
||||
<label for="bValue">B</label>
|
||||
<input type="number" id="bValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<button id="originalPreview" type="button" class="color-preview original-preview" title="Original color" data-i18n-title="Original color">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#ff6f00;"/>
|
||||
<g style="fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;">
|
||||
<polyline points="5.5 5.5 5.5 9.5 9.5 9.5" />
|
||||
<path d="M5.84,9.5c.82-2.33,3.05-4,5.66-4,3.31,0,6,2.69,6,6s-2.69,6-6,6c-2.95,0-5.41-2.13-5.91-4.94"/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="colorPreview" class="color-preview" title="Save current color" data-i18n-title="Save current color">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#009688"/>
|
||||
<path d="M11.85,5.11l1.43,4.39h4.61c.35,0,.5.45.22.66l-3.73,2.71,1.43,4.39c.11.34-.28.62-.56.41l-3.73-2.71-3.73,2.71c-.29.21-.67-.07-.56-.41l1.43-4.39-3.73-2.71c-.29-.21-.14-.66.22-.66h4.61s1.43-4.39,1.43-4.39c.11-.34.59-.34.7,0Z" style="fill:none; stroke:#fff; stroke-linecap:round; stroke-linejoin:round;"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="saved-grid" id="userColors"></div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button type="button" id="paletteToggle" class="icon-button" aria-expanded="false" aria-controls="palettePanel" title="Palettes" data-i18n-title="Palettes">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="11" rx="2" fill="none" stroke="currentColor"/><path d="M5.5 2.5v11" stroke="currentColor"/></svg>
|
||||
</button>
|
||||
<div class="stretch-spacer"></div>
|
||||
<button id="confirmBtn" class="ButtonTypeChoice ButtonStyleConfirm" data-i18n="OK">OK</button>
|
||||
<button id="cancelBtn" class="ButtonTypeChoice ButtonStyleRegular" data-i18n="Cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<script src="palettes.js"></script><script src="colorpicker-model.js"></script><script src="colorpicker.js"></script></body></html>
|
||||
@@ -0,0 +1,459 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
const cssColors = [
|
||||
{hex: "#FF0000", name: "Red"},
|
||||
{hex: "#DC143C", name: "Crimson"},
|
||||
{hex: "#FF4500", name: "Orange red"},
|
||||
{hex: "#FF6347", name: "Tomato"},
|
||||
{hex: "#FFA500", name: "Orange"},
|
||||
{hex: "#FF8C00", name: "Dark orange"},
|
||||
{hex: "#FFD700", name: "Gold"},
|
||||
{hex: "#DEB887", name: "Burly wood"},
|
||||
{hex: "#FFE4C4", name: "Bisque"},
|
||||
{hex: "#FFE4E1", name: "Misty rose"},
|
||||
{hex: "#FFFF00", name: "Yellow"},
|
||||
{hex: "#FFFFE0", name: "Light yellow"},
|
||||
{hex: "#FFFACD", name: "Lemon chiffon"},
|
||||
{hex: "#F5F5DC", name: "Beige"},
|
||||
{hex: "#FAFAD2", name: "Light golden rod yellow"},
|
||||
{hex: "#EEE8AA", name: "Pale golden rod"},
|
||||
{hex: "#F0E68C", name: "Khaki"},
|
||||
{hex: "#BDB76B", name: "Dark khaki"},
|
||||
{hex: "#FFDAB9", name: "Peach puff"},
|
||||
{hex: "#FFE4B5", name: "Moccasin"},
|
||||
{hex: "#FFEFD5", name: "Papaya whip"},
|
||||
{hex: "#FFEBCD", name: "Blanched almond"},
|
||||
{hex: "#FAEBD7", name: "Antique white"},
|
||||
{hex: "#FFDEAD", name: "Navajo white"},
|
||||
{hex: "#F5DEB3", name: "Wheat"},
|
||||
{hex: "#DAA520", name: "Golden rod"},
|
||||
{hex: "#D2691E", name: "Chocolate"},
|
||||
{hex: "#CD853F", name: "Peru"},
|
||||
{hex: "#B22222", name: "Fire brick"},
|
||||
{hex: "#A0522D", name: "Sienna"},
|
||||
{hex: "#8B4513", name: "Saddle brown"},
|
||||
{hex: "#A52A2A", name: "Brown"},
|
||||
{hex: "#BC8F8F", name: "Rosy brown"},
|
||||
{hex: "#FA8072", name: "Salmon"},
|
||||
{hex: "#FFA07A", name: "Light salmon"},
|
||||
{hex: "#E9967A", name: "Dark salmon"},
|
||||
{hex: "#F4A460", name: "Sandy brown"},
|
||||
{hex: "#FF7F50", name: "Coral"},
|
||||
{hex: "#F08080", name: "Light coral"},
|
||||
{hex: "#CD5C5C", name: "Indian red"},
|
||||
{hex: "#8B0000", name: "Dark red"},
|
||||
{hex: "#800000", name: "Maroon"},
|
||||
{hex: "#ADFF2F", name: "Green yellow"},
|
||||
{hex: "#9ACD32", name: "Yellow green"},
|
||||
{hex: "#7FFF00", name: "Chartreuse"},
|
||||
{hex: "#7CFC00", name: "Lawn green"},
|
||||
{hex: "#32CD32", name: "Lime green"},
|
||||
{hex: "#00FF7F", name: "Spring green"},
|
||||
{hex: "#00FA9A", name: "Medium spring green"},
|
||||
{hex: "#00FF00", name: "Lime"},
|
||||
{hex: "#008000", name: "Green"},
|
||||
{hex: "#228B22", name: "Forest green"},
|
||||
{hex: "#006400", name: "Dark green"},
|
||||
{hex: "#6B8E23", name: "Olive drab"},
|
||||
{hex: "#808000", name: "Olive"},
|
||||
{hex: "#556B2F", name: "Dark olive green"},
|
||||
{hex: "#98FB98", name: "Pale green"},
|
||||
{hex: "#90EE90", name: "Light green"},
|
||||
{hex: "#3CB371", name: "Medium sea green"},
|
||||
{hex: "#2E8B57", name: "Sea green"},
|
||||
{hex: "#8FBC8F", name: "Dark sea green"},
|
||||
{hex: "#5F9EA0", name: "Cadet blue"},
|
||||
{hex: "#66CDAA", name: "Medium aqua marine"},
|
||||
{hex: "#40E0D0", name: "Turquoise"},
|
||||
{hex: "#48D1CC", name: "Medium turquoise"},
|
||||
{hex: "#00CED1", name: "Dark turquoise"},
|
||||
{hex: "#AFEEEE", name: "Pale turquoise"},
|
||||
{hex: "#00FFFF", name: "Aqua"},
|
||||
{hex: "#00FFFF", name: "Cyan"},
|
||||
{hex: "#E0FFFF", name: "Light cyan"},
|
||||
{hex: "#F5FFFA", name: "Mint cream"},
|
||||
{hex: "#F0FFF0", name: "Honey dew"},
|
||||
{hex: "#7FFFD4", name: "Aquamarine"},
|
||||
{hex: "#20B2AA", name: "Light sea green"},
|
||||
{hex: "#008B8B", name: "Dark cyan"},
|
||||
{hex: "#008080", name: "Teal"},
|
||||
{hex: "#2F4F4F", name: "Dark slate gray"},
|
||||
{hex: "#778899", name: "Light slate gray"},
|
||||
{hex: "#708090", name: "Slate gray"},
|
||||
{hex: "#00BFFF", name: "Deep sky blue"},
|
||||
{hex: "#1E90FF", name: "Dodger Blue"},
|
||||
{hex: "#87CEEB", name: "Sky blue"},
|
||||
{hex: "#87CEFA", name: "Light sky blue"},
|
||||
{hex: "#ADD8E6", name: "Light blue"},
|
||||
{hex: "#B0E0E6", name: "Powder blue"},
|
||||
{hex: "#B0C4DE", name: "Light steel blue"},
|
||||
{hex: "#4682B4", name: "Steel blue"},
|
||||
{hex: "#6495ED", name: "Cornflower blue"},
|
||||
{hex: "#4169E1", name: "Royal blue"},
|
||||
{hex: "#0000FF", name: "Blue"},
|
||||
{hex: "#0000CD", name: "Medium blue"},
|
||||
{hex: "#191970", name: "Midnight blue"},
|
||||
{hex: "#00008B", name: "Dark blue"},
|
||||
{hex: "#000080", name: "Navy"},
|
||||
{hex: "#6A5ACD", name: "Slate blue"},
|
||||
{hex: "#483D8B", name: "Dark slate blue"},
|
||||
{hex: "#7B68EE", name: "Medium slate blue"},
|
||||
{hex: "#8A2BE2", name: "Blue violet"},
|
||||
{hex: "#4B0082", name: "Indigo"},
|
||||
{hex: "#9400D3", name: "Dark violet"},
|
||||
{hex: "#9932CC", name: "Dark orchid"},
|
||||
{hex: "#BA55D3", name: "Medium orchid"},
|
||||
{hex: "#DA70D6", name: "Orchid"},
|
||||
{hex: "#EE82EE", name: "Violet"},
|
||||
{hex: "#9370DB", name: "Medium purple"},
|
||||
{hex: "#800080", name: "Purple"},
|
||||
{hex: "#663399", name: "Rebecca purple"},
|
||||
{hex: "#C71585", name: "Medium violet red"},
|
||||
{hex: "#DB7093", name: "PaleV violet red"},
|
||||
{hex: "#FF1493", name: "Deep pink"},
|
||||
{hex: "#FF69B4", name: "Hot pink"},
|
||||
{hex: "#FFB6C1", name: "Light pink"},
|
||||
{hex: "#FFC0CB", name: "Pink"},
|
||||
{hex: "#DDA0DD", name: "Plum"},
|
||||
{hex: "#D8BFD8", name: "Thistle"},
|
||||
{hex: "#FF00FF", name: "Magenta"},
|
||||
{hex: "#FF00FF", name: "Fuchsia"},
|
||||
{hex: "#8B008B", name: "Dark magenta"},
|
||||
|
||||
{hex: "#FAF0E6", name: "Linen"},
|
||||
{hex: "#FDF5E6", name: "Old lace"},
|
||||
{hex: "#FFF5EE", name: "Sea shell"},
|
||||
{hex: "#FFFFF0", name: "Ivory"},
|
||||
{hex: "#FFFAF0", name: "Floral white"},
|
||||
{hex: "#FFF0F5", name: "Lavender blush"},
|
||||
{hex: "#F8F8FF", name: "Ghost white"},
|
||||
{hex: "#F0F8FF", name: "Alice blue"},
|
||||
{hex: "#E6E6FA", name: "Lavender"},
|
||||
{hex: "#F5F5F5", name: "White smoke"},
|
||||
{hex: "#DCDCDC", name: "Gainsboro"},
|
||||
];
|
||||
|
||||
const palettes = {
|
||||
basic: [
|
||||
{hex: "#FF0000", name: "Red"},
|
||||
{hex: "#FF8000", name: "Orange"},
|
||||
{hex: "#FFFF00", name: "Yellow"},
|
||||
{hex: "#80FF00", name: "Lime"},
|
||||
{hex: "#00FF00", name: "Green"},
|
||||
{hex: "#00FFFF", name: "Cyan"},
|
||||
{hex: "#0000FF", name: "Blue"},
|
||||
{hex: "#8000FF", name: "Purple"},
|
||||
{hex: "#FF00FF", name: "Magenta"},
|
||||
{hex: "#FF8080", name: "Pink"},
|
||||
{hex: "#804000", name: "Brown"},
|
||||
{hex: "#000000", name: "Black"},
|
||||
{hex: "#191919", name: "%90"},
|
||||
{hex: "#333333", name: "%80"},
|
||||
{hex: "#4c4c4c", name: "%70"},
|
||||
{hex: "#666666", name: "%60"},
|
||||
{hex: "#808080", name: "%50"},
|
||||
{hex: "#999999", name: "%40"},
|
||||
{hex: "#b3b3b3", name: "%30"},
|
||||
{hex: "#cccccc", name: "%20"},
|
||||
{hex: "#e6e6e6", name: "%10"},
|
||||
{hex: "#FFFFFF", name: "White"},
|
||||
...cssColors
|
||||
],
|
||||
orca: [
|
||||
{hex: "#ED1C24", name: "Coral fire"},
|
||||
{hex: "#C41E3A", name: "Sea anemone"},
|
||||
{hex: "#800000", name: "Deep abyss"},
|
||||
{hex: "#B22222", name: "Lobster red"},
|
||||
{hex: "#A0522D", name: "Sandy clay"},
|
||||
{hex: "#8B4513", name: "Driftwood"},
|
||||
{hex: "#D2691E", name: "Sea buckthorn"},
|
||||
{hex: "#F26722", name: "Sunset reef"},
|
||||
{hex: "#FF8C00", name: "Tiger coral"},
|
||||
{hex: "#FA8173", name: "Pink coral"},
|
||||
{hex: "#FFA07A", name: "Seashell glow"},
|
||||
{hex: "#F4E2C1", name: "Sand"},
|
||||
{hex: "#FFD700", name: "Golden kelp"},
|
||||
{hex: "#F7B763", name: "Sunlit dune"},
|
||||
{hex: "#FFAA33", name: "Amber nautilus"},
|
||||
{hex: "#FFEB31", name: "Sea lemon"},
|
||||
{hex: "#D4ED91", name: "Pale starfish"},
|
||||
{hex: "#00FF7F", name: "Spring tide"},
|
||||
{hex: "#32CD32", name: "Lime algae"},
|
||||
{hex: "#A4C41E", name: "Sea grass"},
|
||||
{hex: "#7CFC00", name: "Neon plankton"},
|
||||
{hex: "#228B22", name: "Kelp forest"},
|
||||
{hex: "#008000", name: "Emerald wave"},
|
||||
{hex: "#345B2F", name: "Whale shadow"},
|
||||
{hex: "#2E8B57", name: "Sea turtle"},
|
||||
{hex: "#00C1AE", name: "Lagoon"},
|
||||
{hex: "#20B2AA", name: "Tropical current"},
|
||||
{hex: "#115877", name: "Midnight trench"},
|
||||
{hex: "#008B8B", name: "Dark kelp"},
|
||||
{hex: "#00FFFF", name: "Crystal surf"},
|
||||
{hex: "#40E0D0", name: "Turquoise bay"},
|
||||
{hex: "#2EBDEF", name: "Sky over reef"},
|
||||
{hex: "#87CEEB", name: "Shallow sky"},
|
||||
{hex: "#1E90FF", name: "Electric bel"},
|
||||
{hex: "#4169E1", name: "Dolphin"},
|
||||
{hex: "#0000CD", name: "Ocean depths"},
|
||||
{hex: "#191970", name: "Whale"},
|
||||
{hex: "#4B0082", name: "Indigo abyss"},
|
||||
{hex: "#7841CE", name: "Sea urchin"},
|
||||
{hex: "#9370DB", name: "Lavender coral"},
|
||||
{hex: "#800080", name: "Violet tide"},
|
||||
{hex: "#8B008B", name: "Deep sea orchid"},
|
||||
{hex: "#FF00FF", name: "Neon jelly"},
|
||||
{hex: "#ED1E79", name: "Fuchsia coral"},
|
||||
{hex: "#FF69B4", name: "Pink jellyfish"},
|
||||
{hex: "#FFFFFF", name: "Foam"},
|
||||
{hex: "#D3D3D3", name: "Seashell white"},
|
||||
{hex: "#808080", name: "Pebble gray"},
|
||||
{hex: "#696969", name: "Storm cloud"},
|
||||
{hex: "#000000", name: "Ink black"}
|
||||
],
|
||||
ral: [
|
||||
{hex: "#716F52", name: "1000 Green beige"},
|
||||
{hex: "#9B8E79", name: "1001 Beige"},
|
||||
{hex: "#D4BB87", name: "1002 Sand yellow"},
|
||||
{hex: "#F4A460", name: "1003 Signal yellow"},
|
||||
{hex: "#EFBF04", name: "1004 Golden yellow"},
|
||||
{hex: "#D7A500", name: "1005 Honey yellow"},
|
||||
{hex: "#F2D185", name: "1006 Maize yellow"},
|
||||
{hex: "#FDD185", name: "1007 Dalmatian yellow"},
|
||||
{hex: "#8B7D6B", name: "1011 Brown beige"},
|
||||
{hex: "#F5DB50", name: "1012 Lemon yellow"},
|
||||
{hex: "#F3E5AB", name: "1013 Oyster white"},
|
||||
{hex: "#F8E4BC", name: "1014 Ivory"},
|
||||
{hex: "#FAE6D3", name: "1015 Light ivory"},
|
||||
{hex: "#E4B922", name: "1018 Zinc yellow"},
|
||||
{hex: "#A19D94", name: "1019 Grey beige"},
|
||||
{hex: "#B9975B", name: "1020 Olive yellow"},
|
||||
{hex: "#B8860B", name: "1021 Rape yellow"},
|
||||
{hex: "#FF8C00", name: "1023 Traffic yellow"},
|
||||
{hex: "#CC7722", name: "1024 Ochre yellow"},
|
||||
{hex: "#FDBA37", name: "1026 Traffic orange yellow"},
|
||||
{hex: "#F3A55C", name: "1027 Curry"},
|
||||
{hex: "#E87C33", name: "1028 Melon yellow"},
|
||||
{hex: "#F2A641", name: "1032 Broom yellow"},
|
||||
{hex: "#E09F39", name: "1033 Dahlia yellow"},
|
||||
{hex: "#F0B76E", name: "1034 Pastel yellow"},
|
||||
{hex: "#D1BCA0", name: "1035 Pearl beige"},
|
||||
{hex: "#D4AF37", name: "1036 Pearl gold"},
|
||||
{hex: "#F7C64F", name: "1037 Sun yellow"},
|
||||
{hex: "#F28C38", name: "2000 Yellow orange"},
|
||||
{hex: "#E55B2B", name: "2001 Deep orange"},
|
||||
{hex: "#E34234", name: "2002 Vermilion"},
|
||||
{hex: "#F27B33", name: "2003 Pearl orange"},
|
||||
{hex: "#F37322", name: "2004 Pure orange"},
|
||||
{hex: "#F75C03", name: "2005 Luminous orange"},
|
||||
{hex: "#F46C1F", name: "2007 Luminous bright orange"},
|
||||
{hex: "#E14423", name: "2008 Bright red orange"},
|
||||
{hex: "#F28500", name: "2009 Traffic orange"},
|
||||
{hex: "#F46A0A", name: "2010 Signal orange"},
|
||||
{hex: "#E0490C", name: "2011 Deep orange"},
|
||||
{hex: "#F46F42", name: "2012 Salmon orange"},
|
||||
{hex: "#E6761C", name: "2013 Pearl orange"},
|
||||
{hex: "#E8A18B", name: "2015 Pearl pink"},
|
||||
{hex: "#B8612C", name: "2016 Copper orange"},
|
||||
{hex: "#8B4513", name: "2017 Signal brown"},
|
||||
{hex: "#C14A0F", name: "2018 Fir orange"},
|
||||
{hex: "#BC2E1F", name: "2020 Signal red"},
|
||||
{hex: "#D52713", name: "2021 Luminous red"},
|
||||
{hex: "#9F261E", name: "2022 Carmine red"},
|
||||
{hex: "#D53832", name: "2023 Pearl carmine red"},
|
||||
{hex: "#C2341C", name: "2024 Pure red"},
|
||||
{hex: "#B32D1C", name: "2025 Luminous deep red"},
|
||||
{hex: "#A62320", name: "2027 Currant red"},
|
||||
{hex: "#B9301E", name: "2028 Pearl ruby red"},
|
||||
{hex: "#A91D2E", name: "3000 Fire red"},
|
||||
{hex: "#A1262A", name: "3001 Signal red"},
|
||||
{hex: "#8F2626", name: "3002 Carmine red"},
|
||||
{hex: "#9B2428", name: "3003 Ruby red"},
|
||||
{hex: "#872241", name: "3004 Purple red"},
|
||||
{hex: "#722F37", name: "3005 Wine red"},
|
||||
{hex: "#3D1818", name: "3007 Black red"},
|
||||
{hex: "#8A3324", name: "3009 Oxide red"},
|
||||
{hex: "#A22C29", name: "3011 Brown red"},
|
||||
{hex: "#B22D20", name: "3012 Pine red"},
|
||||
{hex: "#D23741", name: "3013 Tomato red"},
|
||||
{hex: "#A8282E", name: "3014 Antique pink"},
|
||||
{hex: "#C8283C", name: "3015 Light pink"},
|
||||
{hex: "#D23F39", name: "3016 Coral red"},
|
||||
{hex: "#D1425E", name: "3017 Rose"},
|
||||
{hex: "#C4232F", name: "3018 Strawberry red"},
|
||||
{hex: "#C8102E", name: "3020 Traffic red"},
|
||||
{hex: "#E36A5A", name: "3022 Salmon pink"},
|
||||
{hex: "#E63E62", name: "3024 Luminous rose"},
|
||||
{hex: "#E02A27", name: "3026 Luminous bright red"},
|
||||
{hex: "#B8315F", name: "3027 Raspberry red"},
|
||||
{hex: "#C61C23", name: "3028 Pure red"},
|
||||
{hex: "#AD2932", name: "3031 Orient red"},
|
||||
{hex: "#D32F2F", name: "3032 Pearl ruby"},
|
||||
{hex: "#D13C6F", name: "3033 Pearl pink"},
|
||||
{hex: "#7B2B5F", name: "4001 Red purple"},
|
||||
{hex: "#912E8C", name: "4002 Red violet"},
|
||||
{hex: "#9C366F", name: "4003 Heather violet"},
|
||||
{hex: "#6A1F49", name: "4004 Bordeaux violet"},
|
||||
{hex: "#4B2663", name: "4005 Blue violet"},
|
||||
{hex: "#5C2D3C", name: "4006 Mahogany"},
|
||||
{hex: "#5E3559", name: "4007 Grey purple"},
|
||||
{hex: "#9C3568", name: "4008 Signal violet"},
|
||||
{hex: "#5E6E3A", name: "4009 Fir green"},
|
||||
{hex: "#5B5B8A", name: "4010 Sabine grey"},
|
||||
{hex: "#8F3A70", name: "4011 Pearl violet"},
|
||||
{hex: "#5E3252", name: "4012 Pearl blackberry"},
|
||||
{hex: "#1B4F72", name: "5001 Green blue"},
|
||||
{hex: "#18669E", name: "5002 Ultramarine blue"},
|
||||
{hex: "#0F4C8A", name: "5003 Sapphire blue"},
|
||||
{hex: "#0D324A", name: "5004 Black blue"},
|
||||
{hex: "#1B5583", name: "5005 Signal blue"},
|
||||
{hex: "#2C5F9D", name: "5007 Brilliant blue"},
|
||||
{hex: "#3B5AA9", name: "5008 Grey blue"},
|
||||
{hex: "#0077A3", name: "5009 Azure blue"},
|
||||
{hex: "#0A558C", name: "5010 Gentian blue"},
|
||||
{hex: "#2B547E", name: "5011 Steel blue"},
|
||||
{hex: "#0066A1", name: "5012 Light blue"},
|
||||
{hex: "#124780", name: "5013 Cobalt blue"},
|
||||
{hex: "#1F4E79", name: "5014 Pigeon blue"},
|
||||
{hex: "#0384A7", name: "5015 Sky blue"},
|
||||
{hex: "#006A9E", name: "5017 Traffic blue"},
|
||||
{hex: "#005F87", name: "5018 Turquoise blue"},
|
||||
{hex: "#5A3D5B", name: "5019 Capsanthin violet"},
|
||||
{hex: "#04749F", name: "5020 Ocean blue"},
|
||||
{hex: "#009AAA", name: "5021 Water blue"},
|
||||
{hex: "#1A3D5F", name: "5022 Night blue"},
|
||||
{hex: "#047AA6", name: "5023 Distant blue"},
|
||||
{hex: "#5B9BB8", name: "5024 Pastel blue"},
|
||||
{hex: "#3C6A98", name: "5025 Pearl gentian blue"},
|
||||
{hex: "#1C3F5F", name: "5026 Pearl midnight blue"},
|
||||
{hex: "#36673A", name: "6000 Patina green"},
|
||||
{hex: "#0E5C34", name: "6001 Emerald green"},
|
||||
{hex: "#437034", name: "6002 Leaf green"},
|
||||
{hex: "#3F3F2A", name: "6003 Olive green"},
|
||||
{hex: "#2B4E52", name: "6004 Blue green"},
|
||||
{hex: "#3B5F3F", name: "6005 Moss green"},
|
||||
{hex: "#4A563C", name: "6006 Grey olive"},
|
||||
{hex: "#2B4638", name: "6007 Pine green"},
|
||||
{hex: "#4A4139", name: "6008 Pine brown"},
|
||||
{hex: "#3B3A2E", name: "6009 Fir green"},
|
||||
{hex: "#3E7A5F", name: "6010 Grass green"},
|
||||
{hex: "#5E716A", name: "6011 Reseda green"},
|
||||
{hex: "#3A4E48", name: "6012 Brown green"},
|
||||
{hex: "#6B8E23", name: "6013 Reed green"},
|
||||
{hex: "#6B8E23", name: "6014 Yellow green"},
|
||||
{hex: "#1E3E2F", name: "6015 Black green"},
|
||||
{hex: "#1E4D4B", name: "6016 Turquoise green"},
|
||||
{hex: "#4C7C47", name: "6017 May green"},
|
||||
{hex: "#9CBF60", name: "6018 Yellow green"},
|
||||
{hex: "#2E5A3A", name: "6019 Pine green"},
|
||||
{hex: "#2E5A3A", name: "6020 Chrome oxide green"},
|
||||
{hex: "#9DC0A5", name: "6021 Pale green"},
|
||||
{hex: "#4A563C", name: "6022 Brown green"},
|
||||
{hex: "#009E60", name: "6024 Traffic green"},
|
||||
{hex: "#3F7044", name: "6025 Fern green"},
|
||||
{hex: "#4F7942", name: "6026 Opal green"},
|
||||
{hex: "#3B5F3F", name: "6027 Pine green"},
|
||||
{hex: "#3B5F3F", name: "6028 Pine green"},
|
||||
{hex: "#00A550", name: "6029 Mint green"},
|
||||
{hex: "#3E5A40", name: "6031 Bronze green"},
|
||||
{hex: "#3D7E5A", name: "6032 Pearl green"},
|
||||
{hex: "#007D6E", name: "6033 Sax turquoise"},
|
||||
{hex: "#3D7E5A", name: "6034 Pearl turquoise"},
|
||||
{hex: "#3D7E5A", name: "6035 Pearl green"},
|
||||
{hex: "#3D7E5A", name: "6036 Pearl jade"},
|
||||
{hex: "#00A550", name: "6037 Pure green"},
|
||||
{hex: "#00A550", name: "6038 Luminous green"},
|
||||
{hex: "#00A550", name: "6039 Mint green"},
|
||||
{hex: "#7D7D7D", name: "7000 Squirrel grey"},
|
||||
{hex: "#A8A9AD", name: "7001 Silver grey"},
|
||||
{hex: "#6F7368", name: "7002 Olivestone"},
|
||||
{hex: "#8F9080", name: "7003 Moss grey"},
|
||||
{hex: "#8E9291", name: "7004 Signal grey"},
|
||||
{hex: "#8B8788", name: "7005 Mouse grey"},
|
||||
{hex: "#9A8373", name: "7006 Beige grey"},
|
||||
{hex: "#828C6A", name: "7008 Khaki grey"},
|
||||
{hex: "#6F7368", name: "7009 Green grey"},
|
||||
{hex: "#5F6563", name: "7010 Tarpaulin grey"},
|
||||
{hex: "#4A5656", name: "7011 Iron grey"},
|
||||
{hex: "#4D5656", name: "7012 Basalt grey"},
|
||||
{hex: "#635147", name: "7013 Brown grey"},
|
||||
{hex: "#6B6B83", name: "7015 Slate grey"},
|
||||
{hex: "#32373D", name: "7016 Anthracite grey"},
|
||||
{hex: "#8B8B83", name: "7021 Light grey"},
|
||||
{hex: "#716347", name: "7022 Umber grey"},
|
||||
{hex: "#76787A", name: "7023 Concrete grey"},
|
||||
{hex: "#5E6565", name: "7024 Graphite grey"},
|
||||
{hex: "#5E6565", name: "7026 Granite grey"},
|
||||
{hex: "#8F9494", name: "7030 Stone grey"},
|
||||
{hex: "#4A5656", name: "7031 Iron grey"},
|
||||
{hex: "#B2BFC0", name: "7032 Pebble grey"},
|
||||
{hex: "#9B9B9B", name: "7033 Cement grey"},
|
||||
{hex: "#8F9D9B", name: "7034 Yellow grey"},
|
||||
{hex: "#CBD0CC", name: "7035 Light grey"},
|
||||
{hex: "#6D7A80", name: "7036 Platinum grey"},
|
||||
{hex: "#8A9191", name: "7037 Dusty grey"},
|
||||
{hex: "#7E8181", name: "7038 Agate grey"},
|
||||
{hex: "#6E7D7E", name: "7039 Quartz grey"},
|
||||
{hex: "#8A9191", name: "7040 Window grey"},
|
||||
{hex: "#8A9191", name: "7042 Traffic grey A"},
|
||||
{hex: "#6E7D7E", name: "7043 Traffic grey B"},
|
||||
{hex: "#9B9B9B", name: "7044 Silk grey"},
|
||||
{hex: "#A1A7B0", name: "7045 Telegrey 1"},
|
||||
{hex: "#7E848C", name: "7046 Telegrey 2"},
|
||||
{hex: "#7E848C", name: "7047 Telegrey 4"},
|
||||
{hex: "#6D7A80", name: "7048 Pearl mouse grey"},
|
||||
{hex: "#786D5F", name: "8000 Green brown"},
|
||||
{hex: "#8B7355", name: "8001 Ochre brown"},
|
||||
{hex: "#8B6F47", name: "8003 Clay brown"},
|
||||
{hex: "#8B6F47", name: "8004 Copper brown"},
|
||||
{hex: "#5C4033", name: "8007 Chocolate brown"},
|
||||
{hex: "#6E5C4A", name: "8008 Olive brown"},
|
||||
{hex: "#8B4513", name: "8011 Nut brown"},
|
||||
{hex: "#8B4513", name: "8012 Red brown"},
|
||||
{hex: "#5C3317", name: "8014 Sepia brown"},
|
||||
{hex: "#854636", name: "8015 Chestnut brown"},
|
||||
{hex: "#8B4513", name: "8016 Mahogany brown"},
|
||||
{hex: "#5C3317", name: "8017 Chocolate brown"},
|
||||
{hex: "#786D5F", name: "8019 Grey brown"},
|
||||
{hex: "#854636", name: "8022 Chestnut brown"},
|
||||
{hex: "#8B6F47", name: "8023 Pearl copper"},
|
||||
{hex: "#8B7355", name: "8024 Beige brown"},
|
||||
{hex: "#8B6F47", name: "8025 Pearl mocha brown"},
|
||||
{hex: "#8B6F47", name: "8027 Clay brown"},
|
||||
{hex: "#8B6F47", name: "8028 Earth brown"},
|
||||
{hex: "#8B6F47", name: "8029 Pearl brown"},
|
||||
{hex: "#FAF0E6", name: "9001 Cream"},
|
||||
{hex: "#EDECE6", name: "9002 Grey white"},
|
||||
{hex: "#FFFFFF", name: "9003 Signal white"},
|
||||
{hex: "#000000", name: "9004 Signal black"},
|
||||
{hex: "#1D1D1D", name: "9005 Jet black"},
|
||||
{hex: "#C0C0C0", name: "9006 White aluminium"},
|
||||
{hex: "#8C7853", name: "9007 Grey aluminium"},
|
||||
{hex: "#FFFFFF", name: "9010 Pure white"},
|
||||
{hex: "#1D1D1D", name: "9011 Graphite black"},
|
||||
{hex: "#FFFFFF", name: "9016 Traffic white"},
|
||||
{hex: "#000000", name: "9017 Traffic black"},
|
||||
{hex: "#FAF0E6", name: "9018 Papyrus white"},
|
||||
{hex: "#B2BFC0", name: "9022 Pearl light grey"}
|
||||
],
|
||||
gradient: [
|
||||
{grad: "#FFF47B-#CD9800", name: "Metal: Gold"},
|
||||
{grad: "#373737-#F5F4F4", name: "Metal: Silver"},
|
||||
{grad: "#C39292-#C73725", name: "Metal: Copper"},
|
||||
{grad: "#DED699-#C99620", name: "Metal: Brass"},
|
||||
{grad: "#6E4316-#B0742E", name: "Metal: Bronze"},
|
||||
// More than two colors: only listed when the page enables multi-color gradients.
|
||||
{grad: "#FF0000-#FFFF00-#00FF00-#00FFFF-#0000FF-#FF00FF-#FF0000", name: "Nature: Rainbow"},
|
||||
|
||||
{grad: "#D8FEE7-#FFF5BD", name: "Nature: Pearl"},
|
||||
{grad: "#1E5AA8-#3D8BD9-#7DB9F0-#C9E6FF", name: "Nature: Clear Sky"},
|
||||
{grad: "#2B1B5A-#7A2E7E-#D9466E-#F57C3A-#FFC857", name: "Nature: Sunset"},
|
||||
{grad: "#3B2416-#5A3A24-#45291A-#6B4630-#3B2416", name: "Wood: Dark"},
|
||||
{grad: "#8B5A2B-#A8733F-#946235-#B5844F-#8B5A2B", name: "Wood: Medium"},
|
||||
{grad: "#D9B98C-#E8CFA6-#D3AE7C-#EBD5B0-#D9B98C", name: "Wood: Light"},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
window.ColorPickerPalettes = palettes;
|
||||
})();
|
||||
@@ -0,0 +1,534 @@
|
||||
:root {
|
||||
--thumb-size : 20px;
|
||||
--field-height: 28px;
|
||||
--number-width: 52px;
|
||||
--preview-size: 52px;
|
||||
--checkerboard: conic-gradient(var(--checker-light) 25%, var(--checker-dark) 0 50%, var(--checker-light) 0 75%, var(--checker-dark) 0);
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
/* Report the full content width even before the native host expands its viewport.
|
||||
WebKit's fit-content can clip the sidebar and retain the expanded width on close. */
|
||||
width: max-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--orca-font, system-ui, sans-serif);
|
||||
background: var(--bg-color);
|
||||
color: var(--fg-color-text);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#selectionNotice {
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
margin-bottom: 8px;
|
||||
color: var(--fg-color-label);
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
/* ---------- Palette sidebar ---------- */
|
||||
.left-panel {
|
||||
flex: 0 0 160px;
|
||||
width: 160px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
.left-panel.open {
|
||||
display: flex;
|
||||
padding-right: 0px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#paletteCombo {
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Combo box or filter box, with the search / close button on the right. */
|
||||
.palette-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
height: 25px;
|
||||
margin-right:5px;
|
||||
}
|
||||
.palette-toolbar > .palette-select { flex: 1 }
|
||||
.palette-filter {
|
||||
flex: 1;
|
||||
height: 25px;
|
||||
padding: 0 6px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
color: var(--fg-color-text);
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--main-color);
|
||||
outline: none;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
.palette-filter::-webkit-search-cancel-button { -webkit-appearance: none }
|
||||
.palette-filter:focus-visible{
|
||||
background-color: var(--focus-bg-box);
|
||||
}
|
||||
|
||||
.palette-search-button {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: var(--button-bg-normal);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 0;
|
||||
}
|
||||
.palette-search-button:hover {
|
||||
background-color: var(--button-bg-hover);
|
||||
}
|
||||
|
||||
.palette-search-button .icon-search {
|
||||
--icon-url: var(--icon-search);
|
||||
}
|
||||
.palette-search-button .icon-close {
|
||||
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g style="fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round"><line x1="3.5" y1="2.5" x2="13.5" y2="12.5"/><line x1="3.5" y1="12.5" x2="13.5" y2="2.5"/></g></svg>');;
|
||||
}
|
||||
.palette-search-button .icon-close,
|
||||
.palette-search-button[aria-pressed="true"] .icon-search { display: none }
|
||||
.palette-search-button[aria-pressed="true"] .icon-close { display: block }
|
||||
.palette-empty {
|
||||
flex: 1;
|
||||
padding: 8px 2px;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color-label);
|
||||
}
|
||||
|
||||
.palette {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 1px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1px 2px 1px 0;
|
||||
border:1px solid transparent;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color-text);
|
||||
}
|
||||
|
||||
.color-item:hover {
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
flex: 0 0 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
margin-right: 5px;
|
||||
margin: 0 5px 0 1px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.color-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ---------- Picker ---------- */
|
||||
.right-panel {
|
||||
flex: 0 0 330px;
|
||||
/* WebKit uses width, rather than flex-basis, for the parent's intrinsic width. */
|
||||
width: 330px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spectrum-row {
|
||||
--spectrum-height: 150px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
height: var(--spectrum-height);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.spectrum-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
border-radius: 7px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#spectrum {
|
||||
position: absolute; /* Absolute so the canvas's pixel size never feeds back into the flex layout */
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#pickerEditors:disabled #spectrum { pointer-events: none }
|
||||
|
||||
#colorIndicator {
|
||||
position: absolute;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.saturation-slider {
|
||||
position: relative;
|
||||
flex: 0 0 var(--thumb-size);
|
||||
}
|
||||
|
||||
#satSlider {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
flex: none;
|
||||
width: var(--spectrum-height);
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
}
|
||||
|
||||
.valueSlider {
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.valueSlider::-webkit-slider-runnable-track {
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--track-gradient);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.valueSlider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
background: var(--thumb-color);
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
/* Left column: mode switch, hex + alpha, gradient endpoints. Right: H S L and R G B columns. */
|
||||
.editor-grid {
|
||||
--editor-grid-gap: 5px;
|
||||
display: flex;
|
||||
gap: var(--editor-grid-gap);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.editor-column:first-of-type {flex:1}
|
||||
.editor-column {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, var(--field-height));
|
||||
gap: var(--editor-grid-gap);
|
||||
}
|
||||
|
||||
.hex-row {
|
||||
display: flex;
|
||||
gap: var(--editor-grid-gap);
|
||||
}
|
||||
|
||||
.switch-check {
|
||||
display: none !important;
|
||||
}
|
||||
.switch-label {
|
||||
font-size: 13px;
|
||||
padding: 2px 10px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.switch-slider {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
border-radius: 20px;
|
||||
background: var(--bg-color-alt);
|
||||
border: 2px solid var(--bg-color-alt);
|
||||
height:26px;
|
||||
}
|
||||
.switch-slider:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
left: 0%;
|
||||
border-radius:20px;
|
||||
background: var(--main-color);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.switch-check:checked + .switch-slider:before {
|
||||
left: 50%;
|
||||
}
|
||||
.switch-check:checked + .switch-slider div:first-child,
|
||||
.switch-check + .switch-slider div:last-child{
|
||||
color: var(--fg-color-label);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.switch-check:checked + .switch-slider div:last-child,
|
||||
.switch-check + .switch-slider div:first-child{
|
||||
color: var(--button-fg-light);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* H S L / R G B / Hex / Alpha input boxes */
|
||||
.inline-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
.inline-field > label {
|
||||
width: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--fg-color-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.inline-field > input {
|
||||
flex: 1;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0 2px 0 5px;
|
||||
color: var(--fg-color-text);
|
||||
font-size: 15px;
|
||||
line-height: var(--field-height);
|
||||
appearance: textfield; /* No spinner buttons; Up/Down keys and the wheel still step by 1. */
|
||||
}
|
||||
.inline-field:focus-within {
|
||||
border-color: var(--main-color);
|
||||
background: var(--focus-bg-box);
|
||||
}
|
||||
.inline-field:has(input:invalid) {
|
||||
border-color: var(--control-invalid);
|
||||
}
|
||||
.inline-field > input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inline-field > input[type="number"]::-webkit-inner-spin-button,
|
||||
.inline-field > input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none }
|
||||
|
||||
.value-field { width: var(--number-width)}
|
||||
.field-hex { flex: 1 }
|
||||
#alphaControls {
|
||||
width: calc(var(--number-width) + 2px)
|
||||
}
|
||||
#alphaControls > input {
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
/* Third row holds the gradient bar and the opacity slider in the same cell. The slider only
|
||||
shows while the opacity field or the slider itself is in use, and hides the gradient bar. */
|
||||
.third-row { position: relative }
|
||||
.third-row > * {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.third-row.opacity-open > #opacitySliderBar { display: flex }
|
||||
.third-row.opacity-open > #gradientCtrlBar { display: none }
|
||||
|
||||
#opacitySlider { width: 100%; }
|
||||
#opacitySlider::-webkit-slider-runnable-track {
|
||||
background: var(--track-gradient), var(--checkerboard); /* Transparent end shows the checkerboard through the color. */
|
||||
background-size: auto, 8px 8px;
|
||||
}
|
||||
|
||||
/* Gradient colors: thumbs are evenly spaced and fixed; a click anywhere selects the nearest. */
|
||||
#gradientCtrlBar {
|
||||
display: flex;
|
||||
}
|
||||
.gradient-stops {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
/* Kept 10px away from the gradient bar so a click near its right end can't hit − or +. */
|
||||
.gradient-step-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 10px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.gradient-step-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border-radius: 4px;
|
||||
background: var(--button-bg-normal);
|
||||
color: var(--icon-color);
|
||||
}
|
||||
.gradient-step-button svg { display: block; flex: none }
|
||||
.gradient-step-button:hover:not([aria-disabled="true"]) { background: var(--button-bg-hover) }
|
||||
.gradient-step-button[aria-disabled="true"] { opacity: 0.4; cursor: default }
|
||||
.gradient-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 50%;
|
||||
height: var(--thumb-size);
|
||||
transform: translateY(-50%);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
|
||||
background-size: auto, 8px 8px;
|
||||
}
|
||||
.gradient-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
border: 1px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 1px var(--color-marker-shadow);
|
||||
background: var(--thumb-color);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
/* Thumb i of n sits at 10px + (100% - 20px) * i / (n - 1), set inline as --stop. */
|
||||
.gradient-thumb { left: calc(10px + (100% - 20px) * var(--stop, 0)) }
|
||||
.gradient-thumb[aria-pressed="true"] {
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.gradient-thumb:hover {
|
||||
box-shadow: 0 0 0 3px var(--color-marker-shadow);
|
||||
}
|
||||
|
||||
/* ---------- Original / current / favorites ---------- */
|
||||
.bottom-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: var(--preview-size);
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--preview-size);
|
||||
height: var(--preview-size);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.color-preview svg {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.color-preview:hover svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.saved-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(0, 24px));
|
||||
grid-template-rows: repeat(2, minmax(0, 24px));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.saved-swatch {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saved-swatch.empty {
|
||||
background: var(--bg-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Each layer is separate so transparent RGBA shows the checkerboard beneath it. */
|
||||
.color-swatch, .saved-swatch, .color-preview {
|
||||
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
|
||||
background-size: auto, 12px 12px;
|
||||
}
|
||||
.color-item { width: 100%; background: transparent; text-align: left; font-family: inherit; }
|
||||
|
||||
/* ---------- Actions ---------- */
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.dialog-actions button { margin: 0 }
|
||||
#confirmBtn[disabled] { opacity: 0.5 }
|
||||
.stretch-spacer { flex: 1 }
|
||||
|
||||
.icon-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
background: var(--button-bg-normal);
|
||||
color: var(--fg-color-text);
|
||||
}
|
||||
.icon-button:hover {
|
||||
background: var(--button-bg-hover);
|
||||
}
|
||||
.icon-button[aria-expanded="true"] {
|
||||
background: var(--main-color);
|
||||
color: var(--button-fg-light);
|
||||
}
|
||||
.icon-button[aria-expanded="true"]:hover {
|
||||
background: var(--main-color-hover);
|
||||
}
|
||||
|
||||
.hidden { display: none !important }
|
||||
.invisible { visibility: hidden }
|
||||
|
||||
.color-item:focus-visible, .saved-swatch:focus-visible,
|
||||
select:focus-visible, button:focus-visible { outline: 2px solid var(--main-color); outline-offset: -1px; }
|
||||
@@ -55,6 +55,13 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #efefef;
|
||||
--speed-tile-border: #d8d8d8;
|
||||
|
||||
/* Transparency and control markers are UI surfaces, never color data. */
|
||||
--checker-light: #eeeeee;
|
||||
--checker-dark: #aaaaaa;
|
||||
--color-marker-outline: #ffffff;
|
||||
--color-marker-shadow: rgba(0, 0, 0, 0.7);
|
||||
--control-invalid: var(--plugin-status-danger);
|
||||
}
|
||||
|
||||
:root[data-orca-theme="dark"] {
|
||||
@@ -93,6 +100,8 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #34343b;
|
||||
--speed-tile-border: #50505a;
|
||||
--checker-light: #68686f;
|
||||
--checker-dark: #414148;
|
||||
}
|
||||
|
||||
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
|
||||
|
||||
@@ -10,6 +10,11 @@ function OnInit()
|
||||
// Orca: fallback timeout in case the C++ -> JS signal fails (e.g., WebKit issues).
|
||||
// Jump to the target page after 3 minutes so slow computers don't get stuck on a partially loaded page.
|
||||
setTimeout("JumpToTarget()",180*1000);
|
||||
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="userguide_loading_page_ready";
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
}
|
||||
|
||||
function HandleStudio( pVal )
|
||||
|
||||
@@ -506,11 +506,6 @@ body
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.BtnShortcut
|
||||
{
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.ShortcutBrandIcon
|
||||
{
|
||||
width: 20px;
|
||||
@@ -519,25 +514,6 @@ body
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ShortcutTextRow
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ShortcutMark
|
||||
{
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-left: auto;
|
||||
color: currentColor;
|
||||
opacity: 0.72;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
/*--------------------*/
|
||||
#RightBoard
|
||||
@@ -548,6 +524,20 @@ body
|
||||
overflow-y:auto;
|
||||
}
|
||||
|
||||
/*---- Cloud mode: OrcaCloud is a native pane to the right of the app panel, so this page
|
||||
is reduced to its left nav column (the class is toggled by SetCloudMode in js/home.js). ----*/
|
||||
body.cloud-mode #RightBoard
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.cloud-mode #LeftBoard
|
||||
{
|
||||
/*---- 262px of column plus its 1px separator is the width the app gives the nav. ----*/
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#ContentBoard
|
||||
{
|
||||
overflow-y:auto;
|
||||
|
||||
@@ -90,16 +90,9 @@
|
||||
<div class="BtnIcon "><img class="LeftIcon" src="img/i2.png" /></div>
|
||||
<div data-i18n="recent">Recent</div>
|
||||
</div>
|
||||
<div class="BtnItem BtnShortcut" onClick="OpenUrlInLocalBrowser('https://cloud.orcaslicer.com')">
|
||||
<div class="BtnItem BtnShortcut" onClick="GotoCloud()">
|
||||
<div class="BtnIcon "><img class="ShortcutBrandIcon" src="../image/logo.png" /></div>
|
||||
<div class="ShortcutTextRow">
|
||||
<div>OrcaCloud</div>
|
||||
<svg class="ShortcutMark" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M6 3.5H3.5C2.95 3.5 2.5 3.95 2.5 4.5V12.5C2.5 13.05 2.95 13.5 3.5 13.5H11.5C12.05 13.5 12.5 13.05 12.5 12.5V10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.5 3.5H13.5V8.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5 3.5L7 10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>OrcaCloud</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -145,8 +145,36 @@ function HandleStudio( pVal )
|
||||
}
|
||||
}
|
||||
|
||||
/*---- OrcaCloud opens as a native pane beside this page, so the shell only switches its
|
||||
own cloud-mode state and asks the app to show or hide that pane (WebViewPanel::set_cloud_mode). ----*/
|
||||
var m_CloudMode=false;
|
||||
|
||||
function SetCloudMode( bOn )
|
||||
{
|
||||
if( m_CloudMode==bOn )
|
||||
return;
|
||||
|
||||
m_CloudMode=bOn;
|
||||
$("body").toggleClass("cloud-mode", bOn);
|
||||
}
|
||||
|
||||
function GotoCloud()
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
SetCloudMode(true);
|
||||
SendSimpleCommand("homepage_open_cloud");
|
||||
}
|
||||
|
||||
function GotoMenu( strMenu )
|
||||
{
|
||||
/*---- Any board in the right region means leaving the cloud pane. ----*/
|
||||
if( m_CloudMode )
|
||||
{
|
||||
SetCloudMode(false);
|
||||
SendSimpleCommand("homepage_close_cloud");
|
||||
}
|
||||
|
||||
let MenuList=$(".BtnItem");
|
||||
let nAll=MenuList.length;
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ REQUIRED_DEV_PACKAGES=(
|
||||
libtool
|
||||
m4
|
||||
Mesa-libGL-devel
|
||||
ninja-build
|
||||
ninja
|
||||
openssl-devel
|
||||
perl-FindBin-Real
|
||||
texinfo
|
||||
wayland-protocols-devel
|
||||
webkit2gtk4-devel
|
||||
webkitgtk3-devel
|
||||
wget
|
||||
libcurl-devel
|
||||
pkgconf
|
||||
|
||||
+16
-1
@@ -144,6 +144,7 @@
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/GCode/ToolOrdering.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
@@ -207,6 +208,7 @@ std::map<int, std::string> cli_errors = {
|
||||
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
|
||||
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
|
||||
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
|
||||
{CLI_EXPORT_STATISTICS_ERROR, "Failed exporting the G-code statistics."},
|
||||
{CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."},
|
||||
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
|
||||
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
|
||||
@@ -1539,6 +1541,9 @@ int CLI::run(int argc, char **argv)
|
||||
if (allow_mix_temp_option)
|
||||
allow_mix_temp = allow_mix_temp_option->value;
|
||||
|
||||
const ConfigOptionBool* export_statistics_option = m_config.option<ConfigOptionBool>("export_statistics");
|
||||
const bool export_statistics = export_statistics_option != nullptr && export_statistics_option->value;
|
||||
|
||||
ConfigOptionBool* avoid_extrusion_cali_region_option = m_config.option<ConfigOptionBool>("avoid_extrusion_cali_region");
|
||||
if (avoid_extrusion_cali_region_option)
|
||||
avoid_extrusion_cali_region = avoid_extrusion_cali_region_option->value;
|
||||
@@ -7081,6 +7086,16 @@ int CLI::run(int argc, char **argv)
|
||||
record_exit_reson(outfile_dir, CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, index + 1, cli_errors[CLI_GCODE_PATH_IN_UNPRINTABLE_AREA], sliced_info);
|
||||
flush_and_exit(CLI_GCODE_PATH_IN_UNPRINTABLE_AREA);
|
||||
}
|
||||
// Without an output directory the G-code is a temporary file, kept only in an exported 3MF.
|
||||
if (export_statistics && !outfile_dir.empty()) {
|
||||
try {
|
||||
export_gcode_statistics(*gcode_result, gcode_statistics_path(outfile));
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "plate " << index + 1 << ": " << ex.what();
|
||||
record_exit_reson(outfile_dir, CLI_EXPORT_STATISTICS_ERROR, index + 1, cli_errors[CLI_EXPORT_STATISTICS_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_EXPORT_STATISTICS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//outfile_final = (dynamic_cast<Print*>(print))->print_statistics().finalize_output_path(outfile);
|
||||
@@ -7209,7 +7224,7 @@ int CLI::run(int argc, char **argv)
|
||||
std::vector<ThumbnailData *> thumbnails, no_light_thumbnails, top_thumbnails, pick_thumbnails;
|
||||
std::vector<PlateBBoxData*> plate_bboxes;
|
||||
PlateDataPtrs plate_data_list;
|
||||
partplate_list.store_to_3mf_structure(plate_data_list);
|
||||
partplate_list.store_to_3mf_structure(plate_data_list, true, -1, export_statistics);
|
||||
|
||||
if (sliced_plate == -1) {
|
||||
for (int i = 0; i < plate_data_list.size(); i++) {
|
||||
|
||||
@@ -399,6 +399,9 @@ void AppConfig::set_defaults()
|
||||
if (get("show_plate_gridlines").empty())
|
||||
set_bool("show_plate_gridlines", true);
|
||||
|
||||
if (get("focus_mode").empty())
|
||||
set_bool("focus_mode", false);
|
||||
|
||||
if (get("show_outline").empty())
|
||||
set_bool("show_outline", false);
|
||||
|
||||
|
||||
@@ -265,6 +265,8 @@ set(lisbslic3r_sources
|
||||
GCode/FanMover.hpp
|
||||
GCode/GCodeProcessor.cpp
|
||||
GCode/GCodeProcessor.hpp
|
||||
GCode/GCodeStatistics.cpp
|
||||
GCode/GCodeStatistics.hpp
|
||||
GCode/ElegooGCodeProcessorHelper.cpp
|
||||
GCode.hpp
|
||||
GCode/PchipInterpolatorHelper.cpp
|
||||
|
||||
@@ -9053,6 +9053,16 @@ bool _BBS_3MF_Exporter::_add_gcode_file_to_archive(mz_zip_archive& archive, cons
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", store %1% to 3mf %2%\n") % src_gcode_file % gcode_in_3mf;
|
||||
}
|
||||
});
|
||||
for (const PlateData *plate_data : plate_data_list2) {
|
||||
if (plate_data->statistics.empty())
|
||||
continue;
|
||||
const std::string statistics_in_3mf = (boost::format(STATISTICS_FILE_FORMAT) % (plate_data->plate_index + 1)).str();
|
||||
if (!mz_zip_writer_add_mem(&archive, statistics_in_3mf.c_str(), plate_data->statistics.data(), plate_data->statistics.size(),
|
||||
MZ_DEFAULT_COMPRESSION)) {
|
||||
add_error("Unable to add statistics file to archive");
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ struct ThumbnailData;
|
||||
#define PLATE_THUMBNAIL_SMALL_HEIGHT 128
|
||||
|
||||
#define GCODE_FILE_FORMAT "Metadata/plate_%1%.gcode"
|
||||
#define STATISTICS_FILE_FORMAT "Metadata/plate_%1%.statistics.json"
|
||||
#define THUMBNAIL_FILE_FORMAT "Metadata/plate_%1%.png"
|
||||
#define NO_LIGHT_THUMBNAIL_FILE_FORMAT "Metadata/plate_no_light_%1%.png"
|
||||
#define TOP_FILE_FORMAT "Metadata/top_%1%.png"
|
||||
@@ -100,6 +101,8 @@ struct PlateData
|
||||
std::string nozzle_volume_types;
|
||||
std::string gcode_file;
|
||||
std::string gcode_file_md5;
|
||||
// The G-code's statistics as JSON, stored with the G-code when not empty.
|
||||
std::string statistics;
|
||||
std::string thumbnail_file;
|
||||
std::string no_light_thumbnail_file;
|
||||
ThumbnailData plate_thumbnail;
|
||||
@@ -174,6 +177,8 @@ enum class SaveStrategy
|
||||
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
|
||||
// production-extension value.
|
||||
MinimalPublished = 1 << 13,
|
||||
// With WithGcode, also the G-code's statistics.
|
||||
WithStatistics = 1 << 14,
|
||||
|
||||
SplitModel = 0x1000 | ProductionExt,
|
||||
Encrypted = SecureContentExt | SplitModel,
|
||||
|
||||
+20
-11
@@ -2625,8 +2625,15 @@ static void set_mass_locator(GCodeProcessor &processor, const Print &print)
|
||||
}
|
||||
Object &o = objects.emplace_back(Object{ object, int(object_masses.size()), count, bodies_total, {}, std::move(bodies), {}, {} });
|
||||
object_masses.resize(object_masses.size() + object->instances().size());
|
||||
for (size_t instance = 0; instance < object->instances().size(); ++instance)
|
||||
object_masses[o.first_instance + instance].assembly = assembly;
|
||||
const ModelInstancePtrs &model_instances = object->model_object()->instances;
|
||||
for (size_t instance = 0; instance < object->instances().size(); ++instance) {
|
||||
GCodeProcessorResult::ObjectMass &mass = object_masses[o.first_instance + instance];
|
||||
mass.assembly = assembly;
|
||||
mass.name = object->model_object()->name;
|
||||
// Copies of another rotation or scale slice as objects of their own, so number them in the model object.
|
||||
mass.instance = int(std::find(model_instances.begin(), model_instances.end(), object->instances()[instance].model_instance) -
|
||||
model_instances.begin());
|
||||
}
|
||||
bodies_total += int(count * object->instances().size());
|
||||
for (const Layer *layer : layers) {
|
||||
o.print_zs.emplace_back(layer->print_z);
|
||||
@@ -3563,6 +3570,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
|
||||
print.throw_if_canceled();
|
||||
|
||||
// Taken before the object loop, which reassigns initial_extruder_id when printing by object.
|
||||
const int compat_first_layer_bed_temp = m_config.bed_temperature_formula == BedTempFormula::btfHighestTemp ?
|
||||
get_highest_bed_temperature(true, print) :
|
||||
get_bed_temperature(initial_extruder_id, true, print.config().curr_bed_type);
|
||||
const int compat_first_layer_nozzle_temp =
|
||||
print.config().nozzle_temperature_initial_layer.get_at(get_filament_config_index((int) initial_extruder_id));
|
||||
|
||||
int extruder_id = get_extruder_id(initial_extruder_id);
|
||||
|
||||
m_cooling_buffer = make_unique<CoolingBuffer>(*this);
|
||||
@@ -4153,12 +4167,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple image
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n",
|
||||
first_layer_bed_temperature);
|
||||
file.write_format(
|
||||
"; first_layer_temperature = %d\n",
|
||||
print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
|
||||
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
|
||||
file.write("; CONFIG_BLOCK_END\n\n");
|
||||
} else if (thumbnail_cb != nullptr) {
|
||||
// generate the thumbnails
|
||||
@@ -4743,10 +4753,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple info
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
|
||||
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
|
||||
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
|
||||
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
|
||||
|
||||
//SF TODO
|
||||
|
||||
@@ -298,6 +298,9 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
BoundingBoxf3 box;
|
||||
// Of an object, whether it is an assembly.
|
||||
bool assembly{ false };
|
||||
// Of an object instance, its model object's name and its index among that object's instances.
|
||||
std::string name;
|
||||
int instance{ -1 };
|
||||
|
||||
Sum total() const { return printed_up_to_layer.empty() ? Sum{} : printed_up_to_layer.back(); }
|
||||
void add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "GCodeStatistics.hpp"
|
||||
|
||||
#include "GCodeProcessor.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <ios>
|
||||
#include <map>
|
||||
#include <math.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::ordered_json;
|
||||
|
||||
double rounded(double value, int decimals)
|
||||
{
|
||||
const double scale = std::pow(10., decimals);
|
||||
return std::round(value * scale) / scale;
|
||||
}
|
||||
|
||||
// As the object masses weigh it, at the default density for a filament without one.
|
||||
double weight_of(const GCodeProcessorResult &result, size_t filament, double volume)
|
||||
{
|
||||
const bool has_density = filament < result.filament_densities.size() && result.filament_densities[filament] > 0.f;
|
||||
// mm³ at g/cm³ weigh mg.
|
||||
return 0.001 * volume * (has_density ? result.filament_densities[filament] : DEFAULT_FILAMENT_DENSITY);
|
||||
}
|
||||
|
||||
struct Usage
|
||||
{
|
||||
double time{ 0. };
|
||||
double volume{ 0. };
|
||||
double weight{ 0. };
|
||||
|
||||
void add(const Usage &other)
|
||||
{
|
||||
time += other.time;
|
||||
volume += other.volume;
|
||||
weight += other.weight;
|
||||
}
|
||||
|
||||
json to_json() const { return json{ { "time", rounded(time, 3) }, { "volume", rounded(volume, 3) }, { "weight", rounded(weight, 4) } }; }
|
||||
};
|
||||
|
||||
json features_json(const std::map<ExtrusionRole, Usage> &features)
|
||||
{
|
||||
json out = json::object();
|
||||
for (const auto &[role, usage] : features)
|
||||
out[ExtrusionEntity::role_to_string(role)] = usage.to_json();
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string gcode_statistics_json(const GCodeProcessorResult &result)
|
||||
{
|
||||
constexpr size_t normal = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
|
||||
struct Layer
|
||||
{
|
||||
float z{ 0.f };
|
||||
double travel_time{ 0. };
|
||||
Usage total;
|
||||
std::map<ExtrusionRole, Usage> features;
|
||||
};
|
||||
std::vector<Layer> layers;
|
||||
std::map<ExtrusionRole, Usage> features;
|
||||
double travel_time = 0.;
|
||||
|
||||
// As the preview sums them: a layer takes the time of all its moves, a feature that of its extrusions.
|
||||
for (const GCodeProcessorResult::MoveVertex &move : result.moves) {
|
||||
if (layers.size() <= move.layer_id)
|
||||
layers.resize(move.layer_id + 1);
|
||||
Layer &layer = layers[move.layer_id];
|
||||
const double time = move.time[normal];
|
||||
layer.total.time += time;
|
||||
if (move.type == EMoveType::Travel) {
|
||||
layer.travel_time += time;
|
||||
travel_time += time;
|
||||
}
|
||||
// A vertex added inside a move to show its acceleration repeats the move's extrusion.
|
||||
if (move.type != EMoveType::Extrude || move.internal_only)
|
||||
continue;
|
||||
if (move.extrusion_role != erCustom)
|
||||
layer.z = move.position.z();
|
||||
Usage usage;
|
||||
usage.time = time;
|
||||
usage.volume = double(move.delta_extruder) * M_PI * sqr(0.5 * double(result.filament_diameters[move.extruder_id]));
|
||||
usage.weight = weight_of(result, move.extruder_id, usage.volume);
|
||||
layer.total.volume += usage.volume;
|
||||
layer.total.weight += usage.weight;
|
||||
layer.features[move.extrusion_role].add(usage);
|
||||
features[move.extrusion_role].add(usage);
|
||||
}
|
||||
|
||||
// As the preview's filament table, which counts the flush too.
|
||||
const PrintEstimatedStatistics &stats = result.print_statistics;
|
||||
double volume = 0., weight = 0., cost = 0.;
|
||||
json filaments_json = json::array();
|
||||
for (const auto &entry : stats.total_volumes_per_extruder) {
|
||||
const size_t filament = entry.first;
|
||||
const auto use = [&result, filament](const std::map<size_t, double> &volumes) {
|
||||
const auto it = volumes.find(filament);
|
||||
const double used = it == volumes.end() ? 0. : it->second;
|
||||
return json{ { "volume", rounded(used, 3) }, { "weight", rounded(weight_of(result, filament, used), 4) } };
|
||||
};
|
||||
const double filament_weight = weight_of(result, filament, entry.second);
|
||||
// Priced per kilogram.
|
||||
const double filament_cost = filament < result.filament_costs.size() ? 0.001 * filament_weight * result.filament_costs[filament] : 0.;
|
||||
volume += entry.second;
|
||||
weight += filament_weight;
|
||||
cost += filament_cost;
|
||||
filaments_json.push_back(json{ { "id", filament + 1 },
|
||||
{ "length", rounded(entry.second / (M_PI * sqr(0.5 * double(result.filament_diameters[filament]))), 2) },
|
||||
{ "volume", rounded(entry.second, 3) },
|
||||
{ "weight", rounded(filament_weight, 4) },
|
||||
{ "cost", rounded(filament_cost, 2) },
|
||||
{ "model", use(stats.model_volumes_per_extruder) },
|
||||
{ "support", use(stats.support_volumes_per_extruder) },
|
||||
{ "prime_tower", use(stats.wipe_tower_volumes_per_extruder) },
|
||||
{ "flush", use(stats.flush_per_filament) } });
|
||||
}
|
||||
|
||||
json summary = json{ { "time", rounded(stats.modes[normal].time, 3) },
|
||||
{ "prepare_time", rounded(stats.modes[normal].prepare_time, 3) },
|
||||
{ "travel_time", rounded(travel_time, 3) },
|
||||
{ "travel_distance", rounded(stats.total_travel_distance, 2) },
|
||||
{ "layer_count", layers.size() },
|
||||
{ "volume", rounded(volume, 3) },
|
||||
{ "weight", rounded(weight, 4) },
|
||||
{ "filament_cost", rounded(cost, 2) },
|
||||
{ "filament_changes", stats.total_filament_changes },
|
||||
{ "filaments", filaments_json },
|
||||
{ "features", features_json(features) } };
|
||||
|
||||
json layers_json = json::array();
|
||||
for (const Layer &layer : layers)
|
||||
layers_json.push_back(json{ { "z", rounded(layer.z, 3) },
|
||||
{ "time", rounded(layer.total.time, 3) },
|
||||
{ "travel_time", rounded(layer.travel_time, 3) },
|
||||
{ "volume", rounded(layer.total.volume, 3) },
|
||||
{ "weight", rounded(layer.total.weight, 4) },
|
||||
{ "features", features_json(layer.features) } });
|
||||
|
||||
// The supports of an instance are numbered as the instance.
|
||||
json objects_json = json::array();
|
||||
for (size_t i = 0; i < result.object_masses.size(); ++i) {
|
||||
const GCodeProcessorResult::ObjectMass &object = result.object_masses[i];
|
||||
const GCodeProcessorResult::ObjectMass::Sum part = object.total();
|
||||
const GCodeProcessorResult::ObjectMass::Sum support = i < result.support_masses.size() ? result.support_masses[i].total() :
|
||||
GCodeProcessorResult::ObjectMass::Sum{};
|
||||
// In mg, as volumes in mm³ at g/cm³.
|
||||
objects_json.push_back(json{ { "name", object.name },
|
||||
{ "instance", object.instance },
|
||||
{ "volume", rounded(part.volume, 3) },
|
||||
{ "weight", rounded(0.001 * part.mass, 4) },
|
||||
{ "support_volume", rounded(support.volume, 3) },
|
||||
{ "support_weight", rounded(0.001 * support.mass, 4) } });
|
||||
}
|
||||
|
||||
// The version goes up only when a key is removed, renamed or changes meaning.
|
||||
const json out = json{ { "version", 1 },
|
||||
{ "generator", std::string(SLIC3R_APP_NAME) + " " + SoftFever_VERSION },
|
||||
{ "units", json{ { "time", "s" }, { "length", "mm" }, { "volume", "mm3" }, { "weight", "g" } } },
|
||||
{ "summary", summary },
|
||||
{ "layers", layers_json },
|
||||
{ "objects", objects_json } };
|
||||
return out.dump(2);
|
||||
}
|
||||
|
||||
std::string gcode_statistics_path(const std::string &gcode_path)
|
||||
{
|
||||
return boost::filesystem::path(gcode_path).replace_extension(".statistics.json").string();
|
||||
}
|
||||
|
||||
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path)
|
||||
{
|
||||
// Binary, so the file reads the same on every platform and inside a 3MF.
|
||||
boost::nowide::ofstream file(path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
file << gcode_statistics_json(result);
|
||||
file.close();
|
||||
if (file.fail())
|
||||
throw ExportError(format(_u8L("Failed to write the G-code statistics to %1%."), path));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct GCodeProcessorResult;
|
||||
|
||||
// The statistics of a plate's G-code as JSON, in seconds, millimeters and grams: totals, per layer and per object instance.
|
||||
std::string gcode_statistics_json(const GCodeProcessorResult &result);
|
||||
// Where the statistics of the G-code at gcode_path are written.
|
||||
std::string gcode_statistics_path(const std::string &gcode_path);
|
||||
// Throws ExportError when the file cannot be written.
|
||||
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -4607,6 +4607,16 @@ 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("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
} else if (use_map) {
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
@@ -4615,12 +4625,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
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({});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -4984,6 +4988,10 @@ 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]);
|
||||
@@ -5008,8 +5016,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(total, 1);
|
||||
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
filament_map->values.resize(result_presets.size(), 1);
|
||||
filament_volume_map->values.resize(result_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
} else {
|
||||
// BBL: existing wholesale replace
|
||||
filament_color->values = ams_filament_colors;
|
||||
|
||||
@@ -13040,6 +13040,13 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def->cli_params = "dir";
|
||||
def->set_default_value(new ConfigOptionString());
|
||||
|
||||
def = this->add("export_statistics", coBool);
|
||||
def->label = L("Export statistics");
|
||||
def->tooltip = L("Write the statistics of each sliced plate as JSON, as plate_N.statistics.json next to its G-code in the output "
|
||||
"directory and inside the 3MF of --export-3mf: print time, filament and feature totals, and the same per layer "
|
||||
"and per object.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("debug", coInt);
|
||||
def->label = L("Debug level");
|
||||
def->tooltip = L("Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:trace\n");
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
#define CLI_OBJECT_ORIENT_FAILED -22
|
||||
#define CLI_MODIFIED_PARAMS_TO_PRINTER -23
|
||||
#define CLI_FILE_VERSION_NOT_SUPPORTED -24
|
||||
#define CLI_EXPORT_STATISTICS_ERROR -25
|
||||
|
||||
|
||||
#define CLI_NO_SUITABLE_OBJECTS -50
|
||||
|
||||
@@ -129,6 +129,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Shortcuts.hpp
|
||||
GUI/SpeedDialDialog.cpp
|
||||
GUI/SpeedDialDialog.hpp
|
||||
GUI/ColorPickerDialog.cpp
|
||||
GUI/ColorPickerDialog.hpp
|
||||
GUI/ActionRegistry.cpp
|
||||
GUI/ActionRegistry.hpp
|
||||
GUI/NativeCommands.cpp
|
||||
|
||||
@@ -297,6 +297,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
|
||||
, printable(true)
|
||||
, visible(true)
|
||||
, is_active(true)
|
||||
, on_hidden_plate(false)
|
||||
, zoom_to_volumes(true)
|
||||
, shader_outside_printer_detection_enabled(false)
|
||||
, is_outside(false)
|
||||
@@ -824,6 +825,8 @@ bool GLVolume::is_below_printbed() const
|
||||
|
||||
void GLVolume::render_sinking_contours()
|
||||
{
|
||||
if (!is_active)
|
||||
return;
|
||||
m_sinking_contours.render();
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,8 @@ public:
|
||||
bool visible : 1;
|
||||
// Whether or not this volume is active for rendering
|
||||
bool is_active : 1;
|
||||
// Whether or not is_active was cleared because the other plates are hidden
|
||||
bool on_hidden_plate : 1;
|
||||
// Whether or not to use this volume when applying zoom_to_volumes()
|
||||
bool zoom_to_volumes : 1;
|
||||
// Wheter or not this volume is enabled for outside print volume detection in shader.
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
#include "AMSMaterialsSetting.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <cstdint>
|
||||
#include "ExtrusionCalibration.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -36,11 +41,9 @@
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
|
||||
#include "slic3r/GUI/Widgets/StaticBox.hpp"
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
@@ -1963,11 +1966,6 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
sizer_custom->Add(m_ts_stbitmap_custom, 0, wxEXPAND, 0);
|
||||
m_custom_cp->Layout();
|
||||
|
||||
m_clrData = new wxColourData();
|
||||
m_clrData->SetChooseFull(true);
|
||||
m_clrData->SetChooseAlpha(false);
|
||||
|
||||
|
||||
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
|
||||
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));
|
||||
@@ -1991,46 +1989,34 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
wxGetApp().UpdateDarkUIWin(this);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& event)
|
||||
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
|
||||
{
|
||||
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
|
||||
const bool was_shown = IsShown();
|
||||
std::optional<wxColour> picker_color;
|
||||
{
|
||||
const ColorRGBA initial(m_def_col.Red(), m_def_col.Green(), m_def_col.Blue(), static_cast<unsigned char>(255));
|
||||
// Snapshot the custom control's anchor before releasing the transient popup grab.
|
||||
ColorPickerDialog dialog(GetParent(), initial, {}, false, m_custom_cp);
|
||||
Dismiss();
|
||||
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
|
||||
const auto& result = std::get<ColorRGBA>(*dialog.selection());
|
||||
picker_color = wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.b() * 255.f)));
|
||||
}
|
||||
} // Destroy the modal before restoring the first-level popup and its focus/grab.
|
||||
if (!picker_color) {
|
||||
if (was_shown) Popup();
|
||||
return;
|
||||
}
|
||||
auto clr_dialog = new wxColourDialog(nullptr, m_clrData);
|
||||
wxColour picker_color;
|
||||
|
||||
if (clr_dialog->ShowModal() == wxID_OK) {
|
||||
m_clrData = &(clr_dialog->GetColourData());
|
||||
if (colors.size() != CUSTOM_COLOR_COUNT) {
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
}
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
|
||||
}
|
||||
wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
|
||||
picker_color = wxColour(
|
||||
m_clrData->GetColour().Red(),
|
||||
m_clrData->GetColour().Green(),
|
||||
m_clrData->GetColour().Blue(),
|
||||
255
|
||||
);
|
||||
|
||||
if (picker_color.Alpha() == 0) {
|
||||
m_ts_stbitmap_custom->Show();
|
||||
}
|
||||
else {
|
||||
m_ts_stbitmap_custom->Hide();
|
||||
m_custom_cp->SetBackgroundColor(picker_color);
|
||||
}
|
||||
|
||||
set_def_colour(picker_color);
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
unsigned long g_col = ((picker_color.Red() & 0xff) << 24) + ((picker_color.Green() & 0xff) << 16) + ((picker_color.Blue() & 0xff) << 8) + (picker_color.Alpha() & 0xff);
|
||||
evt.SetInt(g_col);
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
set_def_colour(*picker_color);
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
const std::uint32_t packed = (std::uint32_t(picker_color->Red()) << 24) |
|
||||
(std::uint32_t(picker_color->Green()) << 16) |
|
||||
(std::uint32_t(picker_color->Blue()) << 8) | 255u;
|
||||
evt.SetInt(static_cast<int>(packed));
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
|
||||
@@ -85,7 +85,6 @@ public:
|
||||
ScalableBitmap m_ts_bitmap_custom;
|
||||
wxStaticBitmap* m_ts_stbitmap_custom;
|
||||
StaticBox* m_custom_cp;
|
||||
wxColourData* m_clrData;
|
||||
StaticBox* m_def_color_box;
|
||||
wxFlexGridSizer* m_ams_fg_sizer;
|
||||
wxColour m_def_col;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -57,6 +58,7 @@
|
||||
#include "libslic3r_version.h"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/Utils/PrintHost.hpp"
|
||||
@@ -781,6 +783,7 @@ void BackgroundSlicingProcess::schedule_export(const std::string& path, bool exp
|
||||
this->invalidate_step(bspsGCodeFinalize);
|
||||
m_export_path = path;
|
||||
m_export_path_on_removable_media = export_path_on_removable_media;
|
||||
m_export_statistics = GUI::wxGetApp().app_config->get_bool("export_statistics");
|
||||
}
|
||||
|
||||
void BackgroundSlicingProcess::schedule_upload(Slic3r::PrintHostJob upload_job)
|
||||
@@ -906,6 +909,8 @@ void BackgroundSlicingProcess::finalize_gcode()
|
||||
BOOST_LOG_TRIVIAL(error) << "Unexpected fail code(" << (int) copy_ret_val << ") durring copy_file() to " << export_path << ".";
|
||||
break;
|
||||
}
|
||||
if (m_export_statistics)
|
||||
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
|
||||
|
||||
m_print->set_status(100, GUI::format(_L("G-code file exported to %1%"), export_path));
|
||||
}
|
||||
@@ -953,6 +958,8 @@ void BackgroundSlicingProcess::export_gcode()
|
||||
// throw Slic3r::ExportError(_utf8(L("Unknown error when exporting G-code.")));
|
||||
break;
|
||||
}
|
||||
if (m_export_statistics)
|
||||
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
|
||||
|
||||
// BBS
|
||||
auto evt = new wxCommandEvent(m_event_export_finished_id, GUI::wxGetApp().mainframe->m_plater->GetId());
|
||||
|
||||
@@ -258,6 +258,8 @@ private:
|
||||
// but once set, it cannot be re-set.
|
||||
std::string m_export_path;
|
||||
bool m_export_path_on_removable_media = false;
|
||||
// Read from the preferences when the export is scheduled, as the background thread may not read them.
|
||||
bool m_export_statistics = false;
|
||||
// Print host upload job to schedule after slicing is complete, used by schedule_upload(),
|
||||
// empty by default (ie. no upload to schedule)
|
||||
PrintHostJob m_upload_job;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <variant>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <wx/gdicmn.h>
|
||||
|
||||
class wxWindow;
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Most colors an ordered gradient may hold. This is the native switch for multi-color gradients:
|
||||
// 2 keeps the classic two-color gradients. The page receives it as options.max_gradient_colors
|
||||
// and never offers more, so the picker, favorites and project colors all share one limit.
|
||||
inline constexpr std::size_t COLOR_PICKER_MAX_GRADIENT_COLORS = 7;
|
||||
|
||||
// Gradient colors retain their order (2..COLOR_PICKER_MAX_GRADIENT_COLORS); a solid has exactly one color.
|
||||
using ColorGradient = std::vector<ColorRGBA>;
|
||||
using ColorSelection = std::variant<ColorRGBA, ColorGradient>;
|
||||
|
||||
struct ColorPickerOptions {
|
||||
bool allow_gradient = false;
|
||||
bool allow_alpha = false;
|
||||
};
|
||||
|
||||
// Project storage preserves direction; FilamentColor's set is only for the
|
||||
// official palette and cannot represent ordered custom gradients.
|
||||
struct FilamentColorPickerValue {
|
||||
std::vector<std::string> colors;
|
||||
bool gradient = false;
|
||||
std::string primary;
|
||||
};
|
||||
|
||||
std::optional<ColorSelection> filament_color_picker_selection(const FilamentColorPickerValue& value);
|
||||
ColorSelection filament_color_picker_initial(const FilamentColorPickerValue& value);
|
||||
FilamentColorPickerValue filament_color_picker_result(const ColorSelection& selection);
|
||||
bool filament_color_picker_changed(const ColorSelection& initial, const std::optional<ColorSelection>& result);
|
||||
|
||||
class ColorPickerDialog : public WebViewHostDialog
|
||||
{
|
||||
public:
|
||||
ColorPickerDialog(wxWindow* parent, const ColorSelection& initial, ColorPickerOptions options = {},
|
||||
bool preserve_multi_color = false, wxWindow* anchor = nullptr);
|
||||
~ColorPickerDialog() override;
|
||||
|
||||
const std::optional<ColorSelection>& selection() const { return m_selection; }
|
||||
|
||||
private:
|
||||
void add_user_scripts() override;
|
||||
void on_script_message(const nlohmann::json& payload) override;
|
||||
void handle_web_command(const nlohmann::json& payload);
|
||||
void send_initial_state();
|
||||
void finish(int return_code);
|
||||
void position_panel();
|
||||
void move_to_anchor();
|
||||
void resize_to_content(int height);
|
||||
void focus_webview();
|
||||
void repaint_webview();
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
void on_sys_color_changed() override;
|
||||
|
||||
ColorSelection m_initial;
|
||||
ColorPickerOptions m_options;
|
||||
std::optional<ColorSelection> m_selection;
|
||||
std::vector<ColorSelection> m_favorites;
|
||||
std::string m_page_id;
|
||||
bool m_preserve_multi_color = false;
|
||||
bool m_init_sent = false;
|
||||
bool m_page_ready = false;
|
||||
bool m_closing = false;
|
||||
bool m_favorites_writable = false;
|
||||
bool m_sidebar_open = false;
|
||||
std::string m_palette;
|
||||
bool m_initial_positioned = false;
|
||||
wxRect m_anchor_rect;
|
||||
wxRect m_work_area;
|
||||
int m_content_height = 360;
|
||||
int m_content_width = 330;
|
||||
bool m_positioning = false;
|
||||
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -163,8 +163,8 @@ public:
|
||||
~devPrinterUtil() = delete;
|
||||
|
||||
public:
|
||||
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); }
|
||||
static bool IsVirtualSlot(int ams_id);
|
||||
static bool IsVirtualSlot(const std::string& ams_id);
|
||||
};
|
||||
|
||||
namespace GUI
|
||||
@@ -198,3 +198,6 @@ 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,6 +8,7 @@
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
@@ -33,6 +34,36 @@
|
||||
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)
|
||||
@@ -200,18 +231,22 @@ wxString DevAms::GetDisplayName() const
|
||||
|
||||
int DevAms::GetSlotCount() const
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
// 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)
|
||||
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 1;
|
||||
return static_cast<int>(m_trays.size());
|
||||
}
|
||||
|
||||
DevAmsTray* DevAms::GetTray(const std::string& tray_id) const
|
||||
@@ -302,13 +337,21 @@ int DevFilaSystem::GetExtruderIdByAmsId(const std::string& ams_id) const
|
||||
{
|
||||
return it->second->GetExtruderId();
|
||||
}
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_MAIN_ID)
|
||||
{
|
||||
return MAIN_EXTRUDER_ID;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
{
|
||||
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_MAIN_ID_STR)
|
||||
return MAIN_EXTRUDER_ID;
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
if (!is_bbl_vendor && devPrinterUtil::IsVirtualSlot(ams_id)) {
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
|
||||
assert(false && __FUNCTION__);
|
||||
@@ -325,9 +368,22 @@ std::string DevFilaSystem::GetNozzleFlowStringByAmsId(const std::string& ams_id)
|
||||
std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
{
|
||||
std::map<int, DevAmsSlotId> tray_id_map;
|
||||
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};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -335,9 +391,11 @@ 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 (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
if (!is_bbl_vendor) {
|
||||
tray_index = generic_tray_index + slot_id_int;
|
||||
} else 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);
|
||||
@@ -348,6 +406,8 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor)
|
||||
generic_tray_index += static_cast<int>(ams_item->GetTrays().size());
|
||||
}
|
||||
|
||||
return tray_id_map;
|
||||
@@ -456,6 +516,13 @@ 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;
|
||||
@@ -795,7 +862,18 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
|
||||
if (type_id < 4)
|
||||
{
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
else if (type_id == DevAms::AMS_LITE_MIXED)
|
||||
{
|
||||
@@ -851,6 +929,9 @@ 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++)
|
||||
@@ -913,6 +994,8 @@ 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);
|
||||
@@ -995,16 +1078,22 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
// --- external / direct spools -> obj->vt_slot ---
|
||||
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
|
||||
// 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, ...).
|
||||
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);
|
||||
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
|
||||
if (ext < MAIN_EXTRUDER_ID || ext >= VIRTUAL_TRAY_MAIN_ID)
|
||||
continue;
|
||||
|
||||
const int vt_id = VIRTUAL_TRAY_MAIN_ID - ext;
|
||||
DevAmsTray tray(std::to_string(vt_id));
|
||||
tray.is_exists = e.value("loaded", false);
|
||||
tray.m_fila_type = e.value("material", std::string());
|
||||
@@ -1013,8 +1102,11 @@ 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);
|
||||
obj->vt_slot.push_back(tray);
|
||||
external_slots.insert_or_assign(ext, std::move(tray));
|
||||
}
|
||||
|
||||
for (auto& entry : external_slots)
|
||||
obj->vt_slot.push_back(std::move(entry.second));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -348,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 the two virtual (external-spool) trays,
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes virtual external-spool trays,
|
||||
// N3S single-slot units, and the A2L/N9 AMS-Lite-mixed layout (trays 24-27).
|
||||
std::map<int, DevAmsSlotId> GetTrayIndexMap();
|
||||
|
||||
@@ -433,4 +433,4 @@ struct DevFilamentDryingPreset
|
||||
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
|
||||
};
|
||||
|
||||
}// namespace Slic3r
|
||||
}// namespace Slic3r
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#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"
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -176,34 +178,52 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
|
||||
int DevNozzle::GetLogicExtruderId() const
|
||||
{
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
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;
|
||||
|
||||
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);
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
assert(0);
|
||||
return LOGIC_UNIQUE_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;
|
||||
}
|
||||
|
||||
int DevNozzle::GetExtruderId() const
|
||||
{
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
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 (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
return MAIN_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
return MAIN_EXTRUDER_ID;
|
||||
return m_nozzle_id;
|
||||
}
|
||||
|
||||
bool DevNozzle::AtLeftExtruder() const
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <tuple>
|
||||
#include <wx/app.h>
|
||||
@@ -806,8 +807,12 @@ bool MachineObject::is_extrusion_cali_finished()
|
||||
DevAmsTray *MachineObject::get_curr_tray()
|
||||
{
|
||||
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
|
||||
if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) {
|
||||
return &vt_slot[0];
|
||||
if (devPrinterUtil::IsVirtualSlot(cur_ams_id)) {
|
||||
for (auto& tray : vt_slot) {
|
||||
if (tray.id == cur_ams_id)
|
||||
return &tray;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DevAms* curr_ams = get_curr_Ams();
|
||||
@@ -1273,19 +1278,24 @@ 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) {
|
||||
// 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)
|
||||
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;
|
||||
}
|
||||
@@ -2627,13 +2637,10 @@ void MachineObject::reset()
|
||||
json empty_j;
|
||||
print_json.diff2all_base_reset(empty_j);
|
||||
|
||||
for (auto i = 0; i < vt_slot.size(); i++) {
|
||||
vt_slot[i].reset();
|
||||
|
||||
if (i == 1) {
|
||||
vt_slot.erase(vt_slot.begin() + 1);
|
||||
}
|
||||
}
|
||||
for (auto& tray : vt_slot)
|
||||
tray.reset();
|
||||
if (vt_slot.size() > 1)
|
||||
vt_slot.erase(vt_slot.begin() + 1, vt_slot.end());
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
@@ -4114,25 +4121,28 @@ 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++) {
|
||||
auto vslot = parse_vt_tray(it.value().get<json>());
|
||||
try {
|
||||
const auto tray_json = it.value().get<json>();
|
||||
if (!tray_json.is_object() || !tray_json.contains("id") || !tray_json["id"].is_string())
|
||||
continue;
|
||||
|
||||
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);
|
||||
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);
|
||||
const auto index = static_cast<std::vector<DevAmsTray>::size_type>(VIRTUAL_TRAY_MAIN_ID - tray_id);
|
||||
while (vt_slot.size() <= index) {
|
||||
const int placeholder_id = VIRTUAL_TRAY_MAIN_ID - static_cast<int>(vt_slot.size());
|
||||
vt_slot.emplace_back(std::to_string(placeholder_id));
|
||||
}
|
||||
vt_slot[index] = vslot;
|
||||
}
|
||||
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);
|
||||
}
|
||||
catch (...) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5270,16 +5280,17 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
|
||||
bool MachineObject::contains_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
{
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
|
||||
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;
|
||||
} else {
|
||||
|
||||
if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
for (const auto& tray : vt_slot) {
|
||||
if (tray.id == ams_id) { return true; }
|
||||
if (tray.id == ams_id) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return !is_bbl_vendor && m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
}
|
||||
|
||||
DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
@@ -5289,13 +5300,19 @@ DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string
|
||||
return DevAmsTray(tray_id);
|
||||
}
|
||||
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
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) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) { return *tray;};
|
||||
if (tray) return *tray;
|
||||
}
|
||||
else {
|
||||
else if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
for (const auto &tray : vt_slot) {
|
||||
if (tray.id == ams_id) { return tray; }
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+74
-165
@@ -2,6 +2,7 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Field.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "Plater.hpp"
|
||||
@@ -45,7 +46,11 @@
|
||||
#include <wx/arrstr.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/clrpicker.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/brush.h>
|
||||
#include <wx/pen.h>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <wx/image.h>
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/dc.h>
|
||||
@@ -2618,92 +2623,41 @@ void PluginConfigField::msw_rescale()
|
||||
|
||||
void ColourPicker::BUILD()
|
||||
{
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
|
||||
// An empty borderless wxButton is created with zero height on macOS. Give the bitmap
|
||||
// field the same height as other parameter buttons before drawing its swatch.
|
||||
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
|
||||
if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit);
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
|
||||
|
||||
// Validate the color
|
||||
wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value : m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
|
||||
wxColour clr(clr_str);
|
||||
if (clr_str.IsEmpty() || !clr.IsOk()) {
|
||||
clr = wxTransparentColour;
|
||||
}
|
||||
|
||||
auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
|
||||
const wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value :
|
||||
m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
|
||||
auto* button = new wxButton(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, wxBORDER_NONE);
|
||||
window = button;
|
||||
button->SetBitmapMargins(0, 0);
|
||||
if (parent_is_custom_ctrl && m_opt.height < 0)
|
||||
opt_height = (double)temp->GetSize().GetHeight() / m_em_unit;
|
||||
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
|
||||
convert_to_picker_widget(temp);
|
||||
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
|
||||
wxGetApp().UpdateDarkUI(temp->GetPickerCtrl());
|
||||
|
||||
// // recast as a wxWindow to fit the calling convention
|
||||
window = dynamic_cast<wxWindow*>(temp);
|
||||
|
||||
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([
|
||||
#ifdef __WXMSW__
|
||||
temp,
|
||||
#endif
|
||||
this](wxCommandEvent e) {
|
||||
#ifdef __WXMSW__
|
||||
draw_bmp_btn(temp, temp->GetColour());
|
||||
#endif
|
||||
on_change_field();
|
||||
}), temp->GetId());
|
||||
|
||||
// ORCA reset value to default on right click. previously no way to switch back on windows
|
||||
temp->GetPickerCtrl()->Bind(wxEVT_RIGHT_DOWN, [this, temp](wxMouseEvent e){
|
||||
#ifdef __WXMSW__
|
||||
temp->SetColour(wxTransparentColour);
|
||||
draw_bmp_btn(temp, wxTransparentColour);
|
||||
#else
|
||||
set_undef_value(temp);
|
||||
#endif
|
||||
on_change_field();
|
||||
e.Skip();
|
||||
opt_height = double(button->GetSize().GetHeight()) / m_em_unit;
|
||||
button->SetFont(wxGetApp().normal_font());
|
||||
if (!wxOSX) button->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
set_value(boost::any(clr_str));
|
||||
button->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
|
||||
button->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) {
|
||||
apply_user_color(wxTransparentColour);
|
||||
event.Skip();
|
||||
});
|
||||
|
||||
temp->SetToolTip(get_tooltip_text(clr_str));
|
||||
// Native layouts may assign the final field size after construction.
|
||||
button->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
|
||||
draw_bmp_btn();
|
||||
event.Skip();
|
||||
});
|
||||
button->SetToolTip(get_tooltip_text(clr_str));
|
||||
}
|
||||
|
||||
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
|
||||
void ColourPicker::draw_bmp_btn()
|
||||
{
|
||||
field->SetColour(wxTransparentColour);
|
||||
|
||||
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
|
||||
if (!btn->GetBitmap().IsOk()) return;
|
||||
|
||||
wxImage image(btn->GetBitmap().GetSize());
|
||||
image.InitAlpha();
|
||||
memset(image.GetAlpha(), 0, image.GetWidth() * image.GetHeight());
|
||||
wxBitmap bmp(std::move(image));
|
||||
wxMemoryDC dc(bmp);
|
||||
if (!dc.IsOk()) return;
|
||||
#ifdef __WXMSW__
|
||||
wxGCDC dc2(dc);
|
||||
#else
|
||||
wxDC &dc2(dc);
|
||||
#endif
|
||||
dc2.SetPen(wxPen("#F1754E", 1));
|
||||
|
||||
const wxRect rect = wxRect(0, 0, bmp.GetWidth(), bmp.GetHeight());
|
||||
dc2.DrawLine(rect.GetLeftBottom(), rect.GetTopRight());
|
||||
|
||||
dc.SelectObject(wxNullBitmap);
|
||||
btn->SetBitmapLabel(bmp);
|
||||
}
|
||||
|
||||
// ORCA match style with button on windows
|
||||
void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
{
|
||||
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
|
||||
|
||||
if (!btn->GetBitmap().IsOk()) return;
|
||||
btn->SetWindowStyle(wxBORDER_NONE); // ORCA just in case to prevent any overflow
|
||||
btn->SetBackgroundColour(*wxWHITE);
|
||||
if (!window) return;
|
||||
auto* btn = static_cast<wxButton*>(window);
|
||||
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
|
||||
wxGetApp().UpdateDarkUI(btn);
|
||||
|
||||
auto create_bitmap = [btn](const wxColour& picker_color,const wxColour& bg_color, bool focus) -> wxBitmap {
|
||||
wxSize btn_sz = btn->GetSize();
|
||||
wxImage image(btn_sz);
|
||||
@@ -2712,11 +2666,11 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
wxBitmap bmp(std::move(image));
|
||||
wxMemoryDC dc(bmp);
|
||||
if (!dc.IsOk()) return bmp;
|
||||
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
|
||||
wxGCDC dc2(dc);
|
||||
|
||||
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
|
||||
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
|
||||
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
|
||||
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
|
||||
|
||||
int padding = btn->FromDIP(5);
|
||||
dc2.SetPen(*wxTRANSPARENT_PEN);
|
||||
@@ -2735,117 +2689,72 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
return bmp;
|
||||
};
|
||||
|
||||
btn->SetBitmap( create_bitmap(color, wxColour("#DFDFDF"), false)); // Normal
|
||||
btn->SetBitmapFocus( create_bitmap(color, wxColour("#DFDFDF"), true )); // Focus
|
||||
btn->SetBitmapCurrent( create_bitmap(color, wxColour("#D4D4D4"), false)); // Hover
|
||||
btn->SetBitmap( create_bitmap(m_colour, wxColour("#DFDFDF"), false)); // Normal
|
||||
btn->SetBitmapFocus( create_bitmap(m_colour, wxColour("#DFDFDF"), true )); // Focus
|
||||
btn->SetBitmapCurrent( create_bitmap(m_colour, wxColour("#D4D4D4"), false)); // Hover
|
||||
}
|
||||
|
||||
void ColourPicker::set_value(const boost::any& value, bool change_event)
|
||||
void ColourPicker::set_value(const std::string& value, bool change_event)
|
||||
{
|
||||
m_disable_change_event = !change_event;
|
||||
const wxString clr_str(boost::any_cast<wxString>(value));
|
||||
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
|
||||
set_value(boost::any(wxString::FromUTF8(value)), change_event);
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
const wxColour parsed_clr(clr_str);
|
||||
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
|
||||
field->SetColour(clr);
|
||||
draw_bmp_btn(field, clr);
|
||||
#else
|
||||
wxColour clr(clr_str);
|
||||
if (clr_str.IsEmpty() || !clr.IsOk())
|
||||
set_undef_value(field);
|
||||
else
|
||||
field->SetColour(clr);
|
||||
#endif
|
||||
|
||||
m_disable_change_event = false;
|
||||
void ColourPicker::set_value(const boost::any& value, bool /*change_event*/)
|
||||
{
|
||||
// Like wxColourPickerCtrl::SetColour, programmatic assignment is silent.
|
||||
const wxString clr_str = boost::any_cast<wxString>(value);
|
||||
const wxColour parsed(clr_str);
|
||||
m_colour = clr_str.IsEmpty() || !parsed.IsOk() ? wxTransparentColour : parsed;
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
boost::any& ColourPicker::get_value()
|
||||
{
|
||||
save_colors_to_config();
|
||||
auto colour = static_cast<wxColourPickerCtrl*>(window)->GetColour();
|
||||
if (colour == wxTransparentColour)
|
||||
m_value = std::string("");
|
||||
else {
|
||||
m_value = encode_color(ColorRGB(colour.Red(), colour.Green(), colour.Blue()));
|
||||
}
|
||||
return m_value;
|
||||
m_value = m_colour == wxTransparentColour ? std::string() :
|
||||
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_colour.Blue()));
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void ColourPicker::apply_user_color(const std::optional<wxColour>& color)
|
||||
{
|
||||
if (!color || !color->IsOk() || *color == m_colour) return;
|
||||
const std::string previous = boost::any_cast<std::string>(get_value());
|
||||
m_colour = *color;
|
||||
draw_bmp_btn();
|
||||
if (previous != boost::any_cast<std::string>(get_value())) on_change_field();
|
||||
}
|
||||
|
||||
void ColourPicker::msw_rescale()
|
||||
{
|
||||
Field::msw_rescale();
|
||||
|
||||
wxColourPickerCtrl* field = dynamic_cast<wxColourPickerCtrl*>(window);
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width with parameters
|
||||
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
|
||||
if (m_opt.height >= 0)
|
||||
size.SetHeight(m_opt.height * m_em_unit);
|
||||
else if (parent_is_custom_ctrl && opt_height > 0)
|
||||
size.SetHeight(lround(opt_height * m_em_unit));
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
|
||||
if (parent_is_custom_ctrl)
|
||||
field->SetSize(size);
|
||||
else
|
||||
field->SetMinSize(size);
|
||||
|
||||
#ifdef __WXMSW__
|
||||
draw_bmp_btn(field, field->GetColour());
|
||||
#else
|
||||
if (field->GetColour() == wxTransparentColour)
|
||||
set_undef_value(field);
|
||||
#endif
|
||||
|
||||
if (parent_is_custom_ctrl) window->SetSize(size);
|
||||
else window->SetMinSize(size);
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
void ColourPicker::sys_color_changed()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (wxWindow* win = this->getWindow())
|
||||
if (wxColourPickerCtrl* picker = dynamic_cast<wxColourPickerCtrl*>(win)){
|
||||
wxGetApp().UpdateDarkUI(picker->GetPickerCtrl(), true);
|
||||
draw_bmp_btn(picker, picker->GetColour());
|
||||
}
|
||||
#endif
|
||||
if (window) wxGetApp().UpdateDarkUI(window, true);
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
void ColourPicker::on_button_click(wxCommandEvent &event) {
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
if (m_clrData) {
|
||||
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
|
||||
}
|
||||
}
|
||||
m_picker_widget->OnButtonClick(event);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::convert_to_picker_widget(wxColourPickerCtrl *widget)
|
||||
void ColourPicker::on_button_click(wxCommandEvent& /*event*/)
|
||||
{
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
m_picker_widget = dynamic_cast<wxColourPickerWidget*>(widget->GetPickerCtrl());
|
||||
if (m_picker_widget) {
|
||||
m_picker_widget->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
|
||||
m_clrData = m_picker_widget->GetColourData();
|
||||
const wxColour initial = m_colour == wxTransparentColour ? wxColour(0, 0, 0) : m_colour;
|
||||
const ColorRGBA color(initial.Red(), initial.Green(), initial.Blue(), static_cast<unsigned char>(255));
|
||||
ColorPickerDialog dialog(m_parent, color, {}, false, window);
|
||||
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
|
||||
const auto& result = std::get<ColorRGBA>(*dialog.selection());
|
||||
apply_user_color(wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.b() * 255.f))));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::save_colors_to_config() {
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
if (m_clrData) {
|
||||
std::vector<std::string> colors;
|
||||
if (colors.size() != CUSTOM_COLOR_COUNT) {
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
}
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
|
||||
}
|
||||
wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void PointCtrl::BUILD()
|
||||
|
||||
+10
-24
@@ -22,10 +22,10 @@
|
||||
#include <boost/any.hpp>
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/bmpcbox.h>
|
||||
#include <wx/clrpicker.h>
|
||||
#include <optional>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Config.hpp"
|
||||
@@ -610,41 +610,27 @@ private:
|
||||
};
|
||||
|
||||
class ColourPicker : public Field {
|
||||
using Field::Field;
|
||||
|
||||
void set_undef_value(wxColourPickerCtrl* field);
|
||||
void draw_bmp_btn(wxColourPickerCtrl* field, wxColour color);
|
||||
public:
|
||||
ColourPicker(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
|
||||
ColourPicker(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
|
||||
~ColourPicker() {}
|
||||
|
||||
wxWindow* window{ nullptr };
|
||||
void BUILD() override;
|
||||
|
||||
void set_value(const std::string& value, bool change_event = false) {
|
||||
m_disable_change_event = !change_event;
|
||||
dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
|
||||
m_disable_change_event = false;
|
||||
}
|
||||
void set_value(const std::string& value, bool change_event = false);
|
||||
void set_value(const boost::any& value, bool change_event = false) override;
|
||||
boost::any& get_value() override;
|
||||
void msw_rescale() override;
|
||||
void sys_color_changed() override;
|
||||
|
||||
void enable() override { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); }
|
||||
void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); }
|
||||
void enable() override { if (window) window->Enable(); }
|
||||
void disable() override{ if (window) window->Disable(); }
|
||||
wxWindow* getWindow() override { return window; }
|
||||
|
||||
private:
|
||||
void convert_to_picker_widget(wxColourPickerCtrl *widget);
|
||||
void on_button_click(wxCommandEvent &WXUNUSED(ev));
|
||||
void save_colors_to_config();
|
||||
private:
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
wxColourData* m_clrData{nullptr};
|
||||
wxColourPickerWidget* m_picker_widget{nullptr};
|
||||
#endif
|
||||
// An absent result is cancellation; transparent color is the undefined field.
|
||||
void apply_user_color(const std::optional<wxColour>& color);
|
||||
void draw_bmp_btn();
|
||||
void on_button_click(wxCommandEvent& event);
|
||||
wxColour m_colour = wxTransparentColour;
|
||||
};
|
||||
|
||||
class PointCtrl : public Field {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
#include "EncodedFilament.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
@@ -278,19 +277,6 @@ static wxBitmap create_transparent_bitmap(const wxSize& size) {
|
||||
return bdc.bitmap;
|
||||
}
|
||||
|
||||
// Sort colors by HSV values (primarily by hue, then saturation, then value)
|
||||
static void sort_colors_by_hsv(std::vector<wxColour>& colors) {
|
||||
if (colors.size() < 2) return;
|
||||
std::sort(colors.begin(), colors.end(),
|
||||
[](const wxColour& a, const wxColour& b) {
|
||||
ColourHSV ha = wxColourToHSV(a);
|
||||
ColourHSV hb = wxColourToHSV(b);
|
||||
if (ha.h != hb.h) return ha.h < hb.h;
|
||||
if (ha.s != hb.s) return ha.s < hb.s;
|
||||
return ha.v < hb.v;
|
||||
});
|
||||
}
|
||||
|
||||
static wxBitmap create_single_filament_bitmap(const wxColour& color, const wxSize& size)
|
||||
{
|
||||
// Check if color is transparent
|
||||
@@ -430,24 +416,16 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
|
||||
{
|
||||
if (colors.empty()) return wxNullBitmap;
|
||||
|
||||
// Make a copy to sort without modifying original
|
||||
std::vector<wxColour> sorted_colors = colors;
|
||||
|
||||
// Sort colors by HSV when there are 2 or more colors
|
||||
if (sorted_colors.size() >= 2) {
|
||||
sort_colors_by_hsv(sorted_colors);
|
||||
if (force_gradient && colors.size() >= 2) {
|
||||
return create_gradient_filament_bitmap(colors, size);
|
||||
}
|
||||
|
||||
if (force_gradient && sorted_colors.size() >= 2) {
|
||||
return create_gradient_filament_bitmap(sorted_colors, size);
|
||||
}
|
||||
|
||||
switch (sorted_colors.size()) {
|
||||
case 1: return create_single_filament_bitmap(sorted_colors[0], size);
|
||||
case 2: return create_dual_filament_bitmap(sorted_colors[0], sorted_colors[1], size);
|
||||
case 3: return create_triple_filament_bitmap(sorted_colors, size);
|
||||
case 4: return create_quadruple_filament_bitmap(sorted_colors, size);
|
||||
default: return create_gradient_filament_bitmap(sorted_colors, size);
|
||||
switch (colors.size()) {
|
||||
case 1: return create_single_filament_bitmap(colors[0], size);
|
||||
case 2: return create_dual_filament_bitmap(colors[0], colors[1], size);
|
||||
case 3: return create_triple_filament_bitmap(colors, size);
|
||||
case 4: return create_quadruple_filament_bitmap(colors, size);
|
||||
default: return create_gradient_filament_bitmap(colors, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#include "FilamentPickerDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -79,7 +83,8 @@ void FilamentPickerDialog::StartFlashing()
|
||||
m_flash_timer->Start(50);
|
||||
}
|
||||
|
||||
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type)
|
||||
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type,
|
||||
std::optional<FilamentColorPickerValue> ordered_color)
|
||||
: DPIDialog(parent ? parent : wxGetApp().mainframe,
|
||||
wxID_ANY,
|
||||
_L("Select Filament"),
|
||||
@@ -92,6 +97,7 @@ FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fil
|
||||
m_color_query = new FilamentColorCodeQuery();
|
||||
m_is_data_loaded = LoadFilamentData(fila_id);
|
||||
m_cur_filament_color = fila_color;
|
||||
m_ordered_color = std::move(ordered_color);
|
||||
wxString color_name = m_color_query->GetFilaColorName(fila_id, fila_color);
|
||||
m_cur_color_name = new wxString(color_name);
|
||||
|
||||
@@ -441,6 +447,9 @@ wxScrolledWindow* FilamentPickerDialog::CreateColorGrid()
|
||||
|
||||
// Bind click
|
||||
btn->Bind(wxEVT_LEFT_DOWN, [this, btn, color_code](wxMouseEvent& evt) {
|
||||
m_selection_changed = true;
|
||||
m_ordered_color.reset();
|
||||
m_custom_selection.reset();
|
||||
m_cur_filament_color = color_code->GetFilaColor();
|
||||
UpdatePreview(*color_code);
|
||||
UpdateButtonStates(btn);
|
||||
@@ -511,24 +520,27 @@ void FilamentPickerDialog::UpdatePreview(const FilamentColorCode& color_code)
|
||||
Layout();
|
||||
}
|
||||
|
||||
void FilamentPickerDialog::UpdateCustomColorPreview(const wxColour& custom_color)
|
||||
void FilamentPickerDialog::UpdateCustomColorPreview(const std::vector<wxColour>& wx_colors, bool gradient)
|
||||
{
|
||||
std::vector<wxColour> wx_colors = {custom_color};
|
||||
|
||||
// Update preview bitmap
|
||||
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, false);
|
||||
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, gradient);
|
||||
|
||||
if (bmp.IsOk()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Custom color bitmap created successfully: " << bmp.GetWidth() << "x" << bmp.GetHeight();
|
||||
m_color_demo->SetBitmap(bmp);
|
||||
m_color_demo->SetBackgroundColour(custom_color);
|
||||
m_color_demo->SetBackgroundColour(wx_colors.front());
|
||||
m_color_demo->Refresh();
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to create custom color bitmap";
|
||||
}
|
||||
|
||||
// Update preview labels for custom color
|
||||
m_label_preview_color->SetLabel(custom_color.GetAsString(wxC2S_HTML_SYNTAX));
|
||||
wxString label;
|
||||
for (const auto& color : wx_colors) {
|
||||
if (!label.empty()) label += " / ";
|
||||
label += color.GetAsString(wxC2S_HTML_SYNTAX);
|
||||
}
|
||||
m_label_preview_color->SetLabel(label);
|
||||
m_label_preview_idx->SetLabel("");
|
||||
Layout();
|
||||
}
|
||||
@@ -557,16 +569,6 @@ void FilamentPickerDialog::CreateMoreInfoButton()
|
||||
m_more_btn->SetStyle(ButtonStyle::Regular, ButtonType::Expanded);
|
||||
}
|
||||
|
||||
wxColourData FilamentPickerDialog::GetSingleColorData()
|
||||
{
|
||||
wxColourData data;
|
||||
data.SetChooseFull(true);
|
||||
if (m_cur_filament_color.ColorCount() > 0) {
|
||||
data.SetColour(*m_cur_filament_color.m_colors.begin());
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void FilamentPickerDialog::BindEvents()
|
||||
{
|
||||
// Bind mouse events for window dragging
|
||||
@@ -588,27 +590,37 @@ void FilamentPickerDialog::BindEvents()
|
||||
// Pause click detection while color picker is open
|
||||
StopClickDetection();
|
||||
|
||||
wxColourData original_data = GetSingleColorData();
|
||||
wxColourData result = show_sys_picker_dialog(this, original_data);
|
||||
|
||||
// Resume click detection after color picker closes
|
||||
StartClickDetection();
|
||||
|
||||
// Check if user actually selected a different color
|
||||
if (result.GetColour() != original_data.GetColour()) {
|
||||
wxColour selected_color = result.GetColour();
|
||||
|
||||
// Update m_current_filament_color with the selected color
|
||||
m_cur_filament_color.m_colors.clear();
|
||||
m_cur_filament_color.m_colors.insert(selected_color);
|
||||
m_cur_filament_color.m_color_type = FilamentColor::ColorType::SINGLE_CLR;
|
||||
|
||||
// Update preview
|
||||
UpdateCustomColorPreview(selected_color);
|
||||
|
||||
// Clear currently selected button since custom color selected
|
||||
UpdateButtonStates(nullptr);
|
||||
FilamentColorPickerValue current;
|
||||
if (m_ordered_color) {
|
||||
current = *m_ordered_color;
|
||||
} else {
|
||||
for (const auto& color : m_cur_filament_color.m_colors)
|
||||
current.colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
current.gradient = m_cur_filament_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR;
|
||||
}
|
||||
const auto initial = filament_color_picker_initial(current);
|
||||
std::optional<ColorSelection> selected;
|
||||
{
|
||||
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(current), m_more_btn);
|
||||
if (dialog.ShowModal() == wxID_OK)
|
||||
selected = dialog.selection();
|
||||
}
|
||||
StartClickDetection();
|
||||
if (!filament_color_picker_changed(initial, selected))
|
||||
return;
|
||||
|
||||
m_custom_selection = selected;
|
||||
m_ordered_color = filament_color_picker_result(*selected);
|
||||
m_selection_changed = true;
|
||||
std::vector<wxColour> colors;
|
||||
m_cur_filament_color.m_colors.clear();
|
||||
for (const auto& color : m_ordered_color->colors) {
|
||||
colors.emplace_back(wxString::FromUTF8(color));
|
||||
m_cur_filament_color.m_colors.insert(colors.back());
|
||||
}
|
||||
m_cur_filament_color.m_color_type = m_ordered_color->gradient ? FilamentColor::ColorType::GRADIENT_CLR : FilamentColor::ColorType::SINGLE_CLR;
|
||||
UpdateCustomColorPreview(colors, m_ordered_color->gradient);
|
||||
UpdateButtonStates(nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include <optional>
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "EncodedFilament.hpp"
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/event.h>
|
||||
@@ -25,10 +26,13 @@ namespace Slic3r { namespace GUI {
|
||||
class FilamentPickerDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type);
|
||||
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type,
|
||||
std::optional<FilamentColorPickerValue> ordered_color = std::nullopt);
|
||||
virtual ~FilamentPickerDialog();
|
||||
|
||||
// Public interface methods
|
||||
bool HasSelectionChanged() const { return m_selection_changed; }
|
||||
const std::optional<ColorSelection>& GetCustomColorSelection() const { return m_custom_selection; }
|
||||
bool IsDataLoaded() const { return m_is_data_loaded; }
|
||||
wxColour GetSelectedColour() const;
|
||||
const FilamentColor& GetSelectedFilamentColor() const { return m_cur_filament_color; }
|
||||
@@ -68,7 +72,7 @@ private:
|
||||
|
||||
// UI update methods
|
||||
void UpdatePreview(const FilamentColorCode& filament);
|
||||
void UpdateCustomColorPreview(const wxColour& custom_color);
|
||||
void UpdateCustomColorPreview(const std::vector<wxColour>& colors, bool gradient);
|
||||
void UpdateButtonStates(wxBitmapButton* selected_btn);
|
||||
|
||||
// Shaped window methods
|
||||
@@ -77,7 +81,6 @@ private:
|
||||
|
||||
// Data loading
|
||||
bool LoadFilamentData(const wxString& fila_id);
|
||||
wxColourData GetSingleColorData();
|
||||
|
||||
// Flash effect
|
||||
void StartFlashing();
|
||||
@@ -98,6 +101,9 @@ private:
|
||||
FilamentColorCodes* m_cur_color_codes{nullptr};
|
||||
wxBitmapButton* m_cur_selected_btn{nullptr};
|
||||
FilamentColor m_cur_filament_color;
|
||||
std::optional<FilamentColorPickerValue> m_ordered_color;
|
||||
std::optional<ColorSelection> m_custom_selection;
|
||||
bool m_selection_changed = false;
|
||||
|
||||
// Shaped window members
|
||||
wxBitmap m_shape_bmp;
|
||||
|
||||
+231
-31
@@ -1679,6 +1679,7 @@ GLCanvas3D::~GLCanvas3D()
|
||||
|
||||
m_sel_plate_toolbar.del_all_item();
|
||||
m_sel_plate_toolbar.del_stats_item();
|
||||
m_plate_switcher_items.clear();
|
||||
}
|
||||
|
||||
void GLCanvas3D::post_event(wxEvent &&event)
|
||||
@@ -2360,6 +2361,8 @@ void GLCanvas3D::zoom_to_gcode()
|
||||
void GLCanvas3D::zoom_to_plate(int plate_idx)
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE && _hides_inactive_plates())
|
||||
plate_idx = REQUIRES_ZOOM_TO_CUR_PLATE;
|
||||
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE) {
|
||||
box = wxGetApp().plater()->get_partplate_list().get_bounding_box();
|
||||
box.min.z() = 0.0;
|
||||
@@ -2416,12 +2419,22 @@ float GLCanvas3D::get_collapse_toolbar_height() const
|
||||
}
|
||||
|
||||
// The bottom-left corner: the 3D navigator, then the column of round canvas buttons beside it, in
|
||||
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar and
|
||||
// get_canvas_toolbar_right, so an overlay kept clear of the corner follows its layout.
|
||||
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar,
|
||||
// get_canvas_toolbar_right and canvas_corner_height, so an overlay kept clear of the corner follows its layout.
|
||||
static constexpr float NAVIGATOR_SIZE = 128.f;
|
||||
static constexpr float CANVAS_TOOLBAR_MARGIN = 10.f; // off the canvas edge, when there is no navigator
|
||||
static constexpr float CANVAS_TOOLBAR_PADDING = 2.f;
|
||||
static constexpr float CANVAS_TOOLBAR_BUTTON = 36.f;
|
||||
static constexpr float CANVAS_TOOLBAR_SPACING = 6.f;
|
||||
|
||||
// What the corner takes at the left edge of the canvas: the 3D navigator, or else the canvas toolbar.
|
||||
static float canvas_corner_height()
|
||||
{
|
||||
if (wxGetApp().show_3d_navigator())
|
||||
return NAVIGATOR_SIZE;
|
||||
const int buttons = wxGetApp().show_canvas_zoom_button() ? 3 : 2;
|
||||
return CANVAS_TOOLBAR_MARGIN + 2.f * CANVAS_TOOLBAR_PADDING + buttons * CANVAS_TOOLBAR_BUTTON + (buttons - 1) * CANVAS_TOOLBAR_SPACING;
|
||||
}
|
||||
|
||||
float GLCanvas3D::get_canvas_toolbar_right() const
|
||||
{
|
||||
@@ -2546,6 +2559,9 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
|
||||
|
||||
wxGetApp().imgui()->new_frame();
|
||||
|
||||
if (_update_inactive_plates_visibility())
|
||||
scene_dirty = true;
|
||||
|
||||
if (m_picking_enabled && !m_benchmarking) {
|
||||
if (m_rectangle_selection.is_dragging())
|
||||
// picking pass using rectangle selection
|
||||
@@ -7669,7 +7685,6 @@ void GLCanvas3D::_update_select_plate_toolbar_stats_item(bool force_selected) {
|
||||
|
||||
bool GLCanvas3D::_update_imgui_select_plate_toolbar()
|
||||
{
|
||||
bool result = true;
|
||||
if (!m_sel_plate_toolbar.is_enabled()) {
|
||||
return false;
|
||||
}
|
||||
@@ -7694,19 +7709,13 @@ bool GLCanvas3D::_update_imgui_select_plate_toolbar()
|
||||
PartPlateList& plate_list = p_plater->get_partplate_list();
|
||||
for (int i = 0; i < plate_list.get_plate_count(); i++) {
|
||||
IMToolbarItem* item = new IMToolbarItem();
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
if (plate && plate->thumbnail_data.is_valid()) {
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
item->image_data = plate->thumbnail_data.pixels;
|
||||
item->image_width = plate->thumbnail_data.width;
|
||||
item->image_height = plate->thumbnail_data.height;
|
||||
result = item->generate_texture();
|
||||
}
|
||||
if (PartPlate* plate = plate_list.get_plate(i))
|
||||
item->set_thumbnail(plate->thumbnail_data);
|
||||
m_sel_plate_toolbar.m_items.push_back(item);
|
||||
}
|
||||
p_plater->clear_plate_toolbar_image_dirty();
|
||||
m_sel_plate_toolbar.is_display_scrollbar = false;
|
||||
return result;
|
||||
return true;
|
||||
}
|
||||
|
||||
//BBS: GUI refactor
|
||||
@@ -8901,11 +8910,75 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
|
||||
const Vec2d bc = m_bed.build_volume().bed_center();
|
||||
m_bed.set_axes_origin(Vec3d(bc.x(), bc.y(), 0.0));
|
||||
} else {
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
|
||||
// With the other plates hidden, the axes go to the current plate as in Preview.
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled() && !_hides_inactive_plates());
|
||||
}
|
||||
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_hides_inactive_plates() const
|
||||
{
|
||||
return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode();
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_update_inactive_plates_visibility()
|
||||
{
|
||||
const bool hide = _hides_inactive_plates();
|
||||
if (!hide && !m_inactive_plates_hidden)
|
||||
return false;
|
||||
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
PartPlate* plate = hide ? plate_list.get_curr_plate() : nullptr;
|
||||
|
||||
struct Instance
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
bool selected{ false };
|
||||
};
|
||||
std::map<std::pair<int, int>, Instance> instances;
|
||||
if (plate != nullptr)
|
||||
for (const GLVolume* volume : m_volumes.volumes) {
|
||||
Instance& instance = instances[{ volume->object_idx(), volume->instance_idx() }];
|
||||
instance.box.merge(volume->transformed_convex_hull_bounding_box());
|
||||
instance.selected |= volume->selected;
|
||||
}
|
||||
|
||||
// A gizmo that hides the other objects shows them all again when it closes.
|
||||
const bool keep_hidden = m_gizmos.is_hiding_instances();
|
||||
bool changed = false;
|
||||
bool any_hidden = false;
|
||||
std::vector<bool> shown_again(m_volumes.volumes.size(), false);
|
||||
for (size_t i = 0; i < m_volumes.volumes.size(); ++i) {
|
||||
GLVolume* volume = m_volumes.volumes[i];
|
||||
const Instance* instance = plate != nullptr ? &instances[{ volume->object_idx(), volume->instance_idx() }] : nullptr;
|
||||
if (instance != nullptr && !instance->selected && instance->box.defined && !plate->get_bounding_box().overlap(instance->box)) {
|
||||
changed |= volume->is_active;
|
||||
volume->is_active = false;
|
||||
volume->on_hidden_plate = true;
|
||||
} else if (volume->on_hidden_plate && !keep_hidden) {
|
||||
volume->is_active = true;
|
||||
volume->on_hidden_plate = false;
|
||||
shown_again[i] = true;
|
||||
changed = true;
|
||||
}
|
||||
any_hidden |= volume->on_hidden_plate;
|
||||
}
|
||||
|
||||
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Volume))
|
||||
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters) {
|
||||
const int i = SceneRaycaster::decode_id(SceneRaycaster::EType::Volume, item->get_id());
|
||||
if (0 <= i && i < int(m_volumes.volumes.size()) && (m_volumes.volumes[i]->on_hidden_plate || shown_again[i]))
|
||||
item->set_active(shown_again[i]);
|
||||
}
|
||||
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Bed))
|
||||
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters)
|
||||
item->set_active(!hide || SceneRaycaster::decode_id(SceneRaycaster::EType::Bed, item->get_id()) / PartPlate::GRABBER_COUNT ==
|
||||
plate_list.get_curr_plate_index());
|
||||
|
||||
m_inactive_plates_hidden = hide || any_hidden;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
|
||||
{
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
@@ -8914,7 +8987,7 @@ void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transfo
|
||||
PartPlate* curr_plate = plate_list.get_curr_plate();
|
||||
const Transform3d plate_view_matrix = m_design_canvas && curr_plate != nullptr ?
|
||||
Transform3d(view_matrix * Geometry::translation_transform(-curr_plate->get_origin())) : view_matrix;
|
||||
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
|
||||
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled, _hides_inactive_plates());
|
||||
}
|
||||
|
||||
BoundingBoxf3 GLCanvas3D::_current_plate_box() const
|
||||
@@ -9932,6 +10005,7 @@ void GLCanvas3D::_render_overlays()
|
||||
|
||||
//BBS: GUI refactor: GLToolbar
|
||||
_render_imgui_select_plate_toolbar();
|
||||
_render_plate_switcher();
|
||||
_render_return_toolbar();
|
||||
// BBS
|
||||
//_render_view_toolbar();
|
||||
@@ -10200,6 +10274,35 @@ void GLCanvas3D::_render_main_toolbar()
|
||||
m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
|
||||
}
|
||||
|
||||
// The scrollbar of the plate toolbars of Preview and Prepare, in units of the toolbar scale.
|
||||
static constexpr float PLATE_TOOLBAR_SCROLLBAR = 14.f;
|
||||
|
||||
// Pushes 4 colors and 1 style var.
|
||||
static void push_plate_toolbar_scrollbar_style(bool is_dark, float scrollbar_size)
|
||||
{
|
||||
const ImVec4 grab(0.77f, 0.77f, 0.77f, is_dark ? .6f : 1.f);
|
||||
// A background with opacity over the window's would make a second color.
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, grab);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
|
||||
}
|
||||
|
||||
// The border of a hovered plate in the plate toolbars, when it is not the current one.
|
||||
static ImVec4 plate_toolbar_hover_color(bool is_dark)
|
||||
{
|
||||
return ImVec4(0.67f, 0.67f, 0.67f, is_dark ? .6f : 1.f);
|
||||
}
|
||||
|
||||
// Not IsWindowHovered(), which is false while a button is held down and would drop the bar.
|
||||
static bool plate_toolbar_keeps_scrollbar(bool shown, float scrollbar_size)
|
||||
{
|
||||
const ImVec2 pos = ImGui::GetWindowPos();
|
||||
return ImGui::IsMouseHoveringRect(pos, pos + ImGui::GetWindowSize() + ImVec2(shown ? scrollbar_size : 0.f, 0.f), !shown) ||
|
||||
(ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow());
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: GLToolbar adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
|
||||
void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
@@ -10330,16 +10433,15 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
int item_count = m_sel_plate_toolbar.m_items.size() + (m_sel_plate_toolbar.show_stats_item ? 1 : 0);
|
||||
float window_height_calc = (item_count * (button_height + (margin_size + button_margin) * 2.0f) + (item_count - 1) * ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().WindowPadding.y * 2.0f);
|
||||
bool show_scroll = m_sel_plate_toolbar.is_display_scrollbar && (window_height_calc > window_height_max);
|
||||
float scrollbar_size = 14.0f * f_scale;
|
||||
float scrollbar_size = PLATE_TOOLBAR_SCROLLBAR * f_scale;
|
||||
float window_height = std::min(window_height_calc, window_height_max);
|
||||
float window_width = button_width + (margin_size + button_margin + ImGui::GetStyle().WindowPadding.x) * 2 + (show_scroll ? scrollbar_size : 0);
|
||||
|
||||
ImVec4 window_bg = m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f);
|
||||
ImVec4 button_active = ImGuiWrapper::COL_ORCA; // ORCA: Use orca color for selected sliced plate border
|
||||
ImVec4 button_hover = ImVec4(0.67f, 0.67f, 0.67, m_is_dark ? .6f : 1.0f);
|
||||
ImVec4 button_hover = plate_toolbar_hover_color(m_is_dark);
|
||||
ImVec4 orca_active = m_is_dark ? ImGuiWrapper::COL_ORCA_DARK : ImGuiWrapper::COL_ORCA;
|
||||
ImVec4 orca_hover = m_is_dark ? ImGuiWrapper::COL_ORCA_HOVER_DARK : ImGuiWrapper::COL_ORCA_HOVER;
|
||||
ImVec4 scroll_col = ImVec4(0.77f, 0.77f, 0.77f, m_is_dark ? .6f : 1.0f);
|
||||
ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
|
||||
ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
|
||||
float button_radius = 1.0f * f_scale;
|
||||
@@ -10347,14 +10449,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
//use white text as the background switch to black
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, m_is_dark ? ImVec4(.9f, .9f, .9f, 1) : ImVec4(.3f, .3f, .3f, 1)); // ORCA Plate number text > Add support for dark mode
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, window_bg);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f)); // ORCA using background color with opacity creates a second color. This prevents secondary color
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, button_active);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, button_hover);
|
||||
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar_size);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, show_scroll ? (6.f * f_scale) : (button_radius + margin_size + frame_padding + ImGui::GetStyle().WindowPadding.x));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, button_radius + margin_size);
|
||||
@@ -10625,18 +10723,109 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
ImGui::PopStyleColor(8);
|
||||
ImGui::PopStyleVar(7);
|
||||
|
||||
//ORCA ImGui::IsWindowHovered() returns false when left_down events on buttons that causes scrollbar disappears for a short time
|
||||
auto win_pos = ImGui::GetWindowPos();
|
||||
bool is_win_hovered = ImGui::IsMouseHoveringRect(win_pos, win_pos + ImVec2(window_width + (show_scroll ? scrollbar_size : 0), window_height), !show_scroll);
|
||||
|
||||
// Also show scrollbar visible and continue to capture mouse position
|
||||
const bool is_scrollbar_active_drag = GImGui != nullptr && ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow();
|
||||
|
||||
m_sel_plate_toolbar.is_display_scrollbar = is_win_hovered || is_scrollbar_active_drag;
|
||||
m_sel_plate_toolbar.is_display_scrollbar = plate_toolbar_keeps_scrollbar(show_scroll, scrollbar_size);
|
||||
|
||||
imgui.end();
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_plate_switcher()
|
||||
{
|
||||
Plater* plater = wxGetApp().plater();
|
||||
PartPlateList& plate_list = plater->get_partplate_list();
|
||||
const int plate_count = plate_list.get_plate_count();
|
||||
if (!_hides_inactive_plates() || plate_count < 2 || m_gizmos.get_current_type() != GLGizmosManager::Undefined)
|
||||
return;
|
||||
|
||||
// The thumbnails are the ones of the Preview plate toolbar.
|
||||
const unsigned version = plater->plate_toolbar_image_version();
|
||||
if (m_plate_switcher_version != version || m_plate_switcher_items.size() != size_t(plate_count)) {
|
||||
// Only the current plate is edited here; the others are redrawn when their thumbnails go invalid.
|
||||
for (int i = 0; i < plate_count && !plater->is_gcode_3mf(); ++i)
|
||||
plater->update_plate_thumbnails(i, i == plate_list.get_curr_plate_index());
|
||||
m_plate_switcher_items.clear();
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
m_plate_switcher_items.push_back(std::make_unique<IMToolbarItem>());
|
||||
m_plate_switcher_items.back()->set_thumbnail(plate_list.get_plate(i)->thumbnail_data);
|
||||
}
|
||||
m_plate_switcher_version = version;
|
||||
}
|
||||
|
||||
float sc = get_scale();
|
||||
#ifdef WIN32
|
||||
sc *= (float) get_dpi_for_window(wxGetApp().GetTopWindow()) / (float) DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
ImGuiWrapper& imgui = *wxGetApp().imgui();
|
||||
const ImVec2 btn_size = ImVec2(72.f, 72.f) * sc;
|
||||
const ImVec2 padding = ImVec2(8.f, 8.f) * sc;
|
||||
const float spacing = 8.f * sc;
|
||||
const float rounding = 4.f * sc;
|
||||
const float scrollbar = PLATE_TOOLBAR_SCROLLBAR * sc;
|
||||
const float top = std::max(m_main_toolbar.get_height(), m_gizmos.get_scaled_total_height()) + CANVAS_TOOLBAR_MARGIN * sc;
|
||||
const float content_h = plate_count * (btn_size.y + spacing) - spacing + 2.f * padding.y;
|
||||
const float max_h = std::max(btn_size.y + 2.f * padding.y, float(get_canvas_size().get_height()) - top - (canvas_corner_height() + CANVAS_TOOLBAR_MARGIN) * sc);
|
||||
// As in the Preview plate toolbar, the scrollbar shows only while the mouse is over the column.
|
||||
const bool scroll = m_plate_switcher_scrollbar && content_h > max_h;
|
||||
|
||||
const ImVec4 text_color = m_is_dark ? ImVec4(.9f, .9f, .9f, 1.f) : ImVec4(.3f, .3f, .3f, 1.f);
|
||||
const ImVec4 empty_color = ImVec4(text_color.x, text_color.y, text_color.z, .45f);
|
||||
const ImVec4 hover = plate_toolbar_hover_color(m_is_dark);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
|
||||
// Concentric with the thumbnails; tighter around the scrollbar, as in the Preview plate toolbar.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, scroll ? 6.f * sc : rounding + padding.x);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f));
|
||||
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar);
|
||||
|
||||
imgui.set_next_window_pos(CANVAS_TOOLBAR_MARGIN * sc, top, ImGuiCond_Always, 0.f, 0.f);
|
||||
imgui.set_next_window_size(btn_size.x + 2.f * padding.x + (scroll ? scrollbar : 0.f), std::min(content_h, max_h), ImGuiCond_Always);
|
||||
imgui.begin(std::string("##PlateSwitcher"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoCollapse | (scroll ? 0 : ImGuiWindowFlags_NoScrollbar));
|
||||
|
||||
const int curr_plate = plate_list.get_curr_plate_index();
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
const ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
|
||||
const ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
const bool current = i == curr_plate;
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImGui::PushID(i);
|
||||
const bool clicked = ImGui::InvisibleButton("##plate", btn_size);
|
||||
ImGui::PopID();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
draw_list->AddRectFilled(pos, pos + btn_size, plate_bg, rounding);
|
||||
if (ImTextureID texture = m_plate_switcher_items[i]->texture_id)
|
||||
draw_list->AddImageRounded(texture, pos, pos + btn_size, ImVec2(0.f, 1.f), ImVec2(1.f, 0.f), IM_COL32_WHITE, rounding);
|
||||
if (plate->empty())
|
||||
draw_list->AddRectFilled(pos, pos + btn_size, plate_dim, rounding);
|
||||
if (current || hovered) {
|
||||
const float border = 2.f * sc;
|
||||
draw_list->AddRect(pos - ImVec2(border, border) * .5f, pos + btn_size + ImVec2(border, border) * .5f,
|
||||
ImGui::GetColorU32(current ? (hovered ? ImGuiWrapper::COL_ORCA_HOVER : ImGuiWrapper::COL_ORCA) : hover),
|
||||
rounding + border * .5f, 0, border);
|
||||
}
|
||||
draw_list->AddText(pos + ImVec2(4.f, 2.f) * sc, ImGui::GetColorU32(plate->empty() ? empty_color : text_color), std::to_string(i + 1).c_str());
|
||||
|
||||
if (clicked && !current)
|
||||
plater->CallAfter([i]() {
|
||||
wxGetApp().plater()->select_plate(i);
|
||||
wxGetApp().plater()->deselect_all();
|
||||
});
|
||||
if (hovered) {
|
||||
const std::string name = plate->get_plate_name();
|
||||
imgui.tooltip(_u8L("Plate") + " " + std::to_string(i + 1) + (name.empty() ? "" : " (" + name + ")"), ImGui::GetFontSize() * 20.f);
|
||||
}
|
||||
}
|
||||
|
||||
m_plate_switcher_scrollbar = plate_toolbar_keeps_scrollbar(scroll, scrollbar);
|
||||
imgui.end();
|
||||
ImGui::PopStyleColor(5);
|
||||
ImGui::PopStyleVar(5);
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: GLToolbar adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
|
||||
void GLCanvas3D::_render_assemble_view_toolbar() const
|
||||
@@ -10743,7 +10932,7 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
|
||||
ImVec2 btn_size = ImVec2(CANVAS_TOOLBAR_BUTTON, CANVAS_TOOLBAR_BUTTON) * sc;
|
||||
ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (CANVAS_TOOLBAR_MARGIN * sc), CANVAS_TOOLBAR_MARGIN * sc);
|
||||
ImVec2 spacing = ImVec2(6.f, 6.f) * sc;
|
||||
ImVec2 spacing = ImVec2(CANVAS_TOOLBAR_SPACING, CANVAS_TOOLBAR_SPACING) * sc;
|
||||
ImVec2 padding = ImVec2(CANVAS_TOOLBAR_PADDING, CANVAS_TOOLBAR_PADDING) * sc;
|
||||
Vec2i32 pos = {
|
||||
m_canvas_toolbar_pos[0] + margin.x,
|
||||
@@ -10946,6 +11135,17 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
[]{wxGetApp().toggle_show_plate_gridlines();}
|
||||
);
|
||||
|
||||
create_menu_item( _utf8(L("Focus mode")),
|
||||
m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas, // work only on prepare
|
||||
wxGetApp().focus_mode(),
|
||||
[this]{
|
||||
wxGetApp().toggle_focus_mode();
|
||||
if (wxGetApp().focus_mode())
|
||||
zoom_to_plate(REQUIRES_ZOOM_TO_CUR_PLATE);
|
||||
m_dirty = true;
|
||||
}
|
||||
);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
create_menu_item( _utf8(L("Labels")),
|
||||
|
||||
@@ -660,6 +660,10 @@ private:
|
||||
mutable GLToolbar m_main_toolbar;
|
||||
mutable GLToolbar m_separator_toolbar;
|
||||
mutable IMToolbar m_sel_plate_toolbar;
|
||||
// Thumbnails of the plate switcher (Focus mode), refreshed when the plater's version changes.
|
||||
std::vector<std::unique_ptr<IMToolbarItem>> m_plate_switcher_items;
|
||||
unsigned m_plate_switcher_version{ unsigned(-1) };
|
||||
bool m_plate_switcher_scrollbar{ false };
|
||||
mutable GLToolbar m_assemble_view_toolbar;
|
||||
mutable IMReturnToolbar m_return_toolbar;
|
||||
mutable Vec2i32 m_canvas_toolbar_pos = {140, 5};
|
||||
@@ -778,6 +782,8 @@ private:
|
||||
//BBS:add plate related logic
|
||||
mutable std::vector<int> m_hover_volume_idxs;
|
||||
std::vector<int> m_hover_plate_idxs;
|
||||
// Some volume or plate raycaster may still be hidden with the inactive plates.
|
||||
bool m_inactive_plates_hidden{ false };
|
||||
// IMEX ghost hover state (plate-owned transparent ghost volumes).
|
||||
int m_hover_ghost_head { -1 }; // physical head index, -1 when not hovering a ghost
|
||||
int m_hover_ghost_plate { -1 }; // plate index for the hovered ghost, -1 when none
|
||||
@@ -1527,6 +1533,9 @@ private:
|
||||
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
//BBS: add part plate related logic
|
||||
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
|
||||
bool _hides_inactive_plates() const;
|
||||
// Returns whether what the scene shows changed.
|
||||
bool _update_inactive_plates_visibility();
|
||||
// The current plate's box (XY, at z = 0); in the Design tab, its own bed's.
|
||||
BoundingBoxf3 _current_plate_box() const;
|
||||
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
|
||||
@@ -1565,6 +1574,8 @@ private:
|
||||
void _render_gizmos_overlay();
|
||||
void _render_main_toolbar();
|
||||
void _render_imgui_select_plate_toolbar();
|
||||
// Prepare: picks the plate to show while the inactive ones are hidden.
|
||||
void _render_plate_switcher();
|
||||
void _render_assemble_view_toolbar() const;
|
||||
void _render_return_toolbar() const;
|
||||
void _render_canvas_toolbar();
|
||||
|
||||
+149
-8
@@ -41,6 +41,7 @@
|
||||
#include <boost/filesystem/exception.hpp>
|
||||
#include <boost/locale/encoding_utf.hpp>
|
||||
#include <boost/log/detail/native_typeof.hpp>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
@@ -325,6 +326,13 @@ namespace GUI {
|
||||
|
||||
class MainFrame;
|
||||
|
||||
// The OrcaCloud agent behind the shared NetworkAgent, or null when networking/OrcaCloud is not
|
||||
// available (e.g. stealth mode or the network plugin still loading).
|
||||
static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent* agent)
|
||||
{
|
||||
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
|
||||
}
|
||||
|
||||
void start_ping_test()
|
||||
{
|
||||
return;
|
||||
@@ -2262,7 +2270,9 @@ void GUI_App::init_networking_callbacks()
|
||||
if (return_code == 5) {
|
||||
GUI::wxGetApp().CallAfter([this, provider = event.provider] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: login expired";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: the desktop lost its session on its own. Leave the webview's
|
||||
// own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
if (msg_dlg.ShowModal() == wxOK) {
|
||||
return;
|
||||
@@ -5238,7 +5248,7 @@ void GUI_App::post_logout_to_webview(const std::string& provider)
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/)
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/, bool user_initiated)
|
||||
{
|
||||
if (m_agent && m_agent->is_user_login(provider)) {
|
||||
m_agent->user_logout(true, provider);
|
||||
@@ -5251,6 +5261,21 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
|
||||
}
|
||||
|
||||
if (provider == ORCA_CLOUD_PROVIDER) {
|
||||
// Invalidate any handoff still in flight, so a token minted for the signed-out
|
||||
// account is never loaded into the pane.
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
|
||||
// An explicit user sign-out also ends the webview's own cloud session (guide §8).
|
||||
// A session the desktop lost on its own must NOT do this: a 2FA user would otherwise
|
||||
// loop through the code prompt on every desktop re-login.
|
||||
if (user_initiated) {
|
||||
if (auto orca = get_orca_cloud_agent(m_agent)) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->cloud_signed_out(from_u8(orca->build_web_session_signout_url()));
|
||||
}
|
||||
}
|
||||
|
||||
/* delete old user settings */
|
||||
bool transfer_preset_changes = false;
|
||||
wxString header = _L("Some presets are modified.") + "\n" +
|
||||
@@ -5372,7 +5397,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_logout";
|
||||
request_user_logout();
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_orca_login_info") == 0) {
|
||||
@@ -5387,7 +5412,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_bambu_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_bambu_logout";
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_orca_login_or_register") == 0) {
|
||||
@@ -5396,7 +5421,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_orca_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_orca_logout";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_modeldepot") == 0) {
|
||||
@@ -5408,6 +5433,20 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_openproject") == 0) {
|
||||
this->request_open_project({});
|
||||
}
|
||||
else if (command_str.compare("homepage_open_cloud") == 0) {
|
||||
// Deferred: set_cloud_mode() may create a second native webview, which must
|
||||
// not run inside this webview's script-message callback.
|
||||
CallAfter([this]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_close_cloud") == 0) {
|
||||
CallAfter([]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(false);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_recent_projects") == 0) {
|
||||
if (mainframe) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
@@ -5680,7 +5719,9 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
if (!provider.empty() && m_agent->is_user_login(provider)) {
|
||||
if (std::chrono::steady_clock::now() - m_last_401_error_time > 30s) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: a 401 means the desktop session was rejected. Leave the
|
||||
// webview's own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
|
||||
if (!m_show_http_error_msgdlg) {
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
@@ -5824,6 +5865,93 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
|
||||
}*/
|
||||
}
|
||||
|
||||
wxString GUI_App::cloud_webview_handoff()
|
||||
{
|
||||
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
|
||||
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
|
||||
// post request_web_session, which is what opens the desktop login dialog.
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return {};
|
||||
return from_u8(orca->build_web_session_start_url());
|
||||
}
|
||||
|
||||
void GUI_App::request_cloud_web_session(const std::string& state)
|
||||
{
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
// Only the newest request counts: a newer state supersedes any mint or login still in flight.
|
||||
m_cloud_handoff_state = state;
|
||||
const uint64_t gen = ++m_cloud_handoff_gen;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca || !orca->is_user_login()) {
|
||||
// Desktop signed out: open the desktop login dialog and stop here. On success
|
||||
// on_user_login_handle reloads the Start URL, which makes the page request again; on
|
||||
// cancel nothing happens (the guide's "on cancel, do nothing"). The newest state is
|
||||
// already stored, so a repeat request while the dialog is open is dropped rather than
|
||||
// stacking another dialog.
|
||||
if (!login_dlg || !login_dlg->IsShown())
|
||||
request_login(true, ORCA_CLOUD_PROVIDER);
|
||||
return;
|
||||
}
|
||||
|
||||
mint_cloud_web_session(state, gen);
|
||||
}
|
||||
|
||||
void GUI_App::mint_cloud_web_session(const std::string& state, uint64_t gen)
|
||||
{
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// The endpoint call blocks on network I/O (and possibly a cross-process token refresh), so
|
||||
// it must not run on the UI thread. Hand off to a worker and marshal the token back.
|
||||
boost::thread([this, orca, state, gen]() {
|
||||
std::string token_hash;
|
||||
std::string error_code;
|
||||
unsigned int http_code = 0;
|
||||
orca->request_web_session_handoff(token_hash, error_code, http_code);
|
||||
|
||||
if (is_closing())
|
||||
return;
|
||||
|
||||
if (token_hash.empty()) {
|
||||
// No token: 401 after retry, 409/429/503 or a transport error. The page owns its own
|
||||
// "Sign-in link expired"/"Try again" UI; the desktop stays silent. Never log the body
|
||||
// (it may echo account data), only the status/error code.
|
||||
BOOST_LOG_TRIVIAL(warning) << "cloud web session handoff not minted - http_code=" << http_code
|
||||
<< " error_code=" << error_code;
|
||||
return;
|
||||
}
|
||||
|
||||
GUI::wxGetApp().CallAfter([this, state, gen, token_hash]() {
|
||||
on_cloud_web_session_minted(state, gen, token_hash);
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void GUI_App::on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash)
|
||||
{
|
||||
if (is_closing())
|
||||
return;
|
||||
// Drop a result for a superseded request (a newer request_web_session, login or sign-out).
|
||||
if (gen != m_cloud_handoff_gen || state != m_cloud_handoff_state)
|
||||
return;
|
||||
if (token_hash.empty())
|
||||
return;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// Load the Complete URL in the SAME webview that posted request_web_session, because the
|
||||
// state lives in that webview's session storage (guide §3/§7).
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->load_cloud_url(from_u8(orca->build_web_session_complete_url(token_hash, state)));
|
||||
}
|
||||
|
||||
void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
{
|
||||
if (!m_agent) { return; }
|
||||
@@ -5879,6 +6007,16 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
if (app_config->get("sync_user_preset") == "true") {
|
||||
start_sync_user_preset();
|
||||
}
|
||||
|
||||
// Re-seed the embedded OrcaCloud pane if it is already open, so it picks up the new session
|
||||
// without waiting for the next open. On a fresh login or account switch this invalidates any
|
||||
// in-flight handoff for the previous account and loads the Start URL with the new user_id, so
|
||||
// the page drops the old account and re-requests (guide §8).
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
if (home->cloud_mode_active())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
}
|
||||
|
||||
|
||||
@@ -8012,8 +8150,11 @@ void GUI_App::on_stealth_mode_enter()
|
||||
{
|
||||
stop_sync_user_preset();
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: on_stealth_mode_enter";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
// Stealth mode forces a desktop logout, but the user did not choose "Sign out": guide §8 ties
|
||||
// ?signout=1 to the explicit desktop sign-out only. Leave the cloud webview's own session
|
||||
// alone, so it is re-checked (and re-seeded with the right account) on the next open.
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
home->SendCloudProvidersInfo();
|
||||
}
|
||||
|
||||
@@ -361,6 +361,11 @@ private:
|
||||
bool m_side_popup_status{false};
|
||||
bool m_show_http_error_msgdlg{false};
|
||||
std::chrono::steady_clock::time_point m_last_401_error_time;
|
||||
// OrcaCloud webview session handoff: the newest request_web_session state from the cloud pane
|
||||
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
|
||||
// newest request counts").
|
||||
std::string m_cloud_handoff_state;
|
||||
uint64_t m_cloud_handoff_gen { 0 };
|
||||
bool m_show_error_msgdlg{false};
|
||||
wxString m_info_dialog_content;
|
||||
HttpServer m_http_server;
|
||||
@@ -431,6 +436,8 @@ public:
|
||||
|
||||
bool show_plate_gridlines() const { return app_config->get_bool("show_plate_gridlines"); }
|
||||
void toggle_show_plate_gridlines() const { app_config->set_bool("show_plate_gridlines", !show_plate_gridlines()); }
|
||||
bool focus_mode() const { return app_config->get_bool("focus_mode"); }
|
||||
void toggle_focus_mode() const { app_config->set_bool("focus_mode", !focus_mode()); }
|
||||
|
||||
bool show_canvas_zoom_button() const { return app_config->get_bool("show_canvas_zoom_button"); }
|
||||
void toggle_canvas_zoom_button() const { app_config->set_bool("show_canvas_zoom_button", !show_canvas_zoom_button()); }
|
||||
@@ -540,7 +547,11 @@ public:
|
||||
|
||||
void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
// `user_initiated` distinguishes a user sign-out from the desktop losing its session on its
|
||||
// own (refresh failed, 401). Only an explicit sign-out also ends the cloud webview's own
|
||||
// session; an involuntary one leaves the webview alone (guide §8). Defaults to the safe
|
||||
// direction: a new caller that forgets the flag will not silently sign the webview out.
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER, bool user_initiated = false);
|
||||
void post_logout_to_webview(const std::string& provider);
|
||||
int request_user_unbind(std::string dev_id, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
std::string handle_web_request(std::string cmd);
|
||||
@@ -556,6 +567,15 @@ public:
|
||||
void on_update_machine_list(wxCommandEvent& evt);
|
||||
void on_user_login(wxCommandEvent &evt);
|
||||
void on_user_login_handle(wxCommandEvent& evt);
|
||||
// OrcaCloud webview session handoff (guide §5/§7). cloud_webview_handoff builds the cloud
|
||||
// pane's Start URL (with the desktop user_id while signed in). request_cloud_web_session is
|
||||
// called from the pane when the page asks for a session; it opens the desktop login dialog if
|
||||
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
|
||||
// in the same webview.
|
||||
wxString cloud_webview_handoff();
|
||||
void request_cloud_web_session(const std::string& state);
|
||||
void mint_cloud_web_session(const std::string& state, uint64_t gen);
|
||||
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
|
||||
void enable_user_preset_folder(bool enable);
|
||||
|
||||
// BBS
|
||||
|
||||
@@ -2400,7 +2400,7 @@ void MenuFactory::append_menu_item_drop(wxMenu* menu)
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
else {
|
||||
return (std::abs(plater()->get_view3D_canvas3D()->get_selection().get_bounding_box().min.z()) > -SINKING_Z_THRESHOLD);
|
||||
return plater()->get_view3D_canvas3D()->get_selection().can_drop();
|
||||
} //disable if model is on the bed / not in View3D
|
||||
}, m_parent);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
|
||||
#include <slic3r/GUI/AnimatedTexture.hpp>
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/CameraUtils.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
|
||||
@@ -57,24 +57,6 @@ const double GizmoObjectManipulation::mm_to_in = 0.0393700787;
|
||||
const double GizmoObjectManipulation::oz_to_g = 28.34952;
|
||||
const double GizmoObjectManipulation::g_to_oz = 0.035274;
|
||||
|
||||
// Helper function to be used by drop to bed button. Returns lowest point of this
|
||||
// volume in world coordinate system.
|
||||
static double get_volume_min_z(const GLVolume* volume)
|
||||
{
|
||||
const Transform3f& world_matrix = volume->world_matrix().cast<float>();
|
||||
|
||||
// need to get the ModelVolume pointer
|
||||
const ModelObject* mo = wxGetApp().model().objects[volume->composite_id.object_id];
|
||||
const ModelVolume* mv = mo->volumes[volume->composite_id.volume_id];
|
||||
const TriangleMesh& hull = mv->get_convex_hull();
|
||||
|
||||
float min_z = std::numeric_limits<float>::max();
|
||||
for (const stl_vertex& vert : hull.its.vertices) {
|
||||
min_z = std::min(min_z, Vec3f::UnitZ().dot(world_matrix * vert));
|
||||
}
|
||||
return min_z;
|
||||
}
|
||||
|
||||
GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas)
|
||||
: m_glcanvas(glcanvas), m_alignment_helper(glcanvas)
|
||||
{
|
||||
@@ -295,12 +277,14 @@ void GizmoObjectManipulation::update_reset_buttons_visibility()
|
||||
{
|
||||
const Selection& selection = m_glcanvas.get_selection();
|
||||
|
||||
// Assembly view has its own selection and transformations, which drop_selection() doesn't update
|
||||
m_enable_drop_to_bed = m_glcanvas.get_canvas_type() == GLCanvas3D::ECanvasType::CanvasView3D && selection.can_drop();
|
||||
|
||||
if (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()) {
|
||||
const GLVolume * volume = selection.get_first_volume();
|
||||
|
||||
Vec3d rotation;
|
||||
Vec3d scale;
|
||||
double min_z = 0.;
|
||||
|
||||
if (selection.is_single_full_instance()) {
|
||||
rotation = volume->get_instance_rotation();
|
||||
@@ -309,12 +293,10 @@ void GizmoObjectManipulation::update_reset_buttons_visibility()
|
||||
else {
|
||||
rotation = volume->get_volume_rotation();
|
||||
scale = volume->get_volume_scaling_factor();
|
||||
min_z = get_volume_min_z(volume);
|
||||
}
|
||||
m_show_clear_rotation = !rotation.isApprox(m_init_rotation);
|
||||
m_show_reset_0_rotation = !rotation.isApprox(Vec3d::Zero());
|
||||
m_show_clear_scale = (m_cache.scale / 100.0f - Vec3d::Ones()).norm() > 0.001;
|
||||
m_show_drop_to_bed = (std::abs(min_z) > EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -771,6 +753,11 @@ void GizmoObjectManipulation::reset_align_state()
|
||||
|
||||
void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, std::string window_name, float x, float y, float bottom_limit)
|
||||
{
|
||||
float scale = m_glcanvas.get_scale();
|
||||
#ifdef WIN32
|
||||
scale *= (float)get_dpi_for_window(wxGetApp().GetTopWindow()) / (float)DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
// BBS: GUI refactor: move gizmo to the right
|
||||
if (abs(last_move_input_window_width) > 0.01f) {
|
||||
if (x + last_move_input_window_width > m_glcanvas.get_canvas_size().get_width()) {
|
||||
@@ -842,7 +829,7 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
// Rotation
|
||||
float unit_size = imgui_wrapper->calc_text_size(MAX_SIZE).x + space_size;
|
||||
// Four align buttons sit under each axis' input, a space before the next axis, so the inputs widen to hold them.
|
||||
const float align_icon_size = 16.f * m_glcanvas.get_scale(); // ORCA use 16px icons
|
||||
const float align_icon_size = 16.f * scale;
|
||||
const float align_button_size = align_icon_size + 2.f * style.FramePadding.x; // ORCA
|
||||
if (show_align) {
|
||||
unit_size = std::max(unit_size, 3.f * align_button_size);
|
||||
@@ -1072,6 +1059,17 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
|
||||
GLGizmoUtils::render_tooltip_button(imgui_wrapper, m_glcanvas, m_shortcuts_move, x, y);
|
||||
|
||||
// ORCA use existing function for drop and show button on bottom bar to keep gizmo compact
|
||||
imgui_wrapper->disabled_begin(!m_enable_drop_to_bed);
|
||||
ImGui::SameLine(0, space_size * 1.5f);
|
||||
if (imgui_wrapper->button(_L("Drop"))){
|
||||
wxGetApp().plater()->drop_selection();
|
||||
UpdateAndShow(true);
|
||||
}
|
||||
imgui_wrapper->disabled_end();
|
||||
if(ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
imgui_wrapper->tooltip(_L("Move the selection along Z so that its lowest point touches the bed."), ImGui::GetFontSize() * 20.0f);
|
||||
|
||||
ImGui::SameLine();
|
||||
GLGizmoUtils::begin_right_aligned_buttons({ _L("Done") });
|
||||
if (imgui_wrapper->button(_L("Done"))) {
|
||||
|
||||
@@ -103,7 +103,7 @@ public:
|
||||
bool m_show_reset_0_rotation{false};
|
||||
bool m_show_clear_rotation { false };
|
||||
bool m_show_clear_scale { false };
|
||||
bool m_show_drop_to_bed { false };
|
||||
bool m_enable_drop_to_bed { false };
|
||||
enum class RotateType { None, Relative, Absolute
|
||||
};
|
||||
RotateType m_last_rotate_type{RotateType::None}; // 0:no input 1:relative 2:absolute
|
||||
|
||||
@@ -23,27 +23,23 @@ IMToolbarItem::~IMToolbarItem()
|
||||
}
|
||||
|
||||
|
||||
bool IMToolbarItem::generate_texture()
|
||||
void IMToolbarItem::set_thumbnail(const ThumbnailData& thumbnail)
|
||||
{
|
||||
GLint last_texture;
|
||||
unsigned m_image_texture{ 0 };
|
||||
unsigned char* pixels = (unsigned char*)(&image_data[0]);
|
||||
if (!thumbnail.is_valid())
|
||||
return;
|
||||
|
||||
GLint last_texture;
|
||||
GLuint id = (GLuint)(intptr_t)texture_id;
|
||||
glsafe(::glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture));
|
||||
glsafe(::glGenTextures(1, &m_image_texture));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_image_texture));
|
||||
if (id == 0)
|
||||
glsafe(::glGenTextures(1, &id));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, id));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
|
||||
glsafe(::glPixelStorei(GL_UNPACK_ROW_LENGTH, 0));
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels));
|
||||
|
||||
// Store our identifier
|
||||
texture_id = (ImTextureID)(intptr_t)m_image_texture;
|
||||
|
||||
// Restore state
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, thumbnail.width, thumbnail.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, thumbnail.pixels.data()));
|
||||
texture_id = (ImTextureID)(intptr_t)id;
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, last_texture));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IMToolbar::del_all_item()
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#define DEFAULT_TOOLBAR_BUTTON_HEIGHT 80.0f
|
||||
|
||||
namespace Slic3r {
|
||||
struct ThumbnailData;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class IMToolbarItem
|
||||
@@ -33,11 +35,9 @@ public:
|
||||
SliceState slice_state;
|
||||
|
||||
ImTextureID texture_id { 0 };
|
||||
std::vector<unsigned char> image_data;
|
||||
unsigned int image_width;
|
||||
unsigned int image_height;
|
||||
|
||||
bool generate_texture();
|
||||
// Uploads a valid thumbnail as the texture; the item keeps no copy of its pixels.
|
||||
void set_thumbnail(const ThumbnailData& thumbnail);
|
||||
~IMToolbarItem();
|
||||
};
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
|
||||
});
|
||||
add_with_icon("obj_drop", L("Drop to Bed"), L("Object"), "toolbar_flatten", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->get_selection().can_drop(); }, [](Plater* p) { p->drop_selection(); });
|
||||
});
|
||||
add("obj_fit_volume", L("Scale to Fit Print Volume"), L("Object"), [](const std::string&) {
|
||||
return object_op(
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include <ios>
|
||||
#include <iomanip>
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
@@ -6154,6 +6155,14 @@ int PartPlateList::select_plate(int index)
|
||||
//wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT));
|
||||
}
|
||||
|
||||
// With the other plates hidden, the view follows the selected plate.
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && m_plater->is_view3D_shown() && wxGetApp().focus_mode()) {
|
||||
Camera& camera = m_plater->get_camera();
|
||||
Vec3d target = camera.get_target();
|
||||
target.head<2>() = m_plate_list[index]->get_bounding_box().center().head<2>();
|
||||
camera.set_target(target);
|
||||
}
|
||||
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = m_plate_list[index]->get_plate_name();
|
||||
@@ -7063,7 +7072,7 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
|
||||
|
||||
/*rendering related functions*/
|
||||
//render
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome, bool hide_inactive)
|
||||
{
|
||||
const std::lock_guard<std::mutex> local_lock(m_plates_mutex);
|
||||
std::vector<PartPlate*>::iterator it = m_plate_list.begin();
|
||||
@@ -7078,7 +7087,7 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
|
||||
load_icon_textures();
|
||||
for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) {
|
||||
int current_index = (*it)->get_index();
|
||||
if (only_current && (current_index != m_current_plate))
|
||||
if ((only_current || hide_inactive) && (current_index != m_current_plate))
|
||||
continue;
|
||||
if (current_index == m_current_plate) {
|
||||
PartPlate::HeightLimitMode height_mode = (only_current)?PartPlate::HEIGHT_LIMIT_NONE:m_height_limit_mode;
|
||||
@@ -7539,7 +7548,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
|
||||
return ret;
|
||||
}
|
||||
|
||||
int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info, int plate_idx)
|
||||
int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info, int plate_idx, bool with_statistics)
|
||||
{
|
||||
int ret = 0;
|
||||
|
||||
@@ -7613,6 +7622,8 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
}
|
||||
//parse filament info
|
||||
plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result());
|
||||
if (with_statistics)
|
||||
plate_data_item->statistics = gcode_statistics_json(*m_plate_list[i]->get_slice_result());
|
||||
|
||||
// Record mixed (virtual) filaments actually used on this plate.
|
||||
// Source is ToolOrdering::used_mixed_filaments (slots that appeared in
|
||||
|
||||
@@ -1008,7 +1008,7 @@ public:
|
||||
|
||||
/*rendering related functions*/
|
||||
void on_change_color_mode(bool is_dark) { m_is_dark = is_dark; }
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false, bool hide_inactive = false);
|
||||
void set_render_option(bool bedtype_texture, bool plate_settings);
|
||||
void set_render_cali(bool value = true) { render_cali_logo = value; }
|
||||
void render_hover_tooltip() const;
|
||||
@@ -1070,7 +1070,7 @@ public:
|
||||
/* load/store releted functions, with_gcode = true and plate_idx = -1, export all gcode
|
||||
* if with_gcode = true and specify plate_idx, export plate_idx gcode only
|
||||
*/
|
||||
int store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info = true, int plate_idx = -1);
|
||||
int store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info = true, int plate_idx = -1, bool with_statistics = false);
|
||||
int load_from_3mf_structure(PlateDataPtrs& plate_data_list, int filament_count = 1);
|
||||
//load gcode files
|
||||
int load_gcode_files();
|
||||
|
||||
+37
-13
@@ -436,6 +436,7 @@ void Plater::show_illegal_characters_warning(wxWindow* parent)
|
||||
void Plater::mark_plate_toolbar_image_dirty()
|
||||
{
|
||||
m_b_plate_toolbar_image_dirty = true;
|
||||
++m_plate_toolbar_image_version;
|
||||
}
|
||||
|
||||
bool Plater::is_plate_toolbar_image_dirty() const
|
||||
@@ -6070,6 +6071,8 @@ void Sidebar::on_bed_type_change(BedType bed_type)
|
||||
* - Map key encoding:
|
||||
* - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary)
|
||||
* - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary)
|
||||
* - Generic (non-Bambu) units are not four slots wide: their trays index by a cumulative lane
|
||||
* count across units instead of ams_id * 4 + slot_id
|
||||
* - The 0x10000 flag indicates the main/right extruder
|
||||
* - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.)
|
||||
*
|
||||
@@ -6135,13 +6138,26 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
return std::string();
|
||||
};
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
|
||||
auto list = obj->GetFilaSystem()->GetAmsList();
|
||||
for (auto ams : list) {
|
||||
int ams_id = std::stoi(ams.first);
|
||||
int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right
|
||||
// Bambu units are four slots wide; a generic unit reports its own lane count and its trays
|
||||
// carry a cumulative lane index. The AMS map is keyed by string id, so count preceding units
|
||||
// numerically to keep this the same index the parser and the agent use.
|
||||
int generic_tray_base = 0;
|
||||
if (!is_bbl_vendor) {
|
||||
for (const auto& other : list) {
|
||||
if (std::stoi(other.first) < ams_id)
|
||||
generic_tray_base += static_cast<int>(other.second->GetTrays().size());
|
||||
}
|
||||
}
|
||||
for (auto tray : ams.second->GetTrays()) {
|
||||
int slot_id = std::stoi(tray.first);
|
||||
filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id),
|
||||
const int tray_index = is_bbl_vendor ? ams_id * 4 + slot_id : generic_tray_base + slot_id;
|
||||
filament_ams_list.emplace(extruder + tray_index,
|
||||
build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id)));
|
||||
}
|
||||
}
|
||||
@@ -18268,16 +18284,20 @@ wxString Plater::get_project_name()
|
||||
|
||||
void Plater::update_all_plate_thumbnails(bool force_update)
|
||||
{
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++) {
|
||||
PartPlate* plate = get_partplate_list().get_plate(i);
|
||||
ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i};
|
||||
if (force_update || !plate->thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho);
|
||||
}
|
||||
if (force_update || !plate->no_light_thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
}
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++)
|
||||
update_plate_thumbnails(i, force_update);
|
||||
}
|
||||
|
||||
void Plater::update_plate_thumbnails(int plate_idx, bool force_update)
|
||||
{
|
||||
PartPlate* plate = get_partplate_list().get_plate(plate_idx);
|
||||
ThumbnailsParams thumbnail_params = { {}, false, true, true, true, plate_idx};
|
||||
if (force_update || !plate->thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho);
|
||||
}
|
||||
if (force_update || !plate->no_light_thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19564,7 +19584,10 @@ void Plater::export_gcode_3mf(bool export_all)
|
||||
int plate_idx = get_partplate_list().get_curr_plate_index();
|
||||
if (export_all)
|
||||
plate_idx = PLATE_ALL_IDX;
|
||||
export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, plate_idx); // BBS: silence
|
||||
SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence
|
||||
if (wxGetApp().app_config->get_bool("export_statistics"))
|
||||
strategy = strategy | SaveStrategy::WithStatistics;
|
||||
export_3mf(output_path, strategy, plate_idx);
|
||||
|
||||
RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager();
|
||||
|
||||
@@ -20418,7 +20441,8 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
|
||||
//BBS: add bbs 3mf logic
|
||||
PlateDataPtrs plate_data_list;
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx);
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx,
|
||||
strategy & SaveStrategy::WithStatistics);
|
||||
|
||||
// BBS: backup
|
||||
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
|
||||
@@ -434,6 +434,7 @@ public:
|
||||
bool is_new_project_and_check_state() { return m_new_project_and_check_state; }
|
||||
wxString get_project_name();
|
||||
void update_all_plate_thumbnails(bool force_update = false);
|
||||
void update_plate_thumbnails(int plate_idx, bool force_update);
|
||||
void update_obj_preview_thumbnail(ModelObject *, int obj_idx, int vol_idx, std::vector<Slic3r::ColorRGBA> colors, int camera_view_angle_type);
|
||||
void invalid_all_plate_thumbnails();
|
||||
void force_update_all_plate_thumbnails();
|
||||
@@ -1071,6 +1072,8 @@ public:
|
||||
void mark_plate_toolbar_image_dirty();
|
||||
bool is_plate_toolbar_image_dirty() const;
|
||||
void clear_plate_toolbar_image_dirty();
|
||||
// Bumped each time the plate thumbnails go stale, so each consumer can track it on its own.
|
||||
unsigned plate_toolbar_image_version() const { return m_plate_toolbar_image_version; }
|
||||
|
||||
private:
|
||||
struct priv;
|
||||
@@ -1095,6 +1098,7 @@ private:
|
||||
int m_valid_plates_count { 0 };
|
||||
int m_check_status = 0; // 0 not check, 1 check success, 2 check failed
|
||||
bool m_b_plate_toolbar_image_dirty{ true };
|
||||
unsigned m_plate_toolbar_image_version{ 0 };
|
||||
|
||||
void suppress_snapshots();
|
||||
void allow_snapshots();
|
||||
|
||||
@@ -645,7 +645,7 @@ wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxStr
|
||||
combobox->SetSelection(current_region);
|
||||
return;
|
||||
} else {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
config->set("region", region.ToStdString());
|
||||
auto area = config->get_country_code();
|
||||
if (agent) {
|
||||
@@ -2312,6 +2312,13 @@ void PreferencesDialog::create_items()
|
||||
auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets");
|
||||
g_sizer->Add(item_show_unsupported);
|
||||
|
||||
auto item_export_statistics = create_item_checkbox(_L("Export statistics with G-code"),
|
||||
_L("If enabled, exporting a G-code file also writes its statistics as JSON next to it, as <name>.statistics.json, "
|
||||
"and an exported sliced 3MF file holds them inside: print time, filament and feature totals, "
|
||||
"and the same per layer and per object."),
|
||||
"export_statistics");
|
||||
g_sizer->Add(item_export_statistics);
|
||||
|
||||
//// DEVELOPER > Experimental Features
|
||||
g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND);
|
||||
|
||||
@@ -2481,7 +2488,7 @@ wxBoxSizer* PreferencesDialog::create_debug_page()
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
std::string country_code = config->get_country_code();
|
||||
if (agent) {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
agent->set_country_code(country_code);
|
||||
}
|
||||
ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"), ConfirmBeforeSendDialog::VisibleButtons::ONLY_CONFIRM); // ORCA VisibleButtons instead ButtonStyle
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "slic3r/GUI/CalibrationWizardPage.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
@@ -34,14 +36,12 @@
|
||||
#include <wx/image.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/statbox.h>
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/wupdlock.h>
|
||||
#include <wx/menu.h>
|
||||
#include <wx/odcombo.h>
|
||||
@@ -69,6 +69,7 @@
|
||||
#include "MsgDialog.hpp"
|
||||
#include "ParamsDialog.hpp"
|
||||
#include "FilamentPickerDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
@@ -84,6 +85,24 @@
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static FilamentColorPickerValue filament_color_picker_value(const DynamicPrintConfig& config, std::size_t index)
|
||||
{
|
||||
FilamentColorPickerValue value;
|
||||
if (const auto* primary = config.opt<ConfigOptionStrings>("filament_colour"); primary && index < primary->values.size())
|
||||
value.primary = primary->values[index];
|
||||
if (const auto* multi = config.opt<ConfigOptionStrings>("filament_multi_colour"); multi && index < multi->values.size()) {
|
||||
std::istringstream stream(multi->values[index]);
|
||||
for (std::string color; stream >> color;)
|
||||
value.colors.push_back(std::move(color));
|
||||
}
|
||||
if (value.colors.empty() && !value.primary.empty())
|
||||
value.colors.push_back(value.primary);
|
||||
if (const auto* types = config.opt<ConfigOptionStrings>("filament_colour_type"); types && index < types->values.size())
|
||||
value.gradient = value.colors.size() > 1 && types->values[index] == "0";
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
#define BORDER_W 10
|
||||
|
||||
// ---------------------------------
|
||||
@@ -911,31 +930,37 @@ PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset
|
||||
FilamentColor fila_color = get_cur_color_info();
|
||||
|
||||
// Show filament picker dialog
|
||||
FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type);
|
||||
FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type,
|
||||
filament_color_picker_value(m_preset_bundle->project_config, m_filament_idx));
|
||||
|
||||
if (!dialog.IsDataLoaded()) {
|
||||
// If FilamentPicker fails, fallback to default color picker
|
||||
show_default_color_picker();
|
||||
} else if (dialog.ShowModal() == wxID_OK) {
|
||||
// Get selected filament color data
|
||||
FilamentColor fila_color = dialog.GetSelectedFilamentColor();
|
||||
|
||||
// Check if we have valid color data
|
||||
if (!fila_color.m_colors.empty()) {
|
||||
// Convert to storage format
|
||||
std::vector<std::string> colors;
|
||||
for (const wxColour& color : fila_color.m_colors) {
|
||||
colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
}
|
||||
|
||||
bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR);
|
||||
this->sync_colour_config(colors, is_gradient);
|
||||
} else if (dialog.ShowModal() == wxID_OK && dialog.HasSelectionChanged()) {
|
||||
if (const auto& custom = dialog.GetCustomColorSelection()) {
|
||||
const auto value = filament_color_picker_result(*custom);
|
||||
sync_colour_config(value.colors, value.gradient);
|
||||
} else {
|
||||
// Fallback to basic color if no FilamentColor data
|
||||
wxColour selected_color = dialog.GetSelectedColour();
|
||||
if (selected_color.IsOk()) {
|
||||
std::vector<std::string> color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
this->sync_colour_config(color, false);
|
||||
// Get selected filament color data
|
||||
FilamentColor fila_color = dialog.GetSelectedFilamentColor();
|
||||
|
||||
// Check if we have valid color data
|
||||
if (!fila_color.m_colors.empty()) {
|
||||
// Convert to storage format
|
||||
std::vector<std::string> colors;
|
||||
for (const wxColour& color : fila_color.m_colors) {
|
||||
colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
}
|
||||
|
||||
bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR);
|
||||
this->sync_colour_config(colors, is_gradient);
|
||||
} else {
|
||||
// Fallback to basic color if no FilamentColor data
|
||||
wxColour selected_color = dialog.GetSelectedColour();
|
||||
if (selected_color.IsOk()) {
|
||||
std::vector<std::string> color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
this->sync_colour_config(color, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1091,30 +1116,8 @@ bool PlaterPresetComboBox::switch_to_tab()
|
||||
|
||||
void PlaterPresetComboBox::change_extruder_color()
|
||||
{
|
||||
// get current color
|
||||
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
|
||||
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
|
||||
wxColour clr(colors->values[m_filament_idx]);
|
||||
if (!clr.IsOk())
|
||||
clr = wxColour(0, 0, 0); // Don't set alfa to transparence
|
||||
|
||||
auto data = new wxColourData();
|
||||
data->SetChooseFull(1);
|
||||
data->SetColour(clr);
|
||||
|
||||
wxColourDialog dialog(this, data);
|
||||
dialog.CenterOnParent();
|
||||
if (dialog.ShowModal() == wxID_OK)
|
||||
{
|
||||
colors->values[m_filament_idx] = dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
|
||||
DynamicPrintConfig cfg_new = *cfg;
|
||||
cfg_new.set_key_value("filament_colour", colors);
|
||||
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new);
|
||||
this->update();
|
||||
wxGetApp().plater()->on_config_change(cfg_new);
|
||||
}
|
||||
// The Linux sidebar menu edits the same ordered project color as its swatch.
|
||||
show_default_color_picker();
|
||||
}
|
||||
|
||||
void PlaterPresetComboBox::show_add_menu()
|
||||
@@ -1608,19 +1611,12 @@ FilamentColor PlaterPresetComboBox::get_cur_color_info()
|
||||
|
||||
void PlaterPresetComboBox::show_default_color_picker()
|
||||
{
|
||||
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
|
||||
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
|
||||
wxColour current_clr(colors->values[m_filament_idx]);
|
||||
if (!current_clr.IsOk())
|
||||
current_clr = wxColour(0, 0, 0); // Don't set alfa to transparence
|
||||
|
||||
m_clrData.SetColour(current_clr);
|
||||
|
||||
wxColourData data = show_sys_picker_dialog(this, m_clrData);
|
||||
if (m_clrData.GetColour() != data.GetColour()) {
|
||||
std::vector<std::string> color = {data.GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
m_clrData.SetColour(data.GetColour());
|
||||
sync_colour_config(color, false);
|
||||
const auto value = filament_color_picker_value(wxGetApp().preset_bundle->project_config, m_filament_idx);
|
||||
const auto initial = filament_color_picker_initial(value);
|
||||
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(value), clr_picker);
|
||||
if (dialog.ShowModal() == wxID_OK && filament_color_picker_changed(initial, dialog.selection())) {
|
||||
const auto selected = filament_color_picker_result(*dialog.selection());
|
||||
sync_colour_config(selected.colors, selected.gradient);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/clrpicker.h>
|
||||
@@ -201,7 +200,6 @@ public:
|
||||
|
||||
// BBS
|
||||
wxButton* clr_picker { nullptr };
|
||||
wxColourData m_clrData;
|
||||
|
||||
wxColor get_color() { return m_color; }
|
||||
|
||||
|
||||
@@ -558,15 +558,37 @@ void Selection::center()
|
||||
return;
|
||||
}
|
||||
|
||||
double Selection::get_drop_min_z() const
|
||||
{
|
||||
if (!m_valid)
|
||||
return 0.;
|
||||
if (m_mode == Volume)
|
||||
return get_bounding_box().min.z();
|
||||
|
||||
double min_z = DBL_MAX;
|
||||
for (const auto& [obj_idx, inst_idxs] : m_cache.content) {
|
||||
if (obj_idx >= (int)m_model->objects.size())
|
||||
continue; // wipe tower
|
||||
for (int inst_idx : inst_idxs)
|
||||
min_z = std::min(min_z, m_model->objects[obj_idx]->get_instance_min_z(inst_idx));
|
||||
}
|
||||
return min_z == DBL_MAX ? 0. : min_z;
|
||||
}
|
||||
|
||||
bool Selection::can_drop() const
|
||||
{
|
||||
return std::abs(get_drop_min_z()) > -SINKING_Z_THRESHOLD;
|
||||
}
|
||||
|
||||
void Selection::drop()
|
||||
{
|
||||
if (std::abs(this->get_bounding_box().min.z()) < -SINKING_Z_THRESHOLD) {
|
||||
return; // shouldnt happen, but better check anyways, already checked in append_menu_item_drop()
|
||||
}
|
||||
const double min_z = get_drop_min_z();
|
||||
if (std::abs(min_z) < -SINKING_Z_THRESHOLD)
|
||||
return; // already checked in can_drop()
|
||||
|
||||
wxGetApp().plater()->take_snapshot(L("Move object"));
|
||||
|
||||
this->move_to_center(Vec3d(0, 0, -this->get_bounding_box().min.z()));
|
||||
this->move_to_center(Vec3d(0, 0, -min_z));
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& volume = *(*m_volumes)[i];
|
||||
|
||||
@@ -248,6 +248,9 @@ public:
|
||||
void clone(int numbers = 1);
|
||||
void center();
|
||||
void drop();
|
||||
bool can_drop() const;
|
||||
// Lowest world Z that drop() moves to the bed; whole objects count model parts only, as auto drop does.
|
||||
double get_drop_min_z() const;
|
||||
void center_plate(const int plate_idx);
|
||||
void set_printable(bool printable);
|
||||
bool get_auto_drop() const;
|
||||
|
||||
+831
-255
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
#ifndef slic3r_StatusPanel_hpp_
|
||||
#define slic3r_StatusPanel_hpp_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include "DeviceManager.hpp"
|
||||
#include "MonitorPage.hpp"
|
||||
@@ -66,6 +68,7 @@
|
||||
#include "StagedBuild.hpp"
|
||||
|
||||
class StepIndicator;
|
||||
class wxChoice;
|
||||
|
||||
#define COMMAND_TIMEOUT 5
|
||||
|
||||
@@ -140,28 +143,36 @@ class ExtruderImage : public wxWindow
|
||||
ScalableBitmap *m_left_extruder_active_empty;
|
||||
ScalableBitmap *m_left_extruder_unactive_filled;
|
||||
ScalableBitmap *m_left_extruder_unactive_empty;
|
||||
|
||||
ScalableBitmap *m_right_extruder_active_filled;
|
||||
ScalableBitmap *m_right_extruder_active_empty;
|
||||
ScalableBitmap *m_right_extruder_unactive_filled;
|
||||
ScalableBitmap *m_right_extruder_unactive_empty;
|
||||
|
||||
|
||||
ScalableBitmap *m_extruder_single_nozzle_empty_load;
|
||||
ScalableBitmap *m_extruder_single_nozzle_empty_unload;
|
||||
ScalableBitmap *m_extruder_single_nozzle_filled_load;
|
||||
ScalableBitmap *m_extruder_single_nozzle_filled_unload;
|
||||
|
||||
|
||||
ExtruderState m_left_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
ExtruderState m_right_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
ExtruderState m_single_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
std::vector<ExtruderState> m_multi_extruder_states;
|
||||
bool m_generic_nozzle_display{false};
|
||||
|
||||
public:
|
||||
void update(int nozzle_num, int nozzle_id);
|
||||
void update(ExtruderState single_state);
|
||||
void update(ExtruderState right_state, ExtruderState left_state);
|
||||
|
||||
void update(ExtruderState state, int idx);
|
||||
|
||||
void msw_rescale();
|
||||
void updateGenericSize();
|
||||
void setExtruderCount(int nozzle_num);
|
||||
void setGenericNozzleDisplay(bool enabled);
|
||||
void setExtruderUsed(std::string loc);
|
||||
void setExtruderUsed(int nozzle_idx);
|
||||
void paintEvent(wxPaintEvent &evt);
|
||||
|
||||
void render(wxDC &dc);
|
||||
@@ -495,6 +506,8 @@ protected:
|
||||
std::vector<ExtruderImage *> m_extruderImage;
|
||||
|
||||
SwitchBoard * m_nozzle_btn_panel;
|
||||
wxChoice* m_generic_nozzle_selector{nullptr};
|
||||
int m_generic_nozzle_selector_count{0};
|
||||
|
||||
wxStaticText * m_text_tasklist_caption;
|
||||
|
||||
@@ -507,10 +520,23 @@ protected:
|
||||
wxBoxSizer * m_misc_ctrl_sizer;
|
||||
StaticBox* m_fan_panel;
|
||||
StaticLine * m_line_nozzle;
|
||||
TempInput* m_tempCtrl_nozzle;
|
||||
wxWindowID m_nozzle_temp_control_id{wxID_ANY};
|
||||
wxWindow* m_temp_nozzle_parent{nullptr};
|
||||
wxBoxSizer* m_temp_nozzle_sizer{nullptr};
|
||||
size_t m_temp_nozzle_active_count{0};
|
||||
TempInput* m_tempCtrl_nozzle{nullptr};
|
||||
int m_temp_nozzle_timeout{ 0 };
|
||||
TempInput* m_tempCtrl_nozzle_deputy;
|
||||
TempInput* m_tempCtrl_nozzle_deputy{nullptr};
|
||||
int m_temp_nozzle_deputy_timeout{ 0 };
|
||||
|
||||
std::vector<TempInput*> m_tempCtrl_nozzles;
|
||||
std::vector<int> m_temp_nozzle_timeouts;
|
||||
|
||||
// Last (dev_id, device toolhead count, preset nozzle count) we warned about, to avoid log spam.
|
||||
std::string m_last_mismatch_dev_id;
|
||||
int m_last_mismatch_device_count{-1};
|
||||
int m_last_mismatch_preset_count{-1};
|
||||
|
||||
TempInput * m_tempCtrl_bed;
|
||||
int m_temp_bed_timeout {0};
|
||||
TempInput * m_tempCtrl_chamber;
|
||||
@@ -550,6 +576,7 @@ protected:
|
||||
|
||||
/* AMS control box <-> live nozzle-rack panel toggle (rack printers only) */
|
||||
SwitchBoard* m_ams_rack_switch{ nullptr };
|
||||
MultiSwitchButton* m_ams_nozzle_switch{ nullptr };
|
||||
|
||||
AMSControl* m_ams_control;
|
||||
StaticBox* m_ams_control_box;
|
||||
@@ -624,6 +651,9 @@ public:
|
||||
|
||||
wxBoxSizer *create_temp_axis_group(wxWindow *parent);
|
||||
wxBoxSizer *create_temp_control(wxWindow *parent);
|
||||
TempInput* create_nozzle_temp_control(wxWindow *parent, wxWindowID id);
|
||||
void set_temp_input_colors(TempInput* temp_ctrl);
|
||||
void ensure_nozzle_temp_controls(size_t count);
|
||||
wxBoxSizer *create_misc_control(wxWindow *parent);
|
||||
wxBoxSizer *create_axis_control(wxWindow *parent);
|
||||
wxPanel *create_bed_control(wxWindow *parent);
|
||||
@@ -654,6 +684,7 @@ private:
|
||||
friend class MonitorPanel;
|
||||
void wire_controls();
|
||||
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
|
||||
void sync_nozzle_temp_controls(size_t count);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
|
||||
@@ -801,6 +832,7 @@ protected:
|
||||
void update_cloud_subtask(MachineObject *obj);
|
||||
void update_sdcard_subtask(MachineObject *obj);
|
||||
void update_temp_ctrl(MachineObject *obj);
|
||||
void check_extruder_count_mismatch(MachineObject* obj);
|
||||
void update_misc_ctrl(MachineObject *obj);
|
||||
void update_ams(MachineObject* obj);
|
||||
void update_rack(MachineObject* obj);
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "ColorDecomposeDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <wx/weakref.h>
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
#include "Widgets/StaticLine.hpp"
|
||||
@@ -32,9 +37,7 @@
|
||||
#include <mutex>
|
||||
#include <wx/button.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcbuffer.h>
|
||||
#include <wx/dialog.h>
|
||||
@@ -509,30 +512,6 @@ static wxString auto_mix_mode_label(TextureAutoMixMode mode)
|
||||
_L("One-click RYBW auto-mix");
|
||||
}
|
||||
|
||||
static wxPoint constrained_dialog_position(wxWindow* anchor, const wxSize& dialog_size)
|
||||
{
|
||||
if (!anchor)
|
||||
return wxDefaultPosition;
|
||||
|
||||
wxSize size = dialog_size;
|
||||
if (size.x <= 0 || size.y <= 0)
|
||||
size = wxSize(anchor->FromDIP(450), anchor->FromDIP(350));
|
||||
|
||||
wxPoint pos = anchor->ClientToScreen(wxPoint(0, anchor->GetSize().y));
|
||||
wxRect display_rect;
|
||||
int display_idx = wxDisplay::GetFromPoint(pos);
|
||||
if (display_idx != wxNOT_FOUND)
|
||||
display_rect = wxDisplay(display_idx).GetClientArea();
|
||||
else
|
||||
display_rect = wxDisplay().GetClientArea();
|
||||
|
||||
pos.x = std::clamp(pos.x, display_rect.GetLeft(),
|
||||
std::max(display_rect.GetLeft(), display_rect.GetRight() - size.x));
|
||||
pos.y = std::clamp(pos.y, display_rect.GetTop(),
|
||||
std::max(display_rect.GetTop(), display_rect.GetBottom() - size.y));
|
||||
return pos;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FilamentSelectPopup
|
||||
// ============================================================
|
||||
@@ -653,24 +632,22 @@ public:
|
||||
auto on_add_filament = m_on_add_filament;
|
||||
wxWindow* popup_parent = GetParent();
|
||||
wxWindow* color_anchor = m_dialog_anchor ? m_dialog_anchor : popup_parent;
|
||||
wxWeakRef<FilamentSelectPopup> popup(this);
|
||||
m_closing_from_action = true;
|
||||
Dismiss();
|
||||
wxColourData cd;
|
||||
cd.SetChooseFull(true);
|
||||
wxColourDialog dlg(popup_parent, &cd);
|
||||
auto move_color_dialog = [&dlg, color_anchor]() {
|
||||
dlg.Move(constrained_dialog_position(color_anchor, dlg.GetBestSize()));
|
||||
};
|
||||
dlg.Bind(wxEVT_SHOW, [move_color_dialog](wxShowEvent& e) mutable {
|
||||
e.Skip();
|
||||
if (e.IsShown())
|
||||
move_color_dialog();
|
||||
});
|
||||
move_color_dialog();
|
||||
if (dlg.ShowModal() == wxID_OK) {
|
||||
wxColour clr = dlg.GetColourData().GetColour();
|
||||
if (on_add_filament) on_add_filament(clr);
|
||||
std::optional<wxColour> selected;
|
||||
{
|
||||
// Capture the stable mapping row before the transient popup is dismissed.
|
||||
ColorPickerDialog dlg(popup_parent, ColorRGBA(0.f, 0.f, 0.f, 1.f), {}, false, color_anchor);
|
||||
if (popup) popup->Dismiss();
|
||||
// Dismissal can delete the popup. Only copied locals are used below.
|
||||
if (dlg.ShowModal() == wxID_OK && dlg.selection()) {
|
||||
const auto& color = std::get<ColorRGBA>(*dlg.selection());
|
||||
selected = wxColour(static_cast<unsigned char>(std::lround(color.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(color.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(color.b() * 255.f)));
|
||||
}
|
||||
}
|
||||
if (selected && on_add_filament) on_add_filament(*selected);
|
||||
});
|
||||
|
||||
m_content->SetSizer(outer);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "WebGuideDialog.hpp"
|
||||
#include "CloudProvider.hpp"
|
||||
#include "ConfigWizard.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -450,6 +451,16 @@ void GuideFrame::on_profile_loaded()
|
||||
SaveProfileData();
|
||||
const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll;
|
||||
m_profile_ready = true;
|
||||
notify_profile_loaded();
|
||||
}
|
||||
|
||||
// The data is often ready before the loading page has run its scripts, and a
|
||||
// call into a page without HandleStudio is lost, so the message waits for both.
|
||||
void GuideFrame::notify_profile_loaded()
|
||||
{
|
||||
if (!m_profile_ready || !m_page_ready)
|
||||
return;
|
||||
json res;
|
||||
res["command"] = "userguide_profile_load_finish";
|
||||
res["sequence_id"] = "10001";
|
||||
@@ -545,7 +556,11 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
|
||||
wxString strCmd = j["command"];
|
||||
BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnScriptMessage;Command:" << strCmd;
|
||||
|
||||
if (strCmd == "close_page") {
|
||||
if (strCmd == "userguide_loading_page_ready") {
|
||||
m_page_ready = true;
|
||||
notify_profile_loaded();
|
||||
}
|
||||
else if (strCmd == "close_page") {
|
||||
this->EndModal(wxID_CANCEL);
|
||||
}
|
||||
if (strCmd == "user_clause") {
|
||||
@@ -755,7 +770,7 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
|
||||
if (wxGetApp().is_user_login()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: user_logout on user_guide_finish";
|
||||
// agent->user_logout();
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ public:
|
||||
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
|
||||
void init_guide_paths();
|
||||
void on_profile_loaded();
|
||||
void notify_profile_loaded();
|
||||
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
|
||||
bool BuildProfileDataFromPresetBundle();
|
||||
bool BuildProfileDataFromVendors();
|
||||
@@ -142,6 +143,10 @@ private:
|
||||
|
||||
//First Load
|
||||
bool bFirstComplete{false};
|
||||
// The loading page gets userguide_profile_load_finish once both are true;
|
||||
// every instance of it reports in, so the flag is never reset.
|
||||
bool m_profile_ready{false};
|
||||
bool m_page_ready{false};
|
||||
// Set once in the destructor. Read through `this` by the loading thread
|
||||
// (joined before `this` dies) and captured as the shared_ptr by CallAfter
|
||||
// lambdas so they don't touch `this` after the object is freed.
|
||||
|
||||
@@ -36,12 +36,17 @@
|
||||
#include <wx/textdlg.h>
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/url.h>
|
||||
#include <wx/uri.h>
|
||||
|
||||
#include <slic3r/GUI/Widgets/WebView.hpp>
|
||||
#include <slic3r/GUI/Widgets/WebViewHostDialog.hpp>
|
||||
#include <wx/webview.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <webkit2/webkit2.h>
|
||||
#endif
|
||||
#include <wx/infobar.h>
|
||||
|
||||
namespace pt = boost::property_tree;
|
||||
@@ -55,6 +60,13 @@ namespace GUI {
|
||||
|
||||
#define LOGIN_INFO_UPDATE_TIMER_ID 10002
|
||||
|
||||
// OrcaCloud is a native pane of its own: the site refuses to be framed
|
||||
// (x-frame-options: DENY / CSP frame-ancestors 'none').
|
||||
static const char *const kCloudUrl = "https://cloud.orcaslicer.com";
|
||||
// #LeftBoard in resources/web/homepage/css/home.css is a 262px column plus a 1px right
|
||||
// border; the homepage keeps that width as the nav column while the cloud pane is open.
|
||||
static const int kLeftNavWidth = 263;
|
||||
|
||||
// The home page's strings (resources/web/homepage), keyed by their data-i18n and T() ids.
|
||||
static json home_page_strings()
|
||||
{
|
||||
@@ -136,7 +148,11 @@ WebViewPanel::WebViewPanel(wxWindow *parent)
|
||||
m_reset_on_show = WebView::NeedsRecreateOnShow();
|
||||
SetSizer(topsizer);
|
||||
|
||||
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
// The homepage shell is the left nav column; when OrcaCloud opens, its page lives in a
|
||||
// second native pane beside it (the site refuses to be framed).
|
||||
m_content_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_content_sizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
topsizer->Add(m_content_sizer, wxSizerFlags().Expand().Proportion(1));
|
||||
|
||||
// Log backend information
|
||||
/* m_browser->GetUserAgent() may lead crash
|
||||
@@ -292,12 +308,282 @@ void WebViewPanel::create_browser()
|
||||
m_browser->Hide();
|
||||
}
|
||||
|
||||
// The cloud page gets its own native view, created on first use and kept alive afterwards
|
||||
// so its session and scroll position survive hiding.
|
||||
void WebViewPanel::create_cloud_browser()
|
||||
{
|
||||
m_cloud_browser = WebView::CreateWebView(this, wxEmptyString);
|
||||
|
||||
#ifdef __linux__
|
||||
// The pane's web session is a cookie (guide §3), and WebKitGTK keeps cookies only in memory
|
||||
// unless a persistent store is set up, so without this the sign-in is lost on every restart
|
||||
// (and on every reset_browser). Each wxWebView has its own context, so give the cloud pane its
|
||||
// own store instead of sharing PrinterWebView's. Mirrors PrinterWebView::create_browser.
|
||||
if (auto* wv = static_cast<WebKitWebView*>(m_cloud_browser->GetNativeBackend())) {
|
||||
const auto cookies_path = boost::filesystem::path(data_dir() + "/cache/cloud_cookies.db");
|
||||
auto* cookie_manager = webkit_web_context_get_cookie_manager(webkit_web_view_get_context(wv));
|
||||
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookies_path.c_str(),
|
||||
WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Webview events are command events, so they would otherwise propagate up to the
|
||||
// panel's handlers, which drive the homepage view. Keep the cloud pane's events here.
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NAVIGATING, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_LOADED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_TITLE_CHANGED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_ERROR, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
|
||||
m_cloud_browser->Hide();
|
||||
if (m_content_sizer)
|
||||
m_content_sizer->Add(m_cloud_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
}
|
||||
|
||||
// The scheme://host[:port] origin of an http(s) URL, lower-cased and with the default port
|
||||
// dropped, or empty for any other scheme or a malformed URL. Comparing origins (not a string
|
||||
// prefix) is what keeps a lookalike host such as "https://cloud.orcaslicer.com.evil.com" out.
|
||||
static wxString http_origin_of(const wxString& url)
|
||||
{
|
||||
const wxURI uri(url);
|
||||
if (!uri.HasScheme() || !uri.HasServer())
|
||||
return {};
|
||||
const wxString scheme = uri.GetScheme().Lower();
|
||||
if (scheme != "http" && scheme != "https")
|
||||
return {};
|
||||
wxString origin = scheme + "://" + uri.GetServer().Lower();
|
||||
const wxString port = uri.GetPort();
|
||||
if (!port.IsEmpty() && port != (scheme == "https" ? "443" : "80"))
|
||||
origin += ":" + port;
|
||||
return origin;
|
||||
}
|
||||
|
||||
// Whether a NAVIGATING event will replace the pane's top-level document rather than load a
|
||||
// subframe. Edge and macOS report the target frame; WebKitGTK does not (IsTargetMainFrame() is
|
||||
// always false there), so a user-gesture navigation stands in: a page's own embedded frames load
|
||||
// without a gesture and stay put, while a user following a link to another site does not.
|
||||
static bool cloud_navigation_is_top_level(const wxWebViewEvent& evt)
|
||||
{
|
||||
#ifdef __WXGTK__
|
||||
return evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER;
|
||||
#else
|
||||
return evt.IsTargetMainFrame();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Sends an off-site navigation to the system browser instead of embedding it. Deferred: this runs
|
||||
// inside a native webview navigation callback, so launching must not re-enter it.
|
||||
static void open_cloud_url_externally(const wxString& url)
|
||||
{
|
||||
if (url.empty())
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(info) << "cloud pane: opening externally " << WebView::RedactUrl(url).ToUTF8().data();
|
||||
wxGetApp().CallAfter([url] { wxLaunchDefaultBrowser(url); });
|
||||
}
|
||||
|
||||
// Consumed on the cloud view so it never reaches the homepage handlers. {web} pages stay in the
|
||||
// pane; every other site (OAuth providers included) opens in the system browser (guide §3).
|
||||
void WebViewPanel::OnCloudWebViewEvent(wxWebViewEvent& evt)
|
||||
{
|
||||
const wxEventType event_type = evt.GetEventType();
|
||||
if (event_type == wxEVT_WEBVIEW_NAVIGATING) {
|
||||
const wxString url = evt.GetURL();
|
||||
// Guide §3: keep every {web} page in the pane and open other sites (OAuth providers
|
||||
// included) in the system browser. Only top-level navigations are diverted, and
|
||||
// should_divert_cloud_url() excludes same-origin URLs and internal schemes.
|
||||
if (should_divert_cloud_url(url) && cloud_navigation_is_top_level(evt)) {
|
||||
evt.Veto();
|
||||
open_cloud_url_externally(url);
|
||||
}
|
||||
} else if (event_type == wxEVT_WEBVIEW_NEWWINDOW) {
|
||||
const wxString url = evt.GetURL();
|
||||
if (!url.empty()) {
|
||||
if (is_cloud_origin(url))
|
||||
load_cloud_url(url);
|
||||
else
|
||||
open_cloud_url_externally(url);
|
||||
}
|
||||
evt.Veto();
|
||||
} else if (event_type == wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED) {
|
||||
on_cloud_script_message(evt.GetString().utf8_string());
|
||||
} else if (event_type == wxEVT_WEBVIEW_LOADED || event_type == wxEVT_WEBVIEW_ERROR) {
|
||||
// A Start URL deferred behind a sign-out (see load_cloud_after_signout).
|
||||
if (!m_cloud_after_signout_url.IsEmpty()) {
|
||||
const wxString next = std::exchange(m_cloud_after_signout_url, wxString());
|
||||
load_cloud_url(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The page asks for a web session by posting JSON through window.wx; only the newest request
|
||||
// counts (tracked in GUI_App). Nothing here touches the UI directly: the desktop login dialog and
|
||||
// the network mint must not run on the webview's message-dispatch stack.
|
||||
void WebViewPanel::on_cloud_script_message(const std::string& body)
|
||||
{
|
||||
const json msg = json::parse(body, nullptr, false);
|
||||
if (msg.is_discarded() || !msg.is_object() || msg.value("command", std::string()) != "request_web_session")
|
||||
return;
|
||||
|
||||
std::string state;
|
||||
if (msg.contains("data") && msg["data"].is_object())
|
||||
state = msg["data"].value("state", std::string());
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
wxGetApp().CallAfter([state = std::move(state)] { wxGetApp().request_cloud_web_session(state); });
|
||||
}
|
||||
|
||||
void WebViewPanel::apply_cloud_layout(bool cloud_on)
|
||||
{
|
||||
if (!m_content_sizer)
|
||||
return;
|
||||
|
||||
// Cloud mode pins the homepage to the nav column width; otherwise it fills the panel.
|
||||
if (m_browser) {
|
||||
if (wxSizerItem* item = m_content_sizer->GetItem(m_browser))
|
||||
item->SetProportion(cloud_on ? 0 : 1);
|
||||
// kLeftNavWidth is in DIP: the web view's CSS px are DIP, so scale to physical pixels.
|
||||
m_browser->SetMinSize(cloud_on ? m_browser->FromDIP(wxSize(kLeftNavWidth, -1)) : wxDefaultSize);
|
||||
}
|
||||
Layout();
|
||||
}
|
||||
|
||||
void WebViewPanel::load_cloud_url(const wxString& url)
|
||||
{
|
||||
if (url.empty())
|
||||
return;
|
||||
// Track the origin of whatever the pane is told to load (Start URL, Complete URL, sign-out or
|
||||
// the plain cloud fallback), so navigation policy has the right origin from the first load on.
|
||||
const wxString origin = http_origin_of(url);
|
||||
if (!origin.IsEmpty())
|
||||
m_cloud_base_url = origin;
|
||||
if (!m_cloud_browser)
|
||||
return;
|
||||
// Raw LoadURL: WebView::LoadUrl re-serialises through wxURI, which must not alter the
|
||||
// fragment that carries the one-time handoff token. Log only the redacted URL.
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(url).ToUTF8().data();
|
||||
m_cloud_browser->LoadURL(url);
|
||||
}
|
||||
|
||||
bool WebViewPanel::is_cloud_origin(const wxString& url) const
|
||||
{
|
||||
const wxString origin = http_origin_of(url);
|
||||
return !origin.IsEmpty() && !m_cloud_base_url.IsEmpty() && origin == m_cloud_base_url;
|
||||
}
|
||||
|
||||
bool WebViewPanel::should_divert_cloud_url(const wxString& url) const
|
||||
{
|
||||
const wxString origin = http_origin_of(url);
|
||||
// Unknown cloud origin or a non-web scheme: never divert (the pane must keep working).
|
||||
if (origin.IsEmpty() || m_cloud_base_url.IsEmpty())
|
||||
return false;
|
||||
return origin != m_cloud_base_url;
|
||||
}
|
||||
|
||||
// Explicit sign-out ends the pane's own web session. That session is a cookie JS cannot clear
|
||||
// (HttpOnly, guide §3/§8) and ClearHistory() is a no-op on WebKitGTK, so drop the pane's stored
|
||||
// data natively. wx 3.3.2 ignores the type mask on every backend (any non-zero mask clears
|
||||
// everything) and needs an invalid `since` to delete cookies, so the default "all" call is used.
|
||||
void WebViewPanel::clear_cloud_storage()
|
||||
{
|
||||
if (m_cloud_browser)
|
||||
m_cloud_browser->ClearBrowsingData(wxWEBVIEW_BROWSING_DATA_ALL);
|
||||
}
|
||||
|
||||
void WebViewPanel::load_cloud_after_signout(const wxString& url)
|
||||
{
|
||||
if (m_cloud_pending_signout && !m_cloud_signout_url.IsEmpty()) {
|
||||
// The pane did not exist when the user signed out, so its persisted session must be
|
||||
// cleared before the sign-out page runs and `url` loads once that navigation finishes
|
||||
// (see OnCloudWebViewEvent).
|
||||
m_cloud_pending_signout = false;
|
||||
m_cloud_after_signout_url = url;
|
||||
clear_cloud_storage();
|
||||
load_cloud_url(m_cloud_signout_url);
|
||||
return;
|
||||
}
|
||||
load_cloud_url(url);
|
||||
}
|
||||
|
||||
void WebViewPanel::schedule_cloud_load(const wxString& url)
|
||||
{
|
||||
if (!m_cloud_browser) {
|
||||
load_cloud_after_signout(url);
|
||||
return;
|
||||
}
|
||||
// CreateWebView queues the "wx" handler add on the view (macOS/Linux); queue the load behind
|
||||
// it so the page's first scripts already see the bridge (guide §3).
|
||||
m_cloud_browser->CallAfter([this, url]() { load_cloud_after_signout(url); });
|
||||
}
|
||||
|
||||
void WebViewPanel::cloud_signed_out(const wxString& signout_url)
|
||||
{
|
||||
if (signout_url.empty())
|
||||
return;
|
||||
m_cloud_signout_url = signout_url;
|
||||
// The next Start URL must re-seed the pane (it now reflects the signed-out state), so forget
|
||||
// the previous seed regardless of which branch below runs.
|
||||
m_cloud_seed_url.clear();
|
||||
if (m_cloud_browser) {
|
||||
m_cloud_pending_signout = false;
|
||||
// Run the sign-out page (ends the server-side session), then clear the pane's stored data.
|
||||
load_cloud_url(signout_url);
|
||||
clear_cloud_storage();
|
||||
} else {
|
||||
// Pane never opened this run: clear the persisted session on its first open.
|
||||
m_cloud_pending_signout = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
{
|
||||
if (on) {
|
||||
// load_cloud_url records the origin from whatever it loads, including the plain cloud
|
||||
// fallback used when `url` is empty.
|
||||
if (!m_cloud_browser) {
|
||||
create_cloud_browser();
|
||||
m_cloud_seed_url = url;
|
||||
schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url);
|
||||
} else if (!url.empty() && url != m_cloud_seed_url) {
|
||||
// Re-seed only when the Start URL actually changed — a login, an account switch or a
|
||||
// sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged
|
||||
// Start URL re-runs the page's auth check on every open, which flashes "Signing you
|
||||
// in…" and re-requests a session (guide §10: mint when the page asks, not per open).
|
||||
// The pane is kept alive, so an unchanged open just shows the page it already holds.
|
||||
m_cloud_seed_url = url;
|
||||
load_cloud_after_signout(url);
|
||||
}
|
||||
m_cloud_browser->Show();
|
||||
} else if (m_cloud_browser) {
|
||||
// Closing keeps the pane alive with its session, so re-opening does not have to sign in
|
||||
// again (guide §10). Nothing is cleared here.
|
||||
m_cloud_browser->Hide();
|
||||
}
|
||||
apply_cloud_layout(on && m_cloud_browser != nullptr);
|
||||
}
|
||||
|
||||
void WebViewPanel::reset_browser()
|
||||
{
|
||||
// Rebuild both views the same way, keeping the cloud pane's open/closed state.
|
||||
const bool cloud_on = m_cloud_browser && m_cloud_browser->IsShown();
|
||||
|
||||
m_browser->Destroy(); // also removes it from the sizer
|
||||
create_browser();
|
||||
GetSizer()->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
Layout();
|
||||
m_content_sizer->Insert(size_t(0), m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
|
||||
if (m_cloud_browser) {
|
||||
m_cloud_browser->Destroy();
|
||||
m_cloud_browser = nullptr;
|
||||
create_cloud_browser();
|
||||
schedule_cloud_load(wxString(kCloudUrl));
|
||||
m_cloud_browser->Show(cloud_on);
|
||||
// The rebuilt pane holds no seed, so the next open loads the Start URL again.
|
||||
m_cloud_seed_url.clear();
|
||||
}
|
||||
|
||||
apply_cloud_layout(cloud_on);
|
||||
}
|
||||
|
||||
bool WebViewPanel::Show(bool show)
|
||||
@@ -647,7 +933,7 @@ void WebViewPanel::update_mode()
|
||||
*/
|
||||
void WebViewPanel::OnNavigationRequest(wxWebViewEvent& evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
const wxString &url = evt.GetURL();
|
||||
if (url.StartsWith("File://") || url.StartsWith("file://")) {
|
||||
if (!url.Contains("/web/homepage/index.html")) {
|
||||
@@ -693,7 +979,7 @@ void WebViewPanel::OnNavigationComplete(wxWebViewEvent& evt)
|
||||
{
|
||||
m_browser->Show();
|
||||
Layout();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
if (wxGetApp().get_mode() == comDevelop)
|
||||
wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'");
|
||||
UpdateState();
|
||||
@@ -712,6 +998,9 @@ void WebViewPanel::OnDocumentLoaded(wxWebViewEvent& evt)
|
||||
if (wxGetApp().get_mode() == comDevelop)
|
||||
wxLogMessage("%s", "Document loaded; url='" + evt.GetURL() + "'");
|
||||
}
|
||||
// A reloaded homepage loses its cloud-mode class; restore it if the pane is open.
|
||||
if (m_cloud_browser && m_cloud_browser->IsShown())
|
||||
RunScript("SetCloudMode(true)");
|
||||
UpdateState();
|
||||
SendCloudProvidersInfo();
|
||||
}
|
||||
@@ -727,7 +1016,7 @@ void WebViewPanel::OnTitleChanged(wxWebViewEvent &evt)
|
||||
*/
|
||||
void WebViewPanel::OnNewWindow(wxWebViewEvent& evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
wxString flag = " (other)";
|
||||
|
||||
if (evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "wx/fs_mem.h"
|
||||
#include "wx/stdpaths.h"
|
||||
#include <wx/panel.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/tbarbase.h>
|
||||
#include "wx/textctrl.h"
|
||||
#include <wx/timer.h>
|
||||
@@ -114,14 +115,62 @@ public:
|
||||
|
||||
void update_mode();
|
||||
|
||||
// Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the
|
||||
// cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide
|
||||
// whether it already has a session; empty falls back to the plain cloud page.
|
||||
void set_cloud_mode(bool on, const wxString& url = wxString());
|
||||
|
||||
// True while the OrcaCloud pane exists and is visible.
|
||||
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
|
||||
|
||||
// Loads `url` in the cloud pane — the webview that owns the session handoff. Raw LoadURL (no
|
||||
// wxURI re-serialisation) so the Complete URL's #fragment stays intact; never logged.
|
||||
void load_cloud_url(const wxString& url);
|
||||
|
||||
// Clears the cloud pane's own session on an explicit desktop sign-out. If the pane has not
|
||||
// been created yet, the sign-out is remembered and applied when it is first opened.
|
||||
void cloud_signed_out(const wxString& signout_url);
|
||||
|
||||
bool Show(bool show = true) override;
|
||||
private:
|
||||
void create_browser();
|
||||
void create_cloud_browser();
|
||||
void reset_browser();
|
||||
// Consumes the cloud pane's webview events so they stay off the homepage handlers.
|
||||
void OnCloudWebViewEvent(wxWebViewEvent& evt);
|
||||
// True when `url` has the same scheme/host/port as the cloud origin ({web}).
|
||||
bool is_cloud_origin(const wxString& url) const;
|
||||
// True when a navigation to `url` must leave the pane: an http(s) URL on another origin than
|
||||
// the (known) cloud origin. False for non-web schemes and while the origin is unknown.
|
||||
bool should_divert_cloud_url(const wxString& url) const;
|
||||
// Parses a request_web_session message and defers it to GUI_App.
|
||||
void on_cloud_script_message(const std::string& body);
|
||||
// Loads `url`, first clearing a lazily-required sign-out (see cloud_signed_out).
|
||||
void load_cloud_after_signout(const wxString& url);
|
||||
// First load of a freshly created pane: deferred so CreateWebView's queued "wx" handler add
|
||||
// runs first and the page sees window.wx at load time (guide §3).
|
||||
void schedule_cloud_load(const wxString& url);
|
||||
// Clears the pane's stored web data (its cookie-backed session) on an explicit sign-out.
|
||||
void clear_cloud_storage();
|
||||
// Re-applies the content sizer's proportions and min sizes for the cloud state.
|
||||
void apply_cloud_layout(bool cloud_on);
|
||||
|
||||
wxWebView* m_browser;
|
||||
wxWebView* m_cloud_browser{nullptr};
|
||||
wxBoxSizer* m_content_sizer{nullptr};
|
||||
wxString m_home_url;
|
||||
bool m_reset_on_show{false};
|
||||
// {web} origin (scheme://host[:port]) of the loaded pane. Set from every load, including the
|
||||
// plain cloud fallback, so navigation policy can keep {web} pages in the pane and send other
|
||||
// sites to the system browser.
|
||||
wxString m_cloud_base_url;
|
||||
// Start URL the pane was last seeded with. An open whose URL matches this is an ordinary
|
||||
// re-open (nothing changed), so it only shows the pane instead of reloading it (guide §10).
|
||||
wxString m_cloud_seed_url;
|
||||
// Sign-out clear the pane must apply once it exists / next loads.
|
||||
wxString m_cloud_signout_url;
|
||||
wxString m_cloud_after_signout_url;
|
||||
bool m_cloud_pending_signout{false};
|
||||
wxButton * m_button_stop;
|
||||
wxTextCtrl *m_url;
|
||||
#if !BBL_RELEASE_TO_PUBLIC
|
||||
|
||||
@@ -244,7 +244,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
|
||||
m_sizer_ams_body->Add(m_sizer_ams_area_right, wxALIGN_CENTER, 0);
|
||||
|
||||
m_sizer_body->Add(m_sizer_ams_items, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
|
||||
m_sizer_ams_gap = m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
|
||||
m_sizer_body->Add(m_sizer_ams_body, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(m_sizer_down_road, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(m_sizer_ams_option, 0, wxEXPAND, 0);
|
||||
@@ -371,7 +371,7 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else{
|
||||
else if (m_total_ext_count == 1){
|
||||
for (auto id : m_item_ids[MAIN_EXTRUDER_ID]){
|
||||
if (id == ams_id){
|
||||
return true;
|
||||
@@ -379,6 +379,10 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AMSControl::AmsSelectedSwitch(wxCommandEvent& event) {
|
||||
@@ -519,7 +523,7 @@ void AMSControl::msw_rescale()
|
||||
m_button_ams_setting_press.msw_rescale();
|
||||
m_button_ams_setting->SetBitmap(m_button_ams_setting_normal.bmp());
|
||||
|
||||
m_extruder->msw_rescale();
|
||||
if (m_extruder) m_extruder->msw_rescale();
|
||||
|
||||
if (m_button_extruder_feed) m_button_extruder_feed->Rescale(); // ORCA
|
||||
if (m_button_extruder_back) m_button_extruder_back->Rescale(); // ORCA
|
||||
@@ -603,6 +607,8 @@ void AMSControl::CreateAms()
|
||||
|
||||
|
||||
void AMSControl::ClearAms() {
|
||||
m_sizer_ams_items->Clear(false);
|
||||
|
||||
m_simplebook_ams_right->DeleteAllPages();
|
||||
m_simplebook_ams_left->DeleteAllPages();
|
||||
m_simplebook_ams_right->DestroyChildren();
|
||||
@@ -613,7 +619,7 @@ void AMSControl::ClearAms() {
|
||||
m_simplebook_ams_left->Refresh();
|
||||
|
||||
for (auto it : m_ams_preview_list) {
|
||||
delete it.second;
|
||||
if (it.second) it.second->Destroy();
|
||||
}
|
||||
m_ams_preview_list.clear();
|
||||
m_ext_image_list.clear();
|
||||
@@ -624,12 +630,181 @@ void AMSControl::ClearAms() {
|
||||
m_ams_item_list.clear();
|
||||
m_sizer_prv_right->Clear();
|
||||
m_sizer_prv_left->Clear();
|
||||
m_item_ids = { {}, {} };
|
||||
|
||||
if (m_nozzle_book) {
|
||||
m_nozzle_book->Destroy();
|
||||
m_nozzle_book = nullptr;
|
||||
} else {
|
||||
for (auto &pane : m_nozzle_panes) {
|
||||
if (pane.preview_panel) pane.preview_panel->Destroy();
|
||||
if (pane.ams_book) pane.ams_book->Destroy();
|
||||
if (pane.page) pane.page->Destroy();
|
||||
}
|
||||
}
|
||||
m_item_ids.assign(std::max(m_total_ext_count, 2), {});
|
||||
m_nozzle_panes.clear();
|
||||
m_active_nozzle_id = 0;
|
||||
m_current_ams.clear();
|
||||
m_current_show_ams_left.clear();
|
||||
m_current_show_ams_right.clear();
|
||||
pair_id.clear();
|
||||
}
|
||||
|
||||
void AMSControl::restore_legacy_ams_layout()
|
||||
{
|
||||
m_sizer_ams_items->Clear(false);
|
||||
m_sizer_ams_items->SetOrientation(wxHORIZONTAL);
|
||||
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
|
||||
m_sizer_ams_items->Add(m_panel_prv_left, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
m_sizer_ams_items->Add(m_panel_prv_right, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
|
||||
m_panel_prv_left->Show();
|
||||
m_panel_prv_right->Show();
|
||||
m_simplebook_ams_left->Show();
|
||||
m_simplebook_ams_right->Show();
|
||||
m_panel_down_road->Show();
|
||||
m_sizer_body->Show(m_sizer_ams_body, true);
|
||||
m_sizer_option_mid->Show(m_extruder, true);
|
||||
m_down_road->SetSingleSideLayout(false);
|
||||
m_sizer_ams_gap->SetProportion(1);
|
||||
m_sizer_ams_gap->SetBorder(FromDIP(10));
|
||||
m_down_road->SetNozzleCount(m_total_ext_count);
|
||||
}
|
||||
|
||||
bool AMSControl::use_multi_nozzle_layout() const
|
||||
{
|
||||
const bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
return is_bbl ? m_total_ext_count > 2 : m_nozzle_book != nullptr;
|
||||
}
|
||||
|
||||
void AMSControl::CreateAmsMultiNozzle(const std::string &series_name, const std::string &printer_type) {
|
||||
const size_t pane_count = static_cast<size_t>(std::max(m_total_ext_count, 0));
|
||||
m_nozzle_panes.resize(pane_count);
|
||||
m_active_nozzle_id = 0;
|
||||
|
||||
m_sizer_ams_items->Clear(false);
|
||||
m_sizer_ams_items->SetOrientation(wxVERTICAL);
|
||||
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
|
||||
m_sizer_body->Show(m_sizer_ams_body, false);
|
||||
m_sizer_ams_gap->SetProportion(0);
|
||||
m_sizer_ams_gap->SetBorder(0);
|
||||
|
||||
m_panel_prv_left->Hide();
|
||||
m_panel_prv_right->Hide();
|
||||
m_simplebook_ams_left->Hide();
|
||||
m_simplebook_ams_right->Hide();
|
||||
m_panel_down_road->Show();
|
||||
m_sizer_option_mid->Show(m_extruder, true);
|
||||
m_switcher->Hide();
|
||||
m_down_road->SetNozzleCount(1);
|
||||
m_down_road->SetSingleSideLayout(true, AMSPanelPos::LEFT_PANEL);
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_SINGLE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_extruder->updateNozzleNum(1, series_name);
|
||||
|
||||
m_nozzle_book = new wxSimplebook(m_amswin, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_nozzle_book->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
m_nozzle_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
m_sizer_ams_items->Add(m_nozzle_book, 0, wxALIGN_CENTER);
|
||||
|
||||
for (size_t nozzle_id = 0; nozzle_id < pane_count; ++nozzle_id) {
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
// A generic page is one H2C side. Keep the left-side geometry as the
|
||||
// canonical single-nozzle presentation; the road and AMS item code
|
||||
// still use the same side-specific paths as the legacy layout.
|
||||
pane.panel_pos = AMSPanelPos::LEFT_PANEL;
|
||||
pane.page = new wxPanel(m_nozzle_book, wxID_ANY);
|
||||
pane.page->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
pane.page->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
pane.page_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
pane.page->SetSizer(pane.page_sizer);
|
||||
|
||||
pane.preview_panel = new wxScrolledWindow(pane.page, wxID_ANY);
|
||||
pane.preview_panel->SetScrollRate(10, 0);
|
||||
pane.preview_panel->SetSize(AMS_ITEMS_PANEL_SIZE);
|
||||
pane.preview_panel->SetMinSize(AMS_ITEMS_PANEL_SIZE);
|
||||
pane.preview_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
|
||||
pane.preview_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
pane.preview_panel->SetSizer(pane.preview_sizer);
|
||||
|
||||
// Generic lane cards grow to their calculated width for 1-8 lanes. Keep the
|
||||
// book at the legacy minimum, but allow the page to expand to that width.
|
||||
pane.ams_book = new wxSimplebook(pane.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
|
||||
pane.ams_book->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
pane.ams_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
// wxALIGN_CENTER does not centre horizontally inside a HORIZONTAL sizer, so a book wider
|
||||
// than pane.ams_area's own minimum would sit at ams_area's left edge and overflow to the
|
||||
// right, shifting the whole card column off the shared down-road connector axis. A pair of
|
||||
// stretch spacers keeps the book centred on the pane (and thus on the connector) at any
|
||||
// book width; ams_area is expanded below so the spacers have room to work with.
|
||||
pane.ams_area = new wxBoxSizer(wxHORIZONTAL);
|
||||
pane.ams_area->AddStretchSpacer(1);
|
||||
pane.ams_area->Add(pane.ams_book, 0, wxALIGN_CENTER, 0);
|
||||
pane.ams_area->AddStretchSpacer(1);
|
||||
|
||||
pane.page_sizer->Add(pane.preview_panel, 0, wxALIGN_CENTER | wxTOP, FromDIP(5));
|
||||
pane.page_sizer->Add(pane.ams_area, 0, wxEXPAND | wxTOP, FromDIP(10));
|
||||
|
||||
m_nozzle_book->AddPage(pane.page, wxEmptyString, nozzle_id == 0);
|
||||
}
|
||||
|
||||
auto add_info = [this, series_name, printer_type, pane_count](AMSinfo info) {
|
||||
if (pane_count == 0) return;
|
||||
|
||||
info.nozzle_id = std::clamp(info.nozzle_id, 0, static_cast<int>(pane_count) - 1);
|
||||
AddAmsPreview(info, info.ams_type);
|
||||
if (info.ams_type == AMSModel::EXT_AMS && info.cans.size() == 1)
|
||||
AddAms({info}, series_name, printer_type, m_nozzle_panes[info.nozzle_id].panel_pos);
|
||||
else
|
||||
AddAms(info);
|
||||
};
|
||||
|
||||
for (const auto &info : m_ams_info)
|
||||
add_info(info);
|
||||
|
||||
for (auto &info : m_ext_info) {
|
||||
const int virtual_tray_id = std::atoi(info.ams_id.c_str());
|
||||
if (virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID &&
|
||||
virtual_tray_id >= VIRTUAL_TRAY_MAIN_ID - static_cast<int>(pane_count) + 1)
|
||||
info.nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
|
||||
add_info(info);
|
||||
}
|
||||
|
||||
for (auto &pane : m_nozzle_panes) {
|
||||
if (pane.ams_book && pane.ams_book->GetPageCount() > 0)
|
||||
pane.ams_book->SetSelection(0);
|
||||
if (pane.preview_sizer) pane.preview_sizer->Layout();
|
||||
if (pane.preview_panel) pane.preview_panel->FitInside();
|
||||
if (pane.ams_book) pane.ams_book->Layout();
|
||||
if (pane.page) pane.page->Layout();
|
||||
|
||||
if (!pane.current_ams.empty()) {
|
||||
auto preview = m_ams_preview_list.find(pane.current_ams);
|
||||
if (preview != m_ams_preview_list.end() && preview->second)
|
||||
preview->second->OnSelected();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_nozzle_book && pane_count > 0) {
|
||||
m_nozzle_book->SetSelection(0);
|
||||
m_current_ams = m_nozzle_panes[0].current_ams;
|
||||
if (!m_current_ams.empty())
|
||||
SwitchAms(m_current_ams);
|
||||
else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
}
|
||||
}
|
||||
m_amswin->Layout();
|
||||
m_amswin->Fit();
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std::string &printer_type)
|
||||
{
|
||||
restore_legacy_ams_layout();
|
||||
|
||||
std::vector<AMSinfo> single_info_left;
|
||||
std::vector<AMSinfo> single_info_right;
|
||||
|
||||
@@ -751,6 +926,8 @@ void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std
|
||||
|
||||
void AMSControl::CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type)
|
||||
{
|
||||
restore_legacy_ams_layout();
|
||||
|
||||
std::vector<int>m_item_nums{0,0};
|
||||
std::vector<AMSinfo> single_info;
|
||||
|
||||
@@ -979,6 +1156,17 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
if (m_ams_info[i].routes_to_main_extruder() != ams_info[i].routes_to_main_extruder()) {
|
||||
fresh = true;
|
||||
}
|
||||
if (m_ams_info[i].nozzle_id != ams_info[i].nozzle_id) {
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(m_ext_info.size()); ++i) {
|
||||
if (m_ext_info[i].ams_id != ext_info[i].ams_id ||
|
||||
m_ext_info[i].nozzle_id != ext_info[i].nozzle_id ||
|
||||
m_ext_info[i].ext_type != ext_info[i].ext_type) {
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
@@ -993,11 +1181,16 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
m_dev_id = dev_id;
|
||||
if (fresh){
|
||||
ClearAms();
|
||||
if (m_total_ext_count >= 2){
|
||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
||||
}else{
|
||||
CreateAmsSingleNozzle(series_name, printer_type);
|
||||
bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl) {
|
||||
if (m_total_ext_count == 1)
|
||||
CreateAmsSingleNozzle(series_name, printer_type);
|
||||
else if (m_total_ext_count == 2)
|
||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
||||
}
|
||||
else
|
||||
CreateAmsMultiNozzle(series_name, printer_type);
|
||||
|
||||
SetSize(wxSize(FromDIP(578), -1));
|
||||
SetMinSize(wxSize(FromDIP(578), -1));
|
||||
Layout();
|
||||
@@ -1070,7 +1263,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
}
|
||||
|
||||
/*update ams extruder*/
|
||||
if (m_extruder->updateNozzleNum(m_total_ext_count, series_name))
|
||||
if (m_total_ext_count <= 2 && m_extruder->updateNozzleNum(m_total_ext_count, series_name))
|
||||
{
|
||||
m_amswin->Layout();
|
||||
}
|
||||
@@ -1106,7 +1299,15 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
||||
{
|
||||
AMSPreview *ams_prv = nullptr;
|
||||
|
||||
if (info.routes_to_main_extruder())
|
||||
// Match AddAms: the generic multi-nozzle layout's preview panel is the visible one for
|
||||
// non-Bambu vendors at any extruder count, not only above two.
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, type);
|
||||
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
}
|
||||
else if (info.routes_to_main_extruder())
|
||||
{
|
||||
ams_prv = new AMSPreview(m_panel_prv_right, wxID_ANY, info, type);
|
||||
m_sizer_prv_right->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
@@ -1129,6 +1330,54 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
||||
void AMSControl::createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos) {
|
||||
auto ams_item = new AmsItem(parent, info, info.ams_type, pos);
|
||||
parent->InsertPage(idx, ams_item, wxEmptyString, true);
|
||||
|
||||
// Generic AMS cards may be wider than the legacy 264 DIP page. Propagate
|
||||
// that calculated width through the page and simplebook so 5-8 lanes are
|
||||
// laid out fully instead of being clipped by the old minimum width.
|
||||
if (info.ams_type == AMSModel::GENERIC_AMS) {
|
||||
const int required_width = ams_item->GetMinSize().x;
|
||||
if (required_width > 0) {
|
||||
const int page_width = std::max(parent->GetMinSize().x, required_width);
|
||||
parent->SetMinSize(wxSize(page_width, -1));
|
||||
if (auto *page = parent->GetParent()) {
|
||||
page->SetMinSize(wxSize(std::max(page->GetMinSize().x, page_width), -1));
|
||||
if (auto *nozzle_book = page->GetParent())
|
||||
nozzle_book->SetMinSize(wxSize(std::max(nozzle_book->GetMinSize().x, page_width), -1));
|
||||
}
|
||||
}
|
||||
// Above eight lanes, AmsItem is a fixed-minimum viewport over a wider
|
||||
// lane strip. Expand the viewport through its page's sizers rather
|
||||
// than propagating the strip's virtual width into the card/book.
|
||||
// Reset back to the centered fixed layout for eight or fewer lanes so a
|
||||
// later small unit is not left in expand mode.
|
||||
const bool generic_lane_layout = !wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (generic_lane_layout) {
|
||||
if (auto *page = parent->GetParent()) {
|
||||
auto pane = std::find_if(m_nozzle_panes.begin(), m_nozzle_panes.end(),
|
||||
[page](const NozzleAmsPane &candidate) { return candidate.page == page; });
|
||||
if (pane != m_nozzle_panes.end() && m_nozzle_book) {
|
||||
const bool expand_viewport = info.cans.size() > 8;
|
||||
if (auto *book_item = pane->ams_area->GetItem(pane->ams_book)) {
|
||||
book_item->SetProportion(expand_viewport ? 1 : 0);
|
||||
book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
}
|
||||
// Keep the centring spacers (items 0 and 2) elastic only in the centred layout;
|
||||
// in the >8-lane viewport they must collapse so the book can fill ams_area.
|
||||
if (auto *lead = pane->ams_area->GetItem(static_cast<size_t>(0)))
|
||||
lead->SetProportion(expand_viewport ? 0 : 1);
|
||||
if (auto *trail = pane->ams_area->GetItem(static_cast<size_t>(2)))
|
||||
trail->SetProportion(expand_viewport ? 0 : 1);
|
||||
if (auto *area_item = pane->page_sizer->GetItem(pane->ams_area))
|
||||
area_item->SetFlag(wxEXPAND | wxTOP);
|
||||
if (auto *nozzle_book_item = m_sizer_ams_items->GetItem(m_nozzle_book))
|
||||
nozzle_book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
if (auto *items_item = m_sizer_body->GetItem(m_sizer_ams_items))
|
||||
items_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
m_amswin->Layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ams_item->set_selection(idx);
|
||||
idx++;
|
||||
|
||||
@@ -1153,7 +1402,10 @@ AMSRoadShowMode AMSControl::findFirstMode(AMSPanelPos pos) {
|
||||
if (item->second->get_ams_model() == AMSModel::EXT_AMS && item->second->get_ext_type() == AMSModelOriginType::LITE_EXT) return AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
}
|
||||
else{
|
||||
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM) {
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
}
|
||||
else {
|
||||
for (auto ids : pair_id){
|
||||
if (ids.first == ams_id || ids.second == ams_id){
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
@@ -1172,8 +1424,13 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
wxPanel* book_panel = new wxPanel(parent);
|
||||
wxBoxSizer* book_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
book_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||
book_panel->SetSize(AMS_PANEL_SIZE);
|
||||
book_panel->SetMinSize(AMS_PANEL_SIZE);
|
||||
// Size the page to the AMS book it becomes, not to the legacy fixed width: an AMS card is
|
||||
// added directly as the book page (so it always spans the book), while a single external
|
||||
// spool is wrapped here. Matching the book keeps the Ext card centred on the same axis as
|
||||
// the AMS cards and the shared down-road connector anchored below them.
|
||||
const wxSize book_panel_size(std::max(parent->GetMinSize().x, AMS_PANEL_SIZE.x), AMS_PANEL_SIZE.y);
|
||||
book_panel->SetSize(book_panel_size);
|
||||
book_panel->SetMinSize(book_panel_size);
|
||||
|
||||
AmsItem* ams1 = nullptr, * ams2 = nullptr;
|
||||
ams1 = new AmsItem(book_panel, infos[0], infos[0].ams_type, pos);
|
||||
@@ -1200,6 +1457,14 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
//book_sizer->Add(ams1, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
book_sizer->Add(ams1, 0, wxLEFT, (book_panel->GetSize().x - ams1->GetSize().x) / 2);
|
||||
}
|
||||
else if (pos == AMSPanelPos::SINGLE_PANEL) {
|
||||
// Centre the single external-spool card explicitly, using the same fixed-margin
|
||||
// technique as the Lite-EXT branch above. Two stretch spacers centred the card
|
||||
// against the wrapper's own width rather than the AMS book, which left the card
|
||||
// (and the connector drawn below it) off the book/connector axis.
|
||||
book_sizer->Add(ams1, 0, wxLEFT,
|
||||
std::max(0, (book_panel_size.x - ams1->GetSize().x) / 2));
|
||||
}
|
||||
else{
|
||||
auto ext_image = new AMSExtImage(book_panel, pos, m_total_ext_count, false);
|
||||
book_sizer->Add(ams1, 0, wxLEFT, FromDIP(30));
|
||||
@@ -1228,7 +1493,20 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
|
||||
void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
|
||||
{
|
||||
if (m_total_ext_count > 1){
|
||||
// The generic multi-nozzle layout (non-Bambu vendors) owns the only visible book; a unit must
|
||||
// land there for any extruder count. Keying off m_total_ext_count instead sent 1- and 2-extruder
|
||||
// generic printers into the legacy simplebooks that CreateAmsMultiNozzle hides.
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
pane.item_ids.push_back(info.ams_id);
|
||||
m_item_ids[nozzle_id].push_back(info.ams_id);
|
||||
if (pane.current_ams.empty()) pane.current_ams = info.ams_id;
|
||||
createAms(pane.ams_book, pane.page_index, info, AMSPanelPos::SINGLE_PANEL);
|
||||
pane.ams_book->Fit();
|
||||
pane.ams_book->Layout();
|
||||
}
|
||||
else if (m_total_ext_count == 2){
|
||||
if (info.routes_to_main_extruder()){
|
||||
createAms(m_simplebook_ams_right, m_right_page_index, info, AMSPanelPos::RIGHT_PANEL);
|
||||
}
|
||||
@@ -1239,6 +1517,9 @@ void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
|
||||
else if (m_total_ext_count == 1){
|
||||
createAms(m_simplebook_ams_left, m_left_page_index, info, AMSPanelPos::LEFT_PANEL);
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
m_simplebook_ams_left->Layout();
|
||||
m_simplebook_ams_right->Layout();
|
||||
m_simplebook_ams_left->Refresh();
|
||||
@@ -1269,7 +1550,19 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
if (single_info.size() <= 0){
|
||||
return;
|
||||
}
|
||||
if (m_total_ext_count == 2) {
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
for (const auto &info : single_info) {
|
||||
pane.item_ids.push_back(info.ams_id);
|
||||
m_item_ids[nozzle_id].push_back(info.ams_id);
|
||||
}
|
||||
if (pane.current_ams.empty()) pane.current_ams = single_info.front().ams_id;
|
||||
createAmsPanel(pane.ams_book, pane.page_index, single_info, series_name, printer_type, AMSPanelPos::SINGLE_PANEL, m_total_ext_count);
|
||||
pane.ams_book->Fit();
|
||||
pane.ams_book->Layout();
|
||||
}
|
||||
else if (m_total_ext_count == 2) {
|
||||
if (single_info[0].routes_to_main_extruder()) {
|
||||
createAmsPanel(m_simplebook_ams_right, m_right_page_index, single_info, series_name, printer_type, AMSPanelPos::RIGHT_PANEL, m_total_ext_count);
|
||||
}
|
||||
@@ -1285,6 +1578,9 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
createAmsPanel(m_simplebook_ams_left, m_left_page_index, single_info, series_name, printer_type, AMSPanelPos::LEFT_PANEL, m_total_ext_count);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
m_simplebook_ams_left->Layout();
|
||||
m_simplebook_ams_right->Layout();
|
||||
@@ -1322,6 +1618,21 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos) {
|
||||
if (single_info.size() <= 0) return;
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
for (const auto &info : single_info) {
|
||||
auto *ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, info.ams_type);
|
||||
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
ams_prv->Bind(wxEVT_LEFT_DOWN, [this, ams_prv](wxMouseEvent &e) {
|
||||
SwitchAms(ams_prv->get_ams_id());
|
||||
e.Skip();
|
||||
});
|
||||
m_ams_preview_list[info.ams_id] = ams_prv;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
AMSPreview* ams_prv = nullptr;
|
||||
AMSPreview* ams_prv2 = nullptr;
|
||||
if (pos == AMSPanelPos::RIGHT_PANEL){
|
||||
@@ -1360,8 +1671,97 @@ void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos)
|
||||
}
|
||||
}
|
||||
|
||||
void AMSControl::SelectNozzle(int nozzle_id)
|
||||
{
|
||||
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
|
||||
if (m_active_nozzle_id == nozzle_id &&
|
||||
(!m_nozzle_book || m_nozzle_book->GetSelection() == nozzle_id))
|
||||
return;
|
||||
|
||||
m_active_nozzle_id = nozzle_id;
|
||||
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
|
||||
m_nozzle_book->SetSelection(nozzle_id);
|
||||
m_current_ams = m_nozzle_panes[nozzle_id].current_ams;
|
||||
if (!m_current_ams.empty()) {
|
||||
SwitchAms(m_current_ams);
|
||||
return;
|
||||
}
|
||||
if (m_down_road) {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
}
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
post_event(SimpleEvent(EVT_AMS_SWITCH));
|
||||
}
|
||||
|
||||
void AMSControl::SwitchAms(std::string ams_id)
|
||||
{
|
||||
if (use_multi_nozzle_layout()) {
|
||||
NozzleAmsPane *pane = nullptr;
|
||||
int nozzle_id = -1;
|
||||
for (int index = 0; index < static_cast<int>(m_nozzle_panes.size()); ++index) {
|
||||
auto &candidate = m_nozzle_panes[index];
|
||||
if (std::find(candidate.item_ids.begin(), candidate.item_ids.end(), ams_id) != candidate.item_ids.end()) {
|
||||
pane = &candidate;
|
||||
nozzle_id = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pane) return;
|
||||
|
||||
m_active_nozzle_id = nozzle_id;
|
||||
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
|
||||
m_nozzle_book->SetSelection(nozzle_id);
|
||||
|
||||
pane->current_ams = ams_id;
|
||||
m_current_ams = ams_id;
|
||||
m_current_select = ams_id;
|
||||
|
||||
for (const auto &id : pane->item_ids) {
|
||||
auto preview = m_ams_preview_list.find(id);
|
||||
if (preview == m_ams_preview_list.end() || !preview->second) continue;
|
||||
if (id == ams_id) preview->second->OnSelected();
|
||||
else preview->second->UnSelected();
|
||||
}
|
||||
|
||||
auto item = m_ams_item_list.find(ams_id);
|
||||
if (item != m_ams_item_list.end() && item->second) {
|
||||
if (pane->ams_book)
|
||||
pane->ams_book->SetSelection(item->second->get_selection());
|
||||
|
||||
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
if (item->second->get_can_count() == GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (item->second->get_ams_model() == AMSModel::N3S_AMS)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
else if (item->second->get_ams_model() == AMSModel::AMS_LITE ||
|
||||
(item->second->get_ams_model() == AMSModel::EXT_AMS &&
|
||||
item->second->get_ext_type() == AMSModelOriginType::LITE_EXT))
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
|
||||
if (m_down_road) {
|
||||
if (pane->panel_pos == AMSPanelPos::LEFT_PANEL) {
|
||||
m_down_road->UpdateLeft(1, road_mode);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
} else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, road_mode);
|
||||
}
|
||||
m_down_road->UpdatePassRoad(pane->panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
}
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
|
||||
post_event(SimpleEvent(EVT_AMS_SWITCH));
|
||||
return;
|
||||
}
|
||||
|
||||
if(ams_id == m_current_show_ams_left || ams_id == m_current_show_ams_right){return;}
|
||||
|
||||
bool is_in_right = IsAmsInRightPanel(ams_id);
|
||||
@@ -1432,13 +1832,17 @@ void AMSControl::SwitchAms(std::string ams_id)
|
||||
else {
|
||||
AMSRoadShowMode mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
|
||||
if (item->get_ams_model() == AMSModel::N3S_AMS)
|
||||
if (item->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (item->get_ams_model() == AMSModel::N3S_AMS)
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
|
||||
for (auto it : pair_id) {
|
||||
if (it.first == ams_id || it.second == ams_id) {
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
break;
|
||||
if (item->get_can_count() <= GENERIC_AMS_SLOT_NUM) {
|
||||
for (auto it : pair_id) {
|
||||
if (it.first == ams_id || it.second == ams_id) {
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos == AMSPanelPos::LEFT_PANEL ? m_down_road->UpdateLeft(m_total_ext_count, mode)
|
||||
@@ -1500,6 +1904,20 @@ void AMSControl::SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std
|
||||
AmsItem *item = nullptr;
|
||||
if (m_ams_item_list.find(ams_id) != m_ams_item_list.end()) { item = m_ams_item_list[ams_id]; }
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
|
||||
if (!m_extruder)
|
||||
return;
|
||||
|
||||
if (on_off && item)
|
||||
m_extruder->OnAmsLoading(true, 0, item->GetTagColr(slot_id));
|
||||
else
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (on_off && item) {
|
||||
auto col = item->GetTagColr(slot_id);
|
||||
m_extruder->OnAmsLoading(true, nozzle_id, col);
|
||||
@@ -1624,6 +2042,69 @@ void AMSControl::SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadTy
|
||||
else{
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(ams->get_nozzle_id(), 0, m_total_ext_count - 1);
|
||||
if (nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
if (ams->get_can_count() == GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
else if (ams->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (ams->get_ams_model() == AMSModel::N3S_AMS)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
else if (ams->get_ams_model() == AMSModel::AMS_LITE ||
|
||||
(ams->get_ams_model() == AMSModel::EXT_AMS && ams->get_ext_type() == AMSModelOriginType::LITE_EXT))
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
|
||||
if (can_index >= 0 && can_index < static_cast<int>(info.cans.size()) && m_down_road)
|
||||
m_down_road->SetPassRoadColour(pane.panel_pos == AMSPanelPos::LEFT_PANEL,
|
||||
info.cans[can_index].material_colour);
|
||||
if (m_down_road) {
|
||||
if (pane.panel_pos == AMSPanelPos::LEFT_PANEL) {
|
||||
m_down_road->UpdateLeft(1, road_mode);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
} else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, road_mode);
|
||||
}
|
||||
}
|
||||
|
||||
int road_length = 0;
|
||||
|
||||
if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_NONE) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP1) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (can_index >= 0 && can_index < info.cans.size())
|
||||
{
|
||||
m_down_road->SetPassRoadColour(left, info.cans[can_index].material_colour);
|
||||
|
||||
@@ -47,6 +47,20 @@ public:
|
||||
void on_retry();
|
||||
|
||||
protected:
|
||||
struct NozzleAmsPane
|
||||
{
|
||||
wxPanel* page{nullptr};
|
||||
wxBoxSizer* page_sizer{nullptr};
|
||||
wxScrolledWindow* preview_panel{nullptr};
|
||||
wxBoxSizer* preview_sizer{nullptr};
|
||||
wxSimplebook* ams_book{nullptr};
|
||||
wxBoxSizer* ams_area{nullptr};
|
||||
AMSPanelPos panel_pos{AMSPanelPos::LEFT_PANEL};
|
||||
std::vector<std::string> item_ids;
|
||||
std::string current_ams;
|
||||
int page_index{0};
|
||||
};
|
||||
|
||||
std::string m_current_ams;
|
||||
std::string m_current_slot_left;
|
||||
std::string m_current_slot_right;
|
||||
@@ -63,6 +77,9 @@ protected:
|
||||
|
||||
std::string m_dev_id;
|
||||
std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
|
||||
std::vector<NozzleAmsPane> m_nozzle_panes;
|
||||
wxSimplebook* m_nozzle_book{nullptr};
|
||||
int m_active_nozzle_id{0};
|
||||
std::vector<std::pair<std::string, std::string>> pair_id;
|
||||
|
||||
int m_total_ext_count = 1;
|
||||
@@ -103,6 +120,7 @@ protected:
|
||||
wxSimplebook *m_simplebook_ams_left{nullptr};
|
||||
wxSimplebook *m_simplebook_ams_right{ nullptr };
|
||||
wxSimplebook *m_simplebook_bottom{nullptr};
|
||||
wxSizerItem *m_sizer_ams_gap{nullptr};
|
||||
wxPanel *m_panel_down_road{ nullptr };
|
||||
int m_left_page_index = 0;
|
||||
int m_right_page_index = 0;
|
||||
@@ -156,6 +174,7 @@ public:
|
||||
void createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos);
|
||||
void createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSinfo> infos, const std::string &series_name, const std::string &printer_type, AMSPanelPos pos, int total_ext_num);
|
||||
AMSRoadShowMode findFirstMode(AMSPanelPos pos);
|
||||
void restore_legacy_ams_layout();
|
||||
|
||||
AMSModel m_ams_model{AMSModel::EXT_AMS};
|
||||
AMSModel m_ext_model{AMSModel::EXT_AMS};
|
||||
@@ -183,6 +202,7 @@ public:
|
||||
|
||||
void UpdatePassRoad(std::string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||
void CreateAms();
|
||||
void CreateAmsMultiNozzle(const std::string& series_name, const std::string& printer_type);
|
||||
void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
|
||||
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
|
||||
void ClearAms();
|
||||
@@ -210,6 +230,8 @@ public:
|
||||
void SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std::string slot_id);
|
||||
void SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||
void SwitchAms(std::string ams_id);
|
||||
void SelectNozzle(int nozzle_id);
|
||||
int GetActiveNozzleId() const { return m_active_nozzle_id; }
|
||||
|
||||
void msw_rescale();
|
||||
void on_filament_load(wxCommandEvent &event);
|
||||
@@ -234,6 +256,11 @@ public:
|
||||
private:
|
||||
std::string get_filament_id(const std::string& ams_id, const std::string& can_id);
|
||||
|
||||
// True when AMS units are laid out on the generic multi-nozzle book. Bambu
|
||||
// printers keep the legacy count-based single/double-nozzle layout at any
|
||||
// extruder count; other vendors use the book created for them.
|
||||
bool use_multi_nozzle_layout() const;
|
||||
|
||||
public:
|
||||
std::string m_current_select;
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
|
||||
#include "CalibUtils.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
@@ -76,6 +77,48 @@ namespace Slic3r { namespace GUI {
|
||||
//#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), 144)
|
||||
#define AMS_CANS_WINDOW_SIZE wxSize(FromDIP(264), FromDIP(174))
|
||||
#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), FromDIP(174))
|
||||
|
||||
int generic_ams_layout_width(wxWindow *window, int lane_count)
|
||||
{
|
||||
const int base_width = window->FromDIP(264);
|
||||
const int lane_width = window->FromDIP(52);
|
||||
const int lane_gap = window->FromDIP(4);
|
||||
if (lane_count <= 4)
|
||||
return base_width;
|
||||
|
||||
// Lane centers are distributed as width * (index + 1) / (count + 1).
|
||||
// Keep at least one card width plus the requested gap between centers.
|
||||
return std::max(base_width, (lane_count + 1) * (lane_width + lane_gap));
|
||||
}
|
||||
|
||||
std::vector<int> generic_ams_lane_centers(int lane_count, int width)
|
||||
{
|
||||
std::vector<int> centers;
|
||||
if (lane_count <= 0)
|
||||
return centers;
|
||||
|
||||
centers.reserve(lane_count);
|
||||
for (int lane = 0; lane < lane_count; ++lane)
|
||||
centers.push_back(width * (lane + 1) / (lane_count + 1));
|
||||
return centers;
|
||||
}
|
||||
|
||||
int generic_ams_preview_width(wxWindow *window, int lane_count)
|
||||
{
|
||||
const int base_width = window->FromDIP(52);
|
||||
const int lane_width = window->FromDIP(9);
|
||||
const int lane_gap = window->FromDIP(4);
|
||||
if (lane_count <= 0)
|
||||
return base_width;
|
||||
|
||||
return std::max(base_width, lane_count * lane_width + (lane_count + 1) * lane_gap);
|
||||
}
|
||||
|
||||
bool use_generic_ams_layout(AMSModel model)
|
||||
{
|
||||
return model == AMSModel::GENERIC_AMS &&
|
||||
(!wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor());
|
||||
}
|
||||
bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, bool humidity_flag)
|
||||
{
|
||||
if (!ams) return false;
|
||||
@@ -170,10 +213,14 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
|
||||
info.can_id = std::to_string(0);
|
||||
this->cans.clear();
|
||||
|
||||
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID))
|
||||
try {
|
||||
const int virtual_tray_id = std::stoi(tray.id);
|
||||
if (virtual_tray_id >= 0 && virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID)
|
||||
this->nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
|
||||
}
|
||||
catch (...) {
|
||||
this->nozzle_id = 0;
|
||||
else if (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
|
||||
this->nozzle_id = 1;
|
||||
}
|
||||
|
||||
if (tray.is_tray_info_ready()) {
|
||||
info.ctype = tray.ctype;
|
||||
@@ -856,7 +903,11 @@ AMSextruder::AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const
|
||||
|
||||
void AMSextruder::TurnOff()
|
||||
{
|
||||
m_left_extruder->TurnOff();
|
||||
if (m_left_extruder) m_left_extruder->TurnOff();
|
||||
if (m_right_extruder) m_right_extruder->TurnOff();
|
||||
for (auto *extruder : m_nozzle_extruders) {
|
||||
if (extruder) extruder->TurnOff();
|
||||
}
|
||||
}
|
||||
|
||||
void AMSextruder::create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, int nozzle_num)
|
||||
@@ -882,7 +933,7 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
|
||||
m_left_extruder->OnAmsLoading(load, col);
|
||||
if (load) m_current_colur_deputy = col;
|
||||
}
|
||||
else if (m_nozzle_num > 1){
|
||||
else if (m_nozzle_num == 2){
|
||||
if (nozzle_id == MAIN_EXTRUDER_ID) {
|
||||
m_right_extruder->OnAmsLoading(load, col);
|
||||
if (m_current_colur != col){
|
||||
@@ -896,6 +947,15 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (m_nozzle_num > 2) {
|
||||
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(m_nozzle_extruders.size())) {
|
||||
m_nozzle_extruders[nozzle_id]->OnAmsLoading(load, col);
|
||||
if (load) m_current_colur = col;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
}
|
||||
|
||||
/*return true if something is updated*/
|
||||
@@ -905,12 +965,26 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
|
||||
m_series_name = series_name;
|
||||
m_nozzle_num = nozzle_num;
|
||||
this->DestroyChildren();
|
||||
m_left_extruder = nullptr;
|
||||
m_right_extruder = nullptr;
|
||||
m_nozzle_extruders.clear();
|
||||
|
||||
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
if (m_nozzle_num == 1) {
|
||||
if (MachineObject::is_series_n(m_series_name)) {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
} else if (MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name)) {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
} else {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
|
||||
wxBoxSizer* m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
if (m_nozzle_num >= 2)
|
||||
m_left_extruder->setShowState(true);
|
||||
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
}
|
||||
else if (m_nozzle_num == 2)
|
||||
{
|
||||
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "left_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_left_extruder->setShowState(true);
|
||||
m_right_extruder->setShowState(true);
|
||||
@@ -918,25 +992,17 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
|
||||
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(2));
|
||||
m_bitmap_sizer->AddSpacer(2);
|
||||
}
|
||||
else if (m_nozzle_num > 2) {
|
||||
for (int nozzle_id = 0; nozzle_id < m_nozzle_num; ++nozzle_id) {
|
||||
auto *extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
extruder->setShowState(true);
|
||||
m_nozzle_extruders.push_back(extruder);
|
||||
m_bitmap_sizer->Add(extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MachineObject::is_series_n(m_series_name))
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
}
|
||||
else if(MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name))
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
|
||||
m_left_extruder->setShowState(true);
|
||||
m_right_extruder->setShowState(false);
|
||||
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(3));
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
SetSizer(m_bitmap_sizer);
|
||||
@@ -951,6 +1017,9 @@ void AMSextruder::msw_rescale()
|
||||
//m_amsSextruder->msw_rescale();
|
||||
if (m_left_extruder) m_left_extruder->msw_rescale();
|
||||
if (m_right_extruder) m_right_extruder->msw_rescale();
|
||||
for (auto *extruder : m_nozzle_extruders) {
|
||||
if (extruder) extruder->msw_rescale();
|
||||
}
|
||||
Layout();
|
||||
Update();
|
||||
Refresh();
|
||||
@@ -2123,7 +2192,10 @@ AMSRoadUpPart::AMSRoadUpPart(wxWindow* parent, wxWindowID id, AMSinfo info, AMSM
|
||||
m_ams_model = model;
|
||||
|
||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||
create(parent, id, pos, wxSize(FromDIP(264), FromDIP(34)));
|
||||
const int width = use_generic_ams_layout(m_ams_model)
|
||||
? generic_ams_layout_width(this, static_cast<int>(m_amsinfo.cans.size()))
|
||||
: FromDIP(264);
|
||||
create(parent, id, pos, wxSize(width, FromDIP(34)));
|
||||
}
|
||||
else{
|
||||
create(parent, id, pos, wxSize(FromDIP(78), FromDIP(34)));
|
||||
@@ -2235,6 +2307,38 @@ void AMSRoadUpPart::doRender(wxDC& dc)
|
||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||
dc.SetBrush(wxBrush(*wxTRANSPARENT_BRUSH));
|
||||
|
||||
if (use_generic_ams_layout(m_ams_model)) {
|
||||
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
|
||||
if (lane_count <= 0)
|
||||
return;
|
||||
|
||||
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
|
||||
const int height = FromDIP(21);
|
||||
for (const int lane_center : lane_centers)
|
||||
dc.DrawLine(lane_center, 0, lane_center, height);
|
||||
|
||||
if (lane_centers.size() > 1)
|
||||
dc.DrawLine(lane_centers.front(), height, lane_centers.back(), height);
|
||||
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
|
||||
|
||||
if (m_load_step != AMSPassRoadSTEP::AMS_ROAD_STEP_NONE &&
|
||||
m_load_slot_index >= 0 && m_load_slot_index < lane_count) {
|
||||
const int lane_center = lane_centers[m_load_slot_index];
|
||||
dc.SetPen(wxPen(_get_diff_clr(this, m_amsinfo.cans[m_load_slot_index].material_colour), 4, wxPENSTYLE_SOLID));
|
||||
dc.DrawLine(lane_center, 0, lane_center, height);
|
||||
dc.DrawLine(std::min(lane_center, size.x / 2), height,
|
||||
std::max(lane_center, size.x / 2), height);
|
||||
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
|
||||
m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3)
|
||||
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
|
||||
}
|
||||
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(wxColour(194, 194, 194)));
|
||||
dc.DrawRectangle(size.x / 2 - FromDIP(14), height - FromDIP(5), FromDIP(28), FromDIP(10));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((m_ams_model == N3S_AMS || m_ams_model == EXT_AMS) && m_amsinfo.cans.size() != 4){
|
||||
dc.DrawLine(((float)size.x / 2), (0), ((float)size.x / 2), (size.y));
|
||||
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 || m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3){
|
||||
@@ -2360,6 +2464,25 @@ void AMSRoadDownPart::UpdateRight(int nozzle_num, AMSRoadShowMode mode)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::SetNozzleCount(int nozzle_num)
|
||||
{
|
||||
const int new_nozzle_num = std::max(1, nozzle_num);
|
||||
if (m_nozzle_num == new_nozzle_num) return;
|
||||
|
||||
m_nozzle_num = new_nozzle_num;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::SetSingleSideLayout(bool enabled, AMSPanelPos pos)
|
||||
{
|
||||
if (m_single_side_layout == enabled && m_single_side_pos == pos)
|
||||
return;
|
||||
|
||||
m_single_side_layout = enabled;
|
||||
m_single_side_pos = pos;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::OnVamsLoading(bool load, wxColour col /*= AMS_CONTROL_GRAY500*/)
|
||||
{
|
||||
/*m_vams_loading = load;
|
||||
@@ -2433,8 +2556,14 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
/*if (m_road_color.Alpha() == 0) { dc.SetPen(wxPen(*wxWHITE, m_passroad_width, wxPENSTYLE_SOLID)); }
|
||||
else { dc.SetPen(wxPen(m_road_color, m_passroad_width, wxPENSTYLE_SOLID)); }*/
|
||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||
|
||||
auto xpos = left_nozzle_pos.x;
|
||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE){
|
||||
if (m_single_side_layout) {
|
||||
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
|
||||
}
|
||||
else if (
|
||||
(m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE ||
|
||||
m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE)){
|
||||
auto length = 50;
|
||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE)
|
||||
length = -13;
|
||||
@@ -2465,6 +2594,9 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE:
|
||||
dc.DrawLine(left_nozzle_pos.x, 0, left_nozzle_pos.x, size.y);
|
||||
break;
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||
// Generic N-lane geometry is intentionally left unimplemented.
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -2492,23 +2624,41 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
dc.DrawLine(left_nozzle_pos.x, (size.y / 2), left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||
dc.DrawLine(left_nozzle_pos.x + FromDIP(145), 0, left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||
break;
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||
// Generic N-lane geometry is intentionally left unimplemented.
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_single_side_layout) {
|
||||
if (m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
|
||||
m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3) {
|
||||
dc.SetPen(wxPen(_get_diff_clr(this, m_road_color[1]), 4, wxPENSTYLE_SOLID));
|
||||
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE){
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
|
||||
}
|
||||
const auto &nozzle_pos = m_single_side_layout && m_single_side_pos == AMSPanelPos::RIGHT_PANEL
|
||||
? right_nozzle_pos
|
||||
: left_nozzle_pos;
|
||||
dc.DrawLine(nozzle_pos.x, (size.y / 2), nozzle_pos.x, (size.y));
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
/*dc.DrawLine(FromDIP(left_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(left_nozzle_pos.x), FromDIP(size.y));
|
||||
dc.DrawLine(FromDIP(right_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(right_nozzle_pos.x), FromDIP(size.y));*/
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
|
||||
dc.DrawLine((right_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y));
|
||||
}
|
||||
else {
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
|
||||
}
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
|
||||
// Generic N Nozzles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2533,10 +2683,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
dc.DrawLine(xpos, size.y / 2, xpos, size.y);*/
|
||||
int x = left_nozzle_pos.x;
|
||||
int len = m_right_road_length;
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
// Keep the single-nozzle path anchored at the left nozzle.
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
x = right_nozzle_pos.x;
|
||||
len = len - 14;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||
@@ -2544,10 +2700,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
else{
|
||||
int x = left_nozzle_pos.x;
|
||||
int len = m_right_road_length;
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
// Keep the single-nozzle path anchored at the left nozzle.
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
x = right_nozzle_pos.x;
|
||||
len = len - 14;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||
@@ -2571,7 +2733,7 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP step) {
|
||||
if (m_nozzle_num >= 2){
|
||||
if (m_nozzle_num == 2){
|
||||
if (pos == AMSPanelPos::LEFT_PANEL){
|
||||
if (m_left_road_length == len && m_pass_road_left_step == step){ return; }
|
||||
m_left_road_length = len;;
|
||||
@@ -2583,7 +2745,7 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
||||
m_pass_road_right_step = step;
|
||||
}
|
||||
}
|
||||
else{
|
||||
else if (m_nozzle_num == 1){
|
||||
if (pos == AMSPanelPos::LEFT_PANEL) {
|
||||
if (m_left_road_length == len && m_pass_road_left_step == step && m_right_road_length == -1) { return; }
|
||||
m_left_road_length = len;
|
||||
@@ -2597,6 +2759,9 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
||||
m_pass_road_right_step = step;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@@ -2615,7 +2780,10 @@ AMSPreview::AMSPreview(wxWindow* parent, wxWindowID id, AMSinfo amsinfo, AMSMode
|
||||
{
|
||||
wxWindow::Create(parent, id, pos);
|
||||
if (itemType == AMSModel::GENERIC_AMS || itemType == AMSModel::AMS_LITE || itemType == AMSModel::N3F_AMS) {
|
||||
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
|
||||
if (itemType == AMSModel::GENERIC_AMS && use_generic_ams_layout(itemType))
|
||||
create(parent, id, pos, wxSize(generic_ams_preview_width(this, static_cast<int>(amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y));
|
||||
else
|
||||
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
|
||||
}
|
||||
else {
|
||||
create(parent, id, pos, AMS_PREV_SINGLE_SIZE);
|
||||
@@ -2650,7 +2818,13 @@ void AMSPreview::UpdateInfo(AMSinfo amsinfo)
|
||||
m_amsinfo = amsinfo;
|
||||
m_ams_item_type = amsinfo.ams_type;
|
||||
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
|
||||
const wxSize generic_size(generic_ams_preview_width(this, static_cast<int>(m_amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y);
|
||||
SetSize(generic_size);
|
||||
SetMinSize(generic_size);
|
||||
SetMaxSize(generic_size);
|
||||
}
|
||||
else if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
|
||||
SetMinSize(AMS_PREV_FOUR_SIZE);
|
||||
SetMaxSize(AMS_PREV_FOUR_SIZE);
|
||||
} else {
|
||||
@@ -2758,8 +2932,41 @@ void AMSPreview::doRender(wxDC &dc)
|
||||
dc.DrawRoundedRectangle(0, 0, size.x, size.y, FromDIP(3));
|
||||
|
||||
auto left = 0;
|
||||
//four slot
|
||||
if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
||||
// Generic AMS previews use the same normalized lane centers as the card row and connector.
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
|
||||
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
|
||||
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
|
||||
const int cube_width = FromDIP(9);
|
||||
const int cube_height = FromDIP(14);
|
||||
|
||||
for (int lane = 0; lane < lane_count; ++lane) {
|
||||
const Caninfo &can = m_amsinfo.cans[lane];
|
||||
const int cube_left = lane_centers[lane] - cube_width / 2;
|
||||
const int cube_top = (size.y - cube_height) / 2;
|
||||
wxRect rect(cube_left, cube_top, cube_width, cube_height);
|
||||
|
||||
if (can.material_colour.Alpha() == 0) {
|
||||
if (wxGetApp().dark_mode())
|
||||
dc.DrawBitmap(m_ts_bitmap_cube_dark.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube_dark.GetBmpHeight()) / 2);
|
||||
else
|
||||
dc.DrawBitmap(m_ts_bitmap_cube.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube.GetBmpHeight()) / 2);
|
||||
}
|
||||
else if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
dc.SetPen(wxPen(wxColor(0, 0, 0)));
|
||||
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1),
|
||||
rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
|
||||
}
|
||||
else {
|
||||
wxColour color = can.material_colour;
|
||||
change_the_opacity(color);
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
dc.SetBrush(wxBrush(color));
|
||||
dc.DrawRoundedRectangle(rect, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
// four slot
|
||||
else if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
||||
left = FromDIP(8);
|
||||
for (std::vector<Caninfo>::iterator iter = m_amsinfo.cans.begin(); iter != m_amsinfo.cans.end(); iter++) {
|
||||
|
||||
@@ -3158,10 +3365,19 @@ AmsItem::AmsItem(wxWindow *parent,AMSinfo info, AMSModel model, AMSPanelPos pos
|
||||
m_panel_pos = pos;
|
||||
|
||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_CANS_WINDOW_SIZE);
|
||||
SetSize(AMS_CANS_WINDOW_SIZE);
|
||||
SetMinSize(AMS_CANS_WINDOW_SIZE);
|
||||
SetMaxSize(AMS_CANS_WINDOW_SIZE);
|
||||
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||
const int width = use_generic_ams_layout(m_ams_model) && lane_count <= 8
|
||||
? generic_ams_layout_width(this, lane_count)
|
||||
: FromDIP(264);
|
||||
const wxSize generic_size(width, FromDIP(174));
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, generic_size);
|
||||
SetSize(generic_size);
|
||||
SetMinSize(generic_size);
|
||||
// For large lane counts the card is a viewport, not the lane strip. Let
|
||||
// its parent allocate the available width; the scrolled child retains
|
||||
// the wider virtual content size.
|
||||
if (!(use_generic_ams_layout(m_ams_model) && lane_count > 8))
|
||||
SetMaxSize(generic_size);
|
||||
}
|
||||
else{
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_SINGLE_CAN_SIZE);
|
||||
@@ -3187,12 +3403,43 @@ void AmsItem::create(wxWindow *parent)
|
||||
if (m_ams_model != AMSModel::AMS_LITE) {
|
||||
sizer_can = new wxBoxSizer(wxHORIZONTAL);
|
||||
sizer_item = new wxBoxSizer(wxVERTICAL);
|
||||
auto it = m_info.cans.begin();
|
||||
for (; it != m_info.cans.end(); it++) {
|
||||
AddCan(*it, m_can_count, m_info.cans.size(), sizer_can);
|
||||
const bool generic_layout = use_generic_ams_layout(m_ams_model);
|
||||
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||
const int content_width = generic_layout ? generic_ams_layout_width(this, lane_count) : 0;
|
||||
const bool scroll_lanes = generic_layout && lane_count > 8;
|
||||
const auto lane_centers = generic_layout
|
||||
? generic_ams_lane_centers(lane_count, content_width)
|
||||
: std::vector<int>();
|
||||
|
||||
m_can_parent = this;
|
||||
if (scroll_lanes) {
|
||||
m_can_scroll = new wxScrolledWindow(this, wxID_ANY);
|
||||
m_can_scroll->SetScrollRate(FromDIP(10), 0);
|
||||
m_can_scroll->SetMinSize(wxSize(FromDIP(264), FromDIP(143)));
|
||||
m_can_scroll->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||
m_can_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
m_can_scroll->SetSizer(m_can_scroll_sizer);
|
||||
m_can_parent = m_can_scroll;
|
||||
}
|
||||
|
||||
int previous_right = 0;
|
||||
for (int can_index = 0; can_index < lane_count; ++can_index) {
|
||||
if (generic_layout) {
|
||||
const int left = lane_centers[can_index] - FromDIP(26);
|
||||
sizer_can->AddSpacer(std::max(0, left - previous_right));
|
||||
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
|
||||
previous_right = left + FromDIP(52);
|
||||
}
|
||||
else {
|
||||
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
|
||||
}
|
||||
m_can_count++;
|
||||
}
|
||||
it = m_info.cans.begin();
|
||||
if (generic_layout)
|
||||
sizer_can->AddSpacer(std::max(0, content_width - previous_right));
|
||||
if (generic_layout)
|
||||
sizer_can->SetMinSize(wxSize(content_width, -1));
|
||||
|
||||
//auto road_panel = new wxWindow(this, wxID_ANY);
|
||||
//auto road_panel = new wxPanel(this, wxID_ANY);
|
||||
//road_panel->SetSize(AMS_CAN_ROAD_SIZE);
|
||||
@@ -3207,11 +3454,18 @@ void AmsItem::create(wxWindow *parent)
|
||||
sizer_item->Add(m_ext_image, 0, wxALIGN_CENTER, 0);
|
||||
}
|
||||
}
|
||||
m_panel_road = new AMSRoadUpPart(this, wxID_ANY, m_info, m_ams_model);
|
||||
m_panel_road = new AMSRoadUpPart(m_can_parent, wxID_ANY, m_info, m_ams_model);
|
||||
|
||||
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
||||
if (scroll_lanes) {
|
||||
m_can_scroll_sizer->Add(sizer_can, 0, wxALIGN_LEFT, 0);
|
||||
m_can_scroll_sizer->Add(m_panel_road, 0, wxALIGN_LEFT, 0);
|
||||
sizer_item->Add(m_can_scroll, 1, wxEXPAND);
|
||||
}
|
||||
else {
|
||||
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
||||
}
|
||||
|
||||
SetSizer(sizer_item);
|
||||
}
|
||||
@@ -3235,13 +3489,17 @@ void AmsItem::create(wxWindow *parent)
|
||||
|
||||
Layout();
|
||||
Fit();
|
||||
if (m_can_scroll) {
|
||||
m_can_scroll->Layout();
|
||||
m_can_scroll->FitInside();
|
||||
}
|
||||
Thaw();
|
||||
//Refresh();
|
||||
}
|
||||
|
||||
void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* sizer)
|
||||
{
|
||||
auto amscan = new wxWindow(this, wxID_ANY);
|
||||
auto amscan = new wxWindow(m_can_parent ? m_can_parent : this, wxID_ANY);
|
||||
|
||||
amscan->SetSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||
amscan->SetMinSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||
@@ -3322,7 +3580,8 @@ void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* size
|
||||
amscan->Layout();
|
||||
amscan->Fit();
|
||||
|
||||
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, FromDIP(5));
|
||||
const int margin = use_generic_ams_layout(m_ams_model) ? 0 : FromDIP(5);
|
||||
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, margin);
|
||||
|
||||
m_can_lib_list[caninfo.can_id] = m_panel_lib;
|
||||
//m_can_road_list[caninfo.can_id] = m_panel_road;
|
||||
@@ -3447,6 +3706,10 @@ void AmsItem::UpdateInfo(AMSinfo info)
|
||||
}
|
||||
|
||||
Layout();
|
||||
if (m_can_scroll) {
|
||||
m_can_scroll->Layout();
|
||||
m_can_scroll->FitInside();
|
||||
}
|
||||
}
|
||||
|
||||
void AmsItem::SetDefSelectCan()
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <wx/string.h>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" // Orca: DevFilaSwitch::SwitchPos for inlet-aware AMS placement
|
||||
|
||||
@@ -96,6 +97,7 @@ enum class AMSRoadShowMode : int {
|
||||
AMS_ROAD_MODE_SINGLE,
|
||||
AMS_ROAD_MODE_SINGLE_N3S,
|
||||
AMS_ROAD_MODE_AMS_LITE,
|
||||
AMS_ROAD_MODE_GENERIC,
|
||||
AMS_ROAD_MODE_NONE
|
||||
};
|
||||
|
||||
@@ -480,6 +482,7 @@ public:
|
||||
//AMSextruderImage *m_amsSextruder{nullptr};
|
||||
AMSextruderImage* m_left_extruder = nullptr;
|
||||
AMSextruderImage* m_right_extruder = nullptr;
|
||||
std::vector<AMSextruderImage*> m_nozzle_extruders;
|
||||
AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
|
||||
~AMSextruder();
|
||||
|
||||
@@ -681,6 +684,8 @@ public:
|
||||
// void Update(AMSRoadDownPartMode nozzle, AMSRoadShowMode left_mode, AMSRoadShowMode right_mode, int left_len, int right_len);
|
||||
void UpdateLeft(int nozzle_num, AMSRoadShowMode mode);
|
||||
void UpdateRight(int nozzle_num, AMSRoadShowMode mode);
|
||||
void SetNozzleCount(int nozzle_num);
|
||||
void SetSingleSideLayout(bool enabled, AMSPanelPos pos = AMSPanelPos::LEFT_PANEL);
|
||||
|
||||
void OnVamsLoading(bool load, wxColour col = AMS_CONTROL_GRAY500);
|
||||
void SetPassRoadColour(bool left, wxColour col);
|
||||
@@ -698,6 +703,8 @@ private:
|
||||
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||
bool m_selected = {false};
|
||||
bool m_single_side_layout = {false};
|
||||
AMSPanelPos m_single_side_pos = {AMSPanelPos::LEFT_PANEL};
|
||||
|
||||
int m_left_road_length = {-1};
|
||||
int m_right_road_length = {-1};
|
||||
@@ -864,6 +871,9 @@ private:
|
||||
|
||||
std::map<std::string, AMSLib*> m_can_lib_list;
|
||||
//std::map<std::string, AMSRoad*> m_can_road_list;
|
||||
wxScrolledWindow* m_can_scroll = { nullptr };
|
||||
wxBoxSizer* m_can_scroll_sizer = { nullptr };
|
||||
wxWindow* m_can_parent = { nullptr };
|
||||
AMSRoadUpPart* m_panel_road = { nullptr };
|
||||
std::map<std::string, AMSrefresh*> m_can_refresh_list;
|
||||
AMSHumidity* m_humidity = { nullptr };
|
||||
|
||||
@@ -420,6 +420,14 @@ ExtruderBadge::ExtruderBadge(wxWindow* parent) : wxPanel(parent)
|
||||
|
||||
void ExtruderBadge::SetExtruderInfo(int extruder_id, const std::string& diameter, const NozzleVolumeType& volume_type)
|
||||
{
|
||||
// The badge renders exactly two extruders (left/right). A generic non-BBL printer with
|
||||
// N > 2 can report an out-of-range id here (see MultiNozzleStatusTable::UpdateRackInfo);
|
||||
// reject it rather than writing past the two-element lists.
|
||||
if (extruder_id < 0 || extruder_id >= static_cast<int>(m_diameter_list.size())) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": out-of-range extruder_id " << extruder_id;
|
||||
return;
|
||||
}
|
||||
|
||||
m_diameter_list[extruder_id] = diameter;
|
||||
m_volume_type_list[extruder_id] = volume_type;
|
||||
|
||||
@@ -867,14 +875,21 @@ void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr<DevNozzleRack> rack)
|
||||
bool has_right = false;
|
||||
for (auto& elem : nozzles_in_extruder) {
|
||||
auto& nozzle = elem.second;
|
||||
int extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
||||
if (nozzle.AtRightExtruder())
|
||||
has_right = true;
|
||||
|
||||
int extruder_id{};
|
||||
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
||||
if (nozzle.AtRightExtruder())
|
||||
has_right = true;
|
||||
}
|
||||
else
|
||||
extruder_id = nozzle.GetExtruderId();
|
||||
|
||||
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow);
|
||||
|
||||
m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type);
|
||||
}
|
||||
// TODO: Update for N extruders
|
||||
m_badge->SetExtruderValid(has_right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,9 @@ void TempInput::SetFinish()
|
||||
wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH);
|
||||
event.SetInt(temp_type);
|
||||
event.SetString(wxString::Format("%d", m_input_type));
|
||||
// N-extruder temp controls all share TEMP_OF_NORMAL_TYPE, so the string payload above can't
|
||||
// tell them apart; carry widget identity so the listener can find which one fired.
|
||||
event.SetEventObject(this);
|
||||
wxPostEvent(this->GetParent(), event);
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +444,12 @@ bool WebView::DownloadAndInstallWebViewRuntime()
|
||||
return DownloadAndInstallWV2RT() == 0;
|
||||
}
|
||||
#endif
|
||||
wxString WebView::RedactUrl(const wxString& url)
|
||||
{
|
||||
const size_t cut = url.find_first_of(wxString("?#"));
|
||||
return cut == wxString::npos ? url : url.Left(cut);
|
||||
}
|
||||
|
||||
void WebView::LoadUrl(wxWebView * webView, wxString const &url)
|
||||
{
|
||||
auto url2 = url;
|
||||
@@ -451,7 +457,7 @@ void WebView::LoadUrl(wxWebView * webView, wxString const &url)
|
||||
url2.Replace("\\", "/");
|
||||
#endif
|
||||
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << url2.ToUTF8();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << RedactUrl(url2).ToUTF8();
|
||||
webView->LoadURL(url2);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ public:
|
||||
#endif
|
||||
static void LoadUrl(wxWebView * webView, wxString const &url);
|
||||
|
||||
// Returns the URL truncated at the first '?' or '#', so a query/fragment carrying secrets
|
||||
// (e.g. a session-handoff token) is never written to a log.
|
||||
static wxString RedactUrl(const wxString& url);
|
||||
|
||||
static bool RunScript(wxWebView * webView, wxString const & msg);
|
||||
|
||||
// Marks "wx" as registered so CreateWebView's deferred add skips the duplicate.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/object.h>
|
||||
#include <wx/popupwin.h>
|
||||
@@ -630,43 +630,6 @@ std::vector<std::vector<std::string>> read_color_pack(std::vector<std::string> c
|
||||
return color_info;
|
||||
}
|
||||
|
||||
wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data)
|
||||
{
|
||||
wxColourData data = clr_data;
|
||||
data.SetChooseFull(true);
|
||||
|
||||
// Load custom colors from config (support both "r,g,b,a" and "#RRGGBB" formats)
|
||||
std::vector<std::string> colors = Slic3r::GUI::wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < (int)colors.size(); i++) {
|
||||
wxColour c;
|
||||
if (colors[i].find(',') != std::string::npos)
|
||||
c = string_to_wxColor(colors[i]);
|
||||
else
|
||||
c = wxColour(colors[i]);
|
||||
if (c.IsOk())
|
||||
data.SetCustomColour(i, c);
|
||||
}
|
||||
|
||||
wxColourDialog dialog(parent, &data);
|
||||
dialog.SetTitle(_L("Please choose the filament color"));
|
||||
|
||||
if (dialog.ShowModal() == wxID_OK) {
|
||||
data = dialog.GetColourData();
|
||||
|
||||
// Save custom colors to config (use RGBA string format for consistency)
|
||||
std::vector<std::string> colors;
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
wxColour custom_clr = data.GetCustomColour(i);
|
||||
if (custom_clr.IsOk())
|
||||
colors[i] = color_to_string(custom_clr);
|
||||
}
|
||||
Slic3r::GUI::wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height,
|
||||
const std::vector<wxColour> *ramp){
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <cstddef>
|
||||
#include <wx/checklst.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/combo.h>
|
||||
#include <wx/dataview.h>
|
||||
#include <wx/button.h>
|
||||
@@ -21,7 +20,6 @@
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/artprov.h>
|
||||
#include <wx/colordlg.h>
|
||||
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
@@ -89,7 +87,6 @@ wxBitmap * get_extruder_color_icon(std::string color, std::string label, int ico
|
||||
wxBitmap * get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height,
|
||||
const std::vector<wxColour> *ramp = nullptr);
|
||||
std::vector<std::vector<std::string>> read_color_pack(std::vector<std::string> color_pack);
|
||||
wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data);
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
@@ -485,6 +485,14 @@ void Http::priv::http_perform()
|
||||
if (!postfields.empty()) {
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields.c_str());
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size());
|
||||
} else if (method == "POST" && form == nullptr && mime == nullptr) {
|
||||
// A POST with no body. Without CURLOPT_POSTFIELDS, curl takes the request body from
|
||||
// the read callback, but form_file_read_cb only has a usable user pointer for a PUT
|
||||
// (set_put_body) or a streamed multipart part (form_add_file); for an empty POST it
|
||||
// dereferences libcurl's default READDATA as a form_file and crashes. Point curl at a
|
||||
// zero-length body so the read callback is never used.
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields.c_str());
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(0));
|
||||
}
|
||||
|
||||
CURLcode res = ::curl_easy_perform(curl);
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cctype>
|
||||
#include <ios>
|
||||
@@ -61,6 +62,42 @@ namespace websocket = beast::websocket;
|
||||
namespace net = boost::asio;
|
||||
using tcp = net::ip::tcp;
|
||||
|
||||
// Decimal value of a string that is nothing but digits, or -1 when it is empty or not a number.
|
||||
int decimal_number(const std::string& digits)
|
||||
{
|
||||
if (digits.empty() || !std::all_of(digits.begin(), digits.end(), [](unsigned char c) { return std::isdigit(c) != 0; }))
|
||||
return -1;
|
||||
try {
|
||||
return std::stoi(digits);
|
||||
} catch (...) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Lane database keys address a lane either as "T<N>" (0-based tool index) or as "lane<N>"
|
||||
// (1-based, the AFC/Happy Hare style). Returns -1 when the key carries no usable lane number.
|
||||
int lane_index_from_key(const std::string& lane_key)
|
||||
{
|
||||
if (lane_key.size() > 1 && (lane_key[0] == 'T' || lane_key[0] == 't'))
|
||||
return decimal_number(lane_key.substr(1));
|
||||
if (lane_key.size() > 4 && lane_key.compare(0, 4, "lane") == 0) {
|
||||
const int lane_number = decimal_number(lane_key.substr(4));
|
||||
return lane_number > 0 ? lane_number - 1 : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// AFC names toolheads "extruder", "extruder1", ... and derives the toolhead index from the name
|
||||
// itself (AFC_lane.lane_extruder_index): no suffix is 0, otherwise the suffix is the index.
|
||||
int extruder_index_from_name(const std::string& toolhead)
|
||||
{
|
||||
if (toolhead == "extruder")
|
||||
return 0;
|
||||
if (toolhead.size() > 8 && toolhead.compare(0, 8, "extruder") == 0)
|
||||
return decimal_number(toolhead.substr(8));
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct WsEndpoint
|
||||
{
|
||||
std::string host;
|
||||
@@ -443,19 +480,7 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn)
|
||||
|
||||
void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays)
|
||||
{
|
||||
|
||||
// Look up MachineObject via DeviceManager
|
||||
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
|
||||
if (!dev_manager) {
|
||||
return;
|
||||
}
|
||||
MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build BBL-format JSON for DevFilaSystemParser::ParseV1_0
|
||||
nlohmann::json ams_json = nlohmann::json::object();
|
||||
nlohmann::json ams_array = nlohmann::json::array();
|
||||
|
||||
// Calculate ams_exist_bits and tray_exist_bits
|
||||
@@ -463,11 +488,9 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
|
||||
unsigned long tray_exist_bits = 0;
|
||||
|
||||
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
|
||||
ams_exist_bits |= (1 << ams_id);
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
ams_unit["info"] = "0002"; // treat as AMS_LITE
|
||||
if (ams_id < 32) {
|
||||
ams_exist_bits |= (1UL << ams_id);
|
||||
}
|
||||
|
||||
nlohmann::json tray_array = nlohmann::json::array();
|
||||
int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4);
|
||||
@@ -483,43 +506,135 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json tray_json = nlohmann::json::object();
|
||||
tray_json["id"] = std::to_string(slot_id);
|
||||
tray_json["tag_uid"] = "0000000000000000";
|
||||
|
||||
if (tray && tray->has_filament) {
|
||||
tray_exist_bits |= (1 << slot_index);
|
||||
|
||||
tray_json["tray_info_idx"] = tray->tray_info_idx;
|
||||
tray_json["tray_type"] = tray->tray_type;
|
||||
tray_json["tray_color"] = normalize_color_value(tray->tray_color);
|
||||
|
||||
// Add temperature data if provided
|
||||
if (tray->bed_temp > 0) {
|
||||
tray_json["bed_temp"] = std::to_string(tray->bed_temp);
|
||||
}
|
||||
if (tray->nozzle_temp > 0) {
|
||||
tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp);
|
||||
}
|
||||
} else {
|
||||
tray_json["tray_info_idx"] = "";
|
||||
tray_json["tray_type"] = "";
|
||||
tray_json["tray_color"] = "00000000";
|
||||
tray_json["tray_slot_placeholder"] = "1";
|
||||
if (tray && tray->has_filament && slot_index < 32) {
|
||||
tray_exist_bits |= (1UL << slot_index);
|
||||
}
|
||||
|
||||
tray_array.push_back(tray_json);
|
||||
static const AmsTrayData empty_tray;
|
||||
tray_array.push_back(make_ams_tray_json(std::to_string(slot_id), tray ? *tray : empty_tray));
|
||||
}
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
// Generic unit: type 1 (AMS) maps to AMSModel::GENERIC_AMS, which drives the
|
||||
// variable-lane rendering. This is not a real Bambu AMS-Lite.
|
||||
ams_unit["info"] = "0001";
|
||||
ams_unit["tray"] = tray_array;
|
||||
ams_array.push_back(ams_unit);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
|
||||
submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits);
|
||||
}
|
||||
|
||||
void MoonrakerPrinterAgent::build_ams_payload_grouped(const std::vector<AmsTrayData>& trays)
|
||||
{
|
||||
// One AMS box per extruder, best effort. Lane-data changers (AFC, recent Happy Hare) report an
|
||||
// extruder per lane but no physical box layout, so a box here is a logical feed group, not a
|
||||
// discovered unit. Lanes without an extruder fall back to the main extruder.
|
||||
std::map<int, std::vector<const AmsTrayData*>> boxes;
|
||||
for (const auto& tray : trays) {
|
||||
boxes[tray.extruder_id].push_back(&tray);
|
||||
}
|
||||
|
||||
nlohmann::json ams_array = nlohmann::json::array();
|
||||
unsigned long ams_exist_bits = 0;
|
||||
unsigned long tray_exist_bits = 0;
|
||||
|
||||
int ams_id = 0;
|
||||
// Cumulative lane index across all boxes, in box then slot order. A lane maps to exactly one
|
||||
// extruder, so this is the lane's identity; the parser derives the same index for generic units.
|
||||
int lane_index = 0;
|
||||
for (auto& [extruder_id, lanes] : boxes) {
|
||||
if (ams_id < 32) {
|
||||
ams_exist_bits |= (1UL << ams_id);
|
||||
}
|
||||
|
||||
// Keep a deterministic slot order within the box.
|
||||
std::stable_sort(lanes.begin(), lanes.end(),
|
||||
[](const AmsTrayData* a, const AmsTrayData* b) { return a->slot_index < b->slot_index; });
|
||||
|
||||
nlohmann::json tray_array = nlohmann::json::array();
|
||||
for (size_t slot = 0; slot < lanes.size(); ++slot) {
|
||||
const AmsTrayData& tray = *lanes[slot];
|
||||
// A logical feed group is not a four-slot Bambu AMS: the bit is the lane's cumulative
|
||||
// index, not ams_id*4+slot. DevFilaSystemParser::ParseV1_0 reads the same index.
|
||||
const int bit = lane_index++;
|
||||
if (tray.has_filament && bit < 32) {
|
||||
tray_exist_bits |= (1UL << bit);
|
||||
}
|
||||
tray_array.push_back(make_ams_tray_json(std::to_string(slot), tray));
|
||||
}
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
// info: bits 0-3 = unit type (1 = AMS, i.e. AMSModel::GENERIC_AMS), bits 8-11 = bound
|
||||
// extruder id. A logical feed group is a generic unit, not a Bambu AMS-Lite.
|
||||
const int info_val = 1 | ((extruder_id & 0x0F) << 8);
|
||||
const char* hex_digits = "0123456789ABCDEF";
|
||||
std::string info = "0000";
|
||||
info[3] = hex_digits[info_val & 0x0F];
|
||||
info[2] = hex_digits[(info_val >> 4) & 0x0F];
|
||||
info[1] = hex_digits[(info_val >> 8) & 0x0F];
|
||||
info[0] = hex_digits[(info_val >> 12) & 0x0F];
|
||||
ams_unit["info"] = info;
|
||||
ams_unit["tray"] = tray_array;
|
||||
ams_array.push_back(ams_unit);
|
||||
++ams_id;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload_grouped: Parsed " << trays.size()
|
||||
<< " trays in " << boxes.size() << " extruder box(es)";
|
||||
submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits);
|
||||
}
|
||||
|
||||
nlohmann::json MoonrakerPrinterAgent::make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray)
|
||||
{
|
||||
nlohmann::json tray_json = nlohmann::json::object();
|
||||
tray_json["id"] = tray_id;
|
||||
tray_json["tag_uid"] = "0000000000000000";
|
||||
|
||||
if (tray.has_filament) {
|
||||
tray_json["tray_info_idx"] = tray.tray_info_idx;
|
||||
tray_json["tray_type"] = tray.tray_type;
|
||||
tray_json["tray_color"] = normalize_color_value(tray.tray_color);
|
||||
|
||||
// Add temperature data if provided
|
||||
if (tray.bed_temp > 0) {
|
||||
tray_json["bed_temp"] = std::to_string(tray.bed_temp);
|
||||
}
|
||||
if (tray.nozzle_temp > 0) {
|
||||
tray_json["nozzle_temp_max"] = std::to_string(tray.nozzle_temp);
|
||||
}
|
||||
} else {
|
||||
tray_json["tray_info_idx"] = "";
|
||||
tray_json["tray_type"] = "";
|
||||
tray_json["tray_color"] = "00000000";
|
||||
tray_json["tray_slot_placeholder"] = "1";
|
||||
}
|
||||
|
||||
return tray_json;
|
||||
}
|
||||
|
||||
void MoonrakerPrinterAgent::submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits)
|
||||
{
|
||||
// Look up MachineObject via DeviceManager
|
||||
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
|
||||
if (!dev_manager) {
|
||||
return;
|
||||
}
|
||||
MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format as hex strings (matching BBL protocol)
|
||||
std::ostringstream ams_exist_ss;
|
||||
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
|
||||
std::ostringstream tray_exist_ss;
|
||||
tray_exist_ss << std::hex << std::uppercase << tray_exist_bits;
|
||||
|
||||
nlohmann::json ams_json = nlohmann::json::object();
|
||||
ams_json["ams"] = ams_array;
|
||||
ams_json["ams_exist_bits"] = ams_exist_ss.str();
|
||||
ams_json["tray_exist_bits"] = tray_exist_ss.str();
|
||||
@@ -530,7 +645,6 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
|
||||
|
||||
// Call the parser to populate DevFilaSystem
|
||||
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
|
||||
|
||||
// Set printer_type so update_sync_status() can match it against the preset's printer type.
|
||||
// Without this, the comparison fails and all sync badges are cleared.
|
||||
@@ -570,17 +684,15 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
|
||||
if (fetch_moonraker_filament_data(trays, max_lane_index)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with "
|
||||
<< (max_lane_index + 1) << " lanes";
|
||||
int ams_count = (max_lane_index + 4) / 4;
|
||||
build_ams_payload(ams_count, max_lane_index, trays);
|
||||
build_ams_payload_grouped(trays);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt Happy Hare first (more widely adopted, supports more filament changers)
|
||||
// Fall back to the Happy Hare `mmu` object for HH builds that do not publish lane_data
|
||||
if (fetch_hh_filament_info(trays, max_lane_index)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with "
|
||||
<< (max_lane_index + 1) << " gates";
|
||||
int ams_count = (max_lane_index + 4) / 4;
|
||||
build_ams_payload(ams_count, max_lane_index, trays);
|
||||
build_ams_payload_grouped(trays);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -686,6 +798,30 @@ int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char*
|
||||
return 0;
|
||||
}
|
||||
|
||||
int MoonrakerPrinterAgent::safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value)
|
||||
{
|
||||
auto it = obj.find(key);
|
||||
if (it == obj.end())
|
||||
return default_value;
|
||||
if (it->is_number_integer())
|
||||
return it->get<int>();
|
||||
if (it->is_string()) {
|
||||
const std::string s = it->get<std::string>();
|
||||
if (s.empty())
|
||||
return default_value;
|
||||
try {
|
||||
std::size_t consumed = 0;
|
||||
const int value = std::stoi(s, &consumed);
|
||||
if (consumed != s.size()) // reject partial parses such as "2oops"
|
||||
return default_value;
|
||||
return value;
|
||||
} catch (...) {
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
|
||||
std::string MoonrakerPrinterAgent::safe_array_string(const nlohmann::json& arr, int idx)
|
||||
{
|
||||
if (arr.is_array() && idx >= 0 && idx < static_cast<int>(arr.size()) && arr[idx].is_string())
|
||||
@@ -789,28 +925,39 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
|
||||
trays.clear();
|
||||
max_lane_index = 0;
|
||||
|
||||
// The lane_data namespace carries no extruder field; read the lane -> toolhead binding from the
|
||||
// AFC printer objects so lanes on different toolheads end up in different AMS boxes.
|
||||
std::map<std::string, int> lane_extruder;
|
||||
fetch_afc_lane_extruders(lane_extruder);
|
||||
|
||||
for (const auto& [lane_key, lane_obj] : value.items()) {
|
||||
if (!lane_obj.is_object()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract lane index from the "lane" field (tool number, 0-based)
|
||||
std::string lane_str = safe_json_string(lane_obj, "lane");
|
||||
int lane_index = -1;
|
||||
if (!lane_str.empty()) {
|
||||
try {
|
||||
lane_index = std::stoi(lane_str);
|
||||
} catch (...) {
|
||||
lane_index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Lane tool number (0-based). AFC reports it as a string in the documented sample but it
|
||||
// can also be a number; accept either and skip lanes without a usable index. Databases
|
||||
// written without a "lane" field fall back to the key, which names the lane either as
|
||||
// "T<N>" (0-based) or as "lane<N>" (1-based).
|
||||
int lane_index = safe_json_int_flexible(lane_obj, "lane", -1);
|
||||
if (lane_index < 0) {
|
||||
continue;
|
||||
lane_index = lane_index_from_key(lane_key);
|
||||
if (lane_index < 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = lane_index;
|
||||
// AFC exposes the lane's extruder/toolhead as "extruder_index"; tolerate "extruder_id" as
|
||||
// an alias. lane_data has neither, so fall back to the AFC lane objects and only then to
|
||||
// the main extruder (best effort).
|
||||
tray.extruder_id = safe_json_int_flexible(lane_obj, "extruder_index",
|
||||
safe_json_int_flexible(lane_obj, "extruder_id", -1));
|
||||
if (tray.extruder_id < 0) {
|
||||
const auto it = lane_extruder.find(lane_key);
|
||||
tray.extruder_id = it != lane_extruder.end() ? it->second : 0;
|
||||
}
|
||||
tray.tray_color = safe_json_string(lane_obj, "color");
|
||||
tray.tray_type = safe_json_string(lane_obj, "material");
|
||||
tray.bed_temp = safe_json_int(lane_obj, "bed_temp");
|
||||
@@ -915,50 +1062,119 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse gate data
|
||||
// Parse gate data. Every gate becomes a lane so empty/unknown gates surface
|
||||
// as empty AMS slots rather than vanishing. HH is single-extruder, so all
|
||||
// lanes belong to feed group 0.
|
||||
trays.clear();
|
||||
max_lane_index = 0;
|
||||
|
||||
for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) {
|
||||
// Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available
|
||||
int status = safe_array_int(gate_status, gate_idx);
|
||||
if (status <= 0) {
|
||||
continue; // Skip unknown or empty gates
|
||||
}
|
||||
|
||||
// Extract gate data
|
||||
std::string material = safe_array_string(gate_material, gate_idx);
|
||||
std::string color = safe_array_string(gate_color, gate_idx);
|
||||
int nozzle_temp = safe_array_int(gate_temperature, gate_idx);
|
||||
|
||||
// Skip if no material type (empty gate)
|
||||
if (material.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = gate_idx;
|
||||
tray.tray_type = material;
|
||||
tray.tray_color = color;
|
||||
tray.nozzle_temp = nozzle_temp;
|
||||
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
|
||||
tray.has_filament = true;
|
||||
tray.extruder_id = 0;
|
||||
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
tray.tray_info_idx = bundle
|
||||
? bundle->filaments.filament_id_by_type(tray.tray_type)
|
||||
: map_filament_type_to_generic_id(tray.tray_type);
|
||||
if (status > 0 && !material.empty()) {
|
||||
tray.tray_type = material;
|
||||
tray.tray_color = color;
|
||||
tray.nozzle_temp = nozzle_temp;
|
||||
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
|
||||
tray.has_filament = true;
|
||||
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
tray.tray_info_idx = bundle
|
||||
? bundle->filaments.filament_id_by_type(tray.tray_type)
|
||||
: map_filament_type_to_generic_id(tray.tray_type);
|
||||
} else {
|
||||
tray.has_filament = false;
|
||||
}
|
||||
|
||||
max_lane_index = std::max(max_lane_index, gate_idx);
|
||||
trays.push_back(tray);
|
||||
}
|
||||
|
||||
if (trays.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No valid HH gates found";
|
||||
max_lane_index = num_gates - 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// AFC writes its filament data into the lane_data database namespace, and the records of the
|
||||
// fixture/agent write path carry no extruder field, so the lane -> toolhead binding has to be read
|
||||
// from the printer objects: each AFC_lane <name>.extruder names the toolhead its lane feeds, and
|
||||
// the toolhead's name carries its own index. Best effort: on failure every lane keeps the fallback
|
||||
// of the main extruder.
|
||||
bool MoonrakerPrinterAgent::fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder)
|
||||
{
|
||||
auto query_objects = [this](const std::string& query_string, nlohmann::json& out) {
|
||||
auto http = Http::get(join_url(device_info.base_url, "/printer/objects/query?" + query_string));
|
||||
if (!device_info.api_key.empty()) {
|
||||
http.header("X-Api-Key", device_info.api_key);
|
||||
}
|
||||
std::string body;
|
||||
unsigned status_code = 0;
|
||||
http.timeout_connect(5)
|
||||
.timeout_max(10)
|
||||
.on_complete([&](std::string response, unsigned status) {
|
||||
body = std::move(response);
|
||||
status_code = status;
|
||||
})
|
||||
.perform_sync();
|
||||
if (status_code != 200) {
|
||||
return false;
|
||||
}
|
||||
out = nlohmann::json::parse(body, nullptr, false, true);
|
||||
return !out.is_discarded();
|
||||
};
|
||||
|
||||
nlohmann::json afc_json;
|
||||
if (!query_objects("AFC", afc_json) || !afc_json.contains("result") ||
|
||||
!afc_json["result"].contains("status") || !afc_json["result"]["status"].contains("AFC")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
const auto& afc = afc_json["result"]["status"]["AFC"];
|
||||
const auto& lanes = afc.contains("lanes") ? afc["lanes"] : nlohmann::json::array();
|
||||
if (!lanes.is_array() || lanes.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Query every lane object in one request; the object names are "AFC_lane <lane name>".
|
||||
std::string query_string;
|
||||
for (const auto& lane : lanes) {
|
||||
if (!lane.is_string()) {
|
||||
return false;
|
||||
}
|
||||
if (!query_string.empty()) {
|
||||
query_string += "&";
|
||||
}
|
||||
query_string += "AFC_lane%20" + lane.get<std::string>();
|
||||
}
|
||||
|
||||
nlohmann::json lanes_json;
|
||||
if (!query_objects(query_string, lanes_json) || !lanes_json.contains("result") ||
|
||||
!lanes_json["result"].contains("status")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& status = lanes_json["result"]["status"];
|
||||
for (const auto& lane : lanes) {
|
||||
const std::string name = lane.get<std::string>();
|
||||
const std::string key = "AFC_lane " + name;
|
||||
if (!status.contains(key) || !status[key].is_object() || !status[key].contains("extruder") ||
|
||||
!status[key]["extruder"].is_string()) {
|
||||
continue;
|
||||
}
|
||||
const std::string toolhead = status[key]["extruder"].get<std::string>();
|
||||
const int toolhead_index = extruder_index_from_name(toolhead);
|
||||
if (toolhead_index >= 0) {
|
||||
lane_extruder[name] = toolhead_index;
|
||||
}
|
||||
}
|
||||
|
||||
return !lane_extruder.empty();
|
||||
}
|
||||
|
||||
int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str)
|
||||
@@ -1059,6 +1275,25 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
|
||||
}
|
||||
}
|
||||
|
||||
// Tool selection is printer-specific G-code. Moonraker forwards the
|
||||
// conventional T<index> command to Klipper, where the printer can
|
||||
// implement logical selection or a physical toolhead attach macro.
|
||||
if (cmd == "select_extruder") {
|
||||
if (!json["print"].contains("extruder_index") || !json["print"]["extruder_index"].is_number_integer()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: select_extruder missing integer extruder_index, full json: " << json_str;
|
||||
return BAMBU_NETWORK_ERR_INVALID_RESULT;
|
||||
}
|
||||
|
||||
const int extruder_idx = json["print"]["extruder_index"].get<int>();
|
||||
if (extruder_idx < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: select_extruder received negative extruder_index: " << extruder_idx;
|
||||
return BAMBU_NETWORK_ERR_INVALID_RESULT;
|
||||
}
|
||||
|
||||
send_gcode_async(dev_id, "T" + std::to_string(extruder_idx));
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
if (cmd == "home") {
|
||||
send_gcode_async(dev_id, "G28");
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
@@ -1155,7 +1390,7 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url,
|
||||
nlohmann::json& status,
|
||||
std::string& error) const
|
||||
{
|
||||
std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&extruder&heater_bed&fan");
|
||||
std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&toolhead&extruder&heater_bed&fan");
|
||||
|
||||
std::string response_body;
|
||||
bool success = false;
|
||||
@@ -1563,11 +1798,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
||||
}
|
||||
|
||||
for (const auto& name : this->available_objects) {
|
||||
if (name == "extruder" || name.rfind("extruder", 0) == 0) {
|
||||
const bool is_main_extruder = name == "extruder";
|
||||
const bool is_additional_extruder =
|
||||
name.rfind("extruder", 0) == 0 && name.size() > 8 &&
|
||||
std::all_of(name.begin() + 8, name.end(), [](unsigned char c) { return std::isdigit(c) != 0; });
|
||||
if (is_main_extruder || is_additional_extruder) {
|
||||
subscribe_objects.insert(name);
|
||||
if (name == "extruder") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1863,6 +2099,81 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
|
||||
}
|
||||
}
|
||||
|
||||
// MachineObject::parse_new_info() only parses device.extruder when the four
|
||||
// legacy new-protocol fields are present. Moonraker has no equivalents, so
|
||||
// empty values act as compatibility markers without enabling any features.
|
||||
std::map<int, const nlohmann::json*> extruder_status;
|
||||
for (const auto& item : status_cache.items()) {
|
||||
int extruder_id = -1;
|
||||
if (item.key() == "extruder") {
|
||||
extruder_id = 0;
|
||||
} else if (item.key().rfind("extruder", 0) == 0 && item.key().size() > 8 &&
|
||||
std::all_of(item.key().begin() + 8, item.key().end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
|
||||
try {
|
||||
extruder_id = std::stoi(item.key().substr(8));
|
||||
} catch (...) {
|
||||
extruder_id = -1;
|
||||
}
|
||||
}
|
||||
if (extruder_id >= 0 && item.value().is_object()) {
|
||||
extruder_status.emplace(extruder_id, &item.value());
|
||||
}
|
||||
}
|
||||
|
||||
if (!extruder_status.empty()) {
|
||||
auto& device_extruder = payload["print"]["device"]["extruder"];
|
||||
|
||||
int current_extruder_id = 0;
|
||||
if (status_cache.contains("toolhead") && status_cache["toolhead"].is_object() &&
|
||||
status_cache["toolhead"].contains("extruder") && status_cache["toolhead"]["extruder"].is_string()) {
|
||||
const std::string active_extruder = status_cache["toolhead"]["extruder"].get<std::string>();
|
||||
if (active_extruder != "extruder" && active_extruder.rfind("extruder", 0) == 0 && active_extruder.size() > 8 &&
|
||||
std::all_of(active_extruder.begin() + 8, active_extruder.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
|
||||
try {
|
||||
const int parsed_id = std::stoi(active_extruder.substr(8));
|
||||
if (parsed_id >= 0 && parsed_id < 16 && extruder_status.count(parsed_id) != 0) {
|
||||
current_extruder_id = parsed_id;
|
||||
}
|
||||
} catch (...) {
|
||||
// Keep the main extruder as the fallback for malformed status data.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int extruder_count = std::min<int>(static_cast<int>(extruder_status.size()), 0xF);
|
||||
device_extruder["state"] = extruder_count | (current_extruder_id << 4);
|
||||
device_extruder["info"] = nlohmann::json::array();
|
||||
|
||||
auto encoded_temperature = [](const nlohmann::json& status, const char* key) {
|
||||
if (!status.contains(key) || !status[key].is_number()) {
|
||||
return 0;
|
||||
}
|
||||
const double temperature = status[key].get<double>();
|
||||
return std::clamp(static_cast<int>(temperature + 0.5), 0, 65535);
|
||||
};
|
||||
|
||||
for (const auto& [extruder_id, status] : extruder_status) {
|
||||
const int current_temperature = encoded_temperature(*status, "temperature");
|
||||
const int target_temperature = encoded_temperature(*status, "target");
|
||||
device_extruder["info"].push_back({
|
||||
{"id", extruder_id},
|
||||
{"filam_bak", nlohmann::json::array()},
|
||||
{"info", 1 << 3}, // The configured Moonraker extruder has a toolhead.
|
||||
{"temp", current_temperature | (target_temperature << 16)},
|
||||
{"spre", 0},
|
||||
{"snow", 0},
|
||||
{"star", 0},
|
||||
{"stat", 0},
|
||||
{"hnow", 0},
|
||||
});
|
||||
}
|
||||
|
||||
payload["print"]["cfg"] = "";
|
||||
payload["print"]["fun"] = "";
|
||||
payload["print"]["aux"] = "";
|
||||
payload["print"]["stat"] = "";
|
||||
}
|
||||
|
||||
if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) {
|
||||
const auto& bed = status_cache["heater_bed"];
|
||||
if (bed.contains("temperature") && bed["temperature"].is_number()) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
@@ -88,6 +89,7 @@ protected:
|
||||
// Tray data for AMS payload building
|
||||
struct AmsTrayData {
|
||||
int slot_index = 0; // 0-based slot index
|
||||
int extruder_id = 0; // Extruder this lane feeds (AFC extruder_index); 0 when unknown
|
||||
bool has_filament = false;
|
||||
std::string tray_type; // Material type (e.g., "PLA", "ASA")
|
||||
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
|
||||
@@ -99,6 +101,10 @@ protected:
|
||||
// Build ams JSON and call parser
|
||||
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
|
||||
|
||||
// Build ams JSON with one box per extruder (best effort, for lane_data-based filament
|
||||
// changers such as AFC) and call the parser. Boxes are ordered by extruder id.
|
||||
void build_ams_payload_grouped(const std::vector<AmsTrayData>& trays);
|
||||
|
||||
// Methods that derived classes may need to override or access
|
||||
virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port);
|
||||
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const;
|
||||
@@ -163,10 +169,22 @@ private:
|
||||
// System-specific filament fetch methods
|
||||
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
// Lane name -> toolhead index, read from the AFC printer objects (the lane_data database has no
|
||||
// extruder field). Returns false when AFC is not present or the objects cannot be read.
|
||||
bool fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder);
|
||||
|
||||
// Build one BBL tray JSON object (placeholder when the tray has no filament).
|
||||
static nlohmann::json make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray);
|
||||
|
||||
// Wrap the AMS boxes into the payload ParseV1_0 understands and apply the pull-mode
|
||||
// readiness state (printer_type, push counters, storage, module versions).
|
||||
void submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits);
|
||||
|
||||
// JSON helper methods
|
||||
static std::string safe_json_string(const nlohmann::json& obj, const char* key);
|
||||
static int safe_json_int(const nlohmann::json& obj, const char* key);
|
||||
// Accepts either a JSON number or a numeric string (AFC reports some fields as strings).
|
||||
static int safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value);
|
||||
static std::string safe_array_string(const nlohmann::json& arr, int idx);
|
||||
static int safe_array_int(const nlohmann::json& arr, int idx);
|
||||
static std::string normalize_color_value(const std::string& color);
|
||||
|
||||
@@ -109,6 +109,10 @@ constexpr const char* ORCA_CLOUD_PRINTER = "/api/v1/printers";
|
||||
|
||||
constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login";
|
||||
|
||||
// Gateway endpoint that mints a one-time web-session handoff token for the signed-in desktop
|
||||
// user. The cloud pane reloads the returned token into the webview (see build_web_session_*).
|
||||
constexpr const char* ORCA_WEB_SESSION_HANDOFF_PATH = "/api/v1/account/web-session-handoff";
|
||||
|
||||
constexpr const char* CONFIG_ORCA_API_URL = "orca_api_url";
|
||||
constexpr const char* CONFIG_ORCA_AUTH_URL = "orca_auth_url";
|
||||
constexpr const char* CONFIG_ORCA_CLOUD_URL = "orca_cloud_url";
|
||||
@@ -3019,6 +3023,148 @@ std::string OrcaCloudServiceAgent::get_cloud_login_url(const std::string& langua
|
||||
return url;
|
||||
}
|
||||
|
||||
// Percent-encodes every byte outside the RFC 3986 unreserved set (A-Za-z0-9-._~), so token values
|
||||
// survive fragment parsing intact. Deliberately tiny: no URL-encoder dependency.
|
||||
static std::string percent_encode_unreserved(const std::string& s)
|
||||
{
|
||||
static const char* const hex = "0123456789ABCDEF";
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (unsigned char c : s) {
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
|
||||
c == '-' || c == '.' || c == '_' || c == '~') {
|
||||
out.push_back(static_cast<char>(c));
|
||||
} else {
|
||||
out.push_back('%');
|
||||
out.push_back(hex[c >> 4]);
|
||||
out.push_back(hex[c & 0x0F]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_start_url(const std::string& next) const
|
||||
{
|
||||
if (cloud_base_url.empty())
|
||||
return "";
|
||||
|
||||
// The page replaces a `next` that is not a same-site path with /app/profiles, so pass a
|
||||
// same-site path (the guide's default) and let it validate.
|
||||
std::string url = cloud_base_url + "/auth/handoff?next=" +
|
||||
percent_encode_unreserved(next.empty() ? "/app/profiles" : next);
|
||||
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
if (session.logged_in && !session.user_id.empty())
|
||||
url += "&user_id=" + percent_encode_unreserved(session.user_id);
|
||||
return url;
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_complete_url(const std::string& token_hash,
|
||||
const std::string& state) const
|
||||
{
|
||||
if (cloud_base_url.empty() || token_hash.empty() || state.empty())
|
||||
return "";
|
||||
|
||||
// Fragment (not query) so the one-time token never reaches a server or a log.
|
||||
return cloud_base_url + "/auth/handoff#token_hash=" + percent_encode_unreserved(token_hash) +
|
||||
"&state=" + percent_encode_unreserved(state);
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_signout_url() const
|
||||
{
|
||||
if (cloud_base_url.empty())
|
||||
return "";
|
||||
return cloud_base_url + "/auth/handoff?signout=1";
|
||||
}
|
||||
|
||||
// Extracts the machine-readable code from a handoff error body. 401 uses {"error":"<code>"};
|
||||
// the other statuses use {"error":{"code":"<code>","message":"..."}}.
|
||||
static std::string handoff_error_code(const std::string& body)
|
||||
{
|
||||
const json j = json::parse(body, nullptr, false);
|
||||
if (j.is_discarded() || !j.is_object() || !j.contains("error"))
|
||||
return "";
|
||||
const auto& err = j["error"];
|
||||
if (err.is_string())
|
||||
return err.get<std::string>();
|
||||
if (err.is_object() && err.contains("code") && err["code"].is_string())
|
||||
return err["code"].get<std::string>();
|
||||
return "";
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::request_web_session_handoff(std::string& token_hash, std::string& error_code,
|
||||
unsigned int& http_code)
|
||||
{
|
||||
token_hash.clear();
|
||||
error_code.clear();
|
||||
http_code = 0;
|
||||
|
||||
const std::string url = api_base_url + ORCA_WEB_SESSION_HANDOFF_PATH;
|
||||
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << ORCA_WEB_SESSION_HANDOFF_PATH;
|
||||
|
||||
// Refresh the desktop token the usual way before minting (newest access token).
|
||||
ensure_token_fresh("web_session_handoff");
|
||||
|
||||
auto perform = [&]() {
|
||||
HttpResult result;
|
||||
try {
|
||||
auto http = Http::post(url);
|
||||
http.tls_verify(true);
|
||||
|
||||
const std::string token = get_access_token();
|
||||
|
||||
auto headers = data_headers();
|
||||
for (const auto& pair : headers) {
|
||||
http.header(pair.first, pair.second);
|
||||
}
|
||||
if (!token.empty()) {
|
||||
http.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
|
||||
// No request body: the endpoint identifies the user by the bearer token alone.
|
||||
http.on_complete([&](std::string resp_body, unsigned resp_status) {
|
||||
result.success = true;
|
||||
result.status = resp_status;
|
||||
result.body = resp_body;
|
||||
})
|
||||
.on_error([&](std::string resp_body, std::string error, unsigned resp_status) {
|
||||
result.success = false;
|
||||
result.status = resp_status == 0 ? 404 : resp_status;
|
||||
result.body = resp_body;
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: web-session-handoff HTTP error - " << error;
|
||||
})
|
||||
.timeout_max(30)
|
||||
.perform_sync();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: web-session-handoff exception - " << e.what();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
HttpResult res = perform();
|
||||
// Reuse the shared 401 policy (refresh once, then retry), exactly like http_get/http_post:
|
||||
// `suppress` is true for a transient refresh failure (keep the session and token, no logout)
|
||||
// and false when the refresh token was rejected — so a 401 that remains after the refresh
|
||||
// surfaces through the generic HTTP-error callback and drives the normal desktop logout.
|
||||
// Every other handoff status (409/429/503/transport) stays silent here, so the page can
|
||||
// offer "Try again" instead of the desktop showing a generic cloud error.
|
||||
const bool suppress = resolve_unauthorized(res, perform, "web_session_handoff");
|
||||
if (!suppress && res.status == 401)
|
||||
invoke_http_error_callback(res.status, res.body);
|
||||
|
||||
http_code = res.status;
|
||||
if (res.status == 200) {
|
||||
const json j = json::parse(res.body, nullptr, false);
|
||||
if (!j.is_discarded() && j.is_object() && j.contains("token_hash") && j["token_hash"].is_string())
|
||||
token_hash = j["token_hash"].get<std::string>();
|
||||
} else {
|
||||
error_code = handoff_error_code(res.body);
|
||||
}
|
||||
|
||||
return (res.success && res.status == 200) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::get_mw_user_preference(std::function<void(std::string)> callback)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_mw_user_preference (stub)";
|
||||
|
||||
@@ -203,6 +203,28 @@ public:
|
||||
// ========================================================================
|
||||
std::string get_cloud_service_host() override;
|
||||
std::string get_cloud_login_url(const std::string& language = "") override;
|
||||
|
||||
// ========================================================================
|
||||
// OrcaCloud webview session handoff (OrcaCloud "Webview Session Handoff" guide)
|
||||
// ========================================================================
|
||||
// The cloud pane never receives the desktop's access/refresh tokens. Instead the page posts
|
||||
// request_web_session, the desktop mints a single-use token through the gateway and loads the
|
||||
// Complete URL; the site then signs itself in. These builders return "" when no cloud URL is
|
||||
// configured.
|
||||
//
|
||||
// Start URL: {web}/auth/handoff?next=<path>[&user_id=<desktop user id>]. `next` defaults to
|
||||
// /app/profiles; user_id is included only while signed in.
|
||||
std::string build_web_session_start_url(const std::string& next = std::string()) const;
|
||||
// Complete URL: {web}/auth/handoff#token_hash=<hash>&state=<state>. Both values URL-encoded.
|
||||
std::string build_web_session_complete_url(const std::string& token_hash, const std::string& state) const;
|
||||
// Clears only the webview's own session; loaded on an explicit desktop sign-out.
|
||||
std::string build_web_session_signout_url() const;
|
||||
// POST {api}/api/v1/account/web-session-handoff with the desktop access token (no body),
|
||||
// refreshing once on 401 through the shared policy. On 200 writes the single-use `token_hash`;
|
||||
// otherwise writes the server's error code (e.g. WEB_SESSION_HANDOFF_UNAVAILABLE) into
|
||||
// `error_code`. Blocking: call from a worker thread. Never logs the token.
|
||||
int request_web_session_handoff(std::string& token_hash, std::string& error_code, unsigned int& http_code);
|
||||
|
||||
int connect_server() override;
|
||||
bool is_server_connected() override;
|
||||
int refresh_connection() override;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user