mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 11:51:33 +00:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4662bc67bb | ||
|
|
edcf865191 | ||
|
|
e30b9fe93e | ||
|
|
b7bff92ede | ||
|
|
d48012e18b | ||
|
|
5d73410281 | ||
|
|
b75af485ce | ||
|
|
6a7763f08d | ||
|
|
70e5b330be | ||
|
|
5e94368863 | ||
|
|
0df9b782bb | ||
|
|
d5403ccc94 | ||
|
|
32d690ecee | ||
|
|
303d7a1a07 | ||
|
|
c0e6db227d | ||
|
|
a078155890 | ||
|
|
3b6e27d725 | ||
|
|
6254ab643a | ||
|
|
9ab0178dd0 |
@@ -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,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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -91,12 +91,16 @@ bool DecodedHeightTexture::texel_tap(const Vec2f &uv, bool tile_enabled, Texture
|
||||
const float fy = v * float(height);
|
||||
const int x0 = std::clamp(int(std::floor(fx)), 0, width - 1);
|
||||
const int y0 = std::clamp(int(std::floor(fy)), 0, height - 1);
|
||||
// Neighbour for bilinear filtering: wrap for tiling methods, clamp at the edge otherwise (a
|
||||
// repeating neighbour would incorrectly blend against the opposite edge of the image).
|
||||
// Neighbour for bilinear filtering. Repeat wraps, because its next tile really does start at the
|
||||
// opposite edge. The other two clamp: an untiled placement has nothing past its border, and a
|
||||
// mirrored tile folds back on itself there, so the texel past the edge is the edge again. Wrapping
|
||||
// a mirrored seam blends the two opposite edges of the image together, which is both a visible
|
||||
// line and a disagreement with the preview - the GPU path binds GL_MIRRORED_REPEAT, which folds.
|
||||
const bool wrap_neighbour = tile_enabled && tile_method != TextureTileMethod::MirroredRepeat;
|
||||
tap.x0 = x0;
|
||||
tap.y0 = y0;
|
||||
tap.x1 = tile_enabled ? (x0 + 1) % width : std::min(x0 + 1, width - 1);
|
||||
tap.y1 = tile_enabled ? (y0 + 1) % height : std::min(y0 + 1, height - 1);
|
||||
tap.x1 = wrap_neighbour ? (x0 + 1) % width : std::min(x0 + 1, width - 1);
|
||||
tap.y1 = wrap_neighbour ? (y0 + 1) % height : std::min(y0 + 1, height - 1);
|
||||
tap.tx = fx - std::floor(fx);
|
||||
tap.ty = fy - std::floor(fy);
|
||||
return true;
|
||||
@@ -692,7 +696,68 @@ TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer)
|
||||
for (int i = 0; i < 8; ++i)
|
||||
top += bins[size_t(i)];
|
||||
out.flat_share = float(double(top) / double(npx));
|
||||
out.flat_colors = out.flat_share >= 0.85f;
|
||||
|
||||
// How much of the principal colour axis carries pixels. Sampled rather than exhaustive: the
|
||||
// axis is a bulk property, and a stride keeps this a few milliseconds on a large image.
|
||||
{
|
||||
constexpr size_t MAX_SAMPLES = size_t(1) << 16;
|
||||
const size_t stride = std::max<size_t>(1, npx / MAX_SAMPLES);
|
||||
std::vector<Vec3f> pts;
|
||||
pts.reserve(npx / stride + 1);
|
||||
for (size_t i = 0; i < npx; i += stride)
|
||||
pts.push_back(tex.has_color() ?
|
||||
Vec3f(float(tex.rgb[i * 3]), float(tex.rgb[i * 3 + 1]), float(tex.rgb[i * 3 + 2])) :
|
||||
Vec3f::Constant(float(tex.pixels[i])));
|
||||
if (pts.size() >= 16) {
|
||||
Vec3f mean = Vec3f::Zero();
|
||||
for (const Vec3f &p : pts)
|
||||
mean += p;
|
||||
mean /= float(pts.size());
|
||||
// Power iteration on the covariance: the axis, without forming the matrix.
|
||||
Vec3f axis(1.f, 0.3f, 0.2f);
|
||||
for (int it = 0; it < 24; ++it) {
|
||||
Vec3f next = Vec3f::Zero();
|
||||
for (const Vec3f &p : pts) {
|
||||
const Vec3f d = p - mean;
|
||||
next += d.dot(axis) * d;
|
||||
}
|
||||
const float n = next.norm();
|
||||
if (n < 1e-6f)
|
||||
break;
|
||||
axis = next / n;
|
||||
}
|
||||
std::vector<float> proj(pts.size());
|
||||
for (size_t i = 0; i < pts.size(); ++i)
|
||||
proj[i] = (pts[i] - mean).dot(axis);
|
||||
std::sort(proj.begin(), proj.end());
|
||||
// Trimmed, so a handful of stray pixels cannot stretch the range and make a flat
|
||||
// image look like it spans one.
|
||||
const float lo = proj[size_t(0.01 * double(proj.size()))];
|
||||
const float hi = proj[std::min(proj.size() - 1, size_t(0.99 * double(proj.size())))];
|
||||
const float span = hi - lo;
|
||||
if (span > 1e-3f) {
|
||||
constexpr int BUCKETS = 32;
|
||||
std::array<size_t, BUCKETS> hist{};
|
||||
for (const float s : proj)
|
||||
if (s >= lo && s <= hi)
|
||||
++hist[size_t(std::min(BUCKETS - 1, int((s - lo) / span * BUCKETS)))];
|
||||
// A bucket counts as carrying pixels at a fifth of a percent of them, which is
|
||||
// above the noise a dithered image leaves between its colours.
|
||||
const size_t floor_count = size_t(0.002 * double(proj.size()));
|
||||
int filled = 0;
|
||||
for (const size_t c : hist)
|
||||
filled += c >= floor_count;
|
||||
out.axis_coverage = float(filled) / float(BUCKETS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both signals, because either alone misreads a common case: the histogram calls a
|
||||
// two-colour gradient flat (measured 0.90 on a green-to-red ramp, and 0.96 on a painted
|
||||
// model texture), while coverage alone would call a noisy two-colour logo a gradient. An
|
||||
// image whose axis could not be measured keeps coverage at 0, so the flat-share test decides
|
||||
// alone - which is what an image of a single colour needs.
|
||||
out.flat_colors = out.flat_share >= 0.85f && out.axis_coverage < 0.25f;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_texture_detail_cache.mutex);
|
||||
auto &entries = g_texture_detail_cache.entries;
|
||||
|
||||
@@ -504,6 +504,15 @@ struct TextureDetail
|
||||
// verdict: a "flat-colour" image (tiles, logos, camouflage) whose colours should each print in a
|
||||
// single filament, versus a photograph or gradient where interleaved filament mixes pay off.
|
||||
float flat_share = 0.f;
|
||||
// Share of the image's principal colour axis that actually carries pixels, in 32 steps. A ramp
|
||||
// covers it end to end; a few flat colours sit in spikes with gaps between them. The histogram
|
||||
// above cannot tell those apart - a two-colour gradient occupies as few bins as the two colours
|
||||
// on their own - so both are needed before an image counts as flat.
|
||||
//
|
||||
// Zero when there was nothing to measure: an image of one colour, or too few pixels to fit an
|
||||
// axis through. That is the right default, because it leaves the flat-share test deciding on its
|
||||
// own, exactly as it did before this signal existed.
|
||||
float axis_coverage = 0.f;
|
||||
bool flat_colors = false;
|
||||
};
|
||||
TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
+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"
|
||||
@@ -285,10 +286,22 @@ constexpr float PREFER_PURE_DE = 2.f;
|
||||
// the quantizer's lookup cube.
|
||||
constexpr size_t MIX_TARGET_SAMPLES = size_t(1) << 20;
|
||||
constexpr size_t MIX_TARGET_BINS = 256;
|
||||
// rank_mixes() stops once the best remaining mix would improve the match by less than this, in
|
||||
// CIEDE2000 averaged over every pixel of the colouring layers (see mix_targets()): a mix that only
|
||||
// touches a few stray pixels is not worth a filament slot.
|
||||
constexpr float MIN_MIX_GAIN = 0.05f;
|
||||
// The longest interleave cycle a mix may use, as the denominator of its ratio. The slicer alternates
|
||||
// a mix layer by layer, so the cycle is how many layers one repeat spans; past this it stops reading
|
||||
// as a colour and starts reading as bands of its two filaments. It also sets how many mixes exist at
|
||||
// all - the coprime ratios per pair of filaments - so it is what decides how finely a gradient can be
|
||||
// stepped. Ten gives 33 ratios per pair against the 11 of six: measured on four filaments, a
|
||||
// green-to-red gradient goes from 11 usable mixes to 16, and the shipped Colour Bricks from 6 to 10.
|
||||
constexpr int MIX_MAX_DENOMINATOR = 10;
|
||||
|
||||
// rank_mixes() stops once the best remaining mix would improve the match by less than this FRACTION
|
||||
// of what the best mix bought: a mix that only touches a few stray pixels is not worth a filament
|
||||
// slot. Relative rather than a fixed amount of CIEDE2000, because the gains decay smoothly and a
|
||||
// fixed cut lands wherever that curve happens to cross it - on a red-to-green gradient with five
|
||||
// filaments the old 0.05 kept a mix worth 0.052 and refused one worth 0.042, and it stopped at the
|
||||
// same count whether the user allowed 8 mixes or 28, and whether four filaments were loaded or six.
|
||||
// As a fraction it scales with the texture, so both the setting and the loaded filaments matter.
|
||||
constexpr double MIN_MIX_GAIN_FRACTION = 0.001;
|
||||
|
||||
// The project's mixed filament slots, one string each, as the palette cache compares them: anything
|
||||
// that changes which of them a mix can reuse changes this.
|
||||
@@ -5350,7 +5363,7 @@ std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement
|
||||
std::vector<PaletteEntry> candidates;
|
||||
for (int i = 0; i < n; ++i)
|
||||
for (int j = i + 1; j < n; ++j)
|
||||
for (int den = 2; den <= 6; ++den)
|
||||
for (int den = 2; den <= MIX_MAX_DENOMINATOR; ++den)
|
||||
for (int num = 1; num < den; ++num) {
|
||||
if (std::gcd(num, den) != 1)
|
||||
continue; // 2/4 is 1/2, already there
|
||||
@@ -5389,6 +5402,8 @@ std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement
|
||||
std::vector<float> current = pure_d;
|
||||
std::vector<char> taken(nc, 0);
|
||||
std::vector<PaletteEntry> out;
|
||||
// Set from the first mix taken, so the floor is read off this texture rather than assumed.
|
||||
double floor_gain = 0.;
|
||||
const auto counts = [&](size_t c, size_t t) {
|
||||
const float d = dist[c * nt + t];
|
||||
return d < current[t] && d <= pure_d[t] - PREFER_PURE_DE;
|
||||
@@ -5408,8 +5423,11 @@ std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
if (best == nc || best_gain < double(MIN_MIX_GAIN))
|
||||
// Nothing left that helps, or the remaining mixes are noise beside the first one.
|
||||
if (best == nc || best_gain <= 0. || best_gain < floor_gain)
|
||||
break;
|
||||
if (out.empty())
|
||||
floor_gain = best_gain * MIN_MIX_GAIN_FRACTION;
|
||||
taken[best] = 1;
|
||||
out.push_back(candidates[best]);
|
||||
for (size_t t = 0; t < nt; ++t)
|
||||
@@ -7357,8 +7375,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// physical filaments.
|
||||
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
|
||||
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad,
|
||||
_L("The most mixed filaments a bake may add. They are picked from the "
|
||||
"texture's colors, and only the ones the bake actually uses are created."),
|
||||
_L("The most mixed filaments a bake may add - a ceiling, not a count. They "
|
||||
"are picked from the texture's colors, so a texture the filaments already "
|
||||
"cover gets fewer, and only the ones the bake actually uses are created."),
|
||||
{ { "mixed_colors_few.png", _L("Few") }, { "mixed_colors_many.png", _L("Many") } }))
|
||||
m_preview_params_dirty = true;
|
||||
// After the slider, so a change shows in the same frame.
|
||||
@@ -7369,6 +7388,27 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// TRN %1% is the number of printable colors, %2% is "from N filaments".
|
||||
ImGui::TextDisabled("%s", Slic3r::format(into_u8(_L_PLURAL("%1% printable color %2%", "%1% printable colors %2%", n_colors)),
|
||||
n_colors, from_filaments).c_str());
|
||||
// Two reasons the palette can hold fewer mixes than the setting asks for, and both
|
||||
// used to be silent, leaving the user raising a number that changed nothing. A
|
||||
// flat-color image is matched against the filaments alone, so it gets none at all;
|
||||
// otherwise the ranking stops once another mix would no longer noticeably improve
|
||||
// the match.
|
||||
const int n_mixes = std::max(0, n_colors - n_filaments);
|
||||
if (n_mixes < opts.color_mix_count) {
|
||||
const bool flat = std::any_of(mv->texture_displacement_layers.begin(),
|
||||
mv->texture_displacement_layers.end(),
|
||||
[](const TextureDisplacementLayer &l) {
|
||||
return l.color_enabled && !l.empty() &&
|
||||
height_texture_has_color(l) &&
|
||||
analyze_texture_detail(l).flat_colors;
|
||||
});
|
||||
ImGui::PushTextWrapPos(content_rx - card_pad);
|
||||
ImGui::TextDisabled("%s", flat ?
|
||||
_u8L("Flat-color image: each color prints in one filament, so no mixes are used.").c_str() :
|
||||
Slic3r::format(_u8L("This texture uses %1% of them - more mixes would not "
|
||||
"change the result."), n_mixes).c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad,
|
||||
_L("Cleans up single stray triangles of the wrong color, which detail finer "
|
||||
|
||||
@@ -103,7 +103,7 @@ public:
|
||||
// actually pick it. Stops early once another mix would make no noticeable difference, so a texture
|
||||
// the filaments already cover gets few mixes or none.
|
||||
//
|
||||
// Ratios are the short-cycle ones (k/d for d up to 6): the slicer interleaves a mix layer by layer,
|
||||
// Ratios are k/d for d up to MIX_MAX_DENOMINATOR: the slicer interleaves a mix layer by layer,
|
||||
// and a long cycle prints as visible bands rather than as a colour. A mix's `rgb` is the colour its
|
||||
// mixed filament slot will show (blend_color_multi(), as the sidebar computes it), so the match, the
|
||||
// preview and the slot all agree on what the mix looks like.
|
||||
|
||||
@@ -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();
|
||||
|
||||
+21
-12
@@ -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
|
||||
@@ -18268,16 +18269,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 +19569,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 +20426,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,6 +9,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_fill.cpp
|
||||
test_flow.cpp
|
||||
test_gcode_processor.cpp
|
||||
test_gcode_statistics.cpp
|
||||
test_gcode_timing.cpp
|
||||
test_gcode.cpp
|
||||
test_gcodeprocessor.cpp
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <string>
|
||||
@@ -50,6 +51,93 @@ TEST_CASE("Klipper object labels name each copy without the characters Klipper c
|
||||
}
|
||||
}
|
||||
|
||||
// Firmwares read the compatibility header lines (Anycubic's GoKlipper heats the bed with
|
||||
// first_layer_bed_temperature), so they must describe the filament the plate prints, not filament 1.
|
||||
TEST_CASE("The G-code header names the first layer temperatures of the filament the plate prints", "[GCode]")
|
||||
{
|
||||
const std::string formula = GENERATE("by_first_filament", "by_highest_temp");
|
||||
DYNAMIC_SECTION("bed_temperature_formula " << formula) {
|
||||
// Filament 1: bed 60, nozzle 200. Filament 2: bed 100, nozzle 250. Everything prints with filament 2.
|
||||
DynamicPrintConfig config = Test::multifilament_config(2, {
|
||||
{"bed_temperature_formula", formula},
|
||||
// Authored 1..n as PresetBundle writes it; the all-1s default collapses every per-filament value
|
||||
// to filament 1.
|
||||
{"filament_self_index", "1,2"},
|
||||
{"filament_extruder_variant", "Direct Drive Standard;Direct Drive Standard"},
|
||||
{"enable_prime_tower", 0},
|
||||
{"nozzle_temperature_initial_layer", "200,250"},
|
||||
{"nozzle_temperature", "200,250"},
|
||||
{"supertack_plate_temp_initial_layer", "60,100"},
|
||||
{"cool_plate_temp_initial_layer", "60,100"},
|
||||
{"textured_cool_plate_temp_initial_layer", "60,100"},
|
||||
{"eng_plate_temp_initial_layer", "60,100"},
|
||||
{"hot_plate_temp_initial_layer", "60,100"},
|
||||
{"textured_plate_temp_initial_layer", "60,100"},
|
||||
{"sparse_infill_filament_id", 2},
|
||||
{"internal_solid_filament_id", 2},
|
||||
{"top_surface_filament_id", 2},
|
||||
{"bottom_surface_filament_id", 2},
|
||||
{"outer_wall_filament_id", 2},
|
||||
{"inner_wall_filament_id", 2},
|
||||
{"skirt_loops", 0},
|
||||
{"brim_type", "no_brim"},
|
||||
});
|
||||
const std::string gcode = Test::slice({Test::cube(20)}, config);
|
||||
|
||||
const auto header_value = [&gcode](const std::string& key) {
|
||||
const std::string token = "; " + key + " = ";
|
||||
const size_t pos = gcode.find(token);
|
||||
REQUIRE(pos != std::string::npos);
|
||||
return std::stoi(gcode.substr(pos + token.size()));
|
||||
};
|
||||
CHECK(header_value("first_layer_bed_temperature") == 100);
|
||||
CHECK(header_value("first_layer_temperature") == 250);
|
||||
}
|
||||
}
|
||||
|
||||
// Printing by object reassigns the initial filament to each object's first filament while it emits the objects, so
|
||||
// the header must still name the filament the print starts with: the firmware heats the bed for the first object.
|
||||
TEST_CASE("The G-code header names the first object's filament when printing by object", "[GCode]")
|
||||
{
|
||||
// Filament 1: bed 60, nozzle 200. Filament 2: bed 100, nozzle 250. Object 1 prints with filament 2, object 2
|
||||
// with filament 1.
|
||||
DynamicPrintConfig config = Test::multifilament_config(2, {
|
||||
{"print_sequence", "by object"},
|
||||
{"bed_temperature_formula", "by_first_filament"},
|
||||
{"filament_self_index", "1,2"},
|
||||
{"filament_extruder_variant", "Direct Drive Standard;Direct Drive Standard"},
|
||||
{"enable_prime_tower", 0},
|
||||
{"nozzle_temperature_initial_layer", "200,250"},
|
||||
{"nozzle_temperature", "200,250"},
|
||||
{"supertack_plate_temp_initial_layer", "60,100"},
|
||||
{"cool_plate_temp_initial_layer", "60,100"},
|
||||
{"textured_cool_plate_temp_initial_layer", "60,100"},
|
||||
{"eng_plate_temp_initial_layer", "60,100"},
|
||||
{"hot_plate_temp_initial_layer", "60,100"},
|
||||
{"textured_plate_temp_initial_layer", "60,100"},
|
||||
{"skirt_loops", 0},
|
||||
{"brim_type", "no_brim"},
|
||||
});
|
||||
const std::string gcode = Test::slice_with_object_overrides({Test::cube(20), Test::cube(20)}, config,
|
||||
{{{"extruder", "2"}}, {{"extruder", "1"}}});
|
||||
|
||||
// Filament 2 starts and filament 1 follows, so the last object's filament differs from the first one's.
|
||||
const size_t first_t1 = gcode.find("\nT1");
|
||||
const size_t first_t0 = gcode.find("\nT0");
|
||||
REQUIRE(first_t1 != std::string::npos);
|
||||
REQUIRE(first_t0 != std::string::npos);
|
||||
REQUIRE(first_t1 < first_t0);
|
||||
|
||||
const auto header_value = [&gcode](const std::string& key) {
|
||||
const std::string token = "; " + key + " = ";
|
||||
const size_t pos = gcode.find(token);
|
||||
REQUIRE(pos != std::string::npos);
|
||||
return std::stoi(gcode.substr(pos + token.size()));
|
||||
};
|
||||
CHECK(header_value("first_layer_bed_temperature") == 100);
|
||||
CHECK(header_value("first_layer_temperature") == 250);
|
||||
}
|
||||
|
||||
TEST_CASE("Object label comments number each object and copy with or without exclude object", "[GCode]")
|
||||
{
|
||||
// repetier has no exclude-object commands, so set_object_info() writes nothing for it.
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("The statistics of a sliced plate add up per layer, feature and object instance", "[GCodeStatistics]")
|
||||
{
|
||||
// A 20 mm cube at 0.2 mm layers prints 100 of them; without skirt or brim all its filament goes to its instances.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({ { "layer_height", 0.2 },
|
||||
{ "initial_layer_print_height", 0.2 },
|
||||
{ "skirt_loops", 0 },
|
||||
{ "brim_type", "no_brim" },
|
||||
{ "filament_density", 1.25 } });
|
||||
Print print;
|
||||
Model model;
|
||||
Test::init_print({ Test::cube(20) }, print, model, config, nullptr, true, 2);
|
||||
GCodeProcessorResult result;
|
||||
Test::gcode(print, &result);
|
||||
|
||||
ScopedTemporaryFile file(".json");
|
||||
export_gcode_statistics(result, file.string());
|
||||
std::string text;
|
||||
load_string_file(file.path(), text);
|
||||
const nlohmann::json statistics = nlohmann::json::parse(text);
|
||||
|
||||
const nlohmann::json &summary = statistics["summary"];
|
||||
const nlohmann::json &layers = statistics["layers"];
|
||||
REQUIRE(layers.size() == 100);
|
||||
CHECK(summary["layer_count"] == 100);
|
||||
CHECK_THAT(layers.front()["z"].get<double>(), Catch::Matchers::WithinAbs(0.2, 1e-3));
|
||||
CHECK_THAT(layers.back()["z"].get<double>(), Catch::Matchers::WithinAbs(20., 1e-3));
|
||||
|
||||
double time = 0., volume = 0.;
|
||||
for (const nlohmann::json &layer : layers) {
|
||||
time += layer["time"].get<double>();
|
||||
volume += layer["volume"].get<double>();
|
||||
double features_time = 0.;
|
||||
for (const auto &[name, feature] : layer["features"].items())
|
||||
features_time += feature["time"].get<double>();
|
||||
CHECK(features_time <= layer["time"].get<double>() + 1e-3);
|
||||
}
|
||||
// Each value is rounded to the millisecond and the cubic millimeter.
|
||||
CHECK_THAT(time, Catch::Matchers::WithinAbs(summary["time"].get<double>(), 0.1));
|
||||
CHECK_THAT(volume, Catch::Matchers::WithinAbs(summary["volume"].get<double>(), 0.1));
|
||||
CHECK_THAT(summary["weight"].get<double>(), Catch::Matchers::WithinRel(summary["volume"].get<double>() * 1.25 / 1000., 1e-4));
|
||||
CHECK(summary.contains("filament_cost"));
|
||||
|
||||
// The filament splits by use as the preview's table does.
|
||||
REQUIRE(summary["filaments"].size() == 1);
|
||||
const nlohmann::json &filament = summary["filaments"].front();
|
||||
double used = 0.;
|
||||
for (const char *use : { "model", "support", "prime_tower", "flush" })
|
||||
used += filament[use]["volume"].get<double>();
|
||||
CHECK_THAT(used, Catch::Matchers::WithinAbs(filament["volume"].get<double>(), 0.01));
|
||||
|
||||
const nlohmann::json &features = summary["features"];
|
||||
CHECK(features.contains("Outer wall"));
|
||||
CHECK(features.contains("Sparse infill"));
|
||||
double model_volume = 0.;
|
||||
for (const auto &[name, feature] : features.items())
|
||||
if (name != "Custom")
|
||||
model_volume += feature["volume"].get<double>();
|
||||
|
||||
const nlohmann::json &objects = statistics["objects"];
|
||||
REQUIRE(objects.size() == 2);
|
||||
double objects_volume = 0.;
|
||||
for (size_t i = 0; i < objects.size(); ++i) {
|
||||
CHECK(objects[i]["name"] == model.objects.front()->name);
|
||||
CHECK(objects[i]["instance"] == int(i));
|
||||
CHECK(objects[i]["support_volume"].get<double>() == 0.);
|
||||
objects_volume += objects[i]["volume"].get<double>();
|
||||
}
|
||||
CHECK_THAT(objects_volume, Catch::Matchers::WithinAbs(model_volume, 0.1));
|
||||
}
|
||||
|
||||
TEST_CASE("Copies sliced as objects of their own keep their number in the model object", "[GCodeStatistics]")
|
||||
{
|
||||
// A copy of another scale slices as a print object of its own.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" } });
|
||||
Print print;
|
||||
Model model;
|
||||
Test::init_print({ Test::cube(20) }, print, model, config, nullptr, true, 2);
|
||||
model.objects.front()->instances.back()->set_scaling_factor(Vec3d(0.5, 0.5, 0.5));
|
||||
model.objects.front()->ensure_on_bed();
|
||||
print.apply(model, config);
|
||||
REQUIRE(print.objects().size() == 2);
|
||||
GCodeProcessorResult result;
|
||||
Test::gcode(print, &result);
|
||||
|
||||
const nlohmann::json objects = nlohmann::json::parse(gcode_statistics_json(result))["objects"];
|
||||
std::set<int> instances;
|
||||
for (const nlohmann::json &object : objects)
|
||||
instances.insert(object["instance"].get<int>());
|
||||
CHECK(instances == std::set<int>{ 0, 1 });
|
||||
}
|
||||
|
||||
TEST_CASE("The statistics file takes the G-code's name with the statistics extension", "[GCodeStatistics]")
|
||||
{
|
||||
CHECK(gcode_statistics_path("plate_1.gcode") == "plate_1.statistics.json");
|
||||
CHECK(gcode_statistics_path("out/Cube_PLA_1h.gcode") == "out/Cube_PLA_1h.statistics.json");
|
||||
}
|
||||
@@ -410,6 +410,44 @@ static bool load_project(const std::string& path, Model& model, const ScopedTemp
|
||||
return loaded;
|
||||
}
|
||||
|
||||
TEST_CASE("A plate's G-code statistics are stored next to its G-code", "[3mf]")
|
||||
{
|
||||
const bool with_statistics = GENERATE(false, true);
|
||||
ScopedTemporaryFile gcode(".gcode");
|
||||
save_string_file(gcode.path(), "G1 X10 Y10 E1\n");
|
||||
|
||||
Model model;
|
||||
ModelObject* object = model.add_object();
|
||||
object->add_volume(make_cube(10., 10., 10.));
|
||||
object->add_instance();
|
||||
ScopedTemporaryDir backup_dir("orca_statistics_src");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
// A sliced plate's info reads the printer settings.
|
||||
DynamicPrintConfig cfg = DynamicPrintConfig::full_print_config();
|
||||
PlateData plate;
|
||||
plate.plate_index = 0;
|
||||
plate.gcode_file = gcode.string();
|
||||
plate.is_sliced_valid = true;
|
||||
if (with_statistics)
|
||||
plate.statistics = R"({ "version": 1 })";
|
||||
ScopedTemporaryFile file(".3mf");
|
||||
const std::string path = file.string();
|
||||
StoreParams sp;
|
||||
sp.path = path.c_str();
|
||||
sp.model = &model;
|
||||
sp.config = &cfg;
|
||||
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::WithGcode;
|
||||
sp.plate_data_list.push_back(&plate);
|
||||
REQUIRE(store_bbs_3mf(sp));
|
||||
|
||||
std::string stored;
|
||||
REQUIRE(read_cad_recipe_entry(path, stored, "Metadata/plate_1.gcode"));
|
||||
CHECK(read_cad_recipe_entry(path, stored, "Metadata/plate_1.statistics.json") == with_statistics);
|
||||
if (with_statistics)
|
||||
CHECK(stored == plate.statistics);
|
||||
}
|
||||
|
||||
TEST_CASE("A project with a plate id below 1 fails to load", "[3mf][Regression]")
|
||||
{
|
||||
const int plate_id = GENERATE(0, -1);
|
||||
|
||||
@@ -94,6 +94,33 @@ static std::shared_ptr<std::vector<unsigned char>> make_flat_rgb_png(uint8_t r,
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||
}
|
||||
|
||||
// An RGB image built by a caller-supplied function of the pixel position, written through the same
|
||||
// temp-file round trip the flat helpers above use.
|
||||
static std::shared_ptr<std::vector<unsigned char>> make_rgb_png(
|
||||
size_t w, size_t h, const std::function<std::array<uint8_t, 3>(size_t, size_t)> &at)
|
||||
{
|
||||
std::vector<uint8_t> rgb;
|
||||
rgb.reserve(w * h * 3);
|
||||
for (size_t y = 0; y < h; ++y)
|
||||
for (size_t x = 0; x < w; ++x) {
|
||||
const std::array<uint8_t, 3> c = at(x, y);
|
||||
rgb.insert(rgb.end(), { c[0], c[1], c[2] });
|
||||
}
|
||||
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
|
||||
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
|
||||
REQUIRE(Slic3r::png::write_rgb_to_file(tmp_path.string(), w, h, rgb));
|
||||
|
||||
std::vector<unsigned char> bytes;
|
||||
{
|
||||
std::ifstream ifs(tmp_path.string(), std::ios::binary);
|
||||
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(tmp_path, ec);
|
||||
REQUIRE_FALSE(bytes.empty());
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||
}
|
||||
|
||||
// A hard-edged black/white checkerboard, the worst case for a height map: every texel boundary is a
|
||||
// step, which is precisely the relief the post-process smoothing exists to round off.
|
||||
static std::shared_ptr<std::vector<unsigned char>> make_checkerboard_png(size_t w = 16, size_t h = 16)
|
||||
@@ -2626,6 +2653,61 @@ TEST_CASE("TextureDisplacement: a texture of flat colours is told apart from a c
|
||||
CHECK(d.flat_share < 0.85f);
|
||||
}
|
||||
|
||||
TEST_CASE("A mirrored tile folds at its seam instead of wrapping to the far edge", "[TextureDisplacement]")
|
||||
{
|
||||
// Mirrored repeat exists so a tile edge lines up with itself, which means the texel past the edge
|
||||
// is the edge again. Wrapping the bilinear neighbour instead blends the image's two opposite edges
|
||||
// together: on a ramp from black to white it turned the white edge into near-black at every seam,
|
||||
// a visible line and a disagreement with the preview, where the GPU binds GL_MIRRORED_REPEAT.
|
||||
TextureDisplacementLayer layer;
|
||||
layer.image_data = make_rgb_png(8, 8, [](size_t x, size_t) {
|
||||
const uint8_t v = uint8_t(x * 36); // 0 at the left edge, 252 at the right
|
||||
return std::array<uint8_t, 3>{ v, v, v };
|
||||
});
|
||||
const DecodedHeightTexture tex = decode_height_texture(layer);
|
||||
REQUIRE(tex.width == 8);
|
||||
const float bright = 252.f / 255.f;
|
||||
|
||||
// Either side of a seam the fold puts the bright edge, not a blend with the dark one.
|
||||
for (const float u : { 0.999f, 1.001f, 2.999f, 3.001f }) {
|
||||
INFO("u = " << u);
|
||||
CHECK_THAT(tex.sample(Vec2f(u, 0.5f), true, TextureTileMethod::MirroredRepeat),
|
||||
WithinAbs(bright, 0.02f));
|
||||
}
|
||||
|
||||
// Plain repeat is the opposite case and must keep wrapping: its next tile really does start at the
|
||||
// far edge, so just inside the seam the neighbour is the dark one.
|
||||
CHECK(tex.sample(Vec2f(0.999f, 0.5f), true, TextureTileMethod::Repeat) < 0.1f);
|
||||
}
|
||||
|
||||
TEST_CASE("A two-colour ramp is not mistaken for an image of flat colours", "[TextureDisplacement]")
|
||||
{
|
||||
// The histogram alone cannot tell these two apart: a ramp between two colours visits as few
|
||||
// coarse bins as the two colours on their own, and both score far above the flat-share
|
||||
// threshold. What separates them is that the ramp fills its colour axis and the pair does not.
|
||||
const std::array<uint8_t, 3> a{ 0x4f, 0xc3, 0x08 };
|
||||
const std::array<uint8_t, 3> b{ 0xd4, 0x2f, 0x25 };
|
||||
|
||||
TextureDisplacementLayer ramp;
|
||||
ramp.image_data = make_rgb_png(96, 96, [&](size_t, size_t y) {
|
||||
const float t = float(y) / 95.f;
|
||||
return std::array<uint8_t, 3>{ uint8_t(std::lround(a[0] + (b[0] - a[0]) * t)),
|
||||
uint8_t(std::lround(a[1] + (b[1] - a[1]) * t)),
|
||||
uint8_t(std::lround(a[2] + (b[2] - a[2]) * t)) };
|
||||
});
|
||||
const TextureDetail ramp_detail = analyze_texture_detail(ramp);
|
||||
CHECK(ramp_detail.flat_share >= 0.85f); // the old signal, on its own, calls this flat
|
||||
CHECK(ramp_detail.axis_coverage > 0.8f); // but its colours run the length of the axis
|
||||
CHECK_FALSE(ramp_detail.flat_colors); // so mixes stay available
|
||||
|
||||
// The same two colours as areas rather than a ramp: nothing in between, so no mix would serve it.
|
||||
TextureDisplacementLayer two_tone;
|
||||
two_tone.image_data = make_rgb_png(96, 96, [&](size_t, size_t y) { return y < 48 ? a : b; });
|
||||
const TextureDetail two_tone_detail = analyze_texture_detail(two_tone);
|
||||
CHECK(two_tone_detail.axis_coverage < 0.25f);
|
||||
CHECK(two_tone_detail.flat_colors);
|
||||
}
|
||||
|
||||
TEST_CASE("Each layer's texture is sampled only on its own painted area", "[TextureDisplacement]")
|
||||
{
|
||||
// Two layers with different textures and depths, one painted on the cube's top, one on its -X side.
|
||||
|
||||
Reference in New Issue
Block a user