mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 11:51:33 +00:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed4095b20 | ||
|
|
ee1cc1b227 | ||
|
|
46096416cd | ||
|
|
26f5fd6251 | ||
|
|
74fe2b7996 | ||
|
|
38f649fc00 | ||
|
|
5d73410281 | ||
|
|
b75af485ce | ||
|
|
6a7763f08d | ||
|
|
70e5b330be | ||
|
|
5e94368863 | ||
|
|
0df9b782bb | ||
|
|
d5403ccc94 | ||
|
|
32d690ecee | ||
|
|
303d7a1a07 | ||
|
|
c0e6db227d | ||
|
|
a078155890 | ||
|
|
3b6e27d725 | ||
|
|
6254ab643a | ||
|
|
9ab0178dd0 | ||
|
|
23f6771dcb | ||
|
|
aebc0a2979 | ||
|
|
6f0ec60d21 | ||
|
|
8e43a2ac13 | ||
|
|
6b366ad8a9 | ||
|
|
7d8261668e | ||
|
|
00127beffa | ||
|
|
927eb9922c | ||
|
|
3d62769664 | ||
|
|
e5e30ee0e8 | ||
|
|
7515fb480f | ||
|
|
acc1d6fd21 | ||
|
|
6ca0fc5ef1 | ||
|
|
bcf7d078cd | ||
|
|
15fc96fca7 | ||
|
|
1fffda0533 | ||
|
|
33e26a127e | ||
|
|
6dd0cb9eba | ||
|
|
9bf0fc2a8e | ||
|
|
5bf6db95b3 | ||
|
|
0feb87b417 |
@@ -8,6 +8,7 @@ on:
|
||||
paths:
|
||||
- 'deps/**'
|
||||
- 'deps_src/**'
|
||||
- 'cmake/**'
|
||||
- 'src/**'
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
@@ -20,8 +21,6 @@ on:
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'signpath/**'
|
||||
- 'scripts/*.ps1'
|
||||
- 'scripts/flatpak/**'
|
||||
- 'scripts/msix/**'
|
||||
- 'tests/**'
|
||||
@@ -33,18 +32,17 @@ on:
|
||||
paths:
|
||||
- 'deps/**'
|
||||
- 'deps_src/**'
|
||||
- 'cmake/**'
|
||||
- 'src/**'
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
- ".github/workflows/build_*.yml"
|
||||
- ".github/workflows/unit_tests*.yml"
|
||||
- 'signpath/**'
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/*.ps1'
|
||||
- 'scripts/flatpak/**'
|
||||
- 'scripts/msix/**'
|
||||
- 'tests/**'
|
||||
|
||||
@@ -504,57 +504,6 @@ jobs:
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload unsigned Windows portable artifact for SignPath
|
||||
id: upload-windows-portable
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable_unsigned
|
||||
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Submit Windows portable artifact to SignPath
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: OrcaSlicer
|
||||
signing-policy-slug: test-signing
|
||||
artifact-configuration-slug: windows-portable-v1
|
||||
github-artifact-id: ${{ steps.upload-windows-portable.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/windows-portable
|
||||
|
||||
- name: Verify SignPath Windows portable signatures
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
shell: pwsh
|
||||
# -AllowUntrustedRoot is required while signing with the SignPath test
|
||||
# certificate (self-signed). Remove it once signing-policy-slug switches
|
||||
# to release-signing with a production CA-issued certificate.
|
||||
run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/windows-portable' -AllowUntrustedRoot
|
||||
|
||||
- name: Replace Windows binaries with signed output
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
shell: pwsh
|
||||
run: |
|
||||
$source = Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/signpath/windows-portable"
|
||||
$destination = Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/OrcaSlicer"
|
||||
if (-not (Test-Path -LiteralPath $source -PathType Container)) {
|
||||
throw "SignPath output directory not found: $source"
|
||||
}
|
||||
Get-ChildItem -LiteralPath $source -Force | Copy-Item -Destination $destination -Recurse -Force
|
||||
|
||||
# cpack's install rules take the binaries from the build output, not from
|
||||
# the portable bundle, so the installer needs the signed copies there too.
|
||||
$buildOutput = Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/src/Release"
|
||||
if (-not (Test-Path -LiteralPath $buildOutput -PathType Container)) {
|
||||
throw "Build output directory not found: $buildOutput"
|
||||
}
|
||||
foreach ($name in "orca-slicer.exe", "OrcaSlicer.dll") {
|
||||
Copy-Item -LiteralPath (Join-Path $source $name) -Destination $buildOutput -Force
|
||||
}
|
||||
|
||||
# NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's
|
||||
# x86 emulation, packaging the native arm64 payload from build-arm64.
|
||||
- name: Create installer Win
|
||||
@@ -563,53 +512,11 @@ jobs:
|
||||
run: |
|
||||
cpack -G NSIS
|
||||
|
||||
- name: Upload unsigned Windows installer for SignPath
|
||||
id: upload-windows-installer
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}_unsigned
|
||||
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Submit Windows installer to SignPath
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: OrcaSlicer
|
||||
signing-policy-slug: test-signing
|
||||
artifact-configuration-slug: windows-installer-v1
|
||||
github-artifact-id: ${{ steps.upload-windows-installer.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/windows-installer
|
||||
|
||||
- name: Verify SignPath Windows installer signature
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
shell: pwsh
|
||||
# -AllowUntrustedRoot: see "Verify SignPath Windows portable signatures".
|
||||
run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/windows-installer' -Files 'OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe' -AllowUntrustedRoot
|
||||
|
||||
- name: Replace Windows installer with signed output
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
shell: pwsh
|
||||
run: |
|
||||
$name = "OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe"
|
||||
$source = Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/signpath/windows-installer/$name"
|
||||
Copy-Item -LiteralPath $source -Destination (Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/$name") -Force
|
||||
|
||||
- name: Pack app
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$zipPath = "OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip"
|
||||
Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue
|
||||
& "C:/Program Files/7-Zip/7z.exe" a -tzip $zipPath "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
shell: cmd
|
||||
run: '"C:/Program Files/7-Zip/7z.exe" a -tzip OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer'
|
||||
|
||||
- name: Pack PDB
|
||||
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
|
||||
@@ -623,7 +530,6 @@ jobs:
|
||||
with:
|
||||
name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable
|
||||
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload artifacts Win installer
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
|
||||
+2
-1
@@ -1345,7 +1345,8 @@ set (CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})
|
||||
set (CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/images\\\\OrcaSlicer.ico")
|
||||
set (CPACK_NSIS_MUI_ICON "${CPACK_PACKAGE_ICON}")
|
||||
set (CPACK_NSIS_MUI_UNIICON "${CPACK_PACKAGE_ICON}")
|
||||
set (CPACK_NSIS_INSTALLED_ICON_NAME "$INSTDIR\\\\orca-slicer.exe")
|
||||
# Relative to the install dir: CPack's NSIS template prefixes $INSTDIR itself.
|
||||
set (CPACK_NSIS_INSTALLED_ICON_NAME "orca-slicer.exe")
|
||||
set (CPACK_PACKAGE_CHECKSUM SHA256)
|
||||
set (CPACK_PACKAGE_INSTALL_REGISTRY_KEY "OrcaSlicer")
|
||||
set (CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
|
||||
|
||||
@@ -86,7 +86,8 @@ The [wiki](https://www.orcaslicer.com/wiki) aims to provide a detailed explanati
|
||||
## Stable Release
|
||||
|
||||
📥 **[Download the Latest Stable Release](https://github.com/OrcaSlicer/OrcaSlicer/releases/latest)**
|
||||
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
|
||||
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
|
||||
Windows release binaries are code signed; see our [Code signing policy](#code-signing-policy).
|
||||
|
||||
## Nightly Builds
|
||||
|
||||
@@ -244,6 +245,16 @@ OrcaSlicer began in that same spirit, drawing from BambuStudio, PrusaSlicer, and
|
||||
|
||||
The OrcaSlicer logo was designed by community member [Justin Levine](https://github.com/jal-co).
|
||||
|
||||
# Code Signing Policy
|
||||
|
||||
Free code signing provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org).
|
||||
|
||||
Windows release binaries are built by GitHub Actions from this repository and signed through the SignPath Foundation program. Every signing request is manually approved.
|
||||
|
||||
**Team roles:** [SoftFever](https://github.com/SoftFever) is the committer, reviewer and approver for all signed releases.
|
||||
|
||||
**Privacy policy:** See the [OrcaSlicer privacy policy](https://www.orcaslicer.com/privacy-policy/).
|
||||
|
||||
# License
|
||||
|
||||
- **OrcaSlicer** is licensed under the GNU Affero General Public License, version 3.
|
||||
|
||||
@@ -42,6 +42,7 @@ call :add_arg build_deps bool d deps "Download and build the dependencies, neede
|
||||
call :add_arg build_slicer bool s slicer "Build OrcaSlicer"
|
||||
call :add_arg build_tests bool "" tests "Build the unit tests"
|
||||
call :add_arg run_tests bool "" run-tests "Build the unit tests and run them"
|
||||
call :add_arg make_installer bool "" installer "Build OrcaSlicer and package it into an NSIS installer"
|
||||
call :add_arg pack_deps bool p pack "Bundle the built dependencies into a zip file"
|
||||
call :add_arg install_deps bool u install-deps "Install or update CMake, Perl and Git with WinGet"
|
||||
call :add_arg install_vs string "" install-vs "Also install Visual Studio: buildtools or ide"
|
||||
@@ -185,6 +186,9 @@ if "%build_deps%%build_slicer%%pack_deps%%install_deps%%install_vs%" == "" (
|
||||
if "%run_tests%" == "ON" set "build_slicer=ON"
|
||||
)
|
||||
|
||||
REM The installer packages the slicer build, so it always needs one.
|
||||
if "%make_installer%" == "ON" set "build_slicer=ON"
|
||||
|
||||
REM Options like --config or -j only shape a build. Without one of the actions
|
||||
REM there is nothing for them to shape, so say so rather than resolving a whole
|
||||
REM build and reporting it took no time. The block above may have added one.
|
||||
@@ -222,6 +226,30 @@ if "%build_slicer%" == "ON" if "%install_slicer%" == "ON" if not "%slicer_target
|
||||
echo --install needs the executable, but --slicer-target names "%slicer_target%".
|
||||
exit /b 1
|
||||
)
|
||||
REM cpack runs the same install, so it has the same need.
|
||||
if "%make_installer%" == "ON" if not "%slicer_target%" == "" if /I not "%slicer_target%" == "OrcaSlicer" (
|
||||
echo --installer needs the executable, but --slicer-target names "%slicer_target%".
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM cpack finds makensis on PATH or through the registry key the NSIS installer
|
||||
REM writes, so look in the same places, and before the build rather than after
|
||||
REM it. The value is read past its REG_SZ marker because the name column is
|
||||
REM localised and may hold spaces.
|
||||
if not "%make_installer%" == "ON" goto :nsis_ready
|
||||
if "%dry_run%" == "ON" goto :nsis_ready
|
||||
where makensis >nul 2>nul && goto :nsis_ready
|
||||
for %%v in (32 64) do (
|
||||
for /f "delims=" %%l in ('reg query "HKLM\SOFTWARE\NSIS" /ve /reg:%%v 2^>nul ^| findstr REG_SZ') do (
|
||||
set "nsis_line=%%l"
|
||||
set "nsis_line=!nsis_line:*REG_SZ=!"
|
||||
for /f "tokens=*" %%d in ("!nsis_line!") do if exist "%%d\makensis.exe" goto :nsis_ready
|
||||
)
|
||||
)
|
||||
echo NSIS was not found, and --installer needs it to build the installer.
|
||||
echo Install it with "winget install NSIS.NSIS", then run this again.
|
||||
exit /b 1
|
||||
:nsis_ready
|
||||
if not "%vs_version%" == "" if "!vs_gen_%vs_version%!" == "" (
|
||||
echo Unknown Visual Studio release "%vs_version%". Known releases: 2019, 2022, 2026.
|
||||
exit /b 1
|
||||
@@ -734,6 +762,14 @@ if "%build_slicer%" == "ON" (
|
||||
call :print_and_run cmake --build "%build_dir%" --target install --config %build_type%
|
||||
%error_check%
|
||||
)
|
||||
|
||||
REM cpack installs into its own staging tree, so this does not need -i.
|
||||
REM Unlike CI it skips the preset cache, whose packaging step prunes the
|
||||
REM checkout's preset JSONs.
|
||||
if "%make_installer%" == "ON" (
|
||||
call :print_and_run cpack -G NSIS -C %build_type% --config "%build_dir%\CPackConfig.cmake" -B "%build_dir%"
|
||||
%error_check%
|
||||
)
|
||||
)
|
||||
|
||||
REM Elapsed wall clock. The 1%%a-100 trick strips a leading zero, which set /A
|
||||
@@ -820,6 +856,16 @@ REM worked out the same way in either run.
|
||||
if "%build_slicer%" == "ON" if not "%linked%" == "ON" echo Target %slicer_target%
|
||||
if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\!solution!
|
||||
if "%pack_deps%" == "ON" if defined bundle echo Bundle !bundle!
|
||||
if "%make_installer%" == "ON" (
|
||||
REM The file name carries the version, which only CMake knows, so name
|
||||
REM the newest match, or the pattern when there is none yet.
|
||||
set "installer="
|
||||
for /f "delims=" %%f in ('dir /b /o-d "!build_full!\OrcaSlicer_Windows_Installer_*.exe" 2^>nul') do (
|
||||
if not defined installer set "installer=!build_full!\%%f"
|
||||
)
|
||||
if not defined installer set "installer=!build_full!\OrcaSlicer_Windows_Installer_*.exe"
|
||||
echo Installer !installer!
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Next
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
# Find the wxWidgets module based on the information provided by the Perl Alien::wxWidgets module.
|
||||
|
||||
# Check for the Perl & PerlLib modules
|
||||
include(LibFindMacros)
|
||||
libfind_package(AlienWx Perl)
|
||||
libfind_package(AlienWx PerlLibs)
|
||||
|
||||
if (AlienWx_DEBUG)
|
||||
message(STATUS " AlienWx_FIND_COMPONENTS=${AlienWx_FIND_COMPONENTS}")
|
||||
endif()
|
||||
|
||||
# Execute an Alien::Wx module to find the relevant information regarding
|
||||
# the wxWidgets used by the Perl interpreter.
|
||||
# Perl specific stuff
|
||||
set(AlienWx_TEMP_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/AlienWx_TEMP_INCLUDE.txt)
|
||||
execute_process(
|
||||
COMMAND ${PERL_EXECUTABLE} -e "
|
||||
# Import Perl modules.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Text::ParseWords;
|
||||
|
||||
BEGIN {
|
||||
# CMake sets the environment variables CC and CXX to the detected C compiler.
|
||||
# There is an issue with the Perl ExtUtils::CBuilder, which does not handle whitespaces
|
||||
# in the paths correctly on Windows, so we rather drop the CMake auto-detected paths.
|
||||
delete \$ENV{CC};
|
||||
delete \$ENV{CXX};
|
||||
}
|
||||
|
||||
use Alien::wxWidgets;
|
||||
use ExtUtils::CppGuess;
|
||||
|
||||
# Test for a Visual Studio compiler
|
||||
my \$cpp_guess = ExtUtils::CppGuess->new;
|
||||
my \$mswin = \$^O eq 'MSWin32';
|
||||
my \$msvc = \$cpp_guess->is_msvc;
|
||||
|
||||
# List of wxWidgets components to be used.
|
||||
my @components = split /;/, '${AlienWx_FIND_COMPONENTS}';
|
||||
|
||||
# Query the available data from Alien::wxWidgets.
|
||||
my \$version = Alien::wxWidgets->version;
|
||||
my \$config = Alien::wxWidgets->config;
|
||||
my \$compiler = Alien::wxWidgets->compiler;
|
||||
my \$linker = Alien::wxWidgets->linker;
|
||||
my \$include_path = ' ' . Alien::wxWidgets->include_path;
|
||||
my \$defines = ' ' . Alien::wxWidgets->defines;
|
||||
my \$cflags = Alien::wxWidgets->c_flags;
|
||||
my \$linkflags = Alien::wxWidgets->link_flags;
|
||||
my \$libraries = ' ' . Alien::wxWidgets->libraries(@components);
|
||||
my \$gui_toolkit = Alien::wxWidgets->config->{toolkit};
|
||||
#my @libraries = Alien::wxWidgets->link_libraries(@components);
|
||||
#my @implib = Alien::wxWidgets->import_libraries(@components);
|
||||
#my @shrlib = Alien::wxWidgets->shared_libraries(@components);
|
||||
#my @keys = Alien::wxWidgets->library_keys; # 'gl', 'adv', ...
|
||||
#my \$library_path = Alien::wxWidgets->shared_library_path;
|
||||
#my \$key = Alien::wxWidgets->key;
|
||||
#my \$prefix = Alien::wxWidgets->prefix;
|
||||
|
||||
my \$filename = '${AlienWx_TEMP_INCLUDE}';
|
||||
open(my $fh, '>', \$filename) or die \"Could not open file '\$filename' \$!\";
|
||||
|
||||
# Convert a space separated lists to CMake semicolon separated lists,
|
||||
# escape the backslashes,
|
||||
# export the resulting list to a temp file.
|
||||
sub cmake_set_var {
|
||||
my (\$varname, \$content) = @_;
|
||||
# Remove line separators.
|
||||
\$content =~ s/\\r|\\n//g;
|
||||
# Escape the path separators.
|
||||
\$content =~ s/\\\\/\\\\\\\\\\\\\\\\/g;
|
||||
my @words = shellwords(\$content);
|
||||
print \$fh \"set(AlienWx_\$varname \\\"\" . join(';', @words) . \"\\\")\\n\";
|
||||
}
|
||||
cmake_set_var('VERSION', \$version);
|
||||
\$include_path =~ s/ -I/ /g;
|
||||
cmake_set_var('INCLUDE_DIRS', \$include_path);
|
||||
\$libraries =~ s/ -L/ -LIBPATH:/g if \$msvc;
|
||||
cmake_set_var('LIBRARIES', \$libraries);
|
||||
#cmake_set_var('LIBRARY_DIRS', );
|
||||
#\$defines =~ s/ -D/ /g;
|
||||
cmake_set_var('DEFINITIONS', \$defines);
|
||||
#cmake_set_var('DEFINITIONS_DEBUG', );
|
||||
cmake_set_var('CXX_FLAGS', \$cflags);
|
||||
cmake_set_var('GUI_TOOLKIT', \$gui_toolkit);
|
||||
close \$fh;
|
||||
")
|
||||
include(${AlienWx_TEMP_INCLUDE})
|
||||
file(REMOVE ${AlienWx_TEMP_INCLUDE})
|
||||
unset(AlienWx_TEMP_INCLUDE)
|
||||
|
||||
if (AlienWx_DEBUG)
|
||||
message(STATUS " AlienWx_VERSION = ${AlienWx_VERSION}")
|
||||
message(STATUS " AlienWx_INCLUDE_DIRS = ${AlienWx_INCLUDE_DIRS}")
|
||||
message(STATUS " AlienWx_LIBRARIES = ${AlienWx_LIBRARIES}")
|
||||
message(STATUS " AlienWx_LIBRARY_DIRS = ${AlienWx_LIBRARY_DIRS}")
|
||||
message(STATUS " AlienWx_DEFINITIONS = ${AlienWx_DEFINITIONS}")
|
||||
message(STATUS " AlienWx_DEFINITIONS_DEBUG = ${AlienWx_DEFINITIONS_DEBUG}")
|
||||
message(STATUS " AlienWx_CXX_FLAGS = ${AlienWx_CXX_FLAGS}")
|
||||
message(STATUS " AlienWx_GUI_TOOLKIT = ${AlienWx_GUI_TOOLKIT}")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(AlienWx
|
||||
REQUIRED_VARS AlienWx_INCLUDE_DIRS AlienWx_LIBRARIES
|
||||
# HANDLE_COMPONENTS
|
||||
VERSION_VAR AlienWx_VERSION)
|
||||
@@ -1,88 +0,0 @@
|
||||
# Find the dependencies for linking with the Perl runtime library.
|
||||
|
||||
# Check for the Perl & PerlLib modules
|
||||
include(LibFindMacros)
|
||||
libfind_package(PerlEmbed Perl)
|
||||
libfind_package(PerlEmbed PerlLibs)
|
||||
|
||||
# Execute an Alien::Wx module to find the relevant information regarding
|
||||
# the wxWidgets used by the Perl interpreter.
|
||||
# Perl specific stuff
|
||||
set(PerlEmbed_TEMP_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/PerlEmbed_TEMP_INCLUDE.txt)
|
||||
execute_process(
|
||||
COMMAND ${PERL_EXECUTABLE} -MExtUtils::Embed -e "
|
||||
# Import Perl modules.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Config;
|
||||
use Text::ParseWords;
|
||||
use ExtUtils::CppGuess;
|
||||
|
||||
# Test for a Visual Studio compiler
|
||||
my \$cpp_guess = ExtUtils::CppGuess->new;
|
||||
my \$mswin = \$^O eq 'MSWin32';
|
||||
my \$msvc = \$cpp_guess->is_msvc;
|
||||
|
||||
# Query the available data from Alien::wxWidgets.
|
||||
my \$ccflags;
|
||||
my \$ldflags;
|
||||
{ local *STDOUT; open STDOUT, '>', \\\$ccflags; ccflags; }
|
||||
{ local *STDOUT; open STDOUT, '>', \\\$ldflags; ldopts; }
|
||||
\$ccflags = ' ' . \$ccflags;
|
||||
\$ldflags = ' ' . \$ldflags;
|
||||
|
||||
my \$filename = '${PerlEmbed_TEMP_INCLUDE}';
|
||||
open(my $fh, '>', \$filename) or die \"Could not open file '\$filename' \$!\";
|
||||
|
||||
# Convert a space separated lists to CMake semicolon separated lists,
|
||||
# escape the backslashes,
|
||||
# export the resulting list to a temp file.
|
||||
sub cmake_set_var {
|
||||
my (\$varname, \$content) = @_;
|
||||
# Remove line separators.
|
||||
\$content =~ s/\\r|\\n//g;
|
||||
# Escape the path separators.
|
||||
\$content =~ s/\\\\/\\\\\\\\\\\\\\\\/g;
|
||||
my @words = shellwords(\$content);
|
||||
print \$fh \"set(PerlEmbed_\$varname \\\"\" . join(';', @words) . \"\\\")\\n\";
|
||||
}
|
||||
cmake_set_var('ARCHNAME', \$Config{archname});
|
||||
cmake_set_var('CCFLAGS', \$ccflags);
|
||||
\$ldflags =~ s/ -L/ -LIBPATH:/g if \$msvc;
|
||||
cmake_set_var('LD', \$Config{ld});
|
||||
cmake_set_var('LDFLAGS', \$ldflags);
|
||||
cmake_set_var('CCCDLFLAGS', \$Config{cccdlflags});
|
||||
cmake_set_var('LDDLFLAGS', \$Config{lddlflags});
|
||||
cmake_set_var('DLEXT', \$Config{dlext});
|
||||
close \$fh;
|
||||
")
|
||||
include(${PerlEmbed_TEMP_INCLUDE})
|
||||
file(REMOVE ${PerlEmbed_TEMP_INCLUDE})
|
||||
unset(PerlEmbed_TEMP_INCLUDE)
|
||||
|
||||
if (PerlEmbed_DEBUG)
|
||||
# First show the configuration extracted by FindPerl & FindPerlLibs:
|
||||
message(STATUS " PERL_INCLUDE_PATH = ${PERL_INCLUDE_PATH}")
|
||||
message(STATUS " PERL_LIBRARY = ${PERL_LIBRARY}")
|
||||
message(STATUS " PERL_EXECUTABLE = ${PERL_EXECUTABLE}")
|
||||
message(STATUS " PERL_SITESEARCH = ${PERL_SITESEARCH}")
|
||||
message(STATUS " PERL_SITELIB = ${PERL_SITELIB}")
|
||||
message(STATUS " PERL_VENDORARCH = ${PERL_VENDORARCH}")
|
||||
message(STATUS " PERL_VENDORLIB = ${PERL_VENDORLIB}")
|
||||
message(STATUS " PERL_ARCHLIB = ${PERL_ARCHLIB}")
|
||||
message(STATUS " PERL_PRIVLIB = ${PERL_PRIVLIB}")
|
||||
message(STATUS " PERL_EXTRA_C_FLAGS = ${PERL_EXTRA_C_FLAGS}")
|
||||
# Second show the configuration extracted by this module (FindPerlEmbed):
|
||||
message(STATUS " PerlEmbed_ARCHNAME = ${PerlEmbed_ARCHNAME}")
|
||||
message(STATUS " PerlEmbed_CCFLAGS = ${PerlEmbed_CCFLAGS}")
|
||||
message(STATUS " PerlEmbed_CCCDLFLAGS = ${PerlEmbed_CCCDLFLAGS}")
|
||||
message(STATUS " LD = ${PerlEmbed_LD}")
|
||||
message(STATUS " PerlEmbed_LDFLAGS = ${PerlEmbed_LDFLAGS}")
|
||||
message(STATUS " PerlEmbed_LDDLFLAGS = ${PerlEmbed_LDDLFLAGS}")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(PerlEmbed
|
||||
REQUIRED_VARS PerlEmbed_CCFLAGS PerlEmbed_LDFLAGS
|
||||
VERSION_VAR PERL_VERSION)
|
||||
@@ -1,265 +0,0 @@
|
||||
# Version 2.2
|
||||
# Public Domain, originally written by Lasse Kärkkäinen <tronic>
|
||||
# Maintained at https://github.com/Tronic/cmake-modules
|
||||
# Please send your improvements as pull requests on Github.
|
||||
|
||||
# Find another package and make it a dependency of the current package.
|
||||
# This also automatically forwards the "REQUIRED" argument.
|
||||
# Usage: libfind_package(<prefix> <another package> [extra args to find_package])
|
||||
macro (libfind_package PREFIX PKG)
|
||||
set(${PREFIX}_args ${PKG} ${ARGN})
|
||||
if (${PREFIX}_FIND_REQUIRED)
|
||||
set(${PREFIX}_args ${${PREFIX}_args} REQUIRED)
|
||||
endif()
|
||||
find_package(${${PREFIX}_args})
|
||||
set(${PREFIX}_DEPENDENCIES ${${PREFIX}_DEPENDENCIES};${PKG})
|
||||
unset(${PREFIX}_args)
|
||||
endmacro()
|
||||
|
||||
# A simple wrapper to make pkg-config searches a bit easier.
|
||||
# Works the same as CMake's internal pkg_check_modules but is always quiet.
|
||||
macro (libfind_pkg_check_modules)
|
||||
find_package(PkgConfig QUIET)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(${ARGN} QUIET)
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Avoid useless copy&pasta by doing what most simple libraries do anyway:
|
||||
# pkg-config, find headers, find library.
|
||||
# Usage: libfind_pkg_detect(<prefix> <pkg-config args> FIND_PATH <name> [other args] FIND_LIBRARY <name> [other args])
|
||||
# E.g. libfind_pkg_detect(SDL2 sdl2 FIND_PATH SDL.h PATH_SUFFIXES SDL2 FIND_LIBRARY SDL2)
|
||||
function (libfind_pkg_detect PREFIX)
|
||||
# Parse arguments
|
||||
set(argname pkgargs)
|
||||
foreach (i ${ARGN})
|
||||
if ("${i}" STREQUAL "FIND_PATH")
|
||||
set(argname pathargs)
|
||||
elseif ("${i}" STREQUAL "FIND_LIBRARY")
|
||||
set(argname libraryargs)
|
||||
else()
|
||||
set(${argname} ${${argname}} ${i})
|
||||
endif()
|
||||
endforeach()
|
||||
if (NOT pkgargs)
|
||||
message(FATAL_ERROR "libfind_pkg_detect requires at least a pkg_config package name to be passed.")
|
||||
endif()
|
||||
# Find library
|
||||
libfind_pkg_check_modules(${PREFIX}_PKGCONF ${pkgargs})
|
||||
if (pathargs)
|
||||
find_path(${PREFIX}_INCLUDE_DIR NAMES ${pathargs} HINTS ${${PREFIX}_PKGCONF_INCLUDE_DIRS})
|
||||
endif()
|
||||
if (libraryargs)
|
||||
find_library(${PREFIX}_LIBRARY NAMES ${libraryargs} HINTS ${${PREFIX}_PKGCONF_LIBRARY_DIRS})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Extracts a version #define from a version.h file, output stored to <PREFIX>_VERSION.
|
||||
# Usage: libfind_version_header(Foobar foobar/version.h FOOBAR_VERSION_STR)
|
||||
# Fourth argument "QUIET" may be used for silently testing different define names.
|
||||
# This function does nothing if the version variable is already defined.
|
||||
function (libfind_version_header PREFIX VERSION_H DEFINE_NAME)
|
||||
# Skip processing if we already have a version or if the include dir was not found
|
||||
if (${PREFIX}_VERSION OR NOT ${PREFIX}_INCLUDE_DIR)
|
||||
return()
|
||||
endif()
|
||||
set(quiet ${${PREFIX}_FIND_QUIETLY})
|
||||
# Process optional arguments
|
||||
foreach(arg ${ARGN})
|
||||
if (arg STREQUAL "QUIET")
|
||||
set(quiet TRUE)
|
||||
else()
|
||||
message(AUTHOR_WARNING "Unknown argument ${arg} to libfind_version_header ignored.")
|
||||
endif()
|
||||
endforeach()
|
||||
# Read the header and parse for version number
|
||||
set(filename "${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
if (NOT EXISTS ${filename})
|
||||
if (NOT quiet)
|
||||
message(AUTHOR_WARNING "Unable to find ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
file(READ "${filename}" header)
|
||||
string(REGEX REPLACE ".*#[ \t]*define[ \t]*${DEFINE_NAME}[ \t]*\"([^\n]*)\".*" "\\1" match "${header}")
|
||||
# No regex match?
|
||||
if (match STREQUAL header)
|
||||
if (NOT quiet)
|
||||
message(AUTHOR_WARNING "Unable to find \#define ${DEFINE_NAME} \"<version>\" from ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
# Export the version string
|
||||
set(${PREFIX}_VERSION "${match}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Do the final processing once the paths have been detected.
|
||||
# If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain
|
||||
# all the variables, each of which contain one include directory.
|
||||
# Ditto for ${PREFIX}_PROCESS_LIBS and library files.
|
||||
# Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES.
|
||||
# Also handles errors in case library detection was required, etc.
|
||||
function (libfind_process PREFIX)
|
||||
# Skip processing if already processed during this configuration run
|
||||
if (${PREFIX}_FOUND)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(found TRUE) # Start with the assumption that the package was found
|
||||
|
||||
# Did we find any files? Did we miss includes? These are for formatting better error messages.
|
||||
set(some_files FALSE)
|
||||
set(missing_headers FALSE)
|
||||
|
||||
# Shorthands for some variables that we need often
|
||||
set(quiet ${${PREFIX}_FIND_QUIETLY})
|
||||
set(required ${${PREFIX}_FIND_REQUIRED})
|
||||
set(exactver ${${PREFIX}_FIND_VERSION_EXACT})
|
||||
set(findver "${${PREFIX}_FIND_VERSION}")
|
||||
set(version "${${PREFIX}_VERSION}")
|
||||
|
||||
# Lists of config option names (all, includes, libs)
|
||||
unset(configopts)
|
||||
set(includeopts ${${PREFIX}_PROCESS_INCLUDES})
|
||||
set(libraryopts ${${PREFIX}_PROCESS_LIBS})
|
||||
|
||||
# Process deps to add to
|
||||
foreach (i ${PREFIX} ${${PREFIX}_DEPENDENCIES})
|
||||
if (DEFINED ${i}_INCLUDE_OPTS OR DEFINED ${i}_LIBRARY_OPTS)
|
||||
# The package seems to export option lists that we can use, woohoo!
|
||||
list(APPEND includeopts ${${i}_INCLUDE_OPTS})
|
||||
list(APPEND libraryopts ${${i}_LIBRARY_OPTS})
|
||||
else()
|
||||
# If plural forms don't exist or they equal singular forms
|
||||
if ((NOT DEFINED ${i}_INCLUDE_DIRS AND NOT DEFINED ${i}_LIBRARIES) OR
|
||||
({i}_INCLUDE_DIR STREQUAL ${i}_INCLUDE_DIRS AND ${i}_LIBRARY STREQUAL ${i}_LIBRARIES))
|
||||
# Singular forms can be used
|
||||
if (DEFINED ${i}_INCLUDE_DIR)
|
||||
list(APPEND includeopts ${i}_INCLUDE_DIR)
|
||||
endif()
|
||||
if (DEFINED ${i}_LIBRARY)
|
||||
list(APPEND libraryopts ${i}_LIBRARY)
|
||||
endif()
|
||||
else()
|
||||
# Oh no, we don't know the option names
|
||||
message(FATAL_ERROR "We couldn't determine config variable names for ${i} includes and libs. Aieeh!")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (includeopts)
|
||||
list(REMOVE_DUPLICATES includeopts)
|
||||
endif()
|
||||
|
||||
if (libraryopts)
|
||||
list(REMOVE_DUPLICATES libraryopts)
|
||||
endif()
|
||||
|
||||
string(REGEX REPLACE ".*[ ;]([^ ;]*(_INCLUDE_DIRS|_LIBRARIES))" "\\1" tmp "${includeopts} ${libraryopts}")
|
||||
if (NOT tmp STREQUAL "${includeopts} ${libraryopts}")
|
||||
message(AUTHOR_WARNING "Plural form ${tmp} found in config options of ${PREFIX}. This works as before but is now deprecated. Please only use singular forms INCLUDE_DIR and LIBRARY, and update your find scripts for LibFindMacros > 2.0 automatic dependency system (most often you can simply remove the PROCESS variables entirely).")
|
||||
endif()
|
||||
|
||||
# Include/library names separated by spaces (notice: not CMake lists)
|
||||
unset(includes)
|
||||
unset(libs)
|
||||
|
||||
# Process all includes and set found false if any are missing
|
||||
foreach (i ${includeopts})
|
||||
list(APPEND configopts ${i})
|
||||
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
|
||||
list(APPEND includes "${${i}}")
|
||||
else()
|
||||
set(found FALSE)
|
||||
set(missing_headers TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Process all libraries and set found false if any are missing
|
||||
foreach (i ${libraryopts})
|
||||
list(APPEND configopts ${i})
|
||||
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
|
||||
list(APPEND libs "${${i}}")
|
||||
else()
|
||||
set (found FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Version checks
|
||||
if (found AND findver)
|
||||
if (NOT version)
|
||||
message(WARNING "The find module for ${PREFIX} does not provide version information, so we'll just assume that it is OK. Please fix the module or remove package version requirements to get rid of this warning.")
|
||||
elseif (version VERSION_LESS findver OR (exactver AND NOT version VERSION_EQUAL findver))
|
||||
set(found FALSE)
|
||||
set(version_unsuitable TRUE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# If all-OK, hide all config options, export variables, print status and exit
|
||||
if (found)
|
||||
foreach (i ${configopts})
|
||||
mark_as_advanced(${i})
|
||||
endforeach()
|
||||
if (NOT quiet)
|
||||
message(STATUS "Found ${PREFIX} ${${PREFIX}_VERSION}")
|
||||
if (LIBFIND_DEBUG)
|
||||
message(STATUS " ${PREFIX}_DEPENDENCIES=${${PREFIX}_DEPENDENCIES}")
|
||||
message(STATUS " ${PREFIX}_INCLUDE_OPTS=${includeopts}")
|
||||
message(STATUS " ${PREFIX}_INCLUDE_DIRS=${includes}")
|
||||
message(STATUS " ${PREFIX}_LIBRARY_OPTS=${libraryopts}")
|
||||
message(STATUS " ${PREFIX}_LIBRARIES=${libs}")
|
||||
endif()
|
||||
set (${PREFIX}_INCLUDE_OPTS ${includeopts} PARENT_SCOPE)
|
||||
set (${PREFIX}_LIBRARY_OPTS ${libraryopts} PARENT_SCOPE)
|
||||
set (${PREFIX}_INCLUDE_DIRS ${includes} PARENT_SCOPE)
|
||||
set (${PREFIX}_LIBRARIES ${libs} PARENT_SCOPE)
|
||||
set (${PREFIX}_FOUND TRUE PARENT_SCOPE)
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Format messages for debug info and the type of error
|
||||
set(vars "Relevant CMake configuration variables:\n")
|
||||
foreach (i ${configopts})
|
||||
mark_as_advanced(CLEAR ${i})
|
||||
set(val ${${i}})
|
||||
if ("${val}" STREQUAL "${i}-NOTFOUND")
|
||||
set (val "<not found>")
|
||||
elseif (val AND NOT EXISTS ${val})
|
||||
set (val "${val} (does not exist)")
|
||||
else()
|
||||
set(some_files TRUE)
|
||||
endif()
|
||||
set(vars "${vars} ${i}=${val}\n")
|
||||
endforeach()
|
||||
set(vars "${vars}You may use CMake GUI, cmake -D or ccmake to modify the values. Delete CMakeCache.txt to discard all values and force full re-detection if necessary.\n")
|
||||
if (version_unsuitable)
|
||||
set(msg "${PREFIX} ${${PREFIX}_VERSION} was found but")
|
||||
if (exactver)
|
||||
set(msg "${msg} only version ${findver} is acceptable.")
|
||||
else()
|
||||
set(msg "${msg} version ${findver} is the minimum requirement.")
|
||||
endif()
|
||||
else()
|
||||
if (missing_headers)
|
||||
set(msg "We could not find development headers for ${PREFIX}. Do you have the necessary dev package installed?")
|
||||
elseif (some_files)
|
||||
set(msg "We only found some files of ${PREFIX}, not all of them. Perhaps your installation is incomplete or maybe we just didn't look in the right place?")
|
||||
if(findver)
|
||||
set(msg "${msg} This could also be caused by incompatible version (if it helps, at least ${PREFIX} ${findver} should work).")
|
||||
endif()
|
||||
else()
|
||||
set(msg "We were unable to find package ${PREFIX}.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Fatal error out if REQUIRED
|
||||
if (required)
|
||||
set(msg "REQUIRED PACKAGE NOT FOUND\n${msg} This package is REQUIRED and you need to install it or adjust CMake configuration in order to continue building ${CMAKE_PROJECT_NAME}.")
|
||||
message(FATAL_ERROR "${msg}\n${vars}")
|
||||
endif()
|
||||
# Otherwise just print a nasty warning
|
||||
if (NOT quiet)
|
||||
message(WARNING "WARNING: MISSING PACKAGE\n${msg} This package is NOT REQUIRED and you may ignore this warning but by doing so you may miss some functionality of ${CMAKE_PROJECT_NAME}. \n${vars}")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -1,89 +0,0 @@
|
||||
# Source: https://gist.github.com/sivachandran/3a0de157dccef822a230#file-bin2h-cmake
|
||||
# Added modifications to suit prusaslicer
|
||||
include(CMakeParseArguments)
|
||||
|
||||
# Function to wrap a given string into multiple lines at the given column position.
|
||||
# Parameters:
|
||||
# VARIABLE - The name of the CMake variable holding the string.
|
||||
# AT_COLUMN - The column position at which string will be wrapped.
|
||||
function(WRAP_STRING)
|
||||
set(oneValueArgs VARIABLE AT_COLUMN)
|
||||
cmake_parse_arguments(WRAP_STRING "${options}" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
string(LENGTH ${${WRAP_STRING_VARIABLE}} stringLength)
|
||||
math(EXPR offset "0")
|
||||
|
||||
while(stringLength GREATER 0)
|
||||
|
||||
if(stringLength GREATER ${WRAP_STRING_AT_COLUMN})
|
||||
math(EXPR length "${WRAP_STRING_AT_COLUMN}")
|
||||
else()
|
||||
math(EXPR length "${stringLength}")
|
||||
endif()
|
||||
|
||||
string(SUBSTRING ${${WRAP_STRING_VARIABLE}} ${offset} ${length} line)
|
||||
set(lines "${lines}\n${line}")
|
||||
|
||||
math(EXPR stringLength "${stringLength} - ${length}")
|
||||
math(EXPR offset "${offset} + ${length}")
|
||||
endwhile()
|
||||
|
||||
set(${WRAP_STRING_VARIABLE} "${lines}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Function to embed contents of a file as byte array in C/C++ header file(.h). The header file
|
||||
# will contain a byte array and integer variable holding the size of the array.
|
||||
# Parameters
|
||||
# SOURCE_FILE - The path of source file whose contents will be embedded in the header file.
|
||||
# VARIABLE_NAME - The name of the variable for the byte array. The string "_SIZE" will be append
|
||||
# to this name and will be used a variable name for size variable.
|
||||
# HEADER_FILE - The path of header file.
|
||||
# APPEND - If specified appends to the header file instead of overwriting it
|
||||
# NULL_TERMINATE - If specified a null byte(zero) will be append to the byte array. This will be
|
||||
# useful if the source file is a text file and we want to use the file contents
|
||||
# as string. But the size variable holds size of the byte array without this
|
||||
# null byte.
|
||||
# Usage:
|
||||
# bin2h(SOURCE_FILE "Logo.png" HEADER_FILE "Logo.h" VARIABLE_NAME "LOGO_PNG")
|
||||
function(BIN2H)
|
||||
set(options APPEND NULL_TERMINATE ADD_WARNING_TEXT)
|
||||
set(oneValueArgs SOURCE_FILE VARIABLE_NAME HEADER_FILE)
|
||||
cmake_parse_arguments(BIN2H "${options}" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
# reads source file contents as hex string
|
||||
file(READ ${BIN2H_SOURCE_FILE} hexString HEX)
|
||||
string(LENGTH ${hexString} hexStringLength)
|
||||
|
||||
# appends null byte if asked
|
||||
if(BIN2H_NULL_TERMINATE)
|
||||
set(hexString "${hexString}00")
|
||||
endif()
|
||||
|
||||
# wraps the hex string into multiple lines at column 32(i.e. 16 bytes per line)
|
||||
wrap_string(VARIABLE hexString AT_COLUMN 32)
|
||||
math(EXPR arraySize "${hexStringLength} / 2")
|
||||
|
||||
# adds '0x' prefix and comma suffix before and after every byte respectively
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1, " arrayValues ${hexString})
|
||||
# removes trailing comma
|
||||
string(REGEX REPLACE ", $" "" arrayValues ${arrayValues})
|
||||
|
||||
# converts the variable name into proper C identifier
|
||||
string(MAKE_C_IDENTIFIER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME)
|
||||
# string(TOUPPER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME)
|
||||
|
||||
# declares byte array and the length variables
|
||||
set(arrayDefinition "const unsigned char ${BIN2H_VARIABLE_NAME}[] = { ${arrayValues} };")
|
||||
set(arraySizeDefinition "const size_t ${BIN2H_VARIABLE_NAME}_SIZE = ${arraySize};")
|
||||
set(warnTxt "")
|
||||
if (BIN2H_ADD_WARNING_TEXT)
|
||||
set(warnTxt "/* WARN: This file is auto-generated from ${BIN2H_SOURCE_FILE} */\n")
|
||||
endif ()
|
||||
|
||||
set(declarations "${warnTxt}${arrayDefinition}\n\n${arraySizeDefinition}\n\n")
|
||||
if(BIN2H_APPEND)
|
||||
file(APPEND ${BIN2H_HEADER_FILE} "${declarations}")
|
||||
else()
|
||||
file(WRITE ${BIN2H_HEADER_FILE} "${declarations}")
|
||||
endif()
|
||||
endfunction()
|
||||
Vendored
-71
@@ -1,71 +0,0 @@
|
||||
From b32e63f75046d186d93ee7d627c978f2f7c892f3 Mon Sep 17 00:00:00 2001
|
||||
From: Steve Dower <steve.dower@python.org>
|
||||
Date: Fri, 10 Jul 2026 14:04:00 +0100
|
||||
Subject: [PATCH] [3.13] gh-153438: Update NuGet download URL (GH-153482)
|
||||
(GH-153515)
|
||||
|
||||
gh-153438: Update NuGet download URL (GH-153482)
|
||||
(cherry picked from commit 106eb532ea3b243423e62a702719e9d3c0e40c16)
|
||||
(cherry picked from commit 62eb50d65d538dda5164f9c4a097a896454a0d1c)
|
||||
|
||||
Co-authored-by: Steve Dower <steve.dower@python.org>
|
||||
Co-authored-by: Harjoth Khara <harjoth.khara@gmail.com>
|
||||
---
|
||||
Doc/using/windows.rst | 6 +++---
|
||||
.../Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst | 2 ++
|
||||
PCbuild/find_python.bat | 2 +-
|
||||
Tools/msi/get_externals.bat | 2 +-
|
||||
4 files changed, 7 insertions(+), 5 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
|
||||
diff --git a/Doc/using/windows.rst b/Doc/using/windows.rst
|
||||
index 2a507675044666..4800789ca2d6cc 100644
|
||||
--- a/Doc/using/windows.rst
|
||||
+++ b/Doc/using/windows.rst
|
||||
@@ -414,9 +414,9 @@ on using nuget. What follows is a summary that is sufficient for Python
|
||||
developers.
|
||||
|
||||
The ``nuget.exe`` command line tool may be downloaded directly from
|
||||
-``https://aka.ms/nugetclidl``, for example, using curl or PowerShell. With the
|
||||
-tool, the latest version of Python for 64-bit or 32-bit machines is installed
|
||||
-using::
|
||||
+``https://dist.nuget.org/win-x86-commandline/latest/nuget.exe``, for example,
|
||||
+using curl or PowerShell. With the tool, the latest version of Python for
|
||||
+64-bit or 32-bit machines is installed using::
|
||||
|
||||
nuget.exe install python -ExcludeVersion -OutputDirectory .
|
||||
nuget.exe install pythonx86 -ExcludeVersion -OutputDirectory .
|
||||
diff --git a/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst b/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
new file mode 100644
|
||||
index 00000000000000..edab8e6ba7b259
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+Update Windows build and installer tooling and documentation to use the
|
||||
+current download URL for ``nuget.exe``.
|
||||
diff --git a/PCbuild/find_python.bat b/PCbuild/find_python.bat
|
||||
index 0af367a3efafad..9612a2860ef9c3 100644
|
||||
--- a/PCbuild/find_python.bat
|
||||
+++ b/PCbuild/find_python.bat
|
||||
@@ -52,7 +52,7 @@
|
||||
@set _Py_HOST_PYTHON=%HOST_PYTHON%
|
||||
@if "%_Py_HOST_PYTHON%"=="" set _Py_HOST_PYTHON=py
|
||||
@if "%_Py_NUGET%"=="" (set _Py_NUGET=%_Py_EXTERNALS_DIR%\nuget.exe)
|
||||
-@if "%_Py_NUGET_URL%"=="" (set _Py_NUGET_URL=https://aka.ms/nugetclidl)
|
||||
+@if "%_Py_NUGET_URL%"=="" (set _Py_NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)
|
||||
@if NOT exist "%_Py_NUGET%" (
|
||||
@if not "%_Py_Quiet%"=="1" @echo Downloading nuget...
|
||||
@rem NB: Must use single quotes around NUGET here, NOT double!
|
||||
diff --git a/Tools/msi/get_externals.bat b/Tools/msi/get_externals.bat
|
||||
index f6602ce9588ff4..c7c7e9f470bc25 100644
|
||||
--- a/Tools/msi/get_externals.bat
|
||||
+++ b/Tools/msi/get_externals.bat
|
||||
@@ -6,7 +6,7 @@ set HERE=%~dp0
|
||||
if "%PCBUILD%"=="" (set PCBUILD=%HERE%..\..\PCbuild\)
|
||||
if "%EXTERNALS_DIR%"=="" (set EXTERNALS_DIR=%HERE%..\..\externals\windows-installer)
|
||||
if "%NUGET%"=="" (set NUGET=%EXTERNALS_DIR%\..\nuget.exe)
|
||||
-if "%NUGET_URL%"=="" (set NUGET_URL=https://aka.ms/nugetclidl)
|
||||
+if "%NUGET_URL%"=="" (set NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)
|
||||
|
||||
set DO_FETCH=true
|
||||
set DO_CLEAN=false
|
||||
Vendored
+80
-15
@@ -8,20 +8,45 @@ set(_python_url "https://www.python.org/ftp/python/${_python_version}/Python-${_
|
||||
set(_python_sha256 "c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684")
|
||||
|
||||
|
||||
set(_patch_cmd "")
|
||||
if(WIN32)
|
||||
|
||||
# Fix python build failure on Windows if python is not available, due to wrong nuget download URL
|
||||
# See https://github.com/python/cpython/issues/153438
|
||||
# Patch from https://github.com/python/cpython/pull/153608
|
||||
# This patch has not been merged to 3.12 yet so we need to apply it manually
|
||||
#
|
||||
# Without core.autocrlf=false the patched find_python.bat comes out LF and
|
||||
# cmd.exe cannot find its goto labels.
|
||||
set(_patch_cmd git init
|
||||
&& ${GIT_EXECUTABLE} -c core.autocrlf=false apply --verbose
|
||||
--ignore-space-change --whitespace=fix
|
||||
${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch)
|
||||
# Build-time Python for PCbuild's scripts, pinned because 3.15 cannot run the 3.12 scripts.
|
||||
set(DEP_PYTHON_HOST "" CACHE FILEPATH
|
||||
"Python 3.10-3.13 that runs CPython's build scripts; empty downloads a pinned one from nuget.org")
|
||||
if(DEP_PYTHON_HOST)
|
||||
execute_process(
|
||||
COMMAND "${DEP_PYTHON_HOST}" -E -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
||||
OUTPUT_VARIABLE _python_host_minor
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE _python_host_result
|
||||
)
|
||||
if(NOT _python_host_result EQUAL 0 OR _python_host_minor VERSION_LESS 3.10 OR _python_host_minor VERSION_GREATER 3.13)
|
||||
message(FATAL_ERROR "DEP_PYTHON_HOST must be Python 3.10 to 3.13, got '${_python_host_minor}' from ${DEP_PYTHON_HOST}")
|
||||
endif()
|
||||
file(TO_NATIVE_PATH "${DEP_PYTHON_HOST}" _python_host)
|
||||
else()
|
||||
set(_python_host_version "3.13.16")
|
||||
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64")
|
||||
set(_python_host_package pythonarm64)
|
||||
set(_python_host_sha256 bfd77aa5b0edff4a7936a3220e8b79e7c78f864ce13e8fa2c835d9946e11694a)
|
||||
else()
|
||||
set(_python_host_package pythonx86)
|
||||
set(_python_host_sha256 c65cf47664b33286dfc5f2a17303668dd59e057869815ed9b270fb3866c1dd15)
|
||||
endif()
|
||||
set(_python_host_dir "${CMAKE_CURRENT_BINARY_DIR}/dep_python3_host-prefix/src/dep_python3_host")
|
||||
ExternalProject_Add(dep_python3_host
|
||||
URL "https://api.nuget.org/v3-flatcontainer/${_python_host_package}/${_python_host_version}/${_python_host_package}.${_python_host_version}.nupkg"
|
||||
URL_HASH SHA256=${_python_host_sha256}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
# Renames the .nupkg to .zip so ExternalProject extracts it.
|
||||
DOWNLOAD_NAME ${_python_host_package}.${_python_host_version}.zip
|
||||
SOURCE_DIR "${_python_host_dir}"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
)
|
||||
file(TO_NATIVE_PATH "${_python_host_dir}/tools/python.exe" _python_host)
|
||||
set(_python_depends dep_python3_host)
|
||||
endif()
|
||||
|
||||
if(MSVC_VERSION EQUAL 1800)
|
||||
set(_python_platform_toolset v120)
|
||||
@@ -63,6 +88,47 @@ if(WIN32)
|
||||
# release python3xx.lib; Py_DEBUG could not load release plugin modules.
|
||||
set(_python_pcbuild_config Release)
|
||||
|
||||
# CPython's own libraries, fetched here instead of by get_externals.bat, which ignores failed
|
||||
# downloads. Must match PCbuild/get_externals.bat for _python_version.
|
||||
set(_python_externals
|
||||
# repo folder commit sha256 of the archive
|
||||
source-deps bzip2-1.0.8 05301997b2f9590f49c672cf3dfd3d3dfa7ad521 44b15bede52b32f8673a2976621a847258b041243c003e30f4d9a05214785fe5
|
||||
source-deps sqlite-3.49.1.0 cb7a3997059739fe9c63b2819aeda7c6907510ef 586fe469463f5b27b749f9eef1938baa8e8a12030341e96160a371356c2d6759
|
||||
source-deps xz-5.2.5 c6bc0c612605622aaef101a33a751f9de2ecc193 d99882a8f9fd48be0e605e27e3c077e5eaab370f6f52ebcc8ff754480ffdf0bc
|
||||
source-deps zlib-1.3.1 4dc98e1909830e2bdc2a9cc2236e3c5d5037335b 39d867c083ebc37c77cf03a442f87c0f83db006eb64343bd4a19f1c84c26f50c
|
||||
bin-deps libffi-3.4.4 94cb9a1c7feb608adf2b9f8fe2dbd6925ffbf90d 6b0f53490e0dd958708bff32df689cf2163c214147fcb8edb76a403fa6a877ca
|
||||
bin-deps openssl-bin-3.0.16.2 1174fd43d858425c345b16d631716fb98ba492ea b741fd1411a36cb899fee2c39b70a4492f4bc1c6e2301d67df32e9754118a63b
|
||||
)
|
||||
set(_python_externals_dir "${CMAKE_CURRENT_BINARY_DIR}/dep_python3_externals")
|
||||
list(LENGTH _python_externals _python_externals_len)
|
||||
math(EXPR _python_externals_last "${_python_externals_len} - 1")
|
||||
foreach(_i RANGE 0 ${_python_externals_last} 4)
|
||||
math(EXPR _j "${_i} + 1")
|
||||
math(EXPR _k "${_i} + 2")
|
||||
math(EXPR _l "${_i} + 3")
|
||||
list(GET _python_externals ${_i} _ext_repo)
|
||||
list(GET _python_externals ${_j} _ext_name)
|
||||
list(GET _python_externals ${_k} _ext_commit)
|
||||
list(GET _python_externals ${_l} _ext_sha256)
|
||||
ExternalProject_Add(dep_python3_ext_${_ext_name}
|
||||
URL "https://github.com/python/cpython-${_ext_repo}/archive/${_ext_commit}.zip"
|
||||
URL_HASH SHA256=${_ext_sha256}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
DOWNLOAD_NAME cpython-${_ext_repo}-${_ext_name}.zip
|
||||
SOURCE_DIR "${_python_externals_dir}/${_ext_name}"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
)
|
||||
list(APPEND _python_depends dep_python3_ext_${_ext_name})
|
||||
endforeach()
|
||||
file(TO_NATIVE_PATH "${_python_externals_dir}" _python_externals_native)
|
||||
|
||||
# PCbuild runs PYTHON without -E, so PYTHONHOME and PYTHONPATH are cleared.
|
||||
set(_python_env_args --unset=PYTHONHOME --unset=PYTHONPATH "PYTHON=${_python_host}"
|
||||
"EXTERNALS_DIR=${_python_externals_native}"
|
||||
"GIT_CEILING_DIRECTORIES=<SOURCE_DIR>/..")
|
||||
|
||||
# CPython's PCbuild needs a 64-bit-hosted toolchain: find_msbuild.bat picks the
|
||||
# 32-bit Bin\MSBuild.exe, whose x86 cl.exe/link.exe run out of address space
|
||||
# (fatal C1002 "out of heap space") building the LTCG-optimized pythoncore.
|
||||
@@ -71,7 +137,6 @@ if(WIN32)
|
||||
# then hosts arm64 tools on its own; the amd64 MSBuild still defaults to x86, so
|
||||
# PreferredToolArchitecture pins it to x64. Scoped to this build step, so
|
||||
# CPython's own sources stay untouched.
|
||||
set(_python_env_args "GIT_CEILING_DIRECTORIES=<SOURCE_DIR>/..")
|
||||
if(CMAKE_GENERATOR_INSTANCE) # empty for non-VS generators (e.g. Ninja)
|
||||
set(_python_msbuild "${CMAKE_GENERATOR_INSTANCE}/MSBuild/Current/Bin/${_python_msbuild_host}/MSBuild.exe")
|
||||
if(EXISTS "${_python_msbuild}")
|
||||
@@ -284,12 +349,12 @@ endif()
|
||||
ExternalProject_Add(dep_python3
|
||||
URL "${_python_url}"
|
||||
URL_HASH SHA256=${_python_sha256}
|
||||
PATCH_COMMAND ${_patch_cmd}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
BUILD_IN_SOURCE ON
|
||||
CONFIGURE_COMMAND ${_conf_cmd}
|
||||
BUILD_COMMAND ${_build_cmd}
|
||||
INSTALL_COMMAND ${_install_cmd}
|
||||
DEPENDS ${_python_depends}
|
||||
)
|
||||
|
||||
# Python depends on OpenSSL and ZLIB
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Color picker
|
||||
|
||||
The color picker is a local HTML dialog hosted by `ColorPickerDialog`, a
|
||||
`WebViewHostDialog` subclass. Its header and implementation own the ordered
|
||||
selection contract and shared filament conversion. Bridge validation, favorites
|
||||
persistence, placement and gettext strings remain private to the implementation.
|
||||
Its colors use the existing `ColorRGBA` utility.
|
||||
It uses a native title bar and fixed window border, allowing dragging but no
|
||||
manual resizing or maximization. Native title-bar colors use the application's
|
||||
shared dialog dark-theme update on creation and theme changes.
|
||||
Like SpeedDial, the host disables browser zoom accelerators, restores native
|
||||
WebView focus on activation and readiness, and synchronizes layout and the macOS
|
||||
viewport before repainting the backend. Activation never dismisses this modal
|
||||
dialog; focus restoration preserves an active editor and its partial input.
|
||||
The initialized page reports
|
||||
its intrinsic content height; native code validates the current page identifier
|
||||
and bounded numeric height, converts CSS pixels to device-independent window
|
||||
size, and fits within the display's work area. Identical sizes are ignored. The
|
||||
page observes content rather than viewport height, so resizing does not create
|
||||
a feedback loop; oversized content remains scrollable.
|
||||
The panel snapshots the triggering color or More Colors button's screen rectangle.
|
||||
It aligns below the button, flips above when needed, and clamps to that button's
|
||||
display work area on initial display. Later content and DPI changes update only
|
||||
the client size, preserving the position chosen by dragging the title bar.
|
||||
wxGTK clears the dialog's default
|
||||
parent-centering constraint and reapplies its native position after showing,
|
||||
bypassing wxGTK's requested-position cache. Absolute top-level placement remains
|
||||
subject to the window manager; Wayland compositors control it. The official filament palette's
|
||||
existing placement is independent of this custom panel.
|
||||
A solid selection contains one color; a gradient contains exactly two ordered
|
||||
endpoints. The bridge represents these as `{"type":"solid|gradient",
|
||||
"colors":["#RRGGBBAA", ...]}`. RGB input is also accepted and becomes opaque.
|
||||
Native validation rejects malformed hex and incorrect cardinality.
|
||||
|
||||
The project filament color action and the official filament picker's More Colors
|
||||
action enable two-endpoint gradients and independent endpoint alpha. The preset
|
||||
combo box reads project colors in their stored vector order locally and applies
|
||||
changes through `sync_colour_config`,
|
||||
retaining the existing RGB/RGBA profile/project formats and dirty marking.
|
||||
Sidebar gradient and segmented multicolor swatches draw colors in the supplied
|
||||
order without sorting them.
|
||||
Opaque colors use `#RRGGBB`; nonopaque colors use `#RRGGBBAA`, including fully
|
||||
transparent alpha `00`. Initial selections, alpha-only edits and cancellation
|
||||
retain stored alpha. Ordered
|
||||
custom results bypass the official palette's `FilamentColor` set. Choosing an
|
||||
official swatch retains its existing palette behavior.
|
||||
The Linux sidebar's Change extruder color menu uses this same custom editor and
|
||||
the filament swatch anchor, including alpha, ordered gradients and configuration syncing.
|
||||
|
||||
Striped colors and gradients with more than two endpoints cannot be represented
|
||||
by this editor. A notice explains that the current selection is retained; the
|
||||
editor starts from the first color, and unchanged confirmation is a no-op.
|
||||
Cancellation is also a no-op. If the WebView backend cannot initialize, a local
|
||||
unavailable panel offers Cancel and Escape, without opening a system color picker.
|
||||
|
||||
Generic color option fields use an ordinary color button on every platform and
|
||||
open the same panel with gradients and alpha disabled. The panel is anchored to
|
||||
the field button. Empty or invalid field values remain undefined (the empty
|
||||
string), distinct from opaque black. Right-click resets to undefined. Cancel does
|
||||
not notify the field; confirmation and reset notify only when its RGB string
|
||||
changes. Programmatic assignments remain silent, and reading a field never
|
||||
writes legacy favorites.
|
||||
|
||||
AMS material settings retain their fixed/AMS swatches and first-level popup. Only
|
||||
its custom-color action opens the solid, opaque editor, anchored to that custom
|
||||
control. The transient popup releases its grab while the modal is open and returns
|
||||
to the same selection on cancellation. Confirmation uses the existing default-color
|
||||
and packed RGBA event path. Device permission/status checks remain at the first-level
|
||||
entry.
|
||||
|
||||
Texture import's color-mapping Add Material action opens a solid, opaque editor
|
||||
anchored to the mapping row. The existing filament limit is checked before opening.
|
||||
The first-level material popup closes as an action; its callback is copied before
|
||||
dismissal, so popup destruction does not invalidate the modal's completion path.
|
||||
Confirmation invokes the existing virtual-filament callback once; cancellation
|
||||
adds no material.
|
||||
|
||||
Native gettext strings are installed at document start. Static page labels and
|
||||
palette names use stable English keys; RAL codes, channel symbols, and numeric
|
||||
labels remain data. The palette color values do not depend on the UI language.
|
||||
The application locale also sets the document language. Numeric editor validation
|
||||
uses the application's gettext messages, rather than browser language defaults;
|
||||
missing translations fall back to the stable English keys.
|
||||
|
||||
The pure color/state regressions run with
|
||||
`node --test resources/web/dialog/ColorPickerDialog/colorpicker.test.js`.
|
||||
Optional real-browser regressions live beside them in
|
||||
`colorpicker.browser.test.js`. They use an existing Playwright installation
|
||||
(`ORCA_PLAYWRIGHT_MODULE` can name its module path) and an installed browser
|
||||
(`ORCA_BROWSER_EXECUTABLE` can name its executable), without adding a production
|
||||
dependency. They cover bridge initialization, capability combinations, invalid
|
||||
editor recovery, ordered endpoint alpha, favorites, gettext keys, and live theme
|
||||
changes. These browser checks complement native contract/persistence tests; they
|
||||
do not establish the full native modal confirmation and favorites journey.
|
||||
|
||||
Gradient and alpha capabilities are independent and default off. Unsupported
|
||||
gradients are rejected; disabled alpha normalizes the selected result to opaque.
|
||||
Favorites retain both capabilities independently of the current caller. The page
|
||||
stores alpha as a byte and only converts to percentage when displaying or editing
|
||||
opacity, so switching endpoints does not change alpha precision.
|
||||
|
||||
The page starts disabled. Each document supplies a page identifier in its ready
|
||||
message, receives native initialization, and acknowledges it before native code
|
||||
accepts confirmation or favorite updates. Repeated initialization does not reset
|
||||
edits, and messages from an obsolete document are ignored. Native WebView message
|
||||
handling is deferred and guarded by a lifetime token. Only a validated confirmation
|
||||
sets the optional result. Cancel, Escape and native window close leave it empty.
|
||||
|
||||
Favorites belong to application configuration, separately from the selected result,
|
||||
in the editor configuration returned by `AppConfig::config_path()` under `data_dir()`.
|
||||
The `color_picker` section has string `version` equal to `1` and a `favorites`
|
||||
string containing a JSON array of typed selections. Native helpers normalize and
|
||||
deduplicate this array in order and limit it to 24 entries. Updates replace the
|
||||
entire section and immediately call `AppConfig::save()`, so accepted favorite
|
||||
changes survive cancellation. Empty arrays are saved explicitly. Core `AppConfig`
|
||||
only stores the raw section; color validation belongs to the GUI boundary.
|
||||
|
||||
When the section is absent, valid solid colors from `custom_color_list` are imported
|
||||
once, without changing that legacy section. The legacy native picker saves decimal
|
||||
`r,g,b,a` byte strings; hexadecimal RGB and RGBA strings are also supported. Migration
|
||||
strictly validates all four decimal channels and preserves alpha. Gradient strings
|
||||
and malformed values are ignored. The new version and an empty array are
|
||||
saved even when there is nothing to import. A present section with an unknown or
|
||||
missing version is left untouched and disables favorite writes. Malformed version
|
||||
1 data is displayed as empty and remains on disk until the user explicitly saves
|
||||
a valid replacement. Other configuration sections and project/profile color
|
||||
formats are independent of this storage.
|
||||
|
||||
Interface colors consume the host's `data-orca-theme` and `--orca-*` contract through
|
||||
shared styles. The picker opts into scoped global control roles. Application theme
|
||||
changes update UI surfaces without reloading the page or changing color data.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Focus mode (hiding inactive plates) — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
"Focus mode" is an entry of the canvas menu in the bottom left corner of Prepare. Turned on (eye
|
||||
lit), the 3D view hides the inactive plates and shows the current plate alone, so a project with
|
||||
many plates can be worked on one plate at a time. Turned off, every plate is shown. It is a view
|
||||
setting: it changes nothing in the model, the slice or the project file, and it does not reach
|
||||
plate thumbnails. The choice is kept in the app config as `focus_mode`, off by default.
|
||||
|
||||
Preview needs no such option, as it already draws the current plate alone and has its own plate
|
||||
toolbar. The assembly view and the Design tab do not lay objects out on plates, so the entry is
|
||||
disabled there.
|
||||
|
||||
## What is shown
|
||||
|
||||
The current plate is drawn with its bed, label and icons; the other plates are not drawn at all. The
|
||||
world axes, which Prepare draws at the corner of the first plate, move to the corner of the current
|
||||
plate, where Preview draws them.
|
||||
|
||||
An object instance is shown when its footprint, the XY bounding box of the convex hulls of its
|
||||
volumes, overlaps the current plate, or when it is selected. The test is made per instance, so the
|
||||
parts of an object are shown or hidden together, and a wipe tower follows its plate the same way.
|
||||
It is geometric rather than the plate membership `PartPlateList` keeps, because an instance stays on
|
||||
the plate it was first placed on: an object of another plate that reaches over this one, or one too
|
||||
large for any plate, would otherwise be invisible while it takes room on the current plate. A
|
||||
selected instance stays visible so that what a selection or a gizmo acts on is never hidden, for
|
||||
instance an object picked in the object list on another plate.
|
||||
|
||||
## Hiding
|
||||
|
||||
A hidden object is neither drawn nor picked. `GLCanvas3D::_update_inactive_plates_visibility()`
|
||||
runs at the start of each frame of Prepare. It clears `GLVolume::is_active` on the volumes of the
|
||||
hidden instances and deactivates their raycasters, which every render pass, the hover and rectangle
|
||||
selection, the shadow and center of mass markers already respect. It also deactivates the raycasters
|
||||
of the other plates, whose icons would otherwise still take clicks where nothing is drawn. The state
|
||||
persists between frames, so the raycasts made while handling the mouse, such as the anchor of a pan,
|
||||
see the same scene as the frame.
|
||||
|
||||
Other features clear `is_active` too: gizmos hide the objects they do not edit and show them all
|
||||
again when they close. `GLVolume::on_hidden_plate` records which volumes this update hid, so it only
|
||||
shows again what it hid itself, and it waits for a gizmo that hides objects to close before doing
|
||||
so. A volume another feature made active again is hidden again on the next frame. While Focus mode
|
||||
is off and nothing is left hidden, the update returns at once.
|
||||
|
||||
## Switching plates
|
||||
|
||||
The other plates cannot be clicked while Focus mode hides them, so a compact counterpart of the Preview
|
||||
plate toolbar takes their place: a column of plate thumbnails in the top left corner, below the top
|
||||
toolbar, shown when the project has more than one plate and no gizmo is open. The thumbnails are the
|
||||
ones the Preview toolbar shows (`PartPlate::thumbnail_data`); the canvas rebuilds its textures when
|
||||
`Plater::plate_toolbar_image_version()` changes, a counter each consumer tracks on its own so that the
|
||||
Preview toolbar's dirty flag is left alone. That counter moves after almost every edit, so the
|
||||
thumbnail of the current plate, the only one edited while the others are hidden, is drawn again each
|
||||
time, and the other plates' only once an edit to them has invalidated theirs. The current plate has an
|
||||
accent border, a hovered one the gray border of the Preview toolbar, empty plates are dimmed and the
|
||||
plate number sits in the corner; the tooltip gives the plate name. A click selects the plate and
|
||||
clears the selection, as a click on a plate row of the object list does. The column scrolls when it
|
||||
does not fit above the 3D navigator or the canvas toolbar. Its scrollbar works as the Preview
|
||||
toolbar's, with which it shares its style: it shows only while the mouse is over the column or drags
|
||||
the bar, and the mouse wheel scrolls the column either way.
|
||||
|
||||
Whatever selects a plate while the others are hidden, the camera moves its target over the new plate
|
||||
and keeps its angle, distance and height (`PartPlateList::select_plate()`), so the plate comes into
|
||||
view where the previous one was. A zoom to all plates, as after loading a project or adding a plate,
|
||||
frames the current plate instead. Turning Focus mode on does the same, so the current plate fills the view.
|
||||
@@ -0,0 +1,199 @@
|
||||
# Multi-extruder and multi-AMS device control
|
||||
|
||||
`DeviceManager`/`MachineObject` model the toolheads, AMS units, and filament slots of a connected
|
||||
printer and drive the device-control UI from that model. The model represents the counts as
|
||||
device-reported runtime values rather than compile-time caps, so a printer with an arbitrary number
|
||||
of toolheads, several AMS units, and a non-uniform number of slots per unit is handled by the same
|
||||
code paths. The printer agent is responsible for translating its printer's state into the shared
|
||||
JSON dialect this model consumes; the agent boundary is described in
|
||||
[printer-agent.md](printer-agent.md), and this document describes the dialect and the model.
|
||||
|
||||
## Model layout
|
||||
|
||||
`MachineObject` owns the device subsystems that this document covers:
|
||||
|
||||
- `DevExtderSystem` — the toolheads (extruders) and their per-toolhead filament/temperature state.
|
||||
- `DevFilaSystem` — the AMS units (`DevAms`) and their trays (`DevAmsTray`).
|
||||
- `DevNozzleSystem` — the nozzles and, on rack printers, `DevNozzleRack`.
|
||||
- `DevFilaSwitch` — a filament switch that can feed several AMS units to more than one extruder.
|
||||
|
||||
Each subsystem exposes its own count — `DevExtderSystem::GetTotalExtderCount()`,
|
||||
`DevFilaSystem::GetAmsCount()`, `DevAms::GetSlotCount()` — and the rest of the application derives
|
||||
its layout from those values rather than from fixed assumptions.
|
||||
|
||||
## Wire dialect
|
||||
|
||||
The dialect is the Bambu-shaped JSON that `DeviceManager` and `MachineObject` already consume. An
|
||||
agent whose printer speaks something else is expected to translate into it; the agent is not free
|
||||
to invent a new shape. Two representations coexist for extruders, and one for AMS/slots.
|
||||
|
||||
### Extruders
|
||||
|
||||
The generic representation is `print.device.extruder`, parsed by
|
||||
`ExtderSystemParser::ParseV2_0` (`DevExtruderSystem.cpp`). It has a packed `state` integer and an
|
||||
`info` array with one object per toolhead:
|
||||
|
||||
```text
|
||||
device.extruder.state packed integer
|
||||
bits 0..3 total extruder count
|
||||
bits 4..7 current extruder id
|
||||
bits 8..11 target extruder id
|
||||
bits 12..14 switch state
|
||||
bits 15..18 currently loading extruder id
|
||||
bit 19 busy for loading
|
||||
|
||||
device.extruder.info[] one entry per toolhead
|
||||
id extruder id
|
||||
filam_bak array of backup-group bits
|
||||
info bit 1 has filament, bit 2 buffer has filament, bit 3 nozzle present
|
||||
temp bits 0..15 current temperature, bits 16..31 target temperature
|
||||
spre slot_id bits 0..7, ams_id bits 8..15 (previous)
|
||||
snow same packing (current)
|
||||
star same packing (target)
|
||||
stat bit 0..15 AMS status, bits 16..31 RFID status
|
||||
hnow current nozzle id
|
||||
```
|
||||
|
||||
The legacy single/dual representation is the pair of scalar fields `nozzle_temper` /
|
||||
`nozzle_target_temper`, plus the `print.ams.tray_tar` / `tray_now` tray pointer, parsed by
|
||||
`ExtderSystemParser::ParseV1_0`. That parser is a fallback and only fills the main extruder when the
|
||||
reported toolhead count is one.
|
||||
|
||||
Physical extruder ids are fixed (`MAIN_EXTRUDER_ID = 0`, `DEPUTY_EXTRUDER_ID = 1`). Bambu printers
|
||||
present the two toolheads as a left/right pair and invert the physical-to-logical mapping; generic
|
||||
printers use the toolhead id itself as the logical id. `DevNozzle::GetLogicExtruderId()` /
|
||||
`GetExtruderId()` centralise that distinction.
|
||||
|
||||
### AMS units and slots
|
||||
|
||||
AMS units arrive as `print.ams.ams[]` and are parsed by `DevFilaSystemParser::ParseV1_0`. Each unit
|
||||
carries an `id` and an `info` bitfield:
|
||||
|
||||
```text
|
||||
info bits 0..3 type: 0 dummy, 1 AMS, 2 AMS-Lite, 3 N3F, 4 N3S, 5 AMS-Lite-mixed (N9)
|
||||
info bits 8..11 bound extruder id (0xE marks a switch-bound unit)
|
||||
info bits 24..27 switch inlet when the unit is switch-bound
|
||||
```
|
||||
|
||||
`DevAmsType` (`DevDefs.h`) enumerates the resulting unit kinds: `EXT_SPOOL`, `AMS`, `AMS_LITE`,
|
||||
`N3F`, `N3S`, and the `AMS_LITE_MIXED` N9 variant. `AMS_LITE_MIXED` reports itself as `AMS_LITE` to
|
||||
generic callers; code that needs the distinction uses `IsAmsLiteMixed()`.
|
||||
|
||||
A unit whose bound-extruder nibble is `0xE` is switch-bound: it is kept only when a `DevFilaSwitch`
|
||||
is installed, in which case its extruder id is pinned to the main extruder and its binding set
|
||||
becomes `{0, 1}`; otherwise the unit is dropped from the AMS list.
|
||||
|
||||
Each unit owns a map of `DevAmsTray` keyed by the tray id from the payload. The slot count is a
|
||||
property of the unit kind and the reported trays:
|
||||
|
||||
- `AMS`, `AMS_LITE`, and `N3F` report four slots on Bambu printers, matching the firmware contract;
|
||||
- `N3S` reports one slot;
|
||||
- `EXT_SPOOL` and any non-Bambu unit report the number of trays actually present.
|
||||
|
||||
The tray ids are the payload's slot indices; they are expected to be unique, 0-based, and contiguous
|
||||
within a unit, because the generic tray-index mapping below is cumulative.
|
||||
|
||||
Bulk/extended spools are carried by `print.vir_slot`, which `DeviceManager` parses into
|
||||
`MachineObject::vt_slot`. Two ids are fixed: `VIRTUAL_TRAY_MAIN_ID = 255` (extruder 0) and
|
||||
`VIRTUAL_TRAY_DEPUTY_ID = 254` (extruder 1); the id convention is `vt_id = 255 - extruder_id`, so
|
||||
further extruders use 253, 252, … . The `vir_slot` parser recognises the 255/254 pair; the agent
|
||||
filament descriptor path `DevFilaSystemParser::ParseAgentFilament` is the entry that stores a fuller
|
||||
descending set in `vt_slot`.
|
||||
|
||||
## Mapping
|
||||
|
||||
Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray
|
||||
index. The Bambu mapping path (`DevMappingUtil`) computes `ams_id * 4 + slot_id` and knows only the
|
||||
Bambu unit kinds; `DevFilaSystem` provides the device model's own mapping below. The two agree for
|
||||
Bambu units and diverge for a non-Bambu unit whose slot count is not four.
|
||||
|
||||
### AMS id to extruder id
|
||||
|
||||
`DevFilaSystem::GetExtruderIdByAmsId()` resolves an AMS id to the extruder it feeds:
|
||||
|
||||
- an AMS unit returns its bound extruder (`DevAms::GetExtruderId()`);
|
||||
- a virtual tray returns `VIRTUAL_TRAY_MAIN_ID - vt_id`;
|
||||
- Bambu's virtual AMS ids map to the main/deputy extruder.
|
||||
|
||||
A unit may be bound to more than one extruder when a `DevFilaSwitch` feeds it; the binding set
|
||||
(`DevAms::m_binded_extruder_set`) and switch inlet are read by switch-aware code while the
|
||||
single-extruder id stays pinned for legacy callers.
|
||||
|
||||
### Tray index
|
||||
|
||||
`DevFilaSystem::GetTrayIndexMap()` maps a global tray index to an `(ams_id, slot_id)` pair and is
|
||||
used for backup-slot translation, calibration addressing, and the mapping popups.
|
||||
|
||||
- Bambu AMS: `ams_id * 4 + slot_id`; N3S: the AMS id; AMS-Lite-mixed (N9): `24 + slot_id`.
|
||||
- Generic (non-Bambu): a running lane index — the count of trays in preceding units plus the slot
|
||||
id. This depends on each unit's tray ids being 0-based and contiguous.
|
||||
|
||||
### Virtual-slot recognition
|
||||
|
||||
`devPrinterUtil::IsVirtualSlot()` decides whether an id denotes a virtual tray. On Bambu printers it
|
||||
is the fixed pair `{255, 254}`. On generic printers it is the descending range
|
||||
`[255 - count + 1, 255]`, where `count` is the selected printer preset's extruder count. The preset
|
||||
is expected to match the connected device; when the two disagree the virtual range is wrong, which
|
||||
is why a mismatch is reported rather than silently accepted.
|
||||
|
||||
## Device-control UI
|
||||
|
||||
The device-control widgets follow the same device-reported counts.
|
||||
|
||||
- `AMSModel` (`AMSItem.hpp`) mirrors `DevAmsType` numerically: `EXT_AMS`, `GENERIC_AMS`, `AMS_LITE`,
|
||||
`N3F_AMS`, `N3S_AMS`. A plain `AMS` maps to `GENERIC_AMS`.
|
||||
- On non-Bambu printers `use_generic_ams_layout()` routes `GENERIC_AMS` units through a variable-lane
|
||||
rendering that draws one lane per reported tray; Bambu keeps its fixed four-slot rendering.
|
||||
- `AMSControl` picks its layout by toolhead count, not vendor: `CreateAmsSingleNozzle` for one
|
||||
toolhead, `CreateAmsDoubleNozzle` for two, and `CreateAmsMultiNozzle` (one preview and one AMS
|
||||
simplebook per toolhead) for three or more. A two-toolhead generic printer therefore keeps the
|
||||
legacy left/right presentation; that is deliberate.
|
||||
- The status panel's `ExtruderImage` renders one per-toolhead state per nozzle and is resized to
|
||||
`GetTotalExtderCount()`. Non-Bambu printers choose the active toolhead through
|
||||
`m_generic_nozzle_selector` (shown when more than one toolhead exists); Bambu keeps its left/right
|
||||
`m_nozzle_btn_panel`. Nozzle temperature controls are created on demand per toolhead.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Single dialect.** The agent translates its protocol into this JSON; the model does not learn
|
||||
vendor protocols. Extending the model with a new unit kind or field means extending the dialect.
|
||||
- **Agent-produced counts.** Toolhead, AMS, and slot counts come from the device payload. Nothing
|
||||
caps them at two toolheads or four slots except the Bambu contract described above.
|
||||
- **Extruder dialect invariants.** `device.extruder.state`'s count field must equal the length of
|
||||
`info[]`, and `info[]` must be ordered by extruder id with `id` equal to the array index: the
|
||||
parser places toolheads by array position while `GetExtderById()` indexes by id.
|
||||
- **One filament road per toolhead.** Each toolhead renders a single AMS filament road rather than an
|
||||
independent per-toolhead road state. A generic scaffold that allowed the latter was removed as
|
||||
unused; the one-extruder-per-toolhead road is the intended model.
|
||||
- **Preset/device agreement.** The generic layout and virtual-slot range derive their count from the
|
||||
selected printer preset. A printer preset must therefore declare the same number of
|
||||
`nozzle_diameter` entries as the device has toolheads. A mismatch is surfaced only as a log warning
|
||||
(`StatusPanel::check_extruder_count_mismatch()`), deliberately log-only, because the sidebar and
|
||||
extruder logic would otherwise silently disagree.
|
||||
- **Bambu compatibility.** Bambu-specific presentation (left/right pair, fixed 4-slot units, fixed
|
||||
virtual ids, physical/logical inversion) is preserved behind the vendor check. Generic paths do
|
||||
not alter it.
|
||||
- **Tray-index invariant.** The generic tray-index map is cumulative, so payload tray ids must be
|
||||
unique, 0-based, and contiguous per unit. A malformed id is a producer error.
|
||||
- **Protocol markers.** The dialect has no neutral equivalent for some Bambu state, so agents that
|
||||
lack it send empty `print.cfg`, `print.fun`, `print.aux`, and `print.stat`. Their presence selects
|
||||
the new-protocol parse path; they are compatibility markers, not feature switches.
|
||||
|
||||
## Main implementation locations
|
||||
|
||||
- [`DevExtruderSystem`](../../src/slic3r/GUI/DeviceCore/DevExtruderSystem.h) — toolhead model and the
|
||||
`device.extruder` parser
|
||||
- [`DevFilaSystem`](../../src/slic3r/GUI/DeviceCore/DevFilaSystem.h) — AMS/tray model, the AMS parser,
|
||||
and the mapping helpers
|
||||
- [`DevDefs.h`](../../src/slic3r/GUI/DeviceCore/DevDefs.h) — `DevAmsType`, extruder ids, and virtual
|
||||
tray ids
|
||||
- [`DeviceManager.cpp`](../../src/slic3r/GUI/DeviceManager.cpp) — message dispatch, `vir_slot`
|
||||
parsing, and device state assembly
|
||||
- [`AMSControl.cpp`](../../src/slic3r/GUI/Widgets/AMSControl.cpp) — per-toolhead AMS panes
|
||||
(`CreateAmsMultiNozzle`) and the generic layout
|
||||
- [`AMSItem.hpp`](../../src/slic3r/GUI/Widgets/AMSItem.hpp) — `AMSModel` and the variable-lane AMS
|
||||
rendering
|
||||
- [`StatusPanel.cpp`](../../src/slic3r/GUI/StatusPanel.cpp) — `ExtruderImage`, the toolhead selector,
|
||||
and per-toolhead temperature controls
|
||||
- [`MoonrakerPrinterAgent.cpp`](../../src/slic3r/Utils/MoonrakerPrinterAgent.cpp) — producer example
|
||||
(AMS payload and the `device.extruder` encoding)
|
||||
@@ -215,6 +215,7 @@ src/libslic3r/GCode.cpp
|
||||
src/libslic3r/GCodeWriter.cpp
|
||||
src/libslic3r/GCode/ToolOrdering.cpp
|
||||
src/libslic3r/GCode/SeamPlacer.cpp
|
||||
src/libslic3r/GCode/GCodeStatistics.cpp
|
||||
src/libslic3r/ExtrusionEntity.cpp
|
||||
src/libslic3r/Flow.cpp
|
||||
src/libslic3r/Format/AMF.cpp
|
||||
@@ -324,6 +325,7 @@ src/slic3r/GUI/BaseTransparentDPIFrame.cpp
|
||||
src/slic3r/GUI/CloneDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeSupport.cpp
|
||||
src/slic3r/GUI/ColorPickerDialog.cpp
|
||||
src/slic3r/GUI/ConfigWizard.cpp
|
||||
src/slic3r/GUI/DeviceCore/DevCalib.cpp
|
||||
src/slic3r/GUI/DeviceCore/DevUpgrade.cpp
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/* Pure color/state helpers shared by the page and its node:test contract tests. */
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
else root.ColorPickerModel = api;
|
||||
})(typeof globalThis === "object" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
function parseHex(hex) {
|
||||
if (typeof hex !== "string" || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)) return null;
|
||||
return [1, 3, 5, 7].map((offset) => offset === 7 && hex.length === 7 ? 255 : parseInt(hex.slice(offset, offset + 2), 16));
|
||||
}
|
||||
|
||||
function toHex(color) {
|
||||
return "#" + color.map((byte) => Math.round(byte).toString(16).padStart(2, "0")).join("").toUpperCase();
|
||||
}
|
||||
|
||||
const MAX_GRADIENT_COLORS = 7; // Most colors a gradient may have. A caller allows 2 (the default) up to this many.
|
||||
const NEW_GRADIENT_HUE_SHIFT = 75; // Hue step used for every newly created gradient color.
|
||||
const gradientLimit = (value) => Math.max(2, Math.min(MAX_GRADIENT_COLORS, Math.floor(Number(value)) || 2));
|
||||
|
||||
// `maxGradientColors` caps how many colors a gradient may have (2 when omitted).
|
||||
function normalizeSelection(value, options = {}, maxGradientColors = 2) {
|
||||
if (!value || !["solid", "gradient"].includes(value.type) || !Array.isArray(value.colors)) return null;
|
||||
if (value.type === "gradient" && !options.allow_gradient) return null;
|
||||
const count = value.colors.length;
|
||||
if (value.type === "solid" ? count !== 1 : count < 2 || count > gradientLimit(maxGradientColors)) return null;
|
||||
const colors = value.colors.map(parseHex);
|
||||
if (colors.some((color) => color === null)) return null;
|
||||
if (!options.allow_alpha) colors.forEach((color) => { color[3] = 255; });
|
||||
return {type: value.type, colors: colors.map(toHex)};
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
|
||||
let h = 0, s = 0;
|
||||
const l = (max + min) / 2;
|
||||
if (delta) {
|
||||
s = delta / (1 - Math.abs(2 * l - 1));
|
||||
if (max === r) h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
|
||||
else if (max === g) h = ((b - r) / delta + 2) / 6;
|
||||
else h = ((r - g) / delta + 4) / 6;
|
||||
}
|
||||
return [h * 360, s * 100, l * 100];
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) {
|
||||
h = ((h % 360) + 360) % 360 / 60; s /= 100; l /= 100;
|
||||
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
const x = c * (1 - Math.abs(h % 2 - 1)), m = l - c / 2;
|
||||
const sectors = [[c,x,0], [x,c,0], [0,c,x], [0,x,c], [x,0,c], [c,0,x]];
|
||||
return sectors[Math.floor(h)].map((channel) => Math.round((channel + m) * 255));
|
||||
}
|
||||
|
||||
function contrastColor(color, hueShift = 90) {
|
||||
if (!Number.isFinite(hueShift)) hueShift = 90;
|
||||
const degrees = Math.max(-180, Math.min(180, hueShift));
|
||||
const amount = Math.abs(degrees) / 180;
|
||||
const [h, s, l] = rgbToHsl(color[0], color[1], color[2]);
|
||||
const grey = s < 10;
|
||||
const target = grey ? (l < 50 ? 100 : 0) : 100 - l;
|
||||
const rgb = hslToRgb(h + degrees, grey ? 0 : s, l + (target - l) * amount);
|
||||
return [...rgb, color[3]];
|
||||
}
|
||||
|
||||
// options.max_gradient_colors (2..7, default 2) is how many colors this page lets a gradient have.
|
||||
function createState(options = {}, selection = {type: "solid", colors: ["#808080"]}) {
|
||||
const capabilities = {allow_gradient: options.allow_gradient === true, allow_alpha: options.allow_alpha === true};
|
||||
// gradient stays null until a selection provides one or the user switches to gradient mode.
|
||||
const state = {options: capabilities, maxGradientColors: gradientLimit(options.max_gradient_colors),
|
||||
mode: "solid", active: 0, solid: [128,128,128,255], gradient: null};
|
||||
if (!applySelection(state, selection)) return null;
|
||||
return state;
|
||||
}
|
||||
|
||||
function applySelection(state, value) {
|
||||
const selection = normalizeSelection(value, state.options, state.maxGradientColors);
|
||||
if (!selection) return false;
|
||||
state.mode = selection.type;
|
||||
const colors = selection.colors.map(parseHex);
|
||||
if (state.mode === "solid") state.solid = colors[0];
|
||||
else {
|
||||
state.gradient = colors;
|
||||
state.active = Math.min(state.active, colors.length - 1); // A gradient with fewer colors must not leave the selected stop past its end.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function activeColor(state) { return state.mode === "solid" ? state.solid : state.gradient[state.active]; }
|
||||
|
||||
function setMode(state, mode) {
|
||||
if (mode !== "solid" && (mode !== "gradient" || !state.options.allow_gradient)) return false;
|
||||
// A new gradient starts at the current solid color and ends at its contrast color.
|
||||
if (mode === "gradient" && !state.gradient) state.gradient = [[...state.solid], contrastColor(state.solid, NEW_GRADIENT_HUE_SHIFT)];
|
||||
state.mode = mode;
|
||||
return true;
|
||||
}
|
||||
|
||||
function addGradientColor(state) {
|
||||
if (state.mode !== "gradient" || state.gradient.length >= state.maxGradientColors) return false;
|
||||
state.gradient.splice(state.active + 1, 0, contrastColor(state.gradient[state.active], NEW_GRADIENT_HUE_SHIFT));
|
||||
state.active += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeGradientColor(state) {
|
||||
if (state.mode !== "gradient" || state.gradient.length <= 2) return false;
|
||||
state.gradient.splice(state.active, 1);
|
||||
state.active = Math.min(state.active, state.gradient.length - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setRgb(state, rgb) {
|
||||
if (rgb.length !== 3 || rgb.some((v) => !Number.isFinite(v) || v < 0 || v > 255)) return false;
|
||||
const color = activeColor(state);
|
||||
rgb.forEach((v, i) => { color[i] = Math.round(v); });
|
||||
return true;
|
||||
}
|
||||
|
||||
function setAlphaPercent(state, value) {
|
||||
if (!state.options.allow_alpha || !Number.isFinite(value) || value < 0 || value > 100) return false;
|
||||
activeColor(state)[3] = Math.round(value * 255 / 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setHex(state, hex) {
|
||||
const color = parseHex(hex);
|
||||
if (!color) return false;
|
||||
if (hex.length === 7) color[3] = activeColor(state)[3];
|
||||
if (!state.options.allow_alpha) color[3] = 255;
|
||||
if (state.mode === "solid") state.solid = color;
|
||||
else state.gradient[state.active] = color;
|
||||
return true;
|
||||
}
|
||||
|
||||
function exportSelection(state) {
|
||||
const colors = state.mode === "solid" ? [state.solid] : state.gradient;
|
||||
return normalizeSelection({type: state.mode, colors: colors.map(toHex)}, state.options, state.maxGradientColors);
|
||||
}
|
||||
|
||||
// Favorites retain full capabilities regardless of what this caller can select.
|
||||
function normalizeFavorites(values) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
return values.slice(0, 24).map((value) => normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, MAX_GRADIENT_COLORS)).filter(Boolean);
|
||||
}
|
||||
|
||||
function selectionBackground(value) {
|
||||
return value.type === "gradient" ? `linear-gradient(to right, ${value.colors.join(", ")})` : `linear-gradient(${value.colors[0]}, ${value.colors[0]})`;
|
||||
}
|
||||
|
||||
return {MAX_GRADIENT_COLORS, NEW_GRADIENT_HUE_SHIFT, contrastColor, parseHex, toHex, normalizeSelection, rgbToHsl, hslToRgb, createState, applySelection, activeColor, setMode,
|
||||
addGradientColor, removeGradientColor, setRgb, setAlphaPercent, setHex, exportSelection, normalizeFavorites, selectionBackground};
|
||||
});
|
||||
@@ -0,0 +1,314 @@
|
||||
// Optional real-DOM regressions. Use an existing Playwright installation:
|
||||
// ORCA_PLAYWRIGHT_MODULE=<module path> ORCA_BROWSER_EXECUTABLE=<browser path>
|
||||
// ORCA_BROWSER_ENGINE=webkit exercises the macOS WebView layout engine.
|
||||
// node --test resources/web/dialog/ColorPickerDialog/colorpicker.browser.test.js
|
||||
const {test, before, after} = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {readFileSync} = require("node:fs");
|
||||
const {resolve} = require("node:path");
|
||||
const {pathToFileURL} = require("node:url");
|
||||
const engineName = process.env.ORCA_BROWSER_ENGINE || "chromium";
|
||||
let engine;
|
||||
try { engine = require(process.env.ORCA_PLAYWRIGHT_MODULE || "playwright")[engineName]; }
|
||||
catch (error) { if (error.code !== "MODULE_NOT_FOUND") throw error; }
|
||||
const options = {skip: engine ? false : "Use an existing Playwright installation via ORCA_PLAYWRIGHT_MODULE"};
|
||||
const url = pathToFileURL(resolve(__dirname, "index.html")).href;
|
||||
let browser;
|
||||
before(async () => { if (engine) browser = await engine.launch({headless: true, ...(process.env.ORCA_BROWSER_EXECUTABLE ? {executablePath: process.env.ORCA_BROWSER_EXECUTABLE} : {})}); });
|
||||
after(async () => { if (browser) await browser.close(); });
|
||||
async function open(t, strings = {}, language = "") {
|
||||
const page = await browser.newPage({viewport: {width: 600, height: 660}}), errors = [];
|
||||
page.on("pageerror", error => errors.push(error.message));
|
||||
t.after(async () => { await page.close(); assert.deepEqual(errors, []); });
|
||||
await page.addInitScript(({table, language}) => {
|
||||
window.ORCA_UI_STRINGS = table;
|
||||
window.ORCA_COLOR_PICKER_LANGUAGE = language;
|
||||
window.messages = [];
|
||||
window.wx = {postMessage: text => messages.push(JSON.parse(text))};
|
||||
}, {table: strings, language});
|
||||
await page.goto(url);
|
||||
const ready = await page.evaluate(() => messages[0]);
|
||||
assert.equal(ready.command, "ready");
|
||||
return {page, ready, send: payload => page.evaluate(value => ColorPickerDialog.handleMessage(value), payload)};
|
||||
}
|
||||
function init(ready, gradient = false, alpha = false) {
|
||||
return {command: "init", page_id: ready.page_id, options: {allow_gradient: gradient, allow_alpha: alpha},
|
||||
selection: {type: "solid", colors: ["#01234567"]},
|
||||
ui: {sidebar_open: true, palette: "basic"},
|
||||
favorites: [{type: "gradient", colors: ["#FF000040", "#0000FF90"]}, {type: "solid", colors: ["#ABCDEF40"]}]};
|
||||
}
|
||||
async function count(page, command) { return page.evaluate(value => messages.filter(message => message.command === value).length, command); }
|
||||
|
||||
for (const gradient of [false, true]) for (const alpha of [false, true]) {
|
||||
test(`Native capabilities gate gradient=${gradient} and alpha=${alpha}`, options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
|
||||
assert.equal(await send({...init(ready), options: {allow_gradient: "yes", allow_alpha: alpha}}), false);
|
||||
assert.equal(await send({...init(ready), page_id: "stale"}), false);
|
||||
assert.equal(await send(init(ready, gradient, alpha)), true);
|
||||
assert.equal(await page.locator("#modeControls").isVisible(), gradient);
|
||||
assert.equal(await page.locator("#alphaControls").isVisible(), alpha);
|
||||
assert.equal(await page.locator("#userColors button:not(.empty)").count(), gradient ? 2 : 1);
|
||||
await page.locator("#userColors button:not(.empty)").last().click();
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
assert.deepEqual(selection, {type: "solid", colors: [alpha ? "#ABCDEF40" : "#ABCDEFFF"]});
|
||||
assert.equal(await send(init(ready, gradient, alpha)), true);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection, "repeated ready/init retains edits");
|
||||
await page.locator("#colorPreview").click();
|
||||
const favorites = await page.evaluate(() => messages.at(-1).favorites);
|
||||
assert(favorites.some(favorite => favorite.colors[0] === "#ABCDEF40"), "original favorites retain their alpha");
|
||||
assert(favorites.some(favorite => favorite.type === "gradient"), "filtered gradients remain stored");
|
||||
await page.locator("#confirmBtn").click();
|
||||
assert.deepEqual(await page.evaluate(() => messages.at(-1).selection), selection);
|
||||
});
|
||||
}
|
||||
|
||||
test("Visible incomplete editors block confirmation and favorites until recovery", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send(init(ready, true, true));
|
||||
for (const [id, value, valid] of [["rValue", "", "21"], ["oValue", "", "50"], ["hexInput", "GG", "12345680"]]) {
|
||||
await page.locator("#" + id).fill(value);
|
||||
const confirms = await count(page, "confirm"), saves = await count(page, "update_favorites");
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click()); await page.locator("#colorPreview").click();
|
||||
assert.equal(await count(page, "confirm"), confirms); assert.equal(await count(page, "update_favorites"), saves);
|
||||
await page.locator("#" + id).fill(valid); await page.locator("#" + id).press("Enter");
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
}
|
||||
await page.locator("#hValue").fill(""); // HSL is now visible beside RGB, so incomplete HSL must block.
|
||||
const hslConfirms = await count(page, "confirm");
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click());
|
||||
assert.equal(await count(page, "confirm"), hslConfirms);
|
||||
await page.locator("#hValue").fill("100");
|
||||
const confirms = await count(page, "confirm"); await page.locator("#confirmBtn").click();
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
await page.locator("#rValue").fill(""); await page.locator("#rValue").press("Escape");
|
||||
assert.equal(await page.evaluate(() => messages.at(-1).command), "cancel");
|
||||
});
|
||||
|
||||
test("Numeric channel and opacity edits clamp to their field bounds", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send(init(ready, true, true));
|
||||
for (const [id, input, expected] of [["rValue", "999", "255"], ["rValue", "-1", "0"],
|
||||
["hValue", "361", "360"], ["oValue", "101", "100"], ["oValue", "-1", "0"]]) {
|
||||
await page.locator("#" + id).fill(input);
|
||||
assert.equal(await page.locator("#" + id).inputValue(), expected);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.checkValidity()), true);
|
||||
}
|
||||
assert.equal((await page.evaluate(() => ColorPickerDialog.exportSelection())).colors[0].slice(-2), "00");
|
||||
});
|
||||
|
||||
test("Endpoint edits and favorites retain ordered byte alpha", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
|
||||
await page.locator(".gradient-thumb[data-index='1']").click();
|
||||
await page.locator('.color-item[title="Red"]').first().click();
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "gradient", colors: ["#FF000001", "#FF000080"]});
|
||||
await page.locator("#hexInput").fill("0000FF80"); await page.locator(".gradient-thumb[data-index='0']").click();
|
||||
assert.equal(await page.locator("#hexInput").inputValue(), "FF000001");
|
||||
await page.locator("#colorPreview").click();
|
||||
assert.deepEqual(await page.evaluate(() => messages.at(-1).favorites[0]), {type: "gradient", colors: ["#FF000001", "#0000FF80"]});
|
||||
});
|
||||
|
||||
test("Read-only favorites remain selectable and late documents cannot initialize", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send({...init(ready), favorites_writable: false});
|
||||
assert.equal(await page.locator("#colorPreview").isDisabled(), true);
|
||||
await page.locator("#userColors button:not(.empty)").first().click();
|
||||
assert.equal(await page.locator("#hexInput").inputValue(), "ABCDEF");
|
||||
await page.reload();
|
||||
assert.equal(await send(init(ready)), false); assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
|
||||
});
|
||||
|
||||
test("Document strings translate palette names and editor errors while retaining RAL codes and numeric labels", options, async t => {
|
||||
const source = readFileSync(resolve(__dirname, "../../../../src/slic3r/GUI/ColorPickerDialog.cpp"), "utf8");
|
||||
const keys = [...source.matchAll(/_u8L\("([^"\n]+)"\)/g)].map(match => match[1]);
|
||||
const strings = Object.fromEntries(keys.map(key => [key, "Translated: " + key]));
|
||||
const {page, ready, send} = await open(t, strings); await send({...init(ready, true, true), options: {allow_gradient: true, allow_alpha: true, max_gradient_colors: 7}, preserve_multi_color: true});
|
||||
assert.equal(await page.title(), strings["Color Picker"]);
|
||||
for (const element of await page.locator("[data-i18n]").all()) assert.equal(await element.textContent(), strings[await element.getAttribute("data-i18n")]);
|
||||
for (const attribute of ["aria-label", "title"]) for (const element of await page.locator(`[data-i18n-${attribute}]`).all())
|
||||
assert.equal((await element.getAttribute(attribute)).split("\n")[0], strings[await element.getAttribute(`data-i18n-${attribute}`)]);
|
||||
assert.equal(await page.locator("#selectionNotice").textContent(), strings["The current multi-color selection is kept until you choose a different color."]);
|
||||
for (const palette of ["basic", "orca", "ral", "gradient"]) {
|
||||
await page.selectOption("#paletteCombo", palette);
|
||||
const names = await page.evaluate(value => ColorPickerPalettes[value].map(entry => entry.name), palette);
|
||||
assert.deepEqual(await page.locator(".color-name").allTextContents(), names.map(name => {
|
||||
const ral = name.match(/^(\d{4}) (.+)$/); return ral ? `${ral[1]} ${strings[ral[2]]}` : /^%\d+$/.test(name) ? name : strings[name];
|
||||
}));
|
||||
}
|
||||
await page.locator("#hexInput").fill("bad");
|
||||
assert.equal(await page.locator("#hexInput").evaluate(element => element.validationMessage), strings["Enter a complete hexadecimal color"]);
|
||||
await page.locator("#hexInput").fill("12345680");
|
||||
for (const [id, invalid, valid, key] of [["rValue", "", "18", "Invalid input"],
|
||||
["oValue", "", "50", "Invalid input"]]) {
|
||||
const confirms = await count(page, "confirm");
|
||||
await page.locator("#" + id).fill(invalid);
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click());
|
||||
await page.locator("#colorPreview").click(); // Saving invokes the same translated validation while OK is disabled.
|
||||
assert.equal(await count(page, "confirm"), confirms);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), strings[key]);
|
||||
await page.locator("#" + id).fill(valid);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), "");
|
||||
await page.locator("#confirmBtn").click();
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
}
|
||||
await page.locator("#hValue").fill("361");
|
||||
await page.locator("#colorPreview").click();
|
||||
assert.equal(await page.locator("#hValue").inputValue(), "360", "out-of-range hue clamps at its boundary");
|
||||
await page.locator(".color-item").first().click();
|
||||
assert.equal(await page.locator("#hValue").evaluate(element => element.validationMessage), "", "a new palette selection clears the previous editor error");
|
||||
});
|
||||
|
||||
test("Document language and escaped labels preserve untranslated English fallback", options, async t => {
|
||||
const cancel = '取消 "Cancel" \\ /\n下一行';
|
||||
const {page, ready, send} = await open(t, {Cancel: cancel}, "zh-CN");
|
||||
await send(init(ready));
|
||||
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN");
|
||||
assert.equal(await page.locator("#cancelBtn").textContent(), cancel);
|
||||
assert.equal(await page.title(), "Color Picker");
|
||||
assert.equal(await page.locator(".color-name").first().textContent(), "Red");
|
||||
assert.deepEqual(await page.locator("label[for=hValue],label[for=sValue],label[for=lValue],label[for=rValue],label[for=gValue],label[for=bValue]").allTextContents(), ["H", "S", "L", "R", "G", "B"]);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "solid", colors: ["#012345FF"]});
|
||||
});
|
||||
|
||||
test("Global control colors follow the system while host semantic colors follow the app", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
|
||||
const initial = await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()}));
|
||||
for (const theme of ["light", "dark"]) for (const system of ["light", "dark"]) {
|
||||
await page.emulateMedia({colorScheme: system});
|
||||
await page.evaluate(value => {
|
||||
document.documentElement.dataset.orcaTheme = value;
|
||||
const colors = value === "light" ? {bg: "#ffffff", fg: "#262e30", muted: "#363636", border: "#dbdbdb", accent: "#009688"} : {bg: "#2d2d31", fg: "#efeff0", muted: "#b2b3b5", border: "#36363b", accent: "#00675b"};
|
||||
for (const [key, color] of Object.entries(colors)) document.documentElement.style.setProperty("--orca-" + key, color);
|
||||
document.documentElement.style.setProperty("--orca-accent-fg", "#ffffff");
|
||||
}, theme);
|
||||
await page.evaluate(async () => {
|
||||
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
|
||||
});
|
||||
const colors = await page.evaluate(() => {
|
||||
const body = getComputedStyle(document.body);
|
||||
const tokenColor = token => {
|
||||
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
|
||||
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
|
||||
};
|
||||
const mode = document.querySelector("label[for=modeSwitch]");
|
||||
return {background: body.backgroundColor, foreground: body.color,
|
||||
modeBackground: getComputedStyle(mode).backgroundColor,
|
||||
accent: getComputedStyle(mode, "::before").backgroundColor,
|
||||
hostPanel: tokenColor("--panel"), hostText: tokenColor("--text")};
|
||||
});
|
||||
assert.equal(colors.background, system === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)", `${theme} app / ${system} system body`);
|
||||
assert.equal(colors.foreground, system === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
|
||||
assert.equal(colors.modeBackground, system === "light" ? "rgb(240, 240, 240)" : "rgb(36, 36, 40)");
|
||||
assert.equal(colors.accent, theme === "light" ? "rgb(0, 150, 136)" : "rgb(0, 103, 91)", "shared accent follows the injected app theme");
|
||||
assert.equal(colors.hostPanel, theme === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)");
|
||||
assert.equal(colors.hostText, theme === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
|
||||
assert.deepEqual(await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()})), initial);
|
||||
for (const id of ["modeSwitch"]) for (const checked of [false, true]) {
|
||||
if (await page.locator("#" + id).isChecked() !== checked) await page.locator(`label[for=${id}]`).click();
|
||||
await page.evaluate(async () => {
|
||||
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
|
||||
});
|
||||
const result = await page.locator(`label[for=${id}]`).evaluate((element, selected) => {
|
||||
const tokenColor = token => {
|
||||
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
|
||||
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
|
||||
};
|
||||
const labels = element.querySelectorAll(".switch-label");
|
||||
return {selected: getComputedStyle(labels[selected ? 1 : 0]).color,
|
||||
unselected: getComputedStyle(labels[selected ? 0 : 1]).color,
|
||||
accentForeground: tokenColor("--button-fg-light"), mutedForeground: tokenColor("--fg-color-label")};
|
||||
}, checked);
|
||||
assert.equal(result.selected, result.accentForeground, `${theme} app / ${system} system ${id} selected text uses the accent foreground`);
|
||||
assert.equal(result.selected, "rgb(254, 254, 254)", "global selected text stays light in both system schemes");
|
||||
assert.equal(result.unselected, result.mutedForeground, `${theme} app / ${system} system ${id} unselected text stays muted`);
|
||||
assert.equal(result.unselected, system === "light" ? "rgb(54, 54, 54)" : "rgb(178, 179, 181)");
|
||||
const selected = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
assert.equal(selected.type, checked ? "gradient" : "solid");
|
||||
if (checked) assert.deepEqual(selected, initial.selection, "returning to gradient keeps ordered endpoint byte alpha");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Sidebar resizes from a compact viewport and shrinks after every collapse", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await page.setViewportSize({width: 340, height: 380});
|
||||
await send({...init(ready, true, true), ui: {sidebar_open: false, palette: "basic"}});
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
for (const [index, sidebarOpen] of [false, true, false, true, false].entries()) {
|
||||
const width = sidebarOpen ? 510 : 340;
|
||||
await page.waitForFunction(expected => messages.filter(message => message.command === "resize").at(-1)?.width === expected, width);
|
||||
const size = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1));
|
||||
assert.equal(size.height, 380);
|
||||
await page.setViewportSize({width: size.width, height: size.height});
|
||||
await page.waitForTimeout(100);
|
||||
assert(await page.evaluate(() => {
|
||||
const body = document.body.getBoundingClientRect();
|
||||
const picker = document.querySelector(".right-panel").getBoundingClientRect();
|
||||
return body.width === innerWidth && picker.right <= body.right && picker.bottom <= body.bottom;
|
||||
}), "the host viewport contains the full picker");
|
||||
const resizes = await count(page, "resize");
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(await count(page, "resize"), resizes, "fitting the host does not cause a resize loop");
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
|
||||
if (index < 4) await page.locator("#paletteToggle").click();
|
||||
}
|
||||
});
|
||||
|
||||
test("Floating panel fits capabilities and wrapped translations without a resize feedback loop", options, async t => {
|
||||
const strings = {Color: "A long translated color label", Gradient: "A long translated gradient label", Cancel: "A longer translated cancel action", OK: "A longer translated confirmation action",
|
||||
"The current multi-color selection is kept until you choose a different color.": "A translated explanation that wraps across the compact floating panel and keeps the original multi-color selection until another color is chosen."};
|
||||
for (const gradient of [false, true]) for (const alpha of [false, true]) {
|
||||
const {page, ready, send} = await open(t, strings);
|
||||
await page.setViewportSize({width: 550, height: 520});
|
||||
await send({...init(ready, gradient, alpha), preserve_multi_color: true});
|
||||
await page.waitForFunction(() => messages.some(message => message.command === "resize"));
|
||||
const height = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1).height);
|
||||
assert(height >= 300 && height <= 900);
|
||||
assert.equal(height, await page.locator("body").evaluate(element => Math.ceil(element.getBoundingClientRect().height)));
|
||||
assert(await page.evaluate(() => messages.findIndex(message => message.command === "initialized") < messages.findIndex(message => message.command === "resize")));
|
||||
await page.setViewportSize({width: 550, height});
|
||||
await page.waitForTimeout(100);
|
||||
const resizes = await count(page, "resize");
|
||||
await page.setViewportSize({width: 550, height: height + 100});
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(await count(page, "resize"), resizes, "extra viewport height never changes intrinsic content height");
|
||||
const controls = await page.evaluate(() => {
|
||||
const body = document.body.getBoundingClientRect();
|
||||
return ["confirmBtn", "cancelBtn", ...(document.getElementById("alphaControls").classList.contains("hidden") ? [] : ["oValue"])].map(id => {
|
||||
const rect = document.getElementById(id).getBoundingClientRect();
|
||||
return rect.left >= body.left && rect.right <= body.right && rect.bottom <= body.bottom;
|
||||
});
|
||||
});
|
||||
assert(controls.every(Boolean), "wrapped buttons and opacity fit inside the panel");
|
||||
const edited = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
await page.evaluate(() => {
|
||||
document.getElementById("selectionNotice").textContent += " Additional wrapped text. ".repeat(15);
|
||||
});
|
||||
await page.waitForFunction(previous => messages.filter(message => message.command === "resize").length > previous, resizes);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), edited, "content resize leaves the selection untouched");
|
||||
// The compact layout now puts saved swatches to the right of both previews.
|
||||
assert(await page.evaluate(() => {
|
||||
const favorites = document.getElementById("userColors").getBoundingClientRect();
|
||||
const preview = document.getElementById("colorPreview").getBoundingClientRect();
|
||||
return favorites.left >= preview.right;
|
||||
}), "favorites do not overlap the preview");
|
||||
await page.locator("#confirmBtn").scrollIntoViewIfNeeded();
|
||||
assert.equal(await page.locator("#confirmBtn").isVisible(), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("Native focus hook waits for initialization and preserves active partial edits", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.notEqual(await page.evaluate(() => document.activeElement.id), "hexInput");
|
||||
await send(init(ready, true, true));
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.equal(await page.evaluate(() => document.activeElement.id), "hexInput");
|
||||
await page.locator("#rValue").fill("");
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.equal(await page.evaluate(() => document.activeElement.id), "rValue");
|
||||
assert.equal(await page.locator("#rValue").inputValue(), "");
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
|
||||
assert.equal(await count(page, "confirm"), 0);
|
||||
await page.locator("#rValue").press("Escape");
|
||||
assert.equal(await count(page, "cancel"), 1);
|
||||
});
|
||||
@@ -0,0 +1,464 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
if (window.ORCA_COLOR_PICKER_LANGUAGE) document.documentElement.lang = window.ORCA_COLOR_PICKER_LANGUAGE;
|
||||
const T = (key) => window.ORCA_UI_STRINGS?.[key] || key;
|
||||
document.title = T("Color Picker");
|
||||
for (const element of document.querySelectorAll("[data-i18n]")) element.textContent = T(element.dataset.i18n);
|
||||
for (const attribute of ["aria-label", "title", "placeholder"])
|
||||
for (const element of document.querySelectorAll(`[data-i18n-${attribute}]`))
|
||||
element.setAttribute(attribute, T(element.getAttribute(`data-i18n-${attribute}`)));
|
||||
function paletteName(name) {
|
||||
const ral = name.match(/^(\d{4}) (.+)$/);
|
||||
return ral ? `${ral[1]} ${T(ral[2])}` : /^%\d+$/.test(name) ? name : T(name);
|
||||
}
|
||||
|
||||
const FAVORITE_SLOTS = 16;
|
||||
const SPECTRUM_FOLLOWS_SATURATION = true; // true: the spectrum is drawn at the current saturation, false: it always shows full saturation
|
||||
const MULTI_COLOR_GRADIENTS = true; // true: gradients may have more than 2 colors (− / + buttons and the multi-color palette entries), false: exactly 2 colors.
|
||||
// up to the native limit the host sends as options.max_gradient_colors.
|
||||
let maxGradientColors = 2; // Stays 2 until init, and with a host that does not send a limit.
|
||||
|
||||
const model = window.ColorPickerModel;
|
||||
const palettes = window.ColorPickerPalettes;
|
||||
const byId = (id) => document.getElementById(id);
|
||||
const canvas = byId("spectrum"), context = canvas.getContext("2d");
|
||||
const paletteCombo = byId("paletteCombo"), palette = byId("palette"), preview = byId("colorPreview"), confirmBtn = byId("confirmBtn");
|
||||
const modeSwitch = byId("modeSwitch"), satSlider = byId("satSlider"), opacitySlider = byId("opacitySlider");
|
||||
const gradientBar = byId("gradientCtrlBar"), gradientStops = byId("gradientStops"), gradientAdd = byId("gradientAdd"), gradientRemove = byId("gradientRemove");
|
||||
const hostGradientLimit = (options) => options.max_gradient_colors === undefined ? 2 : options.max_gradient_colors;
|
||||
const originalPreview = byId("originalPreview"), palettePanel = byId("palettePanel"), paletteToggle = byId("paletteToggle");
|
||||
const paletteSelect = byId("paletteSelect"), paletteFilter = byId("paletteFilter"), paletteSearchToggle = byId("paletteSearchToggle");
|
||||
const rgbInputs = ["r", "g", "b"].map((name) => byId(name + "Value"));
|
||||
const hslInputs = ["h", "s", "l"].map((name) => byId(name + "Value"));
|
||||
const hexInput = byId("hexInput"), opacityInput = byId("oValue");
|
||||
const pageId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
||||
let state = model.createState(), favorites = [], initialized = false, favoritesWritable = true;
|
||||
// The selection the dialog opened with; the original swatch shows it and restores it on click.
|
||||
let originalSelection = model.exportSelection(state);
|
||||
let lastContentHeight = 0, lastContentWidth = 0, resizePending = false;
|
||||
// Last HSL shown or set. Kept while it still produces the current color: hue 360 and 0 are
|
||||
// both red, and greys, black and white have no hue or saturation in RGB.
|
||||
let hslMemory = [0, 0, 50];
|
||||
function displayHsl(color) {
|
||||
const rgb = model.hslToRgb(...hslMemory);
|
||||
if (!rgb.every((value, index) => value === color[index])) hslMemory = model.rgbToHsl(...color.slice(0, 3));
|
||||
return hslMemory;
|
||||
}
|
||||
// Copy of `values` with one entry replaced.
|
||||
const replaced = (values, index, value) => values.map((current, i) => i === index ? value : current);
|
||||
function setHslChannel(index, value) {
|
||||
hslMemory = replaced(displayHsl(model.activeColor(state)), index, value);
|
||||
model.setRgb(state, model.hslToRgb(...hslMemory));
|
||||
}
|
||||
const round1 = (value) => Math.round(value * 10) / 10;
|
||||
|
||||
function emit(command, data = {}) {
|
||||
window.dispatchEvent(new CustomEvent("color-picker-message", {detail: {command, ...data}}));
|
||||
if (window.wx && typeof window.wx.postMessage === "function")
|
||||
window.wx.postMessage(JSON.stringify({command, page_id: pageId, ...data}));
|
||||
}
|
||||
|
||||
function scheduleResize() {
|
||||
if (!initialized || resizePending) return;
|
||||
resizePending = true;
|
||||
requestAnimationFrame(() => {
|
||||
resizePending = false;
|
||||
const rect = document.body.getBoundingClientRect();
|
||||
const height = Math.min(900 , Math.max(300, Math.ceil(rect.height)));
|
||||
const width = Math.min(1200, Math.max(300, Math.ceil(rect.width )));
|
||||
if (height !== lastContentHeight || width !== lastContentWidth) {
|
||||
lastContentHeight = height; lastContentWidth = width;
|
||||
emit("resize", {width, height});
|
||||
}
|
||||
});
|
||||
}
|
||||
new ResizeObserver(scheduleResize).observe(document.body);
|
||||
|
||||
function setSwatch(element, selection) {
|
||||
element.style.setProperty("--swatch-color", model.selectionBackground(selection));
|
||||
}
|
||||
|
||||
// `editing` is the control the user is typing in. Its text is left alone so the caret,
|
||||
// partial input like "05" and fine steps that round-trip through bytes aren't overwritten.
|
||||
function render(resetEditors = false, editing = null) {
|
||||
const color = model.activeColor(state), hsl = displayHsl(color);
|
||||
const show = (input, value) => {
|
||||
if (input === editing) return;
|
||||
if (resetEditors) input.setCustomValidity("");
|
||||
if (resetEditors || input.checkValidity()) input.value = value;
|
||||
};
|
||||
show(hexInput, model.toHex(color).slice(1, state.options.allow_alpha ? 9 : 7));
|
||||
rgbInputs.forEach((input, index) => show(input, color[index]));
|
||||
// Shown in whole numbers; decimals can still be typed, and hslMemory keeps exact values.
|
||||
hslInputs.forEach((input, index) => show(input, Math.round(hsl[index])));
|
||||
// Percentage is a display only until explicitly edited. Never round it back
|
||||
// into the alpha byte on endpoint switches, RGB edits, or confirmation.
|
||||
show(opacityInput, Math.round(color[3] / 255 * 100));
|
||||
if (opacitySlider !== editing) opacitySlider.value = color[3] / 255 * 100;
|
||||
const rgb = color.slice(0, 3).join(",");
|
||||
opacitySlider.style.setProperty("--track-gradient", `linear-gradient(to right, rgba(${rgb},0), rgb(${rgb}))`);
|
||||
opacitySlider.style.setProperty("--thumb-color", `rgba(${rgb},${color[3] / 255})`);
|
||||
if (satSlider !== editing) satSlider.value = hsl[1];
|
||||
satSlider.style.setProperty("--track-gradient", `linear-gradient(to right, hsl(${hsl[0]},0%,${hsl[2]}%), hsl(${hsl[0]},100%,${hsl[2]}%))`);
|
||||
satSlider.style.setProperty("--thumb-color", `hsl(${hsl[0]},${hsl[1]}%,${hsl[2]}%)`);
|
||||
modeSwitch.checked = state.mode === "gradient";
|
||||
const selection = model.exportSelection(state);
|
||||
// Kept in the layout (visibility only) so the grid doesn't shift between Solid and Gradient.
|
||||
gradientBar.classList.toggle("invisible", state.mode !== "gradient");
|
||||
if (state.mode === "gradient") renderGradient(selection);
|
||||
setSwatch(preview, selection);
|
||||
preview.title = `${T("Save current color")}\n${selection.colors.join(" → ")}`;
|
||||
if (SPECTRUM_FOLLOWS_SATURATION && hsl[1] !== spectrumSaturation) { spectrumSaturation = hsl[1]; paintSpectrum(); }
|
||||
updateIndicator(hsl);
|
||||
updateConfirm();
|
||||
}
|
||||
|
||||
// One thumb per gradient color, evenly spaced; rebuilt only when the number of colors changes.
|
||||
function renderGradient(selection) {
|
||||
const count = selection.colors.length;
|
||||
setSwatch(gradientStops.querySelector(".gradient-track"), selection);
|
||||
let thumbs = [...gradientStops.querySelectorAll(".gradient-thumb")];
|
||||
if (thumbs.length !== count) {
|
||||
thumbs.forEach((thumb) => thumb.remove());
|
||||
thumbs = selection.colors.map((_, index) => {
|
||||
const thumb = document.createElement("button");
|
||||
thumb.type = "button"; thumb.className = "gradient-thumb"; thumb.dataset.index = index;
|
||||
thumb.style.setProperty("--stop", index / (count - 1));
|
||||
thumb.setAttribute("aria-label", count === 2 ? T(index ? "Gradient end" : "Gradient start") : `${T("Gradient color")} ${index + 1}`);
|
||||
gradientStops.appendChild(thumb);
|
||||
return thumb;
|
||||
});
|
||||
}
|
||||
thumbs.forEach((thumb, index) => {
|
||||
thumb.style.setProperty("--thumb-color", selection.colors[index]);
|
||||
thumb.setAttribute("aria-pressed", String(state.active === index));
|
||||
});
|
||||
|
||||
const multi = maxGradientColors > 2; // −/+ buttons only exist when more than 2 colors are allowed; − also needs a color to spare.
|
||||
gradientAdd.parentElement.classList.toggle("hidden", !multi);
|
||||
gradientRemove.classList.toggle("hidden", !multi || count <= 2);
|
||||
gradientAdd.classList.toggle("hidden", !multi);
|
||||
// aria-disabled instead of disabled: a disabled button gets no hover, so its tooltip would never show.
|
||||
const full = count >= maxGradientColors;
|
||||
gradientAdd.setAttribute("aria-disabled", String(full));
|
||||
gradientAdd.title = full ? T("Up to %d colors supported.").replace("%d", maxGradientColors) : T("Add color");
|
||||
}
|
||||
|
||||
// Every editor the user can see; alpha only when this caller allows it.
|
||||
function editors() {
|
||||
const inputs = [hexInput, ...rgbInputs, ...hslInputs];
|
||||
if (state.options.allow_alpha) inputs.push(opacityInput);
|
||||
return inputs;
|
||||
}
|
||||
// OK stays disabled while any visible editor holds an invalid or incomplete value.
|
||||
function updateConfirm() {
|
||||
confirmBtn.disabled = !editors().every((input) => input.checkValidity());
|
||||
}
|
||||
|
||||
// Matches the filter against the shown (translated) name, the English name and the hex digits.
|
||||
function paletteMatches(entry, query) {
|
||||
if (!query) return true;
|
||||
const haystack = [paletteName(entry.name), entry.name, entry.hex || "", entry.grad || ""].join(" ").toLowerCase();
|
||||
return query.split(/\s+/).every((word) => haystack.includes(word.replace(/^#/, "")));
|
||||
}
|
||||
|
||||
function populatePalette() {
|
||||
palette.replaceChildren();
|
||||
const query = paletteSearchToggle.getAttribute("aria-pressed") === "true" ? paletteFilter.value.trim().toLowerCase() : "";
|
||||
for (const entry of palettes[paletteCombo.value] || []) {
|
||||
const selection = {type: entry.grad ? "gradient" : "solid", colors: entry.grad ? entry.grad.split("-") : [entry.hex]};
|
||||
if (!model.normalizeSelection(selection, state.options, maxGradientColors) || !paletteMatches(entry, query)) continue;
|
||||
const row = document.createElement("button"); row.type = "button"; row.className = "color-item";
|
||||
const swatch = document.createElement("span"); swatch.className = "color-swatch";
|
||||
const name = document.createElement("span"); name.className = "color-name"; name.textContent = paletteName(entry.name);
|
||||
setSwatch(swatch, model.normalizeSelection(selection, {allow_gradient: true, allow_alpha: true}, maxGradientColors));
|
||||
row.append(swatch, name); row.title = paletteName(entry.name);
|
||||
row.addEventListener("click", () => {
|
||||
// Solid palette colors edit the active endpoint while gradient mode is on.
|
||||
const changed = entry.grad ? model.applySelection(state, selection) : model.setHex(state, entry.hex);
|
||||
if (changed) render(true);
|
||||
});
|
||||
palette.appendChild(row);
|
||||
}
|
||||
byId("paletteEmpty").classList.toggle("hidden", palette.childElementCount > 0);
|
||||
palette.classList.toggle("hidden", palette.childElementCount === 0);
|
||||
}
|
||||
|
||||
// Sidebar state the host keeps in the app config: open/closed and the chosen palette.
|
||||
const uiState = () => ({sidebar_open: palettePanel.classList.contains("open"), palette: paletteCombo.value});
|
||||
function setSidebarOpen(open) {
|
||||
palettePanel.classList.toggle("open", open);
|
||||
paletteToggle.setAttribute("aria-expanded", String(open));
|
||||
}
|
||||
// The search button swaps the combo box for a filter box; closing it clears the filter.
|
||||
function setPaletteSearch(open) {
|
||||
paletteSearchToggle.setAttribute("aria-pressed", String(open));
|
||||
paletteSelect.classList.toggle("hidden", open);
|
||||
paletteFilter.classList.toggle("hidden", !open);
|
||||
if (open) paletteFilter.focus();
|
||||
else paletteFilter.value = "";
|
||||
populatePalette();
|
||||
}
|
||||
|
||||
function drawFavorites() {
|
||||
const grid = byId("userColors"); grid.replaceChildren();
|
||||
// Favorites keep every color, but only those this page can select are shown.
|
||||
const visible = favorites.filter((favorite) => model.normalizeSelection(favorite, {...state.options, allow_alpha: true}, maxGradientColors));
|
||||
for (let index = 0; index < FAVORITE_SLOTS; ++index) {
|
||||
const swatch = document.createElement("button"); swatch.type = "button"; swatch.className = "saved-swatch";
|
||||
const favorite = visible[index];
|
||||
if (favorite) {
|
||||
setSwatch(swatch, favorite); swatch.title = favorite.colors.join(" → ");
|
||||
swatch.addEventListener("click", () => { if (model.applySelection(state, favorite)) render(true); });
|
||||
} else { swatch.classList.add("empty"); swatch.disabled = true; }
|
||||
grid.appendChild(swatch);
|
||||
}
|
||||
}
|
||||
|
||||
function init(payload) {
|
||||
if (!payload || typeof payload !== "object" || (payload.options !== undefined && (!payload.options || typeof payload.options !== "object"))) return false;
|
||||
maxGradientColors = Math.min(MULTI_COLOR_GRADIENTS ? model.MAX_GRADIENT_COLORS : 2, hostGradientLimit(payload.options || {}));
|
||||
// A gradient the host allows but this page does not (switch off) opens as its first color, like
|
||||
// other multi-color selections the picker cannot edit; OK then replaces it, Cancel keeps it.
|
||||
let selection = payload.selection, preserve = payload.preserve_multi_color === true;
|
||||
if (selection?.type === "gradient" && selection.colors?.length > maxGradientColors) {
|
||||
selection = {type: "solid", colors: [selection.colors[0]]};
|
||||
preserve = true;
|
||||
}
|
||||
const next = model.createState({...payload.options, max_gradient_colors: maxGradientColors}, selection);
|
||||
if (!next) return false;
|
||||
byId("selectionNotice").hidden = !preserve;
|
||||
state = next; favorites = model.normalizeFavorites(payload.favorites || []);
|
||||
originalSelection = model.exportSelection(state);
|
||||
setSwatch(originalPreview, originalSelection);
|
||||
originalPreview.title = `${T("Original color")}\n${originalSelection.colors.join(" → ")}`;
|
||||
favoritesWritable = payload.favorites_writable !== false;
|
||||
preview.disabled = !favoritesWritable;
|
||||
byId("modeControls").classList.toggle("hidden", !state.options.allow_gradient);
|
||||
for (const option of paletteCombo.options) option.hidden = option.value === "gradient" && !state.options.allow_gradient;
|
||||
const ui = payload.ui || {}; // Restore the sidebar the way it was left; a palette this caller can't use falls back to basic.
|
||||
const saved = [...paletteCombo.options].find((option) => option.value === ui.palette && !option.hidden);
|
||||
if (saved) paletteCombo.value = saved.value;
|
||||
if (!state.options.allow_gradient && paletteCombo.value === "gradient") paletteCombo.value = "basic";
|
||||
setSidebarOpen(ui.sidebar_open === true);
|
||||
byId("alphaControls").classList.toggle("hidden", !state.options.allow_alpha);
|
||||
hexInput.maxLength = state.options.allow_alpha ? 9 : 7;
|
||||
populatePalette(); drawFavorites(); render(true);
|
||||
initialized = true;
|
||||
byId("pickerEditors").disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
if (!initialized) return;
|
||||
// Restore initial keyboard focus without replacing an editor's in-progress input.
|
||||
const active = document.activeElement;
|
||||
if (!active || active === document.body || active === document.documentElement)
|
||||
hexInput.focus({preventScroll: true});
|
||||
}
|
||||
|
||||
function handleMessage(payload) {
|
||||
if (!payload || typeof payload !== "object" || payload.page_id !== pageId) return false;
|
||||
if (payload.command === "init") {
|
||||
const options = payload.options;
|
||||
const hostLimit = options && hostGradientLimit(options);
|
||||
if (!options || typeof options.allow_gradient !== "boolean" || typeof options.allow_alpha !== "boolean" ||
|
||||
!Number.isInteger(hostLimit) || hostLimit < 2 || hostLimit > model.MAX_GRADIENT_COLORS ||
|
||||
!model.normalizeSelection(payload.selection, options, hostLimit) || !validFavorites(payload.favorites) ||
|
||||
(payload.favorites_writable !== undefined && typeof payload.favorites_writable !== "boolean") ||
|
||||
(payload.preserve_multi_color !== undefined && typeof payload.preserve_multi_color !== "boolean") ||
|
||||
(payload.ui !== undefined && (!payload.ui || typeof payload.ui !== "object" ||
|
||||
(payload.ui.sidebar_open !== undefined && typeof payload.ui.sidebar_open !== "boolean") ||
|
||||
(payload.ui.palette !== undefined && typeof payload.ui.palette !== "string")))) return false;
|
||||
if (initialized) return true;
|
||||
if (!init(payload)) return false;
|
||||
emit("initialized");
|
||||
scheduleResize();
|
||||
return true;
|
||||
}
|
||||
if (payload.command === "favorites" && initialized && validFavorites(payload.favorites)) {
|
||||
favorites = model.normalizeFavorites(payload.favorites); drawFavorites(); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function validFavorites(values) {
|
||||
return Array.isArray(values) && values.length <= 24 && values.every((value) => model.normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, model.MAX_GRADIENT_COLORS));
|
||||
}
|
||||
|
||||
// Saturation the spectrum is currently painted at (always 100 when the option is off).
|
||||
let spectrumSaturation = 100;
|
||||
function drawSpectrum() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.max(1, Math.round(rect.width)); canvas.height = Math.max(1, Math.round(rect.height));
|
||||
paintSpectrum();
|
||||
}
|
||||
function paintSpectrum() {
|
||||
const hue = context.createLinearGradient(0, 0, canvas.width, 0);
|
||||
for (let i = 0; i <= 6; ++i) hue.addColorStop(i / 6, `hsl(${i * 60},100%,50%)`);
|
||||
context.fillStyle = hue; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const shade = context.createLinearGradient(0, 0, 0, canvas.height);
|
||||
shade.addColorStop(0, "#FFFFFF"); shade.addColorStop(0.5, "#FFFFFF00"); shade.addColorStop(0.5, "#00000000"); shade.addColorStop(1, "#000000");
|
||||
context.fillStyle = shade; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
// HSL desaturation blends each color toward the grey of the same lightness, and on this
|
||||
// canvas that grey only depends on the row (white at the top, black at the bottom).
|
||||
if (spectrumSaturation < 100) {
|
||||
const grey = context.createLinearGradient(0, 0, 0, canvas.height);
|
||||
grey.addColorStop(0, "#FFFFFF"); grey.addColorStop(1, "#000000");
|
||||
context.globalAlpha = 1 - spectrumSaturation / 100;
|
||||
context.fillStyle = grey; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const indicator = byId("colorIndicator");
|
||||
function updateIndicator(hsl) {
|
||||
indicator.style.left = hsl[0] / 360 * (canvas.width - 1) + "px";
|
||||
indicator.style.top = (1 - hsl[2] / 100) * (canvas.height - 1) + "px";
|
||||
}
|
||||
|
||||
// Spectrum position maps straight to hue (x, 0..360) and lightness (y, 100..0); saturation is
|
||||
// the third dimension and comes from the slider below. hslMemory keeps these exact values,
|
||||
// including hue 360 at the right edge and the hue of the white and black corners.
|
||||
function sampleSpectrum(event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const fraction = (client, start, size, pixels) =>
|
||||
Math.max(0, Math.min(pixels - 1, Math.floor((client - start) * pixels / size))) / Math.max(1, pixels - 1);
|
||||
const saturation = displayHsl(model.activeColor(state))[1];
|
||||
hslMemory = [round1(fraction(event.clientX, rect.left, rect.width, canvas.width) * 360), saturation,
|
||||
round1((1 - fraction(event.clientY, rect.top, rect.height, canvas.height)) * 100)];
|
||||
model.setRgb(state, model.hslToRgb(...hslMemory));
|
||||
// A pick replaces every editor's value, so clear any invalid state left from typing.
|
||||
render(true);
|
||||
}
|
||||
|
||||
const clamp = (control, value) => Math.min(Number(control.max), Math.max(Number(control.min), value));
|
||||
function bindValue(control, apply) {
|
||||
// Number fields are required, so an empty field is invalid; a range input always has a value.
|
||||
if (control.type === "number") control.required = true;
|
||||
const commit = (value) => { apply(value); render(false, control); };
|
||||
control.addEventListener("input", () => {
|
||||
control.setCustomValidity("");
|
||||
// Out-of-range numbers snap to the field's min/max instead of becoming invalid.
|
||||
if (control.value !== "") {
|
||||
const clamped = clamp(control, Number(control.value));
|
||||
if (clamped !== Number(control.value)) control.value = clamped;
|
||||
}
|
||||
if (control.checkValidity()) commit(Number(control.value));
|
||||
});
|
||||
control.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
const step = control.type === "range" ? 5 : 1;
|
||||
control.value = clamp(control, (Number(control.value) || 0) + (event.deltaY > 0 ? -step : step));
|
||||
control.setCustomValidity("");
|
||||
commit(Number(control.value));
|
||||
}, {passive: false});
|
||||
// Normalize the text (e.g. "05" → "5") once the user leaves the field.
|
||||
control.addEventListener("blur", () => { if (control.checkValidity()) render(); });
|
||||
}
|
||||
|
||||
rgbInputs.forEach((input, index) => bindValue(input, (value) => model.setRgb(state, replaced(model.activeColor(state).slice(0, 3), index, value))));
|
||||
hslInputs.forEach((input, index) => bindValue(input, (value) => setHslChannel(index, value)));
|
||||
bindValue(satSlider, (value) => setHslChannel(1, value));
|
||||
bindValue(opacityInput, (value) => model.setAlphaPercent(state, value));
|
||||
bindValue(opacitySlider, (value) => model.setAlphaPercent(state, value));
|
||||
// The opacity slider takes the gradient bar's row while the opacity field or the slider is in
|
||||
// use: it opens when the field is clicked or focused, and closes on a click or focus elsewhere.
|
||||
const thirdRow = byId("thirdRow"), alphaControls = byId("alphaControls");
|
||||
const inOpacityControls = (target) => target instanceof Element && target.closest("#alphaControls, #opacitySliderBar");
|
||||
const setOpacitySliderOpen = (open) => thirdRow.classList.toggle("opacity-open", open);
|
||||
for (const type of ["focusin", "click"]) alphaControls.addEventListener(type, () => setOpacitySliderOpen(true));
|
||||
for (const type of ["pointerdown", "focusin"])
|
||||
document.addEventListener(type, (event) => { if (!inOpacityControls(event.target)) setOpacitySliderOpen(false); });
|
||||
hexInput.addEventListener("input", () => {
|
||||
const text = hexInput.value;
|
||||
const valid = model.setHex(state, text.startsWith("#") ? text : "#" + text);
|
||||
hexInput.setCustomValidity(valid ? "" : T("Enter a complete hexadecimal color"));
|
||||
// render() leaves the typed text alone, so alpha isn't appended after the sixth digit.
|
||||
if (valid) render(false, hexInput);
|
||||
});
|
||||
hexInput.addEventListener("blur", () => { if (hexInput.checkValidity()) render(); });
|
||||
modeSwitch.addEventListener("change", () => {
|
||||
|
||||
setOpacitySliderOpen(false); // Close the opacity slider before the row changes, so it doesn't linger over the gradient bar.
|
||||
model.setMode(state, modeSwitch.checked ? "gradient" : "solid");
|
||||
render(true);
|
||||
});
|
||||
// Click on the track selects the nearest thumb. Keyboard users tab to a thumb and press Enter or Space.
|
||||
gradientStops.addEventListener("click", (event) => {
|
||||
if (state.mode !== "gradient") return;
|
||||
// A thumb is read from its index: a keyboard-triggered click has no pointer position.
|
||||
const thumb = event.target.closest(".gradient-thumb"), rect = gradientStops.getBoundingClientRect();
|
||||
const last = state.gradient.length - 1;
|
||||
// Thumbs sit at 10px + (width - 20px) * i / last, matching the CSS.
|
||||
const fraction = Math.max(0, Math.min(1, (event.clientX - rect.left - 10) / Math.max(1, rect.width - 20)));
|
||||
const index = thumb ? Number(thumb.dataset.index) : Math.round(fraction * last);
|
||||
if (index !== state.active) { state.active = index; render(true); }
|
||||
});
|
||||
gradientAdd.addEventListener("click", () => { if (gradientAdd.getAttribute("aria-disabled") !== "true" && model.addGradientColor(state)) render(true); });
|
||||
gradientRemove.addEventListener("click", () => { if (model.removeGradientColor(state)) render(true); });
|
||||
originalPreview.addEventListener("click", () => { if (model.applySelection(state, originalSelection)) render(true); });
|
||||
paletteToggle.addEventListener("click", () => {
|
||||
setSidebarOpen(!palettePanel.classList.contains("open"));
|
||||
if (initialized) emit("update_ui", uiState());
|
||||
});
|
||||
paletteCombo.addEventListener("change", () => {
|
||||
populatePalette();
|
||||
if (initialized) emit("update_ui", uiState());
|
||||
});
|
||||
paletteSearchToggle.addEventListener("click", () => setPaletteSearch(paletteSearchToggle.getAttribute("aria-pressed") !== "true"));
|
||||
paletteFilter.addEventListener("input", populatePalette);
|
||||
// Pointer capture ends by itself on pointerup/pointercancel, so it doubles as the drag state.
|
||||
canvas.addEventListener("pointerdown", (event) => { canvas.setPointerCapture(event.pointerId); sampleSpectrum(event); });
|
||||
canvas.addEventListener("pointermove", (event) => { if (canvas.hasPointerCapture(event.pointerId)) sampleSpectrum(event); });
|
||||
// The spectrum takes the panel's free height, which also changes with the notice, alpha and
|
||||
// gradient controls, so redraw on any size change rather than on window resize only.
|
||||
new ResizeObserver(() => { drawSpectrum(); updateIndicator(displayHsl(model.activeColor(state))); }).observe(canvas.parentElement);
|
||||
// Bubble phase on document runs after each editor's own handler has set its validity.
|
||||
document.addEventListener("input", updateConfirm);
|
||||
// Hex keeps its own message from its input handler; number fields get a translated one.
|
||||
function validateEditors() {
|
||||
const inputs = editors();
|
||||
for (const input of inputs.filter((input) => input !== hexInput)) {
|
||||
input.setCustomValidity("");
|
||||
const {valid, rangeUnderflow, rangeOverflow} = input.validity;
|
||||
if (!valid) input.setCustomValidity(T(rangeUnderflow || rangeOverflow ? "Value is out of range." : "Invalid input"));
|
||||
}
|
||||
return inputs.every((input) => input.reportValidity());
|
||||
}
|
||||
function confirm() {
|
||||
if (initialized && validateEditors()) emit("confirm", {selection: model.exportSelection(state)});
|
||||
}
|
||||
preview.addEventListener("click", () => {
|
||||
if (!initialized || !favoritesWritable || !validateEditors()) return;
|
||||
const selection = model.exportSelection(state);
|
||||
const key = JSON.stringify(selection);
|
||||
favorites = [selection, ...favorites.filter((favorite) => JSON.stringify(favorite) !== key)].slice(0, FAVORITE_SLOTS);
|
||||
drawFavorites(); emit("update_favorites", {favorites});
|
||||
});
|
||||
confirmBtn.addEventListener("click", confirm);
|
||||
byId("cancelBtn").addEventListener("click", () => emit("cancel"));
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// Escape in the filter box closes the search first; a second Escape cancels the dialog.
|
||||
if (event.key === "Escape" && event.target === paletteFilter) { event.preventDefault(); setPaletteSearch(false); paletteSearchToggle.focus(); }
|
||||
else if (event.key === "Escape") { event.preventDefault(); emit("cancel"); }
|
||||
else if (event.key === "Enter" && event.target instanceof HTMLInputElement && ["text", "number"].includes(event.target.type)) {
|
||||
event.preventDefault(); confirm();
|
||||
}
|
||||
});
|
||||
|
||||
window.ColorPickerDialog = {
|
||||
init, handleMessage, focusInput, exportSelection: () => model.exportSelection(state),
|
||||
setFavorites: (values) => { favorites = model.normalizeFavorites(values); drawFavorites(); }
|
||||
};
|
||||
// The host passes the saved sidebar state before the page loads (the same values come again in
|
||||
// init), so the first paint already matches the window size the host opened with.
|
||||
const earlyUi = window.ORCA_COLOR_PICKER_UI;
|
||||
if (earlyUi && typeof earlyUi === "object") {
|
||||
if (typeof earlyUi.palette === "string" && [...paletteCombo.options].some((option) => option.value === earlyUi.palette))
|
||||
paletteCombo.value = earlyUi.palette;
|
||||
if (typeof earlyUi.sidebar_open === "boolean") setSidebarOpen(earlyUi.sidebar_open);
|
||||
}
|
||||
setSwatch(originalPreview, originalSelection);
|
||||
drawSpectrum(); populatePalette(); drawFavorites(); render(true); emit("ready");
|
||||
})();
|
||||
@@ -0,0 +1,103 @@
|
||||
"use strict";
|
||||
const {test} = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const model = require("./colorpicker-model.js");
|
||||
|
||||
test("complete RGB/RGBA hex accepts mixed case and rejects partial or unbounded input", () => {
|
||||
assert.deepEqual(model.parseHex("#Ab12cD80"), [171,18,205,128]);
|
||||
for (const hex of ["", "123456", "#123", "#1234567", "#123456789", "#12345Z", "#123456GG", " #123456", null]) assert.equal(model.parseHex(hex), null);
|
||||
for (let byte = 0; byte <= 255; ++byte) {
|
||||
const hex = model.toHex([byte, byte, byte, byte]);
|
||||
assert.deepEqual(model.parseHex(hex), [byte,byte,byte,byte]);
|
||||
}
|
||||
});
|
||||
|
||||
test("HSL conversion preserves primary channels and hue wraps at 360", () => {
|
||||
for (const rgb of [[255,0,0], [0,255,0], [0,0,255], [128,128,128], [37,129,240]]) assert.deepEqual(model.hslToRgb(...model.rgbToHsl(...rgb)), rgb);
|
||||
assert.deepEqual(model.hslToRgb(360,100,50), [255,0,0]);
|
||||
assert.deepEqual(model.hslToRgb(240,100,50), [0,0,255]);
|
||||
});
|
||||
|
||||
test("capabilities default off and reject gradient through every model entry", () => {
|
||||
const gradient = {type: "gradient", colors: ["#FF000080", "#0000FF20"]};
|
||||
const state = model.createState();
|
||||
assert.deepEqual(state.options, {allow_gradient: false, allow_alpha: false});
|
||||
assert.equal(model.createState({}, gradient), null);
|
||||
assert.equal(model.applySelection(state, gradient), false);
|
||||
assert.equal(model.setMode(state, "gradient"), false);
|
||||
assert.equal(model.setAlphaPercent(state, 20), false);
|
||||
assert.equal(model.createState({allow_alpha: true}, gradient), null);
|
||||
const opaque = model.createState({allow_gradient: true}, gradient);
|
||||
assert.deepEqual(model.exportSelection(opaque), {type: "gradient", colors: ["#FF0000FF", "#0000FFFF"]});
|
||||
});
|
||||
|
||||
test("gradient endpoint edits keep order and preserve unedited byte alpha", () => {
|
||||
const initial = {type: "gradient", colors: ["#FF000001", "#0000FF80"]};
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true}, initial);
|
||||
state.active = 0; model.setRgb(state, [0,255,0]);
|
||||
state.active = 1; model.setHex(state, "#112233");
|
||||
assert.deepEqual(model.exportSelection(state), {type: "gradient", colors: ["#00FF0001", "#11223380"]});
|
||||
model.setAlphaPercent(state, 0);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#00FF0001", "#11223300"]);
|
||||
model.setAlphaPercent(state, 100);
|
||||
assert.equal(model.exportSelection(state).colors[1], "#112233FF");
|
||||
assert.deepEqual(initial.colors, ["#FF000001", "#0000FF80"]);
|
||||
});
|
||||
|
||||
test("favorites retain alpha and gradient even when the selection disables them", () => {
|
||||
const favorite = {type: "solid", colors: ["#ABCDEF80"]};
|
||||
const gradient = {type: "gradient", colors: ["#FFFFFF00", "#00000010"]};
|
||||
const favorites = model.normalizeFavorites([favorite, gradient]);
|
||||
const state = model.createState();
|
||||
assert.equal(model.applySelection(state, favorites[0]), true);
|
||||
assert.deepEqual(model.exportSelection(state), {type: "solid", colors: ["#ABCDEFFF"]});
|
||||
assert.deepEqual(favorites, [favorite, gradient]);
|
||||
assert.equal(model.applySelection(state, favorites[1]), false);
|
||||
assert.equal(model.normalizeFavorites(Array(25).fill(favorite)).length, 24);
|
||||
});
|
||||
|
||||
test("invalid cardinality and channel edits cannot change a selection", () => {
|
||||
const state = model.createState({allow_alpha: true});
|
||||
const before = model.exportSelection(state);
|
||||
for (const value of [{type: "solid", colors: []}, {type: "solid", colors: ["#112233", "#445566"]}, {type: "gradient", colors: ["#112233"]}, {type: "other", colors: ["#112233"]}]) assert.equal(model.applySelection(state, value), false);
|
||||
assert.equal(model.setHex(state, "#GG2233"), false);
|
||||
for (const value of [-1, 101, NaN, Infinity]) assert.equal(model.setAlphaPercent(state, value), false);
|
||||
assert.equal(model.setRgb(state, [256,0,0]), false);
|
||||
assert.deepEqual(model.exportSelection(state), before);
|
||||
});
|
||||
|
||||
test("multi-color gradients stay at 2 colors unless the caller raises the limit", () => {
|
||||
const five = {type: "gradient", colors: ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF"]};
|
||||
assert.equal(model.normalizeSelection(five, {allow_gradient: true}), null);
|
||||
assert.equal(model.createState({allow_gradient: true}, five), null);
|
||||
const state = model.createState({allow_gradient: true, max_gradient_colors: 7}, five);
|
||||
assert.equal(state.gradient.length, 5);
|
||||
assert.equal(model.normalizeSelection({...five, colors: Array(8).fill("#000000")}, {allow_gradient: true}, 7), null);
|
||||
assert.equal(model.createState({allow_gradient: true, max_gradient_colors: 99}).maxGradientColors, model.MAX_GRADIENT_COLORS);
|
||||
assert.equal(model.normalizeFavorites([five]).length, 1, "favorites keep every color");
|
||||
});
|
||||
|
||||
test("adding and removing gradient colors keeps order, selection and the 2..max limits", () => {
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true, max_gradient_colors: 4}, {type: "gradient", colors: ["#FF000080", "#0000FFFF"]});
|
||||
assert.equal(model.removeGradientColor(state), false, "never fewer than 2");
|
||||
assert.equal(model.addGradientColor(state), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080", "#0000FFFF"], "selected color turned 75° in hue, alpha kept");
|
||||
assert.equal(model.NEW_GRADIENT_HUE_SHIFT, 75);
|
||||
assert.equal(state.active, 1);
|
||||
state.active = 2; assert.equal(model.addGradientColor(state), true);
|
||||
assert.equal(state.active, 3); assert.equal(state.gradient.length, 4);
|
||||
assert.equal(model.addGradientColor(state), false, "limit reached");
|
||||
state.active = 1; assert.equal(model.removeGradientColor(state), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors.slice(0, 2), ["#FF000080", "#0000FFFF"]);
|
||||
assert.equal(state.active, 1);
|
||||
assert.equal(model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]}), true);
|
||||
state.active = 1; model.addGradientColor(state); model.addGradientColor(state);
|
||||
model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]});
|
||||
assert.equal(state.active, 1, "a shorter gradient clamps the selected color");
|
||||
});
|
||||
|
||||
test("a new gradient starts at the solid color and its 75° hue neighbour", () => {
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true}, {type: "solid", colors: ["#FF000080"]});
|
||||
assert.equal(model.setMode(state, "gradient"), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080"]);
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-orca-page="color-picker">
|
||||
<head><title data-i18n="Color Picker">Color Picker</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../include/global.css">
|
||||
<link rel="stylesheet" href="../css/theme.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<p id="selectionNotice" hidden data-i18n="The current multi-color selection is kept until you choose a different color.">The current multi-color selection is kept until you choose a different color.</p>
|
||||
<fieldset id="pickerEditors" disabled>
|
||||
<div class="main-wrapper">
|
||||
|
||||
<div class="left-panel" id="palettePanel">
|
||||
<div class="palette thin-scroll" id="palette" tabindex="-1"></div>
|
||||
<p class="palette-empty hidden" id="paletteEmpty" data-i18n="No matching colors">No matching colors</p>
|
||||
<div class="palette-toolbar">
|
||||
<div class="palette-select ComboBox" id="paletteSelect">
|
||||
<div class="arrow-icon"></div>
|
||||
<select id="paletteCombo" tabindex="-1">
|
||||
<option value="basic" data-i18n="Basic Colors">Basic Colors</option>
|
||||
<option value="orca" data-i18n="Orca Palette">Orca Palette</option>
|
||||
<option value="ral" data-i18n="RAL Classic" >RAL Classic</option>
|
||||
<option value="gradient" hidden data-i18n="Gradients" >Gradients</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="search" id="paletteFilter" class="palette-filter hidden" autocomplete="off" spellcheck="false"
|
||||
placeholder="Filter colors" data-i18n-placeholder="Filter colors" aria-label="Filter colors" data-i18n-aria-label="Filter colors">
|
||||
<button type="button" id="paletteSearchToggle" class="palette-search-button" aria-pressed="false" title="Search colors" data-i18n-title="Search colors">
|
||||
<div class="icon-search icon16"></div>
|
||||
<div class="icon-close icon16"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="spectrum-row">
|
||||
<div class="saturation-slider">
|
||||
<input class="valueSlider" type="range" id="satSlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Saturation" data-i18n-aria-label="Saturation">
|
||||
</div>
|
||||
<div class="spectrum-container">
|
||||
<canvas id="spectrum"></canvas>
|
||||
<div id="colorIndicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-grid">
|
||||
<div class="editor-column">
|
||||
<div id="modeControls" class="hidden">
|
||||
<input class="switch-check" type="checkbox" id="modeSwitch">
|
||||
<label class="switch-slider" for="modeSwitch">
|
||||
<div class="switch-label" data-i18n="Solid">Solid</div>
|
||||
<div class="switch-label" data-i18n="Gradient">Gradient</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="hex-row">
|
||||
<div class="inline-field field-hex">
|
||||
<label for="hexInput">#</label>
|
||||
<input type="text" id="hexInput" required value="808080" maxlength="8" autocomplete="off" spellcheck="false" aria-label="Hex color" data-i18n-aria-label="Hex color">
|
||||
</div>
|
||||
<div class="inline-field value-field hidden" id="alphaControls" title="Opacity percent">
|
||||
<label for="oValue">
|
||||
<svg viewBox="0 0 12 12" width="12" height="12">
|
||||
<path d="M0,1v10c0,.55.45,1,1,1h10c.55,0,1-.45,1-1V1c0-.55-.45-1-1-1H1C.45,0,0,.45,0,1ZM6,6v5H1v-5h5V1h5v5h-5Z" style="fill:#999"/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="number" id="oValue" min="0" max="100" aria-label="Opacity percent" data-i18n-aria-label="Opacity percent">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Third row: gradient endpoints, replaced by the opacity slider while the opacity field is in use. -->
|
||||
<div id="thirdRow" class="third-row">
|
||||
<!-- Gradient colors: evenly spaced thumbs (built by colorpicker.js); a click selects the nearest. -->
|
||||
<div id="gradientCtrlBar" class="invisible" role="group" aria-label="Gradient" data-i18n-aria-label="Gradient">
|
||||
<div id="gradientStops" class="gradient-stops">
|
||||
<div class="gradient-track"></div>
|
||||
</div>
|
||||
<div class="gradient-step-buttons hidden">
|
||||
<button type="button" id="gradientRemove" class="gradient-step-button hidden" title="Remove color" data-i18n-title="Remove color">
|
||||
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button type="button" id="gradientAdd" class="gradient-step-button hidden" title="Add color" data-i18n-title="Add color">
|
||||
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/><rect x="4" y="0" width="2" height="10" fill="currentColor"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opacitySliderBar" hidden>
|
||||
<input class="valueSlider" type="range" id="opacitySlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Opacity" data-i18n-aria-label="Opacity">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-column">
|
||||
<div class="inline-field value-field" title="Hue">
|
||||
<label for="hValue">H</label>
|
||||
<input type="number" id="hValue" min="0" max="360" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Saturation">
|
||||
<label for="sValue">S</label>
|
||||
<input type="number" id="sValue" min="0" max="100" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Lightness">
|
||||
<label for="lValue">L</label>
|
||||
<input type="number" id="lValue" min="0" max="100" step="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-column">
|
||||
<div class="inline-field value-field" title="Red">
|
||||
<label for="rValue">R</label>
|
||||
<input type="number" id="rValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Green">
|
||||
<label for="gValue">G</label>
|
||||
<input type="number" id="gValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Blue">
|
||||
<label for="bValue">B</label>
|
||||
<input type="number" id="bValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<button id="originalPreview" type="button" class="color-preview original-preview" title="Original color" data-i18n-title="Original color">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#ff6f00;"/>
|
||||
<g style="fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;">
|
||||
<polyline points="5.5 5.5 5.5 9.5 9.5 9.5" />
|
||||
<path d="M5.84,9.5c.82-2.33,3.05-4,5.66-4,3.31,0,6,2.69,6,6s-2.69,6-6,6c-2.95,0-5.41-2.13-5.91-4.94"/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="colorPreview" class="color-preview" title="Save current color" data-i18n-title="Save current color">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#009688"/>
|
||||
<path d="M11.85,5.11l1.43,4.39h4.61c.35,0,.5.45.22.66l-3.73,2.71,1.43,4.39c.11.34-.28.62-.56.41l-3.73-2.71-3.73,2.71c-.29.21-.67-.07-.56-.41l1.43-4.39-3.73-2.71c-.29-.21-.14-.66.22-.66h4.61s1.43-4.39,1.43-4.39c.11-.34.59-.34.7,0Z" style="fill:none; stroke:#fff; stroke-linecap:round; stroke-linejoin:round;"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="saved-grid" id="userColors"></div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button type="button" id="paletteToggle" class="icon-button" aria-expanded="false" aria-controls="palettePanel" title="Palettes" data-i18n-title="Palettes">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="11" rx="2" fill="none" stroke="currentColor"/><path d="M5.5 2.5v11" stroke="currentColor"/></svg>
|
||||
</button>
|
||||
<div class="stretch-spacer"></div>
|
||||
<button id="confirmBtn" class="ButtonTypeChoice ButtonStyleConfirm" data-i18n="OK">OK</button>
|
||||
<button id="cancelBtn" class="ButtonTypeChoice ButtonStyleRegular" data-i18n="Cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<script src="palettes.js"></script><script src="colorpicker-model.js"></script><script src="colorpicker.js"></script></body></html>
|
||||
@@ -0,0 +1,459 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
const cssColors = [
|
||||
{hex: "#FF0000", name: "Red"},
|
||||
{hex: "#DC143C", name: "Crimson"},
|
||||
{hex: "#FF4500", name: "Orange red"},
|
||||
{hex: "#FF6347", name: "Tomato"},
|
||||
{hex: "#FFA500", name: "Orange"},
|
||||
{hex: "#FF8C00", name: "Dark orange"},
|
||||
{hex: "#FFD700", name: "Gold"},
|
||||
{hex: "#DEB887", name: "Burly wood"},
|
||||
{hex: "#FFE4C4", name: "Bisque"},
|
||||
{hex: "#FFE4E1", name: "Misty rose"},
|
||||
{hex: "#FFFF00", name: "Yellow"},
|
||||
{hex: "#FFFFE0", name: "Light yellow"},
|
||||
{hex: "#FFFACD", name: "Lemon chiffon"},
|
||||
{hex: "#F5F5DC", name: "Beige"},
|
||||
{hex: "#FAFAD2", name: "Light golden rod yellow"},
|
||||
{hex: "#EEE8AA", name: "Pale golden rod"},
|
||||
{hex: "#F0E68C", name: "Khaki"},
|
||||
{hex: "#BDB76B", name: "Dark khaki"},
|
||||
{hex: "#FFDAB9", name: "Peach puff"},
|
||||
{hex: "#FFE4B5", name: "Moccasin"},
|
||||
{hex: "#FFEFD5", name: "Papaya whip"},
|
||||
{hex: "#FFEBCD", name: "Blanched almond"},
|
||||
{hex: "#FAEBD7", name: "Antique white"},
|
||||
{hex: "#FFDEAD", name: "Navajo white"},
|
||||
{hex: "#F5DEB3", name: "Wheat"},
|
||||
{hex: "#DAA520", name: "Golden rod"},
|
||||
{hex: "#D2691E", name: "Chocolate"},
|
||||
{hex: "#CD853F", name: "Peru"},
|
||||
{hex: "#B22222", name: "Fire brick"},
|
||||
{hex: "#A0522D", name: "Sienna"},
|
||||
{hex: "#8B4513", name: "Saddle brown"},
|
||||
{hex: "#A52A2A", name: "Brown"},
|
||||
{hex: "#BC8F8F", name: "Rosy brown"},
|
||||
{hex: "#FA8072", name: "Salmon"},
|
||||
{hex: "#FFA07A", name: "Light salmon"},
|
||||
{hex: "#E9967A", name: "Dark salmon"},
|
||||
{hex: "#F4A460", name: "Sandy brown"},
|
||||
{hex: "#FF7F50", name: "Coral"},
|
||||
{hex: "#F08080", name: "Light coral"},
|
||||
{hex: "#CD5C5C", name: "Indian red"},
|
||||
{hex: "#8B0000", name: "Dark red"},
|
||||
{hex: "#800000", name: "Maroon"},
|
||||
{hex: "#ADFF2F", name: "Green yellow"},
|
||||
{hex: "#9ACD32", name: "Yellow green"},
|
||||
{hex: "#7FFF00", name: "Chartreuse"},
|
||||
{hex: "#7CFC00", name: "Lawn green"},
|
||||
{hex: "#32CD32", name: "Lime green"},
|
||||
{hex: "#00FF7F", name: "Spring green"},
|
||||
{hex: "#00FA9A", name: "Medium spring green"},
|
||||
{hex: "#00FF00", name: "Lime"},
|
||||
{hex: "#008000", name: "Green"},
|
||||
{hex: "#228B22", name: "Forest green"},
|
||||
{hex: "#006400", name: "Dark green"},
|
||||
{hex: "#6B8E23", name: "Olive drab"},
|
||||
{hex: "#808000", name: "Olive"},
|
||||
{hex: "#556B2F", name: "Dark olive green"},
|
||||
{hex: "#98FB98", name: "Pale green"},
|
||||
{hex: "#90EE90", name: "Light green"},
|
||||
{hex: "#3CB371", name: "Medium sea green"},
|
||||
{hex: "#2E8B57", name: "Sea green"},
|
||||
{hex: "#8FBC8F", name: "Dark sea green"},
|
||||
{hex: "#5F9EA0", name: "Cadet blue"},
|
||||
{hex: "#66CDAA", name: "Medium aqua marine"},
|
||||
{hex: "#40E0D0", name: "Turquoise"},
|
||||
{hex: "#48D1CC", name: "Medium turquoise"},
|
||||
{hex: "#00CED1", name: "Dark turquoise"},
|
||||
{hex: "#AFEEEE", name: "Pale turquoise"},
|
||||
{hex: "#00FFFF", name: "Aqua"},
|
||||
{hex: "#00FFFF", name: "Cyan"},
|
||||
{hex: "#E0FFFF", name: "Light cyan"},
|
||||
{hex: "#F5FFFA", name: "Mint cream"},
|
||||
{hex: "#F0FFF0", name: "Honey dew"},
|
||||
{hex: "#7FFFD4", name: "Aquamarine"},
|
||||
{hex: "#20B2AA", name: "Light sea green"},
|
||||
{hex: "#008B8B", name: "Dark cyan"},
|
||||
{hex: "#008080", name: "Teal"},
|
||||
{hex: "#2F4F4F", name: "Dark slate gray"},
|
||||
{hex: "#778899", name: "Light slate gray"},
|
||||
{hex: "#708090", name: "Slate gray"},
|
||||
{hex: "#00BFFF", name: "Deep sky blue"},
|
||||
{hex: "#1E90FF", name: "Dodger Blue"},
|
||||
{hex: "#87CEEB", name: "Sky blue"},
|
||||
{hex: "#87CEFA", name: "Light sky blue"},
|
||||
{hex: "#ADD8E6", name: "Light blue"},
|
||||
{hex: "#B0E0E6", name: "Powder blue"},
|
||||
{hex: "#B0C4DE", name: "Light steel blue"},
|
||||
{hex: "#4682B4", name: "Steel blue"},
|
||||
{hex: "#6495ED", name: "Cornflower blue"},
|
||||
{hex: "#4169E1", name: "Royal blue"},
|
||||
{hex: "#0000FF", name: "Blue"},
|
||||
{hex: "#0000CD", name: "Medium blue"},
|
||||
{hex: "#191970", name: "Midnight blue"},
|
||||
{hex: "#00008B", name: "Dark blue"},
|
||||
{hex: "#000080", name: "Navy"},
|
||||
{hex: "#6A5ACD", name: "Slate blue"},
|
||||
{hex: "#483D8B", name: "Dark slate blue"},
|
||||
{hex: "#7B68EE", name: "Medium slate blue"},
|
||||
{hex: "#8A2BE2", name: "Blue violet"},
|
||||
{hex: "#4B0082", name: "Indigo"},
|
||||
{hex: "#9400D3", name: "Dark violet"},
|
||||
{hex: "#9932CC", name: "Dark orchid"},
|
||||
{hex: "#BA55D3", name: "Medium orchid"},
|
||||
{hex: "#DA70D6", name: "Orchid"},
|
||||
{hex: "#EE82EE", name: "Violet"},
|
||||
{hex: "#9370DB", name: "Medium purple"},
|
||||
{hex: "#800080", name: "Purple"},
|
||||
{hex: "#663399", name: "Rebecca purple"},
|
||||
{hex: "#C71585", name: "Medium violet red"},
|
||||
{hex: "#DB7093", name: "PaleV violet red"},
|
||||
{hex: "#FF1493", name: "Deep pink"},
|
||||
{hex: "#FF69B4", name: "Hot pink"},
|
||||
{hex: "#FFB6C1", name: "Light pink"},
|
||||
{hex: "#FFC0CB", name: "Pink"},
|
||||
{hex: "#DDA0DD", name: "Plum"},
|
||||
{hex: "#D8BFD8", name: "Thistle"},
|
||||
{hex: "#FF00FF", name: "Magenta"},
|
||||
{hex: "#FF00FF", name: "Fuchsia"},
|
||||
{hex: "#8B008B", name: "Dark magenta"},
|
||||
|
||||
{hex: "#FAF0E6", name: "Linen"},
|
||||
{hex: "#FDF5E6", name: "Old lace"},
|
||||
{hex: "#FFF5EE", name: "Sea shell"},
|
||||
{hex: "#FFFFF0", name: "Ivory"},
|
||||
{hex: "#FFFAF0", name: "Floral white"},
|
||||
{hex: "#FFF0F5", name: "Lavender blush"},
|
||||
{hex: "#F8F8FF", name: "Ghost white"},
|
||||
{hex: "#F0F8FF", name: "Alice blue"},
|
||||
{hex: "#E6E6FA", name: "Lavender"},
|
||||
{hex: "#F5F5F5", name: "White smoke"},
|
||||
{hex: "#DCDCDC", name: "Gainsboro"},
|
||||
];
|
||||
|
||||
const palettes = {
|
||||
basic: [
|
||||
{hex: "#FF0000", name: "Red"},
|
||||
{hex: "#FF8000", name: "Orange"},
|
||||
{hex: "#FFFF00", name: "Yellow"},
|
||||
{hex: "#80FF00", name: "Lime"},
|
||||
{hex: "#00FF00", name: "Green"},
|
||||
{hex: "#00FFFF", name: "Cyan"},
|
||||
{hex: "#0000FF", name: "Blue"},
|
||||
{hex: "#8000FF", name: "Purple"},
|
||||
{hex: "#FF00FF", name: "Magenta"},
|
||||
{hex: "#FF8080", name: "Pink"},
|
||||
{hex: "#804000", name: "Brown"},
|
||||
{hex: "#000000", name: "Black"},
|
||||
{hex: "#191919", name: "%90"},
|
||||
{hex: "#333333", name: "%80"},
|
||||
{hex: "#4c4c4c", name: "%70"},
|
||||
{hex: "#666666", name: "%60"},
|
||||
{hex: "#808080", name: "%50"},
|
||||
{hex: "#999999", name: "%40"},
|
||||
{hex: "#b3b3b3", name: "%30"},
|
||||
{hex: "#cccccc", name: "%20"},
|
||||
{hex: "#e6e6e6", name: "%10"},
|
||||
{hex: "#FFFFFF", name: "White"},
|
||||
...cssColors
|
||||
],
|
||||
orca: [
|
||||
{hex: "#ED1C24", name: "Coral fire"},
|
||||
{hex: "#C41E3A", name: "Sea anemone"},
|
||||
{hex: "#800000", name: "Deep abyss"},
|
||||
{hex: "#B22222", name: "Lobster red"},
|
||||
{hex: "#A0522D", name: "Sandy clay"},
|
||||
{hex: "#8B4513", name: "Driftwood"},
|
||||
{hex: "#D2691E", name: "Sea buckthorn"},
|
||||
{hex: "#F26722", name: "Sunset reef"},
|
||||
{hex: "#FF8C00", name: "Tiger coral"},
|
||||
{hex: "#FA8173", name: "Pink coral"},
|
||||
{hex: "#FFA07A", name: "Seashell glow"},
|
||||
{hex: "#F4E2C1", name: "Sand"},
|
||||
{hex: "#FFD700", name: "Golden kelp"},
|
||||
{hex: "#F7B763", name: "Sunlit dune"},
|
||||
{hex: "#FFAA33", name: "Amber nautilus"},
|
||||
{hex: "#FFEB31", name: "Sea lemon"},
|
||||
{hex: "#D4ED91", name: "Pale starfish"},
|
||||
{hex: "#00FF7F", name: "Spring tide"},
|
||||
{hex: "#32CD32", name: "Lime algae"},
|
||||
{hex: "#A4C41E", name: "Sea grass"},
|
||||
{hex: "#7CFC00", name: "Neon plankton"},
|
||||
{hex: "#228B22", name: "Kelp forest"},
|
||||
{hex: "#008000", name: "Emerald wave"},
|
||||
{hex: "#345B2F", name: "Whale shadow"},
|
||||
{hex: "#2E8B57", name: "Sea turtle"},
|
||||
{hex: "#00C1AE", name: "Lagoon"},
|
||||
{hex: "#20B2AA", name: "Tropical current"},
|
||||
{hex: "#115877", name: "Midnight trench"},
|
||||
{hex: "#008B8B", name: "Dark kelp"},
|
||||
{hex: "#00FFFF", name: "Crystal surf"},
|
||||
{hex: "#40E0D0", name: "Turquoise bay"},
|
||||
{hex: "#2EBDEF", name: "Sky over reef"},
|
||||
{hex: "#87CEEB", name: "Shallow sky"},
|
||||
{hex: "#1E90FF", name: "Electric bel"},
|
||||
{hex: "#4169E1", name: "Dolphin"},
|
||||
{hex: "#0000CD", name: "Ocean depths"},
|
||||
{hex: "#191970", name: "Whale"},
|
||||
{hex: "#4B0082", name: "Indigo abyss"},
|
||||
{hex: "#7841CE", name: "Sea urchin"},
|
||||
{hex: "#9370DB", name: "Lavender coral"},
|
||||
{hex: "#800080", name: "Violet tide"},
|
||||
{hex: "#8B008B", name: "Deep sea orchid"},
|
||||
{hex: "#FF00FF", name: "Neon jelly"},
|
||||
{hex: "#ED1E79", name: "Fuchsia coral"},
|
||||
{hex: "#FF69B4", name: "Pink jellyfish"},
|
||||
{hex: "#FFFFFF", name: "Foam"},
|
||||
{hex: "#D3D3D3", name: "Seashell white"},
|
||||
{hex: "#808080", name: "Pebble gray"},
|
||||
{hex: "#696969", name: "Storm cloud"},
|
||||
{hex: "#000000", name: "Ink black"}
|
||||
],
|
||||
ral: [
|
||||
{hex: "#716F52", name: "1000 Green beige"},
|
||||
{hex: "#9B8E79", name: "1001 Beige"},
|
||||
{hex: "#D4BB87", name: "1002 Sand yellow"},
|
||||
{hex: "#F4A460", name: "1003 Signal yellow"},
|
||||
{hex: "#EFBF04", name: "1004 Golden yellow"},
|
||||
{hex: "#D7A500", name: "1005 Honey yellow"},
|
||||
{hex: "#F2D185", name: "1006 Maize yellow"},
|
||||
{hex: "#FDD185", name: "1007 Dalmatian yellow"},
|
||||
{hex: "#8B7D6B", name: "1011 Brown beige"},
|
||||
{hex: "#F5DB50", name: "1012 Lemon yellow"},
|
||||
{hex: "#F3E5AB", name: "1013 Oyster white"},
|
||||
{hex: "#F8E4BC", name: "1014 Ivory"},
|
||||
{hex: "#FAE6D3", name: "1015 Light ivory"},
|
||||
{hex: "#E4B922", name: "1018 Zinc yellow"},
|
||||
{hex: "#A19D94", name: "1019 Grey beige"},
|
||||
{hex: "#B9975B", name: "1020 Olive yellow"},
|
||||
{hex: "#B8860B", name: "1021 Rape yellow"},
|
||||
{hex: "#FF8C00", name: "1023 Traffic yellow"},
|
||||
{hex: "#CC7722", name: "1024 Ochre yellow"},
|
||||
{hex: "#FDBA37", name: "1026 Traffic orange yellow"},
|
||||
{hex: "#F3A55C", name: "1027 Curry"},
|
||||
{hex: "#E87C33", name: "1028 Melon yellow"},
|
||||
{hex: "#F2A641", name: "1032 Broom yellow"},
|
||||
{hex: "#E09F39", name: "1033 Dahlia yellow"},
|
||||
{hex: "#F0B76E", name: "1034 Pastel yellow"},
|
||||
{hex: "#D1BCA0", name: "1035 Pearl beige"},
|
||||
{hex: "#D4AF37", name: "1036 Pearl gold"},
|
||||
{hex: "#F7C64F", name: "1037 Sun yellow"},
|
||||
{hex: "#F28C38", name: "2000 Yellow orange"},
|
||||
{hex: "#E55B2B", name: "2001 Deep orange"},
|
||||
{hex: "#E34234", name: "2002 Vermilion"},
|
||||
{hex: "#F27B33", name: "2003 Pearl orange"},
|
||||
{hex: "#F37322", name: "2004 Pure orange"},
|
||||
{hex: "#F75C03", name: "2005 Luminous orange"},
|
||||
{hex: "#F46C1F", name: "2007 Luminous bright orange"},
|
||||
{hex: "#E14423", name: "2008 Bright red orange"},
|
||||
{hex: "#F28500", name: "2009 Traffic orange"},
|
||||
{hex: "#F46A0A", name: "2010 Signal orange"},
|
||||
{hex: "#E0490C", name: "2011 Deep orange"},
|
||||
{hex: "#F46F42", name: "2012 Salmon orange"},
|
||||
{hex: "#E6761C", name: "2013 Pearl orange"},
|
||||
{hex: "#E8A18B", name: "2015 Pearl pink"},
|
||||
{hex: "#B8612C", name: "2016 Copper orange"},
|
||||
{hex: "#8B4513", name: "2017 Signal brown"},
|
||||
{hex: "#C14A0F", name: "2018 Fir orange"},
|
||||
{hex: "#BC2E1F", name: "2020 Signal red"},
|
||||
{hex: "#D52713", name: "2021 Luminous red"},
|
||||
{hex: "#9F261E", name: "2022 Carmine red"},
|
||||
{hex: "#D53832", name: "2023 Pearl carmine red"},
|
||||
{hex: "#C2341C", name: "2024 Pure red"},
|
||||
{hex: "#B32D1C", name: "2025 Luminous deep red"},
|
||||
{hex: "#A62320", name: "2027 Currant red"},
|
||||
{hex: "#B9301E", name: "2028 Pearl ruby red"},
|
||||
{hex: "#A91D2E", name: "3000 Fire red"},
|
||||
{hex: "#A1262A", name: "3001 Signal red"},
|
||||
{hex: "#8F2626", name: "3002 Carmine red"},
|
||||
{hex: "#9B2428", name: "3003 Ruby red"},
|
||||
{hex: "#872241", name: "3004 Purple red"},
|
||||
{hex: "#722F37", name: "3005 Wine red"},
|
||||
{hex: "#3D1818", name: "3007 Black red"},
|
||||
{hex: "#8A3324", name: "3009 Oxide red"},
|
||||
{hex: "#A22C29", name: "3011 Brown red"},
|
||||
{hex: "#B22D20", name: "3012 Pine red"},
|
||||
{hex: "#D23741", name: "3013 Tomato red"},
|
||||
{hex: "#A8282E", name: "3014 Antique pink"},
|
||||
{hex: "#C8283C", name: "3015 Light pink"},
|
||||
{hex: "#D23F39", name: "3016 Coral red"},
|
||||
{hex: "#D1425E", name: "3017 Rose"},
|
||||
{hex: "#C4232F", name: "3018 Strawberry red"},
|
||||
{hex: "#C8102E", name: "3020 Traffic red"},
|
||||
{hex: "#E36A5A", name: "3022 Salmon pink"},
|
||||
{hex: "#E63E62", name: "3024 Luminous rose"},
|
||||
{hex: "#E02A27", name: "3026 Luminous bright red"},
|
||||
{hex: "#B8315F", name: "3027 Raspberry red"},
|
||||
{hex: "#C61C23", name: "3028 Pure red"},
|
||||
{hex: "#AD2932", name: "3031 Orient red"},
|
||||
{hex: "#D32F2F", name: "3032 Pearl ruby"},
|
||||
{hex: "#D13C6F", name: "3033 Pearl pink"},
|
||||
{hex: "#7B2B5F", name: "4001 Red purple"},
|
||||
{hex: "#912E8C", name: "4002 Red violet"},
|
||||
{hex: "#9C366F", name: "4003 Heather violet"},
|
||||
{hex: "#6A1F49", name: "4004 Bordeaux violet"},
|
||||
{hex: "#4B2663", name: "4005 Blue violet"},
|
||||
{hex: "#5C2D3C", name: "4006 Mahogany"},
|
||||
{hex: "#5E3559", name: "4007 Grey purple"},
|
||||
{hex: "#9C3568", name: "4008 Signal violet"},
|
||||
{hex: "#5E6E3A", name: "4009 Fir green"},
|
||||
{hex: "#5B5B8A", name: "4010 Sabine grey"},
|
||||
{hex: "#8F3A70", name: "4011 Pearl violet"},
|
||||
{hex: "#5E3252", name: "4012 Pearl blackberry"},
|
||||
{hex: "#1B4F72", name: "5001 Green blue"},
|
||||
{hex: "#18669E", name: "5002 Ultramarine blue"},
|
||||
{hex: "#0F4C8A", name: "5003 Sapphire blue"},
|
||||
{hex: "#0D324A", name: "5004 Black blue"},
|
||||
{hex: "#1B5583", name: "5005 Signal blue"},
|
||||
{hex: "#2C5F9D", name: "5007 Brilliant blue"},
|
||||
{hex: "#3B5AA9", name: "5008 Grey blue"},
|
||||
{hex: "#0077A3", name: "5009 Azure blue"},
|
||||
{hex: "#0A558C", name: "5010 Gentian blue"},
|
||||
{hex: "#2B547E", name: "5011 Steel blue"},
|
||||
{hex: "#0066A1", name: "5012 Light blue"},
|
||||
{hex: "#124780", name: "5013 Cobalt blue"},
|
||||
{hex: "#1F4E79", name: "5014 Pigeon blue"},
|
||||
{hex: "#0384A7", name: "5015 Sky blue"},
|
||||
{hex: "#006A9E", name: "5017 Traffic blue"},
|
||||
{hex: "#005F87", name: "5018 Turquoise blue"},
|
||||
{hex: "#5A3D5B", name: "5019 Capsanthin violet"},
|
||||
{hex: "#04749F", name: "5020 Ocean blue"},
|
||||
{hex: "#009AAA", name: "5021 Water blue"},
|
||||
{hex: "#1A3D5F", name: "5022 Night blue"},
|
||||
{hex: "#047AA6", name: "5023 Distant blue"},
|
||||
{hex: "#5B9BB8", name: "5024 Pastel blue"},
|
||||
{hex: "#3C6A98", name: "5025 Pearl gentian blue"},
|
||||
{hex: "#1C3F5F", name: "5026 Pearl midnight blue"},
|
||||
{hex: "#36673A", name: "6000 Patina green"},
|
||||
{hex: "#0E5C34", name: "6001 Emerald green"},
|
||||
{hex: "#437034", name: "6002 Leaf green"},
|
||||
{hex: "#3F3F2A", name: "6003 Olive green"},
|
||||
{hex: "#2B4E52", name: "6004 Blue green"},
|
||||
{hex: "#3B5F3F", name: "6005 Moss green"},
|
||||
{hex: "#4A563C", name: "6006 Grey olive"},
|
||||
{hex: "#2B4638", name: "6007 Pine green"},
|
||||
{hex: "#4A4139", name: "6008 Pine brown"},
|
||||
{hex: "#3B3A2E", name: "6009 Fir green"},
|
||||
{hex: "#3E7A5F", name: "6010 Grass green"},
|
||||
{hex: "#5E716A", name: "6011 Reseda green"},
|
||||
{hex: "#3A4E48", name: "6012 Brown green"},
|
||||
{hex: "#6B8E23", name: "6013 Reed green"},
|
||||
{hex: "#6B8E23", name: "6014 Yellow green"},
|
||||
{hex: "#1E3E2F", name: "6015 Black green"},
|
||||
{hex: "#1E4D4B", name: "6016 Turquoise green"},
|
||||
{hex: "#4C7C47", name: "6017 May green"},
|
||||
{hex: "#9CBF60", name: "6018 Yellow green"},
|
||||
{hex: "#2E5A3A", name: "6019 Pine green"},
|
||||
{hex: "#2E5A3A", name: "6020 Chrome oxide green"},
|
||||
{hex: "#9DC0A5", name: "6021 Pale green"},
|
||||
{hex: "#4A563C", name: "6022 Brown green"},
|
||||
{hex: "#009E60", name: "6024 Traffic green"},
|
||||
{hex: "#3F7044", name: "6025 Fern green"},
|
||||
{hex: "#4F7942", name: "6026 Opal green"},
|
||||
{hex: "#3B5F3F", name: "6027 Pine green"},
|
||||
{hex: "#3B5F3F", name: "6028 Pine green"},
|
||||
{hex: "#00A550", name: "6029 Mint green"},
|
||||
{hex: "#3E5A40", name: "6031 Bronze green"},
|
||||
{hex: "#3D7E5A", name: "6032 Pearl green"},
|
||||
{hex: "#007D6E", name: "6033 Sax turquoise"},
|
||||
{hex: "#3D7E5A", name: "6034 Pearl turquoise"},
|
||||
{hex: "#3D7E5A", name: "6035 Pearl green"},
|
||||
{hex: "#3D7E5A", name: "6036 Pearl jade"},
|
||||
{hex: "#00A550", name: "6037 Pure green"},
|
||||
{hex: "#00A550", name: "6038 Luminous green"},
|
||||
{hex: "#00A550", name: "6039 Mint green"},
|
||||
{hex: "#7D7D7D", name: "7000 Squirrel grey"},
|
||||
{hex: "#A8A9AD", name: "7001 Silver grey"},
|
||||
{hex: "#6F7368", name: "7002 Olivestone"},
|
||||
{hex: "#8F9080", name: "7003 Moss grey"},
|
||||
{hex: "#8E9291", name: "7004 Signal grey"},
|
||||
{hex: "#8B8788", name: "7005 Mouse grey"},
|
||||
{hex: "#9A8373", name: "7006 Beige grey"},
|
||||
{hex: "#828C6A", name: "7008 Khaki grey"},
|
||||
{hex: "#6F7368", name: "7009 Green grey"},
|
||||
{hex: "#5F6563", name: "7010 Tarpaulin grey"},
|
||||
{hex: "#4A5656", name: "7011 Iron grey"},
|
||||
{hex: "#4D5656", name: "7012 Basalt grey"},
|
||||
{hex: "#635147", name: "7013 Brown grey"},
|
||||
{hex: "#6B6B83", name: "7015 Slate grey"},
|
||||
{hex: "#32373D", name: "7016 Anthracite grey"},
|
||||
{hex: "#8B8B83", name: "7021 Light grey"},
|
||||
{hex: "#716347", name: "7022 Umber grey"},
|
||||
{hex: "#76787A", name: "7023 Concrete grey"},
|
||||
{hex: "#5E6565", name: "7024 Graphite grey"},
|
||||
{hex: "#5E6565", name: "7026 Granite grey"},
|
||||
{hex: "#8F9494", name: "7030 Stone grey"},
|
||||
{hex: "#4A5656", name: "7031 Iron grey"},
|
||||
{hex: "#B2BFC0", name: "7032 Pebble grey"},
|
||||
{hex: "#9B9B9B", name: "7033 Cement grey"},
|
||||
{hex: "#8F9D9B", name: "7034 Yellow grey"},
|
||||
{hex: "#CBD0CC", name: "7035 Light grey"},
|
||||
{hex: "#6D7A80", name: "7036 Platinum grey"},
|
||||
{hex: "#8A9191", name: "7037 Dusty grey"},
|
||||
{hex: "#7E8181", name: "7038 Agate grey"},
|
||||
{hex: "#6E7D7E", name: "7039 Quartz grey"},
|
||||
{hex: "#8A9191", name: "7040 Window grey"},
|
||||
{hex: "#8A9191", name: "7042 Traffic grey A"},
|
||||
{hex: "#6E7D7E", name: "7043 Traffic grey B"},
|
||||
{hex: "#9B9B9B", name: "7044 Silk grey"},
|
||||
{hex: "#A1A7B0", name: "7045 Telegrey 1"},
|
||||
{hex: "#7E848C", name: "7046 Telegrey 2"},
|
||||
{hex: "#7E848C", name: "7047 Telegrey 4"},
|
||||
{hex: "#6D7A80", name: "7048 Pearl mouse grey"},
|
||||
{hex: "#786D5F", name: "8000 Green brown"},
|
||||
{hex: "#8B7355", name: "8001 Ochre brown"},
|
||||
{hex: "#8B6F47", name: "8003 Clay brown"},
|
||||
{hex: "#8B6F47", name: "8004 Copper brown"},
|
||||
{hex: "#5C4033", name: "8007 Chocolate brown"},
|
||||
{hex: "#6E5C4A", name: "8008 Olive brown"},
|
||||
{hex: "#8B4513", name: "8011 Nut brown"},
|
||||
{hex: "#8B4513", name: "8012 Red brown"},
|
||||
{hex: "#5C3317", name: "8014 Sepia brown"},
|
||||
{hex: "#854636", name: "8015 Chestnut brown"},
|
||||
{hex: "#8B4513", name: "8016 Mahogany brown"},
|
||||
{hex: "#5C3317", name: "8017 Chocolate brown"},
|
||||
{hex: "#786D5F", name: "8019 Grey brown"},
|
||||
{hex: "#854636", name: "8022 Chestnut brown"},
|
||||
{hex: "#8B6F47", name: "8023 Pearl copper"},
|
||||
{hex: "#8B7355", name: "8024 Beige brown"},
|
||||
{hex: "#8B6F47", name: "8025 Pearl mocha brown"},
|
||||
{hex: "#8B6F47", name: "8027 Clay brown"},
|
||||
{hex: "#8B6F47", name: "8028 Earth brown"},
|
||||
{hex: "#8B6F47", name: "8029 Pearl brown"},
|
||||
{hex: "#FAF0E6", name: "9001 Cream"},
|
||||
{hex: "#EDECE6", name: "9002 Grey white"},
|
||||
{hex: "#FFFFFF", name: "9003 Signal white"},
|
||||
{hex: "#000000", name: "9004 Signal black"},
|
||||
{hex: "#1D1D1D", name: "9005 Jet black"},
|
||||
{hex: "#C0C0C0", name: "9006 White aluminium"},
|
||||
{hex: "#8C7853", name: "9007 Grey aluminium"},
|
||||
{hex: "#FFFFFF", name: "9010 Pure white"},
|
||||
{hex: "#1D1D1D", name: "9011 Graphite black"},
|
||||
{hex: "#FFFFFF", name: "9016 Traffic white"},
|
||||
{hex: "#000000", name: "9017 Traffic black"},
|
||||
{hex: "#FAF0E6", name: "9018 Papyrus white"},
|
||||
{hex: "#B2BFC0", name: "9022 Pearl light grey"}
|
||||
],
|
||||
gradient: [
|
||||
{grad: "#FFF47B-#CD9800", name: "Metal: Gold"},
|
||||
{grad: "#373737-#F5F4F4", name: "Metal: Silver"},
|
||||
{grad: "#C39292-#C73725", name: "Metal: Copper"},
|
||||
{grad: "#DED699-#C99620", name: "Metal: Brass"},
|
||||
{grad: "#6E4316-#B0742E", name: "Metal: Bronze"},
|
||||
// More than two colors: only listed when the page enables multi-color gradients.
|
||||
{grad: "#FF0000-#FFFF00-#00FF00-#00FFFF-#0000FF-#FF00FF-#FF0000", name: "Nature: Rainbow"},
|
||||
|
||||
{grad: "#D8FEE7-#FFF5BD", name: "Nature: Pearl"},
|
||||
{grad: "#1E5AA8-#3D8BD9-#7DB9F0-#C9E6FF", name: "Nature: Clear Sky"},
|
||||
{grad: "#2B1B5A-#7A2E7E-#D9466E-#F57C3A-#FFC857", name: "Nature: Sunset"},
|
||||
{grad: "#3B2416-#5A3A24-#45291A-#6B4630-#3B2416", name: "Wood: Dark"},
|
||||
{grad: "#8B5A2B-#A8733F-#946235-#B5844F-#8B5A2B", name: "Wood: Medium"},
|
||||
{grad: "#D9B98C-#E8CFA6-#D3AE7C-#EBD5B0-#D9B98C", name: "Wood: Light"},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
window.ColorPickerPalettes = palettes;
|
||||
})();
|
||||
@@ -0,0 +1,534 @@
|
||||
:root {
|
||||
--thumb-size : 20px;
|
||||
--field-height: 28px;
|
||||
--number-width: 52px;
|
||||
--preview-size: 52px;
|
||||
--checkerboard: conic-gradient(var(--checker-light) 25%, var(--checker-dark) 0 50%, var(--checker-light) 0 75%, var(--checker-dark) 0);
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
/* Report the full content width even before the native host expands its viewport.
|
||||
WebKit's fit-content can clip the sidebar and retain the expanded width on close. */
|
||||
width: max-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--orca-font, system-ui, sans-serif);
|
||||
background: var(--bg-color);
|
||||
color: var(--fg-color-text);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#selectionNotice {
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
margin-bottom: 8px;
|
||||
color: var(--fg-color-label);
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
/* ---------- Palette sidebar ---------- */
|
||||
.left-panel {
|
||||
flex: 0 0 160px;
|
||||
width: 160px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
.left-panel.open {
|
||||
display: flex;
|
||||
padding-right: 0px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#paletteCombo {
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Combo box or filter box, with the search / close button on the right. */
|
||||
.palette-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
height: 25px;
|
||||
margin-right:5px;
|
||||
}
|
||||
.palette-toolbar > .palette-select { flex: 1 }
|
||||
.palette-filter {
|
||||
flex: 1;
|
||||
height: 25px;
|
||||
padding: 0 6px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
color: var(--fg-color-text);
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--main-color);
|
||||
outline: none;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
.palette-filter::-webkit-search-cancel-button { -webkit-appearance: none }
|
||||
.palette-filter:focus-visible{
|
||||
background-color: var(--focus-bg-box);
|
||||
}
|
||||
|
||||
.palette-search-button {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: var(--button-bg-normal);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 0;
|
||||
}
|
||||
.palette-search-button:hover {
|
||||
background-color: var(--button-bg-hover);
|
||||
}
|
||||
|
||||
.palette-search-button .icon-search {
|
||||
--icon-url: var(--icon-search);
|
||||
}
|
||||
.palette-search-button .icon-close {
|
||||
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g style="fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round"><line x1="3.5" y1="2.5" x2="13.5" y2="12.5"/><line x1="3.5" y1="12.5" x2="13.5" y2="2.5"/></g></svg>');;
|
||||
}
|
||||
.palette-search-button .icon-close,
|
||||
.palette-search-button[aria-pressed="true"] .icon-search { display: none }
|
||||
.palette-search-button[aria-pressed="true"] .icon-close { display: block }
|
||||
.palette-empty {
|
||||
flex: 1;
|
||||
padding: 8px 2px;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color-label);
|
||||
}
|
||||
|
||||
.palette {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 1px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1px 2px 1px 0;
|
||||
border:1px solid transparent;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color-text);
|
||||
}
|
||||
|
||||
.color-item:hover {
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
flex: 0 0 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
margin-right: 5px;
|
||||
margin: 0 5px 0 1px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.color-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ---------- Picker ---------- */
|
||||
.right-panel {
|
||||
flex: 0 0 330px;
|
||||
/* WebKit uses width, rather than flex-basis, for the parent's intrinsic width. */
|
||||
width: 330px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spectrum-row {
|
||||
--spectrum-height: 150px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
height: var(--spectrum-height);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.spectrum-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
border-radius: 7px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#spectrum {
|
||||
position: absolute; /* Absolute so the canvas's pixel size never feeds back into the flex layout */
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#pickerEditors:disabled #spectrum { pointer-events: none }
|
||||
|
||||
#colorIndicator {
|
||||
position: absolute;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.saturation-slider {
|
||||
position: relative;
|
||||
flex: 0 0 var(--thumb-size);
|
||||
}
|
||||
|
||||
#satSlider {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
flex: none;
|
||||
width: var(--spectrum-height);
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
}
|
||||
|
||||
.valueSlider {
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.valueSlider::-webkit-slider-runnable-track {
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--track-gradient);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.valueSlider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
background: var(--thumb-color);
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
/* Left column: mode switch, hex + alpha, gradient endpoints. Right: H S L and R G B columns. */
|
||||
.editor-grid {
|
||||
--editor-grid-gap: 5px;
|
||||
display: flex;
|
||||
gap: var(--editor-grid-gap);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.editor-column:first-of-type {flex:1}
|
||||
.editor-column {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, var(--field-height));
|
||||
gap: var(--editor-grid-gap);
|
||||
}
|
||||
|
||||
.hex-row {
|
||||
display: flex;
|
||||
gap: var(--editor-grid-gap);
|
||||
}
|
||||
|
||||
.switch-check {
|
||||
display: none !important;
|
||||
}
|
||||
.switch-label {
|
||||
font-size: 13px;
|
||||
padding: 2px 10px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.switch-slider {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
border-radius: 20px;
|
||||
background: var(--bg-color-alt);
|
||||
border: 2px solid var(--bg-color-alt);
|
||||
height:26px;
|
||||
}
|
||||
.switch-slider:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
left: 0%;
|
||||
border-radius:20px;
|
||||
background: var(--main-color);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.switch-check:checked + .switch-slider:before {
|
||||
left: 50%;
|
||||
}
|
||||
.switch-check:checked + .switch-slider div:first-child,
|
||||
.switch-check + .switch-slider div:last-child{
|
||||
color: var(--fg-color-label);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.switch-check:checked + .switch-slider div:last-child,
|
||||
.switch-check + .switch-slider div:first-child{
|
||||
color: var(--button-fg-light);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* H S L / R G B / Hex / Alpha input boxes */
|
||||
.inline-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
.inline-field > label {
|
||||
width: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--fg-color-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.inline-field > input {
|
||||
flex: 1;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0 2px 0 5px;
|
||||
color: var(--fg-color-text);
|
||||
font-size: 15px;
|
||||
line-height: var(--field-height);
|
||||
appearance: textfield; /* No spinner buttons; Up/Down keys and the wheel still step by 1. */
|
||||
}
|
||||
.inline-field:focus-within {
|
||||
border-color: var(--main-color);
|
||||
background: var(--focus-bg-box);
|
||||
}
|
||||
.inline-field:has(input:invalid) {
|
||||
border-color: var(--control-invalid);
|
||||
}
|
||||
.inline-field > input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inline-field > input[type="number"]::-webkit-inner-spin-button,
|
||||
.inline-field > input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none }
|
||||
|
||||
.value-field { width: var(--number-width)}
|
||||
.field-hex { flex: 1 }
|
||||
#alphaControls {
|
||||
width: calc(var(--number-width) + 2px)
|
||||
}
|
||||
#alphaControls > input {
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
/* Third row holds the gradient bar and the opacity slider in the same cell. The slider only
|
||||
shows while the opacity field or the slider itself is in use, and hides the gradient bar. */
|
||||
.third-row { position: relative }
|
||||
.third-row > * {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.third-row.opacity-open > #opacitySliderBar { display: flex }
|
||||
.third-row.opacity-open > #gradientCtrlBar { display: none }
|
||||
|
||||
#opacitySlider { width: 100%; }
|
||||
#opacitySlider::-webkit-slider-runnable-track {
|
||||
background: var(--track-gradient), var(--checkerboard); /* Transparent end shows the checkerboard through the color. */
|
||||
background-size: auto, 8px 8px;
|
||||
}
|
||||
|
||||
/* Gradient colors: thumbs are evenly spaced and fixed; a click anywhere selects the nearest. */
|
||||
#gradientCtrlBar {
|
||||
display: flex;
|
||||
}
|
||||
.gradient-stops {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
/* Kept 10px away from the gradient bar so a click near its right end can't hit − or +. */
|
||||
.gradient-step-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 10px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.gradient-step-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border-radius: 4px;
|
||||
background: var(--button-bg-normal);
|
||||
color: var(--icon-color);
|
||||
}
|
||||
.gradient-step-button svg { display: block; flex: none }
|
||||
.gradient-step-button:hover:not([aria-disabled="true"]) { background: var(--button-bg-hover) }
|
||||
.gradient-step-button[aria-disabled="true"] { opacity: 0.4; cursor: default }
|
||||
.gradient-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 50%;
|
||||
height: var(--thumb-size);
|
||||
transform: translateY(-50%);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
|
||||
background-size: auto, 8px 8px;
|
||||
}
|
||||
.gradient-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
border: 1px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 1px var(--color-marker-shadow);
|
||||
background: var(--thumb-color);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
/* Thumb i of n sits at 10px + (100% - 20px) * i / (n - 1), set inline as --stop. */
|
||||
.gradient-thumb { left: calc(10px + (100% - 20px) * var(--stop, 0)) }
|
||||
.gradient-thumb[aria-pressed="true"] {
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.gradient-thumb:hover {
|
||||
box-shadow: 0 0 0 3px var(--color-marker-shadow);
|
||||
}
|
||||
|
||||
/* ---------- Original / current / favorites ---------- */
|
||||
.bottom-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: var(--preview-size);
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--preview-size);
|
||||
height: var(--preview-size);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.color-preview svg {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.color-preview:hover svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.saved-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(0, 24px));
|
||||
grid-template-rows: repeat(2, minmax(0, 24px));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.saved-swatch {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saved-swatch.empty {
|
||||
background: var(--bg-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Each layer is separate so transparent RGBA shows the checkerboard beneath it. */
|
||||
.color-swatch, .saved-swatch, .color-preview {
|
||||
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
|
||||
background-size: auto, 12px 12px;
|
||||
}
|
||||
.color-item { width: 100%; background: transparent; text-align: left; font-family: inherit; }
|
||||
|
||||
/* ---------- Actions ---------- */
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.dialog-actions button { margin: 0 }
|
||||
#confirmBtn[disabled] { opacity: 0.5 }
|
||||
.stretch-spacer { flex: 1 }
|
||||
|
||||
.icon-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
background: var(--button-bg-normal);
|
||||
color: var(--fg-color-text);
|
||||
}
|
||||
.icon-button:hover {
|
||||
background: var(--button-bg-hover);
|
||||
}
|
||||
.icon-button[aria-expanded="true"] {
|
||||
background: var(--main-color);
|
||||
color: var(--button-fg-light);
|
||||
}
|
||||
.icon-button[aria-expanded="true"]:hover {
|
||||
background: var(--main-color-hover);
|
||||
}
|
||||
|
||||
.hidden { display: none !important }
|
||||
.invisible { visibility: hidden }
|
||||
|
||||
.color-item:focus-visible, .saved-swatch:focus-visible,
|
||||
select:focus-visible, button:focus-visible { outline: 2px solid var(--main-color); outline-offset: -1px; }
|
||||
@@ -55,6 +55,13 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #efefef;
|
||||
--speed-tile-border: #d8d8d8;
|
||||
|
||||
/* Transparency and control markers are UI surfaces, never color data. */
|
||||
--checker-light: #eeeeee;
|
||||
--checker-dark: #aaaaaa;
|
||||
--color-marker-outline: #ffffff;
|
||||
--color-marker-shadow: rgba(0, 0, 0, 0.7);
|
||||
--control-invalid: var(--plugin-status-danger);
|
||||
}
|
||||
|
||||
:root[data-orca-theme="dark"] {
|
||||
@@ -93,6 +100,8 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #34343b;
|
||||
--speed-tile-border: #50505a;
|
||||
--checker-light: #68686f;
|
||||
--checker-dark: #414148;
|
||||
}
|
||||
|
||||
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
|
||||
|
||||
@@ -10,6 +10,11 @@ function OnInit()
|
||||
// Orca: fallback timeout in case the C++ -> JS signal fails (e.g., WebKit issues).
|
||||
// Jump to the target page after 3 minutes so slow computers don't get stuck on a partially loaded page.
|
||||
setTimeout("JumpToTarget()",180*1000);
|
||||
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="userguide_loading_page_ready";
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
}
|
||||
|
||||
function HandleStudio( pVal )
|
||||
|
||||
@@ -506,11 +506,6 @@ body
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.BtnShortcut
|
||||
{
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.ShortcutBrandIcon
|
||||
{
|
||||
width: 20px;
|
||||
@@ -519,25 +514,6 @@ body
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ShortcutTextRow
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ShortcutMark
|
||||
{
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-left: auto;
|
||||
color: currentColor;
|
||||
opacity: 0.72;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
/*--------------------*/
|
||||
#RightBoard
|
||||
@@ -548,6 +524,20 @@ body
|
||||
overflow-y:auto;
|
||||
}
|
||||
|
||||
/*---- Cloud mode: OrcaCloud is a native pane to the right of the app panel, so this page
|
||||
is reduced to its left nav column (the class is toggled by SetCloudMode in js/home.js). ----*/
|
||||
body.cloud-mode #RightBoard
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.cloud-mode #LeftBoard
|
||||
{
|
||||
/*---- 262px of column plus its 1px separator is the width the app gives the nav. ----*/
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#ContentBoard
|
||||
{
|
||||
overflow-y:auto;
|
||||
|
||||
@@ -90,16 +90,9 @@
|
||||
<div class="BtnIcon "><img class="LeftIcon" src="img/i2.png" /></div>
|
||||
<div data-i18n="recent">Recent</div>
|
||||
</div>
|
||||
<div class="BtnItem BtnShortcut" onClick="OpenUrlInLocalBrowser('https://cloud.orcaslicer.com')">
|
||||
<div class="BtnItem BtnShortcut" onClick="GotoCloud()">
|
||||
<div class="BtnIcon "><img class="ShortcutBrandIcon" src="../image/logo.png" /></div>
|
||||
<div class="ShortcutTextRow">
|
||||
<div>OrcaCloud</div>
|
||||
<svg class="ShortcutMark" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M6 3.5H3.5C2.95 3.5 2.5 3.95 2.5 4.5V12.5C2.5 13.05 2.95 13.5 3.5 13.5H11.5C12.05 13.5 12.5 13.05 12.5 12.5V10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.5 3.5H13.5V8.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5 3.5L7 10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>OrcaCloud</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -145,8 +145,36 @@ function HandleStudio( pVal )
|
||||
}
|
||||
}
|
||||
|
||||
/*---- OrcaCloud opens as a native pane beside this page, so the shell only switches its
|
||||
own cloud-mode state and asks the app to show or hide that pane (WebViewPanel::set_cloud_mode). ----*/
|
||||
var m_CloudMode=false;
|
||||
|
||||
function SetCloudMode( bOn )
|
||||
{
|
||||
if( m_CloudMode==bOn )
|
||||
return;
|
||||
|
||||
m_CloudMode=bOn;
|
||||
$("body").toggleClass("cloud-mode", bOn);
|
||||
}
|
||||
|
||||
function GotoCloud()
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
SetCloudMode(true);
|
||||
SendSimpleCommand("homepage_open_cloud");
|
||||
}
|
||||
|
||||
function GotoMenu( strMenu )
|
||||
{
|
||||
/*---- Any board in the right region means leaving the cloud pane. ----*/
|
||||
if( m_CloudMode )
|
||||
{
|
||||
SetCloudMode(false);
|
||||
SendSimpleCommand("homepage_close_cloud");
|
||||
}
|
||||
|
||||
let MenuList=$(".BtnItem");
|
||||
let nAll=MenuList.length;
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
|
||||
[string]$ArtifactDirectory
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-RelativePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Root,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$rootWithSeparator = $Root.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
|
||||
$rootUri = [Uri]$rootWithSeparator
|
||||
$pathUri = [Uri]$Path
|
||||
|
||||
[Uri]::UnescapeDataString($rootUri.MakeRelativeUri($pathUri).ToString()).Replace("/", [IO.Path]::DirectorySeparatorChar)
|
||||
}
|
||||
|
||||
$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path
|
||||
$binaries = Get-ChildItem -LiteralPath $artifactRoot -Recurse -File |
|
||||
Where-Object { $_.Extension -in @(".exe", ".dll") } |
|
||||
Sort-Object -Property FullName
|
||||
|
||||
if (-not $binaries) {
|
||||
Write-Warning "No .exe or .dll files found under '$artifactRoot'."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$binaries | ForEach-Object {
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
|
||||
|
||||
[pscustomobject]@{
|
||||
RelativePath = Get-RelativePath -Root $artifactRoot -Path $_.FullName
|
||||
Status = $signature.Status
|
||||
SignatureType = $signature.SignatureType
|
||||
Signer = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { "" }
|
||||
SignerThumbprint = if ($signature.SignerCertificate) { $signature.SignerCertificate.Thumbprint } else { "" }
|
||||
Timestamped = [bool]$signature.TimeStamperCertificate
|
||||
TimeStamper = if ($signature.TimeStamperCertificate) { $signature.TimeStamperCertificate.Subject } else { "" }
|
||||
}
|
||||
} | Format-Table -AutoSize
|
||||
@@ -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
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
|
||||
[string]$ArtifactDirectory,
|
||||
|
||||
[string[]]$Files = @(
|
||||
"orca-slicer.exe",
|
||||
"OrcaSlicer.dll"
|
||||
),
|
||||
|
||||
[string]$SignToolPath,
|
||||
|
||||
# Accept signatures whose certificate chain terminates in an untrusted root.
|
||||
# Required for the SignPath test certificate (self-signed). Do NOT pass this
|
||||
# once a production CA-issued certificate is in use, so release builds enforce
|
||||
# a fully trusted chain.
|
||||
[switch]$AllowUntrustedRoot
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-SignToolPath {
|
||||
param(
|
||||
[string]$ExplicitPath
|
||||
)
|
||||
|
||||
if ($ExplicitPath) {
|
||||
if (Test-Path -LiteralPath $ExplicitPath -PathType Leaf) {
|
||||
return (Resolve-Path -LiteralPath $ExplicitPath).Path
|
||||
}
|
||||
|
||||
throw "SignTool was not found at '$ExplicitPath'."
|
||||
}
|
||||
|
||||
$fromPath = Get-Command -Name "signtool.exe" -ErrorAction SilentlyContinue
|
||||
if ($fromPath) {
|
||||
return $fromPath.Source
|
||||
}
|
||||
|
||||
$candidateRoots = @(
|
||||
"${env:ProgramFiles(x86)}\Windows Kits\10\bin",
|
||||
"${env:ProgramFiles}\Windows Kits\10\bin"
|
||||
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) }
|
||||
|
||||
foreach ($root in $candidateRoots) {
|
||||
$candidate = Get-ChildItem -LiteralPath $root -Recurse -Filter "signtool.exe" -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match "\\(x64|arm64)\\signtool\.exe$" } |
|
||||
Sort-Object -Property FullName -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($candidate) {
|
||||
return $candidate.FullName
|
||||
}
|
||||
}
|
||||
|
||||
throw "signtool.exe was not found. Install the Windows SDK or pass -SignToolPath."
|
||||
}
|
||||
|
||||
$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path
|
||||
$signtool = Resolve-SignToolPath -ExplicitPath $SignToolPath
|
||||
|
||||
Write-Host "Using SignTool: $signtool"
|
||||
Write-Host "Verifying Authenticode signatures in: $artifactRoot"
|
||||
|
||||
foreach ($relativePath in $Files) {
|
||||
$filePath = Join-Path $artifactRoot $relativePath
|
||||
if (-not (Test-Path -LiteralPath $filePath -PathType Leaf)) {
|
||||
throw "Expected signed file was not found: $filePath"
|
||||
}
|
||||
|
||||
Write-Host "Verifying $relativePath"
|
||||
|
||||
# Capture signtool output without letting native stderr (redirected via 2>&1)
|
||||
# raise a terminating NativeCommandError under $ErrorActionPreference = 'Stop'.
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
$output = & $signtool verify /pa /all /tw /v $filePath 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
|
||||
$output | ForEach-Object { Write-Host $_ }
|
||||
|
||||
if ($exitCode -eq 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($AllowUntrustedRoot) {
|
||||
# signtool interleaves its stdout (chain details) and stderr (the error)
|
||||
# unpredictably, so the error text cannot be matched reliably. Use the
|
||||
# structured Get-AuthenticodeSignature result instead: accept only when the
|
||||
# file is genuinely signed and the sole problem is that the chain terminates
|
||||
# in an untrusted root (i.e. the self-signed SignPath test certificate).
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $filePath
|
||||
$isSigned = ($signature.SignatureType -eq "Authenticode") -and ($null -ne $signature.SignerCertificate)
|
||||
$untrustedRootOnly = $signature.StatusMessage -match "terminated in a root certificate which is not trusted"
|
||||
if ($isSigned -and $untrustedRootOnly) {
|
||||
Write-Host " Accepted: '$relativePath' is signed but its certificate chains to an untrusted root (expected for the SignPath test certificate)."
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
throw "SignTool verification failed for '$relativePath' with exit code $exitCode."
|
||||
}
|
||||
|
||||
Write-Host "Authenticode verification passed."
|
||||
|
||||
# signtool exits non-zero for the untrusted test-cert root even when we accept
|
||||
# it above, leaving $LASTEXITCODE = 1. The GitHub Actions pwsh wrapper exits
|
||||
# with that lingering code, so reset it to report success explicitly.
|
||||
exit 0
|
||||
@@ -1,22 +0,0 @@
|
||||
# SignPath configurations
|
||||
|
||||
This directory contains SignPath artifact configurations used by GitHub Actions.
|
||||
|
||||
## `windows-portable-v1`
|
||||
|
||||
`windows-portable-v1.xml` is the initial conservative Windows portable-bundle signing configuration. It signs only the two first-party binaries:
|
||||
|
||||
- `orca-slicer.exe`
|
||||
- `OrcaSlicer.dll`
|
||||
|
||||
Do not broaden this to all DLLs without first confirming ownership, provenance, and whether upstream vendor signatures should be verified instead.
|
||||
|
||||
The Windows workflow uploads `${{ env.BUILD_DIR }}/OrcaSlicer` with `actions/upload-artifact`. GitHub stores that artifact as a ZIP, and the uploaded directory contents are rooted at the ZIP root. Because of that, the SignPath configuration uses `<zip-file>` with `orca-slicer.exe` and `OrcaSlicer.dll` directly beneath it.
|
||||
|
||||
The release portable ZIP is a separate archive created with 7-Zip from `${{ env.BUILD_DIR }}/OrcaSlicer`; that archive keeps the top-level `OrcaSlicer/` folder. After SignPath returns the signed artifact, the workflow copies the signed files back into `${{ env.BUILD_DIR }}/OrcaSlicer` and recreates the portable release ZIP so the public ZIP layout stays unchanged. It also copies them over the build output in `${{ env.BUILD_DIR }}/src/Release`, because `cpack` packages the installer from there rather than from the portable bundle.
|
||||
|
||||
## `windows-installer-v1`
|
||||
|
||||
`windows-installer-v1.xml` signs the NSIS installer that `cpack` produces, `OrcaSlicer_Windows_Installer_<version>_<arch>.exe`. The workflow uploads that single file, so it sits at the root of the artifact ZIP. The installer is built after the binaries are signed, so the files it installs carry the `windows-portable-v1` signatures.
|
||||
|
||||
Both configurations must also be created in the SignPath project under the same slugs; the workflow refers to them only by slug.
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||
<zip-file>
|
||||
<pe-file path="OrcaSlicer_Windows_Installer_*.exe">
|
||||
<authenticode-sign />
|
||||
</pe-file>
|
||||
</zip-file>
|
||||
</artifact-configuration>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||
<zip-file>
|
||||
<pe-file path="orca-slicer.exe">
|
||||
<authenticode-sign />
|
||||
</pe-file>
|
||||
|
||||
<pe-file path="OrcaSlicer.dll">
|
||||
<authenticode-sign />
|
||||
</pe-file>
|
||||
</zip-file>
|
||||
</artifact-configuration>
|
||||
+16
-1
@@ -144,6 +144,7 @@
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/GCode/ToolOrdering.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
@@ -207,6 +208,7 @@ std::map<int, std::string> cli_errors = {
|
||||
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
|
||||
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
|
||||
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
|
||||
{CLI_EXPORT_STATISTICS_ERROR, "Failed exporting the G-code statistics."},
|
||||
{CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."},
|
||||
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
|
||||
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
|
||||
@@ -1539,6 +1541,9 @@ int CLI::run(int argc, char **argv)
|
||||
if (allow_mix_temp_option)
|
||||
allow_mix_temp = allow_mix_temp_option->value;
|
||||
|
||||
const ConfigOptionBool* export_statistics_option = m_config.option<ConfigOptionBool>("export_statistics");
|
||||
const bool export_statistics = export_statistics_option != nullptr && export_statistics_option->value;
|
||||
|
||||
ConfigOptionBool* avoid_extrusion_cali_region_option = m_config.option<ConfigOptionBool>("avoid_extrusion_cali_region");
|
||||
if (avoid_extrusion_cali_region_option)
|
||||
avoid_extrusion_cali_region = avoid_extrusion_cali_region_option->value;
|
||||
@@ -7081,6 +7086,16 @@ int CLI::run(int argc, char **argv)
|
||||
record_exit_reson(outfile_dir, CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, index + 1, cli_errors[CLI_GCODE_PATH_IN_UNPRINTABLE_AREA], sliced_info);
|
||||
flush_and_exit(CLI_GCODE_PATH_IN_UNPRINTABLE_AREA);
|
||||
}
|
||||
// Without an output directory the G-code is a temporary file, kept only in an exported 3MF.
|
||||
if (export_statistics && !outfile_dir.empty()) {
|
||||
try {
|
||||
export_gcode_statistics(*gcode_result, gcode_statistics_path(outfile));
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "plate " << index + 1 << ": " << ex.what();
|
||||
record_exit_reson(outfile_dir, CLI_EXPORT_STATISTICS_ERROR, index + 1, cli_errors[CLI_EXPORT_STATISTICS_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_EXPORT_STATISTICS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//outfile_final = (dynamic_cast<Print*>(print))->print_statistics().finalize_output_path(outfile);
|
||||
@@ -7209,7 +7224,7 @@ int CLI::run(int argc, char **argv)
|
||||
std::vector<ThumbnailData *> thumbnails, no_light_thumbnails, top_thumbnails, pick_thumbnails;
|
||||
std::vector<PlateBBoxData*> plate_bboxes;
|
||||
PlateDataPtrs plate_data_list;
|
||||
partplate_list.store_to_3mf_structure(plate_data_list);
|
||||
partplate_list.store_to_3mf_structure(plate_data_list, true, -1, export_statistics);
|
||||
|
||||
if (sliced_plate == -1) {
|
||||
for (int i = 0; i < plate_data_list.size(); i++) {
|
||||
|
||||
@@ -399,6 +399,9 @@ void AppConfig::set_defaults()
|
||||
if (get("show_plate_gridlines").empty())
|
||||
set_bool("show_plate_gridlines", true);
|
||||
|
||||
if (get("focus_mode").empty())
|
||||
set_bool("focus_mode", false);
|
||||
|
||||
if (get("show_outline").empty())
|
||||
set_bool("show_outline", false);
|
||||
|
||||
|
||||
@@ -265,6 +265,8 @@ set(lisbslic3r_sources
|
||||
GCode/FanMover.hpp
|
||||
GCode/GCodeProcessor.cpp
|
||||
GCode/GCodeProcessor.hpp
|
||||
GCode/GCodeStatistics.cpp
|
||||
GCode/GCodeStatistics.hpp
|
||||
GCode/ElegooGCodeProcessorHelper.cpp
|
||||
GCode.hpp
|
||||
GCode/PchipInterpolatorHelper.cpp
|
||||
|
||||
@@ -9053,6 +9053,16 @@ bool _BBS_3MF_Exporter::_add_gcode_file_to_archive(mz_zip_archive& archive, cons
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", store %1% to 3mf %2%\n") % src_gcode_file % gcode_in_3mf;
|
||||
}
|
||||
});
|
||||
for (const PlateData *plate_data : plate_data_list2) {
|
||||
if (plate_data->statistics.empty())
|
||||
continue;
|
||||
const std::string statistics_in_3mf = (boost::format(STATISTICS_FILE_FORMAT) % (plate_data->plate_index + 1)).str();
|
||||
if (!mz_zip_writer_add_mem(&archive, statistics_in_3mf.c_str(), plate_data->statistics.data(), plate_data->statistics.size(),
|
||||
MZ_DEFAULT_COMPRESSION)) {
|
||||
add_error("Unable to add statistics file to archive");
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ struct ThumbnailData;
|
||||
#define PLATE_THUMBNAIL_SMALL_HEIGHT 128
|
||||
|
||||
#define GCODE_FILE_FORMAT "Metadata/plate_%1%.gcode"
|
||||
#define STATISTICS_FILE_FORMAT "Metadata/plate_%1%.statistics.json"
|
||||
#define THUMBNAIL_FILE_FORMAT "Metadata/plate_%1%.png"
|
||||
#define NO_LIGHT_THUMBNAIL_FILE_FORMAT "Metadata/plate_no_light_%1%.png"
|
||||
#define TOP_FILE_FORMAT "Metadata/top_%1%.png"
|
||||
@@ -100,6 +101,8 @@ struct PlateData
|
||||
std::string nozzle_volume_types;
|
||||
std::string gcode_file;
|
||||
std::string gcode_file_md5;
|
||||
// The G-code's statistics as JSON, stored with the G-code when not empty.
|
||||
std::string statistics;
|
||||
std::string thumbnail_file;
|
||||
std::string no_light_thumbnail_file;
|
||||
ThumbnailData plate_thumbnail;
|
||||
@@ -174,6 +177,8 @@ enum class SaveStrategy
|
||||
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
|
||||
// production-extension value.
|
||||
MinimalPublished = 1 << 13,
|
||||
// With WithGcode, also the G-code's statistics.
|
||||
WithStatistics = 1 << 14,
|
||||
|
||||
SplitModel = 0x1000 | ProductionExt,
|
||||
Encrypted = SecureContentExt | SplitModel,
|
||||
|
||||
+20
-11
@@ -2625,8 +2625,15 @@ static void set_mass_locator(GCodeProcessor &processor, const Print &print)
|
||||
}
|
||||
Object &o = objects.emplace_back(Object{ object, int(object_masses.size()), count, bodies_total, {}, std::move(bodies), {}, {} });
|
||||
object_masses.resize(object_masses.size() + object->instances().size());
|
||||
for (size_t instance = 0; instance < object->instances().size(); ++instance)
|
||||
object_masses[o.first_instance + instance].assembly = assembly;
|
||||
const ModelInstancePtrs &model_instances = object->model_object()->instances;
|
||||
for (size_t instance = 0; instance < object->instances().size(); ++instance) {
|
||||
GCodeProcessorResult::ObjectMass &mass = object_masses[o.first_instance + instance];
|
||||
mass.assembly = assembly;
|
||||
mass.name = object->model_object()->name;
|
||||
// Copies of another rotation or scale slice as objects of their own, so number them in the model object.
|
||||
mass.instance = int(std::find(model_instances.begin(), model_instances.end(), object->instances()[instance].model_instance) -
|
||||
model_instances.begin());
|
||||
}
|
||||
bodies_total += int(count * object->instances().size());
|
||||
for (const Layer *layer : layers) {
|
||||
o.print_zs.emplace_back(layer->print_z);
|
||||
@@ -3563,6 +3570,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
|
||||
print.throw_if_canceled();
|
||||
|
||||
// Taken before the object loop, which reassigns initial_extruder_id when printing by object.
|
||||
const int compat_first_layer_bed_temp = m_config.bed_temperature_formula == BedTempFormula::btfHighestTemp ?
|
||||
get_highest_bed_temperature(true, print) :
|
||||
get_bed_temperature(initial_extruder_id, true, print.config().curr_bed_type);
|
||||
const int compat_first_layer_nozzle_temp =
|
||||
print.config().nozzle_temperature_initial_layer.get_at(get_filament_config_index((int) initial_extruder_id));
|
||||
|
||||
int extruder_id = get_extruder_id(initial_extruder_id);
|
||||
|
||||
m_cooling_buffer = make_unique<CoolingBuffer>(*this);
|
||||
@@ -4153,12 +4167,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple image
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n",
|
||||
first_layer_bed_temperature);
|
||||
file.write_format(
|
||||
"; first_layer_temperature = %d\n",
|
||||
print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
|
||||
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
|
||||
file.write("; CONFIG_BLOCK_END\n\n");
|
||||
} else if (thumbnail_cb != nullptr) {
|
||||
// generate the thumbnails
|
||||
@@ -4743,10 +4753,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple info
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
|
||||
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
|
||||
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
|
||||
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
|
||||
|
||||
//SF TODO
|
||||
|
||||
@@ -298,6 +298,9 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
BoundingBoxf3 box;
|
||||
// Of an object, whether it is an assembly.
|
||||
bool assembly{ false };
|
||||
// Of an object instance, its model object's name and its index among that object's instances.
|
||||
std::string name;
|
||||
int instance{ -1 };
|
||||
|
||||
Sum total() const { return printed_up_to_layer.empty() ? Sum{} : printed_up_to_layer.back(); }
|
||||
void add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "GCodeStatistics.hpp"
|
||||
|
||||
#include "GCodeProcessor.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <ios>
|
||||
#include <map>
|
||||
#include <math.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::ordered_json;
|
||||
|
||||
double rounded(double value, int decimals)
|
||||
{
|
||||
const double scale = std::pow(10., decimals);
|
||||
return std::round(value * scale) / scale;
|
||||
}
|
||||
|
||||
// As the object masses weigh it, at the default density for a filament without one.
|
||||
double weight_of(const GCodeProcessorResult &result, size_t filament, double volume)
|
||||
{
|
||||
const bool has_density = filament < result.filament_densities.size() && result.filament_densities[filament] > 0.f;
|
||||
// mm³ at g/cm³ weigh mg.
|
||||
return 0.001 * volume * (has_density ? result.filament_densities[filament] : DEFAULT_FILAMENT_DENSITY);
|
||||
}
|
||||
|
||||
struct Usage
|
||||
{
|
||||
double time{ 0. };
|
||||
double volume{ 0. };
|
||||
double weight{ 0. };
|
||||
|
||||
void add(const Usage &other)
|
||||
{
|
||||
time += other.time;
|
||||
volume += other.volume;
|
||||
weight += other.weight;
|
||||
}
|
||||
|
||||
json to_json() const { return json{ { "time", rounded(time, 3) }, { "volume", rounded(volume, 3) }, { "weight", rounded(weight, 4) } }; }
|
||||
};
|
||||
|
||||
json features_json(const std::map<ExtrusionRole, Usage> &features)
|
||||
{
|
||||
json out = json::object();
|
||||
for (const auto &[role, usage] : features)
|
||||
out[ExtrusionEntity::role_to_string(role)] = usage.to_json();
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string gcode_statistics_json(const GCodeProcessorResult &result)
|
||||
{
|
||||
constexpr size_t normal = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
|
||||
struct Layer
|
||||
{
|
||||
float z{ 0.f };
|
||||
double travel_time{ 0. };
|
||||
Usage total;
|
||||
std::map<ExtrusionRole, Usage> features;
|
||||
};
|
||||
std::vector<Layer> layers;
|
||||
std::map<ExtrusionRole, Usage> features;
|
||||
double travel_time = 0.;
|
||||
|
||||
// As the preview sums them: a layer takes the time of all its moves, a feature that of its extrusions.
|
||||
for (const GCodeProcessorResult::MoveVertex &move : result.moves) {
|
||||
if (layers.size() <= move.layer_id)
|
||||
layers.resize(move.layer_id + 1);
|
||||
Layer &layer = layers[move.layer_id];
|
||||
const double time = move.time[normal];
|
||||
layer.total.time += time;
|
||||
if (move.type == EMoveType::Travel) {
|
||||
layer.travel_time += time;
|
||||
travel_time += time;
|
||||
}
|
||||
// A vertex added inside a move to show its acceleration repeats the move's extrusion.
|
||||
if (move.type != EMoveType::Extrude || move.internal_only)
|
||||
continue;
|
||||
if (move.extrusion_role != erCustom)
|
||||
layer.z = move.position.z();
|
||||
Usage usage;
|
||||
usage.time = time;
|
||||
usage.volume = double(move.delta_extruder) * M_PI * sqr(0.5 * double(result.filament_diameters[move.extruder_id]));
|
||||
usage.weight = weight_of(result, move.extruder_id, usage.volume);
|
||||
layer.total.volume += usage.volume;
|
||||
layer.total.weight += usage.weight;
|
||||
layer.features[move.extrusion_role].add(usage);
|
||||
features[move.extrusion_role].add(usage);
|
||||
}
|
||||
|
||||
// As the preview's filament table, which counts the flush too.
|
||||
const PrintEstimatedStatistics &stats = result.print_statistics;
|
||||
double volume = 0., weight = 0., cost = 0.;
|
||||
json filaments_json = json::array();
|
||||
for (const auto &entry : stats.total_volumes_per_extruder) {
|
||||
const size_t filament = entry.first;
|
||||
const auto use = [&result, filament](const std::map<size_t, double> &volumes) {
|
||||
const auto it = volumes.find(filament);
|
||||
const double used = it == volumes.end() ? 0. : it->second;
|
||||
return json{ { "volume", rounded(used, 3) }, { "weight", rounded(weight_of(result, filament, used), 4) } };
|
||||
};
|
||||
const double filament_weight = weight_of(result, filament, entry.second);
|
||||
// Priced per kilogram.
|
||||
const double filament_cost = filament < result.filament_costs.size() ? 0.001 * filament_weight * result.filament_costs[filament] : 0.;
|
||||
volume += entry.second;
|
||||
weight += filament_weight;
|
||||
cost += filament_cost;
|
||||
filaments_json.push_back(json{ { "id", filament + 1 },
|
||||
{ "length", rounded(entry.second / (M_PI * sqr(0.5 * double(result.filament_diameters[filament]))), 2) },
|
||||
{ "volume", rounded(entry.second, 3) },
|
||||
{ "weight", rounded(filament_weight, 4) },
|
||||
{ "cost", rounded(filament_cost, 2) },
|
||||
{ "model", use(stats.model_volumes_per_extruder) },
|
||||
{ "support", use(stats.support_volumes_per_extruder) },
|
||||
{ "prime_tower", use(stats.wipe_tower_volumes_per_extruder) },
|
||||
{ "flush", use(stats.flush_per_filament) } });
|
||||
}
|
||||
|
||||
json summary = json{ { "time", rounded(stats.modes[normal].time, 3) },
|
||||
{ "prepare_time", rounded(stats.modes[normal].prepare_time, 3) },
|
||||
{ "travel_time", rounded(travel_time, 3) },
|
||||
{ "travel_distance", rounded(stats.total_travel_distance, 2) },
|
||||
{ "layer_count", layers.size() },
|
||||
{ "volume", rounded(volume, 3) },
|
||||
{ "weight", rounded(weight, 4) },
|
||||
{ "filament_cost", rounded(cost, 2) },
|
||||
{ "filament_changes", stats.total_filament_changes },
|
||||
{ "filaments", filaments_json },
|
||||
{ "features", features_json(features) } };
|
||||
|
||||
json layers_json = json::array();
|
||||
for (const Layer &layer : layers)
|
||||
layers_json.push_back(json{ { "z", rounded(layer.z, 3) },
|
||||
{ "time", rounded(layer.total.time, 3) },
|
||||
{ "travel_time", rounded(layer.travel_time, 3) },
|
||||
{ "volume", rounded(layer.total.volume, 3) },
|
||||
{ "weight", rounded(layer.total.weight, 4) },
|
||||
{ "features", features_json(layer.features) } });
|
||||
|
||||
// The supports of an instance are numbered as the instance.
|
||||
json objects_json = json::array();
|
||||
for (size_t i = 0; i < result.object_masses.size(); ++i) {
|
||||
const GCodeProcessorResult::ObjectMass &object = result.object_masses[i];
|
||||
const GCodeProcessorResult::ObjectMass::Sum part = object.total();
|
||||
const GCodeProcessorResult::ObjectMass::Sum support = i < result.support_masses.size() ? result.support_masses[i].total() :
|
||||
GCodeProcessorResult::ObjectMass::Sum{};
|
||||
// In mg, as volumes in mm³ at g/cm³.
|
||||
objects_json.push_back(json{ { "name", object.name },
|
||||
{ "instance", object.instance },
|
||||
{ "volume", rounded(part.volume, 3) },
|
||||
{ "weight", rounded(0.001 * part.mass, 4) },
|
||||
{ "support_volume", rounded(support.volume, 3) },
|
||||
{ "support_weight", rounded(0.001 * support.mass, 4) } });
|
||||
}
|
||||
|
||||
// The version goes up only when a key is removed, renamed or changes meaning.
|
||||
const json out = json{ { "version", 1 },
|
||||
{ "generator", std::string(SLIC3R_APP_NAME) + " " + SoftFever_VERSION },
|
||||
{ "units", json{ { "time", "s" }, { "length", "mm" }, { "volume", "mm3" }, { "weight", "g" } } },
|
||||
{ "summary", summary },
|
||||
{ "layers", layers_json },
|
||||
{ "objects", objects_json } };
|
||||
return out.dump(2);
|
||||
}
|
||||
|
||||
std::string gcode_statistics_path(const std::string &gcode_path)
|
||||
{
|
||||
return boost::filesystem::path(gcode_path).replace_extension(".statistics.json").string();
|
||||
}
|
||||
|
||||
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path)
|
||||
{
|
||||
// Binary, so the file reads the same on every platform and inside a 3MF.
|
||||
boost::nowide::ofstream file(path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
file << gcode_statistics_json(result);
|
||||
file.close();
|
||||
if (file.fail())
|
||||
throw ExportError(format(_u8L("Failed to write the G-code statistics to %1%."), path));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct GCodeProcessorResult;
|
||||
|
||||
// The statistics of a plate's G-code as JSON, in seconds, millimeters and grams: totals, per layer and per object instance.
|
||||
std::string gcode_statistics_json(const GCodeProcessorResult &result);
|
||||
// Where the statistics of the G-code at gcode_path are written.
|
||||
std::string gcode_statistics_path(const std::string &gcode_path);
|
||||
// Throws ExportError when the file cannot be written.
|
||||
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -4607,6 +4607,16 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
maps.erase(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_placeholder) {
|
||||
// Orca: an empty AMS slot is a placeholder, not a filament - never synthesize a
|
||||
// preset for it. Kept here only to keep index alignment with ams_infos; the sync
|
||||
// drops it so no filament slot is created or retained for an empty tray.
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
} else if (use_map) {
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
@@ -4615,12 +4625,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
filament_multi_color.push_back(default_unknown_color);
|
||||
}
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
} else if (is_placeholder) {
|
||||
// Orca: push placeholders to keep index alignment with ams_infos
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -4984,6 +4988,10 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
result_multi_colors.push_back(
|
||||
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
|
||||
: std::vector<std::string>{ams_filament_colors[i]});
|
||||
} else if (i < ams_infos.size() && ams_infos[i].is_placeholder) {
|
||||
// Orca: an empty AMS slot is a placeholder, not a filament, so drop it instead of
|
||||
// keeping or filling a slot for it. The device AMS panel still shows the empty tray.
|
||||
continue;
|
||||
} else if (i < exist_presets.size()) {
|
||||
// Empty tray or beyond tray count: keep existing filament
|
||||
result_colors.push_back(exist_colors[i]);
|
||||
@@ -5008,8 +5016,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
filament_color_type->values = result_color_types;
|
||||
this->filament_presets = result_presets;
|
||||
ams_multi_color_filment = result_multi_colors;
|
||||
filament_map->values.resize(total, 1);
|
||||
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
filament_map->values.resize(result_presets.size(), 1);
|
||||
filament_volume_map->values.resize(result_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
} else {
|
||||
// BBL: existing wholesale replace
|
||||
filament_color->values = ams_filament_colors;
|
||||
|
||||
@@ -13040,6 +13040,13 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def->cli_params = "dir";
|
||||
def->set_default_value(new ConfigOptionString());
|
||||
|
||||
def = this->add("export_statistics", coBool);
|
||||
def->label = L("Export statistics");
|
||||
def->tooltip = L("Write the statistics of each sliced plate as JSON, as plate_N.statistics.json next to its G-code in the output "
|
||||
"directory and inside the 3MF of --export-3mf: print time, filament and feature totals, and the same per layer "
|
||||
"and per object.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("debug", coInt);
|
||||
def->label = L("Debug level");
|
||||
def->tooltip = L("Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:trace\n");
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
#define CLI_OBJECT_ORIENT_FAILED -22
|
||||
#define CLI_MODIFIED_PARAMS_TO_PRINTER -23
|
||||
#define CLI_FILE_VERSION_NOT_SUPPORTED -24
|
||||
#define CLI_EXPORT_STATISTICS_ERROR -25
|
||||
|
||||
|
||||
#define CLI_NO_SUITABLE_OBJECTS -50
|
||||
|
||||
@@ -129,6 +129,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Shortcuts.hpp
|
||||
GUI/SpeedDialDialog.cpp
|
||||
GUI/SpeedDialDialog.hpp
|
||||
GUI/ColorPickerDialog.cpp
|
||||
GUI/ColorPickerDialog.hpp
|
||||
GUI/ActionRegistry.cpp
|
||||
GUI/ActionRegistry.hpp
|
||||
GUI/NativeCommands.cpp
|
||||
|
||||
@@ -297,6 +297,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
|
||||
, printable(true)
|
||||
, visible(true)
|
||||
, is_active(true)
|
||||
, on_hidden_plate(false)
|
||||
, zoom_to_volumes(true)
|
||||
, shader_outside_printer_detection_enabled(false)
|
||||
, is_outside(false)
|
||||
@@ -824,6 +825,8 @@ bool GLVolume::is_below_printbed() const
|
||||
|
||||
void GLVolume::render_sinking_contours()
|
||||
{
|
||||
if (!is_active)
|
||||
return;
|
||||
m_sinking_contours.render();
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,8 @@ public:
|
||||
bool visible : 1;
|
||||
// Whether or not this volume is active for rendering
|
||||
bool is_active : 1;
|
||||
// Whether or not is_active was cleared because the other plates are hidden
|
||||
bool on_hidden_plate : 1;
|
||||
// Whether or not to use this volume when applying zoom_to_volumes()
|
||||
bool zoom_to_volumes : 1;
|
||||
// Wheter or not this volume is enabled for outside print volume detection in shader.
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
#include "AMSMaterialsSetting.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <cstdint>
|
||||
#include "ExtrusionCalibration.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -36,11 +41,9 @@
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
|
||||
#include "slic3r/GUI/Widgets/StaticBox.hpp"
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
@@ -1963,11 +1966,6 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
sizer_custom->Add(m_ts_stbitmap_custom, 0, wxEXPAND, 0);
|
||||
m_custom_cp->Layout();
|
||||
|
||||
m_clrData = new wxColourData();
|
||||
m_clrData->SetChooseFull(true);
|
||||
m_clrData->SetChooseAlpha(false);
|
||||
|
||||
|
||||
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
|
||||
m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
@@ -1991,46 +1989,34 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
wxGetApp().UpdateDarkUIWin(this);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& event)
|
||||
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
|
||||
{
|
||||
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
|
||||
const bool was_shown = IsShown();
|
||||
std::optional<wxColour> picker_color;
|
||||
{
|
||||
const ColorRGBA initial(m_def_col.Red(), m_def_col.Green(), m_def_col.Blue(), static_cast<unsigned char>(255));
|
||||
// Snapshot the custom control's anchor before releasing the transient popup grab.
|
||||
ColorPickerDialog dialog(GetParent(), initial, {}, false, m_custom_cp);
|
||||
Dismiss();
|
||||
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
|
||||
const auto& result = std::get<ColorRGBA>(*dialog.selection());
|
||||
picker_color = wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.b() * 255.f)));
|
||||
}
|
||||
} // Destroy the modal before restoring the first-level popup and its focus/grab.
|
||||
if (!picker_color) {
|
||||
if (was_shown) Popup();
|
||||
return;
|
||||
}
|
||||
auto clr_dialog = new wxColourDialog(nullptr, m_clrData);
|
||||
wxColour picker_color;
|
||||
|
||||
if (clr_dialog->ShowModal() == wxID_OK) {
|
||||
m_clrData = &(clr_dialog->GetColourData());
|
||||
if (colors.size() != CUSTOM_COLOR_COUNT) {
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
}
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
|
||||
}
|
||||
wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
|
||||
picker_color = wxColour(
|
||||
m_clrData->GetColour().Red(),
|
||||
m_clrData->GetColour().Green(),
|
||||
m_clrData->GetColour().Blue(),
|
||||
255
|
||||
);
|
||||
|
||||
if (picker_color.Alpha() == 0) {
|
||||
m_ts_stbitmap_custom->Show();
|
||||
}
|
||||
else {
|
||||
m_ts_stbitmap_custom->Hide();
|
||||
m_custom_cp->SetBackgroundColor(picker_color);
|
||||
}
|
||||
|
||||
set_def_colour(picker_color);
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
unsigned long g_col = ((picker_color.Red() & 0xff) << 24) + ((picker_color.Green() & 0xff) << 16) + ((picker_color.Blue() & 0xff) << 8) + (picker_color.Alpha() & 0xff);
|
||||
evt.SetInt(g_col);
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
set_def_colour(*picker_color);
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
const std::uint32_t packed = (std::uint32_t(picker_color->Red()) << 24) |
|
||||
(std::uint32_t(picker_color->Green()) << 16) |
|
||||
(std::uint32_t(picker_color->Blue()) << 8) | 255u;
|
||||
evt.SetInt(static_cast<int>(packed));
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
|
||||
@@ -85,7 +85,6 @@ public:
|
||||
ScalableBitmap m_ts_bitmap_custom;
|
||||
wxStaticBitmap* m_ts_stbitmap_custom;
|
||||
StaticBox* m_custom_cp;
|
||||
wxColourData* m_clrData;
|
||||
StaticBox* m_def_color_box;
|
||||
wxFlexGridSizer* m_ams_fg_sizer;
|
||||
wxColour m_def_col;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -57,6 +58,7 @@
|
||||
#include "libslic3r_version.h"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/Utils/PrintHost.hpp"
|
||||
@@ -781,6 +783,7 @@ void BackgroundSlicingProcess::schedule_export(const std::string& path, bool exp
|
||||
this->invalidate_step(bspsGCodeFinalize);
|
||||
m_export_path = path;
|
||||
m_export_path_on_removable_media = export_path_on_removable_media;
|
||||
m_export_statistics = GUI::wxGetApp().app_config->get_bool("export_statistics");
|
||||
}
|
||||
|
||||
void BackgroundSlicingProcess::schedule_upload(Slic3r::PrintHostJob upload_job)
|
||||
@@ -906,6 +909,8 @@ void BackgroundSlicingProcess::finalize_gcode()
|
||||
BOOST_LOG_TRIVIAL(error) << "Unexpected fail code(" << (int) copy_ret_val << ") durring copy_file() to " << export_path << ".";
|
||||
break;
|
||||
}
|
||||
if (m_export_statistics)
|
||||
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
|
||||
|
||||
m_print->set_status(100, GUI::format(_L("G-code file exported to %1%"), export_path));
|
||||
}
|
||||
@@ -953,6 +958,8 @@ void BackgroundSlicingProcess::export_gcode()
|
||||
// throw Slic3r::ExportError(_utf8(L("Unknown error when exporting G-code.")));
|
||||
break;
|
||||
}
|
||||
if (m_export_statistics)
|
||||
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
|
||||
|
||||
// BBS
|
||||
auto evt = new wxCommandEvent(m_event_export_finished_id, GUI::wxGetApp().mainframe->m_plater->GetId());
|
||||
|
||||
@@ -258,6 +258,8 @@ private:
|
||||
// but once set, it cannot be re-set.
|
||||
std::string m_export_path;
|
||||
bool m_export_path_on_removable_media = false;
|
||||
// Read from the preferences when the export is scheduled, as the background thread may not read them.
|
||||
bool m_export_statistics = false;
|
||||
// Print host upload job to schedule after slicing is complete, used by schedule_upload(),
|
||||
// empty by default (ie. no upload to schedule)
|
||||
PrintHostJob m_upload_job;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <variant>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <wx/gdicmn.h>
|
||||
|
||||
class wxWindow;
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Most colors an ordered gradient may hold. This is the native switch for multi-color gradients:
|
||||
// 2 keeps the classic two-color gradients. The page receives it as options.max_gradient_colors
|
||||
// and never offers more, so the picker, favorites and project colors all share one limit.
|
||||
inline constexpr std::size_t COLOR_PICKER_MAX_GRADIENT_COLORS = 7;
|
||||
|
||||
// Gradient colors retain their order (2..COLOR_PICKER_MAX_GRADIENT_COLORS); a solid has exactly one color.
|
||||
using ColorGradient = std::vector<ColorRGBA>;
|
||||
using ColorSelection = std::variant<ColorRGBA, ColorGradient>;
|
||||
|
||||
struct ColorPickerOptions {
|
||||
bool allow_gradient = false;
|
||||
bool allow_alpha = false;
|
||||
};
|
||||
|
||||
// Project storage preserves direction; FilamentColor's set is only for the
|
||||
// official palette and cannot represent ordered custom gradients.
|
||||
struct FilamentColorPickerValue {
|
||||
std::vector<std::string> colors;
|
||||
bool gradient = false;
|
||||
std::string primary;
|
||||
};
|
||||
|
||||
std::optional<ColorSelection> filament_color_picker_selection(const FilamentColorPickerValue& value);
|
||||
ColorSelection filament_color_picker_initial(const FilamentColorPickerValue& value);
|
||||
FilamentColorPickerValue filament_color_picker_result(const ColorSelection& selection);
|
||||
bool filament_color_picker_changed(const ColorSelection& initial, const std::optional<ColorSelection>& result);
|
||||
|
||||
class ColorPickerDialog : public WebViewHostDialog
|
||||
{
|
||||
public:
|
||||
ColorPickerDialog(wxWindow* parent, const ColorSelection& initial, ColorPickerOptions options = {},
|
||||
bool preserve_multi_color = false, wxWindow* anchor = nullptr);
|
||||
~ColorPickerDialog() override;
|
||||
|
||||
const std::optional<ColorSelection>& selection() const { return m_selection; }
|
||||
|
||||
private:
|
||||
void add_user_scripts() override;
|
||||
void on_script_message(const nlohmann::json& payload) override;
|
||||
void handle_web_command(const nlohmann::json& payload);
|
||||
void send_initial_state();
|
||||
void finish(int return_code);
|
||||
void position_panel();
|
||||
void move_to_anchor();
|
||||
void resize_to_content(int height);
|
||||
void focus_webview();
|
||||
void repaint_webview();
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
void on_sys_color_changed() override;
|
||||
|
||||
ColorSelection m_initial;
|
||||
ColorPickerOptions m_options;
|
||||
std::optional<ColorSelection> m_selection;
|
||||
std::vector<ColorSelection> m_favorites;
|
||||
std::string m_page_id;
|
||||
bool m_preserve_multi_color = false;
|
||||
bool m_init_sent = false;
|
||||
bool m_page_ready = false;
|
||||
bool m_closing = false;
|
||||
bool m_favorites_writable = false;
|
||||
bool m_sidebar_open = false;
|
||||
std::string m_palette;
|
||||
bool m_initial_positioned = false;
|
||||
wxRect m_anchor_rect;
|
||||
wxRect m_work_area;
|
||||
int m_content_height = 360;
|
||||
int m_content_width = 330;
|
||||
bool m_positioning = false;
|
||||
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -163,8 +163,8 @@ public:
|
||||
~devPrinterUtil() = delete;
|
||||
|
||||
public:
|
||||
static bool IsVirtualSlot(int ams_id) { return (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID);}
|
||||
static bool IsVirtualSlot(const std::string& ams_id) { return (ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR); }
|
||||
static bool IsVirtualSlot(int ams_id);
|
||||
static bool IsVirtualSlot(const std::string& ams_id);
|
||||
};
|
||||
|
||||
namespace GUI
|
||||
@@ -198,3 +198,6 @@ template<> struct std::hash<NozzleDef>
|
||||
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
|
||||
};
|
||||
};
|
||||
|
||||
// key(extruder_id) -> { key1(nozzle type info), val1( number of the nozzle type)}
|
||||
using ExtruderNozzleInfos = std::unordered_map<int, std::unordered_map<NozzleDef, int>>;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
@@ -33,6 +34,36 @@
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool devPrinterUtil::IsVirtualSlot(int ams_id)
|
||||
{
|
||||
// When the app or preset bundle is unavailable we cannot know the vendor's
|
||||
// extruder count, so fall back deterministically to the fixed Bambu pair
|
||||
// {255, 254} rather than guessing from an unknown generic layout.
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr || GUI::wxGetApp().preset_bundle->is_bbl_vendor())
|
||||
return ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID;
|
||||
|
||||
const int extruder_count = GUI::wxGetApp().preset_bundle->get_printer_extruder_count();
|
||||
return ams_id >= VIRTUAL_TRAY_MAIN_ID - extruder_count + 1 && ams_id <= VIRTUAL_TRAY_MAIN_ID;
|
||||
}
|
||||
|
||||
bool devPrinterUtil::IsVirtualSlot(const std::string& ams_id)
|
||||
{
|
||||
// Strict: only a fully numeric string is a valid slot id. std::stoi alone
|
||||
// would accept trailing garbage (e.g. "255abc" -> 255).
|
||||
if (ams_id.empty())
|
||||
return false;
|
||||
for (char c : ams_id) {
|
||||
if (c < '0' || c > '9')
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return IsVirtualSlot(std::stoi(ams_id));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c - '0' : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1; }
|
||||
|
||||
wxColour DevAmsTray::decode_color(const std::string &color)
|
||||
@@ -200,18 +231,22 @@ wxString DevAms::GetDisplayName() const
|
||||
|
||||
int DevAms::GetSlotCount() const
|
||||
{
|
||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
||||
auto ams_type = GetAmsType();
|
||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
else if (ams_type == N3S)
|
||||
{
|
||||
// N3S is a 1-slot Bambu contract, so report 1 regardless of whether the
|
||||
// preset bundle is momentarily unavailable (m_trays.size() can be 0).
|
||||
if (GetAmsType() == N3S)
|
||||
return 1;
|
||||
|
||||
if (wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
||||
auto ams_type = GetAmsType();
|
||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
return static_cast<int>(m_trays.size());
|
||||
}
|
||||
|
||||
DevAmsTray* DevAms::GetTray(const std::string& tray_id) const
|
||||
@@ -302,13 +337,21 @@ int DevFilaSystem::GetExtruderIdByAmsId(const std::string& ams_id) const
|
||||
{
|
||||
return it->second->GetExtruderId();
|
||||
}
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_MAIN_ID)
|
||||
{
|
||||
return MAIN_EXTRUDER_ID;
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (!is_bbl_vendor && GetOwner()) {
|
||||
for (const auto& tray : GetOwner()->vt_slot) {
|
||||
if (tray.id == ams_id)
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
}
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
{
|
||||
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_MAIN_ID_STR)
|
||||
return MAIN_EXTRUDER_ID;
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
if (!is_bbl_vendor && devPrinterUtil::IsVirtualSlot(ams_id)) {
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
|
||||
assert(false && __FUNCTION__);
|
||||
@@ -325,9 +368,22 @@ std::string DevFilaSystem::GetNozzleFlowStringByAmsId(const std::string& ams_id)
|
||||
std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
{
|
||||
std::map<int, DevAmsSlotId> tray_id_map;
|
||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
||||
} else if (GetOwner()) {
|
||||
for (const auto& tray : GetOwner()->vt_slot) {
|
||||
try {
|
||||
const int tray_id = std::stoi(tray.id);
|
||||
tray_id_map[tray_id] = DevAmsSlotId{tray_id, 0};
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid virtual tray id: " << tray.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int generic_tray_index = 0;
|
||||
for (auto& [ams_id, ams_item] : GetAmsList()) {
|
||||
for (auto &[slot_id, slot_item] : ams_item->GetTrays()) {
|
||||
if (ams_item && slot_item) {
|
||||
@@ -335,9 +391,11 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
int ams_id_int = stoi(ams_id);
|
||||
int slot_id_int = stoi(slot_id);
|
||||
int tray_index = -1;
|
||||
if (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
if (!is_bbl_vendor) {
|
||||
tray_index = generic_tray_index + slot_id_int;
|
||||
} else if (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
tray_index = ams_id_int;
|
||||
} else if(ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
|
||||
} else if (ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
|
||||
tray_index = 24 + slot_id_int;
|
||||
} else {
|
||||
tray_index = (ams_id_int * 4 + slot_id_int);
|
||||
@@ -348,6 +406,8 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor)
|
||||
generic_tray_index += static_cast<int>(ams_item->GetTrays().size());
|
||||
}
|
||||
|
||||
return tray_id_map;
|
||||
@@ -456,6 +516,13 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
ams_id_set.insert(it->first);
|
||||
}
|
||||
|
||||
// Bambu trays index their exist bit as ams_id*4+tray_id; a generic (non-Bambu) unit
|
||||
// reports its own lane count, so its trays carry a cumulative lane index instead.
|
||||
// Same index DevFilaSystem::GetTrayIndexMap() builds and the agent sets bits with.
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
int generic_tray_base = 0;
|
||||
|
||||
for (auto it = j_ams.begin(); it != j_ams.end(); it++)
|
||||
{
|
||||
if (!it->contains("id")) continue;
|
||||
@@ -795,7 +862,18 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
|
||||
if (type_id < 4)
|
||||
{
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
if (is_bbl_vendor)
|
||||
{
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A generic unit is not four slots wide, so its trays carry a
|
||||
// cumulative lane index across units instead of ams_id*4+tray_id.
|
||||
const int tray_bit = generic_tray_base + tray_id_int;
|
||||
curr_tray->is_exists = tray_bit >= 0 && tray_bit < 32 &&
|
||||
(obj->tray_exist_bits & (1UL << tray_bit)) != 0;
|
||||
}
|
||||
}
|
||||
else if (type_id == DevAms::AMS_LITE_MIXED)
|
||||
{
|
||||
@@ -851,6 +929,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor && it->contains("tray") && (*it)["tray"].is_array()) {
|
||||
generic_tray_base += static_cast<int>((*it)["tray"].size());
|
||||
}
|
||||
}
|
||||
// remove not in amsList
|
||||
for (auto it = ams_id_set.begin(); it != ams_id_set.end(); it++)
|
||||
@@ -913,6 +994,8 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
ams->m_exist = true;
|
||||
ams->m_binded_extruder_set = {ext_id};
|
||||
ams->m_binded_switcher_pos.reset();
|
||||
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
|
||||
ams->m_humidity_percent = u.value("humidity_percent", -1);
|
||||
ams->m_left_dry_time = u.value("dry_time_min", 0);
|
||||
@@ -995,16 +1078,22 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
// --- external / direct spools -> obj->vt_slot ---
|
||||
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
|
||||
// Keep one virtual slot per extruder. The legacy two-extruder mapping uses
|
||||
// 255 for extruder 0 and 254 for extruder 1; continue that sequence for
|
||||
// additional extruders (253, 252, ...).
|
||||
if (obj && data.contains("external") && data["external"].is_array())
|
||||
{
|
||||
std::map<int, DevAmsTray> external_slots;
|
||||
obj->vt_slot.clear();
|
||||
for (const auto& e : data["external"])
|
||||
{
|
||||
if (!e.is_object())
|
||||
continue;
|
||||
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
|
||||
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
|
||||
if (ext < MAIN_EXTRUDER_ID || ext >= VIRTUAL_TRAY_MAIN_ID)
|
||||
continue;
|
||||
|
||||
const int vt_id = VIRTUAL_TRAY_MAIN_ID - ext;
|
||||
DevAmsTray tray(std::to_string(vt_id));
|
||||
tray.is_exists = e.value("loaded", false);
|
||||
tray.m_fila_type = e.value("material", std::string());
|
||||
@@ -1013,8 +1102,11 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
|
||||
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
|
||||
tray.remain = e.value("remain_percent", -1);
|
||||
obj->vt_slot.push_back(tray);
|
||||
external_slots.insert_or_assign(ext, std::move(tray));
|
||||
}
|
||||
|
||||
for (auto& entry : external_slots)
|
||||
obj->vt_slot.push_back(std::move(entry.second));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ public:
|
||||
DevAmsTray* GetAmsTray(const std::string& ams_id, const std::string& tray_id) const;
|
||||
void CollectAmsColors(std::vector<wxColour>& ams_colors) const;
|
||||
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes the two virtual (external-spool) trays,
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes virtual external-spool trays,
|
||||
// N3S single-slot units, and the A2L/N9 AMS-Lite-mixed layout (trays 24-27).
|
||||
std::map<int, DevAmsSlotId> GetTrayIndexMap();
|
||||
|
||||
@@ -433,4 +433,4 @@ struct DevFilamentDryingPreset
|
||||
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
|
||||
};
|
||||
|
||||
}// namespace Slic3r
|
||||
}// namespace Slic3r
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/string.h>
|
||||
#include <string>
|
||||
#include "libslic3r/CommonDefs.hpp"
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -176,34 +178,52 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
|
||||
int DevNozzle::GetLogicExtruderId() const
|
||||
{
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtLeftExtruder()) {
|
||||
return LOGIC_L_EXTRUDER_ID;
|
||||
} else if (AtRightExtruder()) {
|
||||
return LOGIC_R_EXTRUDER_ID;
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
if (total_ext_count == 1) {
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtLeftExtruder()) {
|
||||
return LOGIC_L_EXTRUDER_ID;
|
||||
} else if (AtRightExtruder()) {
|
||||
return LOGIC_R_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
assert(0);
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
assert(0);
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
// For some reason, BBL's logical extruder ID is inverted:
|
||||
// physical extruder id = 0 (MAIN_EXTRUDER_ID) vs logical extruder id = 1 (LOGIC_R_EXTRUDER_ID)
|
||||
// Likely because logical id reads from left to right (left = 0, right = 1)
|
||||
// For generic N extruders, this inversion does not apply.
|
||||
if (IsOnRack()) return INVALID_EXTRUDER_ID;
|
||||
return m_nozzle_id;
|
||||
}
|
||||
|
||||
int DevNozzle::GetExtruderId() const
|
||||
{
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
return MAIN_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
return MAIN_EXTRUDER_ID;
|
||||
return m_nozzle_id;
|
||||
}
|
||||
|
||||
bool DevNozzle::AtLeftExtruder() const
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <tuple>
|
||||
#include <wx/app.h>
|
||||
@@ -806,8 +807,12 @@ bool MachineObject::is_extrusion_cali_finished()
|
||||
DevAmsTray *MachineObject::get_curr_tray()
|
||||
{
|
||||
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
|
||||
if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) {
|
||||
return &vt_slot[0];
|
||||
if (devPrinterUtil::IsVirtualSlot(cur_ams_id)) {
|
||||
for (auto& tray : vt_slot) {
|
||||
if (tray.id == cur_ams_id)
|
||||
return &tray;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DevAms* curr_ams = get_curr_Ams();
|
||||
@@ -1273,19 +1278,24 @@ int MachineObject::get_bed_temperature_limit()
|
||||
|
||||
bool MachineObject::is_filament_installed()
|
||||
{
|
||||
if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||
// right//or single
|
||||
auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||
if (ext.m_ext_has_filament) {
|
||||
// if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||
// // right//or single
|
||||
// auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||
// if (ext.m_ext_has_filament) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// /*left*/
|
||||
// if (m_extder_system->GetTotalExtderCount() > 1) {
|
||||
// auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
|
||||
// if (ext.m_ext_has_filament) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
for (auto& ext : m_extder_system->m_extders) {
|
||||
if (ext.m_ext_has_filament)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/*left*/
|
||||
if (m_extder_system->GetTotalExtderCount() > 1) {
|
||||
auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
|
||||
if (ext.m_ext_has_filament) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2627,13 +2637,10 @@ void MachineObject::reset()
|
||||
json empty_j;
|
||||
print_json.diff2all_base_reset(empty_j);
|
||||
|
||||
for (auto i = 0; i < vt_slot.size(); i++) {
|
||||
vt_slot[i].reset();
|
||||
|
||||
if (i == 1) {
|
||||
vt_slot.erase(vt_slot.begin() + 1);
|
||||
}
|
||||
}
|
||||
for (auto& tray : vt_slot)
|
||||
tray.reset();
|
||||
if (vt_slot.size() > 1)
|
||||
vt_slot.erase(vt_slot.begin() + 1, vt_slot.end());
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
@@ -4114,25 +4121,28 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||
|
||||
for (auto it = jj["vir_slot"].begin(); it != jj["vir_slot"].end(); it++) {
|
||||
auto vslot = parse_vt_tray(it.value().get<json>());
|
||||
try {
|
||||
const auto tray_json = it.value().get<json>();
|
||||
if (!tray_json.is_object() || !tray_json.contains("id") || !tray_json["id"].is_string())
|
||||
continue;
|
||||
|
||||
if (vslot.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 0);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[0] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
const auto tray_id_str = tray_json["id"].get<std::string>();
|
||||
int tray_id = 0;
|
||||
const auto result = std::from_chars(tray_id_str.data(), tray_id_str.data() + tray_id_str.size(), tray_id);
|
||||
if (result.ec != std::errc{} || result.ptr != tray_id_str.data() + tray_id_str.size()
|
||||
|| tray_id < 0 || tray_id > VIRTUAL_TRAY_MAIN_ID)
|
||||
continue;
|
||||
|
||||
auto vslot = parse_vt_tray(tray_json);
|
||||
const auto index = static_cast<std::vector<DevAmsTray>::size_type>(VIRTUAL_TRAY_MAIN_ID - tray_id);
|
||||
while (vt_slot.size() <= index) {
|
||||
const int placeholder_id = VIRTUAL_TRAY_MAIN_ID - static_cast<int>(vt_slot.size());
|
||||
vt_slot.emplace_back(std::to_string(placeholder_id));
|
||||
}
|
||||
vt_slot[index] = vslot;
|
||||
}
|
||||
else if (vslot.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 1);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[1] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
catch (...) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5270,16 +5280,17 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
|
||||
bool MachineObject::contains_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
{
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
return m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
} else {
|
||||
|
||||
if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
for (const auto& tray : vt_slot) {
|
||||
if (tray.id == ams_id) { return true; }
|
||||
if (tray.id == ams_id) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return !is_bbl_vendor && m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
}
|
||||
|
||||
DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
@@ -5289,13 +5300,19 @@ DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string
|
||||
return DevAmsTray(tray_id);
|
||||
}
|
||||
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) { return *tray;};
|
||||
if (tray) return *tray;
|
||||
}
|
||||
else {
|
||||
else if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
for (const auto &tray : vt_slot) {
|
||||
if (tray.id == ams_id) { return tray; }
|
||||
if (tray.id == ams_id) return tray;
|
||||
}
|
||||
|
||||
if (!is_bbl_vendor) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) return *tray;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+74
-165
@@ -2,6 +2,7 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Field.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "Plater.hpp"
|
||||
@@ -45,7 +46,11 @@
|
||||
#include <wx/arrstr.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/clrpicker.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/brush.h>
|
||||
#include <wx/pen.h>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <wx/image.h>
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/dc.h>
|
||||
@@ -2618,92 +2623,41 @@ void PluginConfigField::msw_rescale()
|
||||
|
||||
void ColourPicker::BUILD()
|
||||
{
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
|
||||
// An empty borderless wxButton is created with zero height on macOS. Give the bitmap
|
||||
// field the same height as other parameter buttons before drawing its swatch.
|
||||
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
|
||||
if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit);
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
|
||||
|
||||
// Validate the color
|
||||
wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value : m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
|
||||
wxColour clr(clr_str);
|
||||
if (clr_str.IsEmpty() || !clr.IsOk()) {
|
||||
clr = wxTransparentColour;
|
||||
}
|
||||
|
||||
auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
|
||||
const wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value :
|
||||
m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
|
||||
auto* button = new wxButton(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, wxBORDER_NONE);
|
||||
window = button;
|
||||
button->SetBitmapMargins(0, 0);
|
||||
if (parent_is_custom_ctrl && m_opt.height < 0)
|
||||
opt_height = (double)temp->GetSize().GetHeight() / m_em_unit;
|
||||
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
|
||||
convert_to_picker_widget(temp);
|
||||
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
|
||||
wxGetApp().UpdateDarkUI(temp->GetPickerCtrl());
|
||||
|
||||
// // recast as a wxWindow to fit the calling convention
|
||||
window = dynamic_cast<wxWindow*>(temp);
|
||||
|
||||
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([
|
||||
#ifdef __WXMSW__
|
||||
temp,
|
||||
#endif
|
||||
this](wxCommandEvent e) {
|
||||
#ifdef __WXMSW__
|
||||
draw_bmp_btn(temp, temp->GetColour());
|
||||
#endif
|
||||
on_change_field();
|
||||
}), temp->GetId());
|
||||
|
||||
// ORCA reset value to default on right click. previously no way to switch back on windows
|
||||
temp->GetPickerCtrl()->Bind(wxEVT_RIGHT_DOWN, [this, temp](wxMouseEvent e){
|
||||
#ifdef __WXMSW__
|
||||
temp->SetColour(wxTransparentColour);
|
||||
draw_bmp_btn(temp, wxTransparentColour);
|
||||
#else
|
||||
set_undef_value(temp);
|
||||
#endif
|
||||
on_change_field();
|
||||
e.Skip();
|
||||
opt_height = double(button->GetSize().GetHeight()) / m_em_unit;
|
||||
button->SetFont(wxGetApp().normal_font());
|
||||
if (!wxOSX) button->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
set_value(boost::any(clr_str));
|
||||
button->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
|
||||
button->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) {
|
||||
apply_user_color(wxTransparentColour);
|
||||
event.Skip();
|
||||
});
|
||||
|
||||
temp->SetToolTip(get_tooltip_text(clr_str));
|
||||
// Native layouts may assign the final field size after construction.
|
||||
button->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
|
||||
draw_bmp_btn();
|
||||
event.Skip();
|
||||
});
|
||||
button->SetToolTip(get_tooltip_text(clr_str));
|
||||
}
|
||||
|
||||
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
|
||||
void ColourPicker::draw_bmp_btn()
|
||||
{
|
||||
field->SetColour(wxTransparentColour);
|
||||
|
||||
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
|
||||
if (!btn->GetBitmap().IsOk()) return;
|
||||
|
||||
wxImage image(btn->GetBitmap().GetSize());
|
||||
image.InitAlpha();
|
||||
memset(image.GetAlpha(), 0, image.GetWidth() * image.GetHeight());
|
||||
wxBitmap bmp(std::move(image));
|
||||
wxMemoryDC dc(bmp);
|
||||
if (!dc.IsOk()) return;
|
||||
#ifdef __WXMSW__
|
||||
wxGCDC dc2(dc);
|
||||
#else
|
||||
wxDC &dc2(dc);
|
||||
#endif
|
||||
dc2.SetPen(wxPen("#F1754E", 1));
|
||||
|
||||
const wxRect rect = wxRect(0, 0, bmp.GetWidth(), bmp.GetHeight());
|
||||
dc2.DrawLine(rect.GetLeftBottom(), rect.GetTopRight());
|
||||
|
||||
dc.SelectObject(wxNullBitmap);
|
||||
btn->SetBitmapLabel(bmp);
|
||||
}
|
||||
|
||||
// ORCA match style with button on windows
|
||||
void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
{
|
||||
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
|
||||
|
||||
if (!btn->GetBitmap().IsOk()) return;
|
||||
btn->SetWindowStyle(wxBORDER_NONE); // ORCA just in case to prevent any overflow
|
||||
btn->SetBackgroundColour(*wxWHITE);
|
||||
if (!window) return;
|
||||
auto* btn = static_cast<wxButton*>(window);
|
||||
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
|
||||
wxGetApp().UpdateDarkUI(btn);
|
||||
|
||||
auto create_bitmap = [btn](const wxColour& picker_color,const wxColour& bg_color, bool focus) -> wxBitmap {
|
||||
wxSize btn_sz = btn->GetSize();
|
||||
wxImage image(btn_sz);
|
||||
@@ -2712,11 +2666,11 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
wxBitmap bmp(std::move(image));
|
||||
wxMemoryDC dc(bmp);
|
||||
if (!dc.IsOk()) return bmp;
|
||||
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
|
||||
wxGCDC dc2(dc);
|
||||
|
||||
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
|
||||
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
|
||||
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
|
||||
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
|
||||
|
||||
int padding = btn->FromDIP(5);
|
||||
dc2.SetPen(*wxTRANSPARENT_PEN);
|
||||
@@ -2735,117 +2689,72 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
return bmp;
|
||||
};
|
||||
|
||||
btn->SetBitmap( create_bitmap(color, wxColour("#DFDFDF"), false)); // Normal
|
||||
btn->SetBitmapFocus( create_bitmap(color, wxColour("#DFDFDF"), true )); // Focus
|
||||
btn->SetBitmapCurrent( create_bitmap(color, wxColour("#D4D4D4"), false)); // Hover
|
||||
btn->SetBitmap( create_bitmap(m_colour, wxColour("#DFDFDF"), false)); // Normal
|
||||
btn->SetBitmapFocus( create_bitmap(m_colour, wxColour("#DFDFDF"), true )); // Focus
|
||||
btn->SetBitmapCurrent( create_bitmap(m_colour, wxColour("#D4D4D4"), false)); // Hover
|
||||
}
|
||||
|
||||
void ColourPicker::set_value(const boost::any& value, bool change_event)
|
||||
void ColourPicker::set_value(const std::string& value, bool change_event)
|
||||
{
|
||||
m_disable_change_event = !change_event;
|
||||
const wxString clr_str(boost::any_cast<wxString>(value));
|
||||
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
|
||||
set_value(boost::any(wxString::FromUTF8(value)), change_event);
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
const wxColour parsed_clr(clr_str);
|
||||
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
|
||||
field->SetColour(clr);
|
||||
draw_bmp_btn(field, clr);
|
||||
#else
|
||||
wxColour clr(clr_str);
|
||||
if (clr_str.IsEmpty() || !clr.IsOk())
|
||||
set_undef_value(field);
|
||||
else
|
||||
field->SetColour(clr);
|
||||
#endif
|
||||
|
||||
m_disable_change_event = false;
|
||||
void ColourPicker::set_value(const boost::any& value, bool /*change_event*/)
|
||||
{
|
||||
// Like wxColourPickerCtrl::SetColour, programmatic assignment is silent.
|
||||
const wxString clr_str = boost::any_cast<wxString>(value);
|
||||
const wxColour parsed(clr_str);
|
||||
m_colour = clr_str.IsEmpty() || !parsed.IsOk() ? wxTransparentColour : parsed;
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
boost::any& ColourPicker::get_value()
|
||||
{
|
||||
save_colors_to_config();
|
||||
auto colour = static_cast<wxColourPickerCtrl*>(window)->GetColour();
|
||||
if (colour == wxTransparentColour)
|
||||
m_value = std::string("");
|
||||
else {
|
||||
m_value = encode_color(ColorRGB(colour.Red(), colour.Green(), colour.Blue()));
|
||||
}
|
||||
return m_value;
|
||||
m_value = m_colour == wxTransparentColour ? std::string() :
|
||||
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_colour.Blue()));
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void ColourPicker::apply_user_color(const std::optional<wxColour>& color)
|
||||
{
|
||||
if (!color || !color->IsOk() || *color == m_colour) return;
|
||||
const std::string previous = boost::any_cast<std::string>(get_value());
|
||||
m_colour = *color;
|
||||
draw_bmp_btn();
|
||||
if (previous != boost::any_cast<std::string>(get_value())) on_change_field();
|
||||
}
|
||||
|
||||
void ColourPicker::msw_rescale()
|
||||
{
|
||||
Field::msw_rescale();
|
||||
|
||||
wxColourPickerCtrl* field = dynamic_cast<wxColourPickerCtrl*>(window);
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width with parameters
|
||||
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
|
||||
if (m_opt.height >= 0)
|
||||
size.SetHeight(m_opt.height * m_em_unit);
|
||||
else if (parent_is_custom_ctrl && opt_height > 0)
|
||||
size.SetHeight(lround(opt_height * m_em_unit));
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
|
||||
if (parent_is_custom_ctrl)
|
||||
field->SetSize(size);
|
||||
else
|
||||
field->SetMinSize(size);
|
||||
|
||||
#ifdef __WXMSW__
|
||||
draw_bmp_btn(field, field->GetColour());
|
||||
#else
|
||||
if (field->GetColour() == wxTransparentColour)
|
||||
set_undef_value(field);
|
||||
#endif
|
||||
|
||||
if (parent_is_custom_ctrl) window->SetSize(size);
|
||||
else window->SetMinSize(size);
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
void ColourPicker::sys_color_changed()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (wxWindow* win = this->getWindow())
|
||||
if (wxColourPickerCtrl* picker = dynamic_cast<wxColourPickerCtrl*>(win)){
|
||||
wxGetApp().UpdateDarkUI(picker->GetPickerCtrl(), true);
|
||||
draw_bmp_btn(picker, picker->GetColour());
|
||||
}
|
||||
#endif
|
||||
if (window) wxGetApp().UpdateDarkUI(window, true);
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
void ColourPicker::on_button_click(wxCommandEvent &event) {
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
if (m_clrData) {
|
||||
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
|
||||
}
|
||||
}
|
||||
m_picker_widget->OnButtonClick(event);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::convert_to_picker_widget(wxColourPickerCtrl *widget)
|
||||
void ColourPicker::on_button_click(wxCommandEvent& /*event*/)
|
||||
{
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
m_picker_widget = dynamic_cast<wxColourPickerWidget*>(widget->GetPickerCtrl());
|
||||
if (m_picker_widget) {
|
||||
m_picker_widget->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
|
||||
m_clrData = m_picker_widget->GetColourData();
|
||||
const wxColour initial = m_colour == wxTransparentColour ? wxColour(0, 0, 0) : m_colour;
|
||||
const ColorRGBA color(initial.Red(), initial.Green(), initial.Blue(), static_cast<unsigned char>(255));
|
||||
ColorPickerDialog dialog(m_parent, color, {}, false, window);
|
||||
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
|
||||
const auto& result = std::get<ColorRGBA>(*dialog.selection());
|
||||
apply_user_color(wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.b() * 255.f))));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::save_colors_to_config() {
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
if (m_clrData) {
|
||||
std::vector<std::string> colors;
|
||||
if (colors.size() != CUSTOM_COLOR_COUNT) {
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
}
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
|
||||
}
|
||||
wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void PointCtrl::BUILD()
|
||||
|
||||
+10
-24
@@ -22,10 +22,10 @@
|
||||
#include <boost/any.hpp>
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/bmpcbox.h>
|
||||
#include <wx/clrpicker.h>
|
||||
#include <optional>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Config.hpp"
|
||||
@@ -610,41 +610,27 @@ private:
|
||||
};
|
||||
|
||||
class ColourPicker : public Field {
|
||||
using Field::Field;
|
||||
|
||||
void set_undef_value(wxColourPickerCtrl* field);
|
||||
void draw_bmp_btn(wxColourPickerCtrl* field, wxColour color);
|
||||
public:
|
||||
ColourPicker(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
|
||||
ColourPicker(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
|
||||
~ColourPicker() {}
|
||||
|
||||
wxWindow* window{ nullptr };
|
||||
void BUILD() override;
|
||||
|
||||
void set_value(const std::string& value, bool change_event = false) {
|
||||
m_disable_change_event = !change_event;
|
||||
dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
|
||||
m_disable_change_event = false;
|
||||
}
|
||||
void set_value(const std::string& value, bool change_event = false);
|
||||
void set_value(const boost::any& value, bool change_event = false) override;
|
||||
boost::any& get_value() override;
|
||||
void msw_rescale() override;
|
||||
void sys_color_changed() override;
|
||||
|
||||
void enable() override { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); }
|
||||
void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); }
|
||||
void enable() override { if (window) window->Enable(); }
|
||||
void disable() override{ if (window) window->Disable(); }
|
||||
wxWindow* getWindow() override { return window; }
|
||||
|
||||
private:
|
||||
void convert_to_picker_widget(wxColourPickerCtrl *widget);
|
||||
void on_button_click(wxCommandEvent &WXUNUSED(ev));
|
||||
void save_colors_to_config();
|
||||
private:
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
wxColourData* m_clrData{nullptr};
|
||||
wxColourPickerWidget* m_picker_widget{nullptr};
|
||||
#endif
|
||||
// An absent result is cancellation; transparent color is the undefined field.
|
||||
void apply_user_color(const std::optional<wxColour>& color);
|
||||
void draw_bmp_btn();
|
||||
void on_button_click(wxCommandEvent& event);
|
||||
wxColour m_colour = wxTransparentColour;
|
||||
};
|
||||
|
||||
class PointCtrl : public Field {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
#include "EncodedFilament.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
@@ -278,19 +277,6 @@ static wxBitmap create_transparent_bitmap(const wxSize& size) {
|
||||
return bdc.bitmap;
|
||||
}
|
||||
|
||||
// Sort colors by HSV values (primarily by hue, then saturation, then value)
|
||||
static void sort_colors_by_hsv(std::vector<wxColour>& colors) {
|
||||
if (colors.size() < 2) return;
|
||||
std::sort(colors.begin(), colors.end(),
|
||||
[](const wxColour& a, const wxColour& b) {
|
||||
ColourHSV ha = wxColourToHSV(a);
|
||||
ColourHSV hb = wxColourToHSV(b);
|
||||
if (ha.h != hb.h) return ha.h < hb.h;
|
||||
if (ha.s != hb.s) return ha.s < hb.s;
|
||||
return ha.v < hb.v;
|
||||
});
|
||||
}
|
||||
|
||||
static wxBitmap create_single_filament_bitmap(const wxColour& color, const wxSize& size)
|
||||
{
|
||||
// Check if color is transparent
|
||||
@@ -430,24 +416,16 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
|
||||
{
|
||||
if (colors.empty()) return wxNullBitmap;
|
||||
|
||||
// Make a copy to sort without modifying original
|
||||
std::vector<wxColour> sorted_colors = colors;
|
||||
|
||||
// Sort colors by HSV when there are 2 or more colors
|
||||
if (sorted_colors.size() >= 2) {
|
||||
sort_colors_by_hsv(sorted_colors);
|
||||
if (force_gradient && colors.size() >= 2) {
|
||||
return create_gradient_filament_bitmap(colors, size);
|
||||
}
|
||||
|
||||
if (force_gradient && sorted_colors.size() >= 2) {
|
||||
return create_gradient_filament_bitmap(sorted_colors, size);
|
||||
}
|
||||
|
||||
switch (sorted_colors.size()) {
|
||||
case 1: return create_single_filament_bitmap(sorted_colors[0], size);
|
||||
case 2: return create_dual_filament_bitmap(sorted_colors[0], sorted_colors[1], size);
|
||||
case 3: return create_triple_filament_bitmap(sorted_colors, size);
|
||||
case 4: return create_quadruple_filament_bitmap(sorted_colors, size);
|
||||
default: return create_gradient_filament_bitmap(sorted_colors, size);
|
||||
switch (colors.size()) {
|
||||
case 1: return create_single_filament_bitmap(colors[0], size);
|
||||
case 2: return create_dual_filament_bitmap(colors[0], colors[1], size);
|
||||
case 3: return create_triple_filament_bitmap(colors, size);
|
||||
case 4: return create_quadruple_filament_bitmap(colors, size);
|
||||
default: return create_gradient_filament_bitmap(colors, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#include "FilamentPickerDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -79,7 +83,8 @@ void FilamentPickerDialog::StartFlashing()
|
||||
m_flash_timer->Start(50);
|
||||
}
|
||||
|
||||
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type)
|
||||
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type,
|
||||
std::optional<FilamentColorPickerValue> ordered_color)
|
||||
: DPIDialog(parent ? parent : wxGetApp().mainframe,
|
||||
wxID_ANY,
|
||||
_L("Select Filament"),
|
||||
@@ -92,6 +97,7 @@ FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fil
|
||||
m_color_query = new FilamentColorCodeQuery();
|
||||
m_is_data_loaded = LoadFilamentData(fila_id);
|
||||
m_cur_filament_color = fila_color;
|
||||
m_ordered_color = std::move(ordered_color);
|
||||
wxString color_name = m_color_query->GetFilaColorName(fila_id, fila_color);
|
||||
m_cur_color_name = new wxString(color_name);
|
||||
|
||||
@@ -441,6 +447,9 @@ wxScrolledWindow* FilamentPickerDialog::CreateColorGrid()
|
||||
|
||||
// Bind click
|
||||
btn->Bind(wxEVT_LEFT_DOWN, [this, btn, color_code](wxMouseEvent& evt) {
|
||||
m_selection_changed = true;
|
||||
m_ordered_color.reset();
|
||||
m_custom_selection.reset();
|
||||
m_cur_filament_color = color_code->GetFilaColor();
|
||||
UpdatePreview(*color_code);
|
||||
UpdateButtonStates(btn);
|
||||
@@ -511,24 +520,27 @@ void FilamentPickerDialog::UpdatePreview(const FilamentColorCode& color_code)
|
||||
Layout();
|
||||
}
|
||||
|
||||
void FilamentPickerDialog::UpdateCustomColorPreview(const wxColour& custom_color)
|
||||
void FilamentPickerDialog::UpdateCustomColorPreview(const std::vector<wxColour>& wx_colors, bool gradient)
|
||||
{
|
||||
std::vector<wxColour> wx_colors = {custom_color};
|
||||
|
||||
// Update preview bitmap
|
||||
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, false);
|
||||
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, gradient);
|
||||
|
||||
if (bmp.IsOk()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Custom color bitmap created successfully: " << bmp.GetWidth() << "x" << bmp.GetHeight();
|
||||
m_color_demo->SetBitmap(bmp);
|
||||
m_color_demo->SetBackgroundColour(custom_color);
|
||||
m_color_demo->SetBackgroundColour(wx_colors.front());
|
||||
m_color_demo->Refresh();
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to create custom color bitmap";
|
||||
}
|
||||
|
||||
// Update preview labels for custom color
|
||||
m_label_preview_color->SetLabel(custom_color.GetAsString(wxC2S_HTML_SYNTAX));
|
||||
wxString label;
|
||||
for (const auto& color : wx_colors) {
|
||||
if (!label.empty()) label += " / ";
|
||||
label += color.GetAsString(wxC2S_HTML_SYNTAX);
|
||||
}
|
||||
m_label_preview_color->SetLabel(label);
|
||||
m_label_preview_idx->SetLabel("");
|
||||
Layout();
|
||||
}
|
||||
@@ -557,16 +569,6 @@ void FilamentPickerDialog::CreateMoreInfoButton()
|
||||
m_more_btn->SetStyle(ButtonStyle::Regular, ButtonType::Expanded);
|
||||
}
|
||||
|
||||
wxColourData FilamentPickerDialog::GetSingleColorData()
|
||||
{
|
||||
wxColourData data;
|
||||
data.SetChooseFull(true);
|
||||
if (m_cur_filament_color.ColorCount() > 0) {
|
||||
data.SetColour(*m_cur_filament_color.m_colors.begin());
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void FilamentPickerDialog::BindEvents()
|
||||
{
|
||||
// Bind mouse events for window dragging
|
||||
@@ -588,27 +590,37 @@ void FilamentPickerDialog::BindEvents()
|
||||
// Pause click detection while color picker is open
|
||||
StopClickDetection();
|
||||
|
||||
wxColourData original_data = GetSingleColorData();
|
||||
wxColourData result = show_sys_picker_dialog(this, original_data);
|
||||
|
||||
// Resume click detection after color picker closes
|
||||
StartClickDetection();
|
||||
|
||||
// Check if user actually selected a different color
|
||||
if (result.GetColour() != original_data.GetColour()) {
|
||||
wxColour selected_color = result.GetColour();
|
||||
|
||||
// Update m_current_filament_color with the selected color
|
||||
m_cur_filament_color.m_colors.clear();
|
||||
m_cur_filament_color.m_colors.insert(selected_color);
|
||||
m_cur_filament_color.m_color_type = FilamentColor::ColorType::SINGLE_CLR;
|
||||
|
||||
// Update preview
|
||||
UpdateCustomColorPreview(selected_color);
|
||||
|
||||
// Clear currently selected button since custom color selected
|
||||
UpdateButtonStates(nullptr);
|
||||
FilamentColorPickerValue current;
|
||||
if (m_ordered_color) {
|
||||
current = *m_ordered_color;
|
||||
} else {
|
||||
for (const auto& color : m_cur_filament_color.m_colors)
|
||||
current.colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
current.gradient = m_cur_filament_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR;
|
||||
}
|
||||
const auto initial = filament_color_picker_initial(current);
|
||||
std::optional<ColorSelection> selected;
|
||||
{
|
||||
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(current), m_more_btn);
|
||||
if (dialog.ShowModal() == wxID_OK)
|
||||
selected = dialog.selection();
|
||||
}
|
||||
StartClickDetection();
|
||||
if (!filament_color_picker_changed(initial, selected))
|
||||
return;
|
||||
|
||||
m_custom_selection = selected;
|
||||
m_ordered_color = filament_color_picker_result(*selected);
|
||||
m_selection_changed = true;
|
||||
std::vector<wxColour> colors;
|
||||
m_cur_filament_color.m_colors.clear();
|
||||
for (const auto& color : m_ordered_color->colors) {
|
||||
colors.emplace_back(wxString::FromUTF8(color));
|
||||
m_cur_filament_color.m_colors.insert(colors.back());
|
||||
}
|
||||
m_cur_filament_color.m_color_type = m_ordered_color->gradient ? FilamentColor::ColorType::GRADIENT_CLR : FilamentColor::ColorType::SINGLE_CLR;
|
||||
UpdateCustomColorPreview(colors, m_ordered_color->gradient);
|
||||
UpdateButtonStates(nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include <optional>
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "EncodedFilament.hpp"
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/event.h>
|
||||
@@ -25,10 +26,13 @@ namespace Slic3r { namespace GUI {
|
||||
class FilamentPickerDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type);
|
||||
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type,
|
||||
std::optional<FilamentColorPickerValue> ordered_color = std::nullopt);
|
||||
virtual ~FilamentPickerDialog();
|
||||
|
||||
// Public interface methods
|
||||
bool HasSelectionChanged() const { return m_selection_changed; }
|
||||
const std::optional<ColorSelection>& GetCustomColorSelection() const { return m_custom_selection; }
|
||||
bool IsDataLoaded() const { return m_is_data_loaded; }
|
||||
wxColour GetSelectedColour() const;
|
||||
const FilamentColor& GetSelectedFilamentColor() const { return m_cur_filament_color; }
|
||||
@@ -68,7 +72,7 @@ private:
|
||||
|
||||
// UI update methods
|
||||
void UpdatePreview(const FilamentColorCode& filament);
|
||||
void UpdateCustomColorPreview(const wxColour& custom_color);
|
||||
void UpdateCustomColorPreview(const std::vector<wxColour>& colors, bool gradient);
|
||||
void UpdateButtonStates(wxBitmapButton* selected_btn);
|
||||
|
||||
// Shaped window methods
|
||||
@@ -77,7 +81,6 @@ private:
|
||||
|
||||
// Data loading
|
||||
bool LoadFilamentData(const wxString& fila_id);
|
||||
wxColourData GetSingleColorData();
|
||||
|
||||
// Flash effect
|
||||
void StartFlashing();
|
||||
@@ -98,6 +101,9 @@ private:
|
||||
FilamentColorCodes* m_cur_color_codes{nullptr};
|
||||
wxBitmapButton* m_cur_selected_btn{nullptr};
|
||||
FilamentColor m_cur_filament_color;
|
||||
std::optional<FilamentColorPickerValue> m_ordered_color;
|
||||
std::optional<ColorSelection> m_custom_selection;
|
||||
bool m_selection_changed = false;
|
||||
|
||||
// Shaped window members
|
||||
wxBitmap m_shape_bmp;
|
||||
|
||||
+231
-31
@@ -1679,6 +1679,7 @@ GLCanvas3D::~GLCanvas3D()
|
||||
|
||||
m_sel_plate_toolbar.del_all_item();
|
||||
m_sel_plate_toolbar.del_stats_item();
|
||||
m_plate_switcher_items.clear();
|
||||
}
|
||||
|
||||
void GLCanvas3D::post_event(wxEvent &&event)
|
||||
@@ -2360,6 +2361,8 @@ void GLCanvas3D::zoom_to_gcode()
|
||||
void GLCanvas3D::zoom_to_plate(int plate_idx)
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE && _hides_inactive_plates())
|
||||
plate_idx = REQUIRES_ZOOM_TO_CUR_PLATE;
|
||||
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE) {
|
||||
box = wxGetApp().plater()->get_partplate_list().get_bounding_box();
|
||||
box.min.z() = 0.0;
|
||||
@@ -2416,12 +2419,22 @@ float GLCanvas3D::get_collapse_toolbar_height() const
|
||||
}
|
||||
|
||||
// The bottom-left corner: the 3D navigator, then the column of round canvas buttons beside it, in
|
||||
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar and
|
||||
// get_canvas_toolbar_right, so an overlay kept clear of the corner follows its layout.
|
||||
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar,
|
||||
// get_canvas_toolbar_right and canvas_corner_height, so an overlay kept clear of the corner follows its layout.
|
||||
static constexpr float NAVIGATOR_SIZE = 128.f;
|
||||
static constexpr float CANVAS_TOOLBAR_MARGIN = 10.f; // off the canvas edge, when there is no navigator
|
||||
static constexpr float CANVAS_TOOLBAR_PADDING = 2.f;
|
||||
static constexpr float CANVAS_TOOLBAR_BUTTON = 36.f;
|
||||
static constexpr float CANVAS_TOOLBAR_SPACING = 6.f;
|
||||
|
||||
// What the corner takes at the left edge of the canvas: the 3D navigator, or else the canvas toolbar.
|
||||
static float canvas_corner_height()
|
||||
{
|
||||
if (wxGetApp().show_3d_navigator())
|
||||
return NAVIGATOR_SIZE;
|
||||
const int buttons = wxGetApp().show_canvas_zoom_button() ? 3 : 2;
|
||||
return CANVAS_TOOLBAR_MARGIN + 2.f * CANVAS_TOOLBAR_PADDING + buttons * CANVAS_TOOLBAR_BUTTON + (buttons - 1) * CANVAS_TOOLBAR_SPACING;
|
||||
}
|
||||
|
||||
float GLCanvas3D::get_canvas_toolbar_right() const
|
||||
{
|
||||
@@ -2546,6 +2559,9 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
|
||||
|
||||
wxGetApp().imgui()->new_frame();
|
||||
|
||||
if (_update_inactive_plates_visibility())
|
||||
scene_dirty = true;
|
||||
|
||||
if (m_picking_enabled && !m_benchmarking) {
|
||||
if (m_rectangle_selection.is_dragging())
|
||||
// picking pass using rectangle selection
|
||||
@@ -7669,7 +7685,6 @@ void GLCanvas3D::_update_select_plate_toolbar_stats_item(bool force_selected) {
|
||||
|
||||
bool GLCanvas3D::_update_imgui_select_plate_toolbar()
|
||||
{
|
||||
bool result = true;
|
||||
if (!m_sel_plate_toolbar.is_enabled()) {
|
||||
return false;
|
||||
}
|
||||
@@ -7694,19 +7709,13 @@ bool GLCanvas3D::_update_imgui_select_plate_toolbar()
|
||||
PartPlateList& plate_list = p_plater->get_partplate_list();
|
||||
for (int i = 0; i < plate_list.get_plate_count(); i++) {
|
||||
IMToolbarItem* item = new IMToolbarItem();
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
if (plate && plate->thumbnail_data.is_valid()) {
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
item->image_data = plate->thumbnail_data.pixels;
|
||||
item->image_width = plate->thumbnail_data.width;
|
||||
item->image_height = plate->thumbnail_data.height;
|
||||
result = item->generate_texture();
|
||||
}
|
||||
if (PartPlate* plate = plate_list.get_plate(i))
|
||||
item->set_thumbnail(plate->thumbnail_data);
|
||||
m_sel_plate_toolbar.m_items.push_back(item);
|
||||
}
|
||||
p_plater->clear_plate_toolbar_image_dirty();
|
||||
m_sel_plate_toolbar.is_display_scrollbar = false;
|
||||
return result;
|
||||
return true;
|
||||
}
|
||||
|
||||
//BBS: GUI refactor
|
||||
@@ -8901,11 +8910,75 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
|
||||
const Vec2d bc = m_bed.build_volume().bed_center();
|
||||
m_bed.set_axes_origin(Vec3d(bc.x(), bc.y(), 0.0));
|
||||
} else {
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
|
||||
// With the other plates hidden, the axes go to the current plate as in Preview.
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled() && !_hides_inactive_plates());
|
||||
}
|
||||
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_hides_inactive_plates() const
|
||||
{
|
||||
return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode();
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_update_inactive_plates_visibility()
|
||||
{
|
||||
const bool hide = _hides_inactive_plates();
|
||||
if (!hide && !m_inactive_plates_hidden)
|
||||
return false;
|
||||
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
PartPlate* plate = hide ? plate_list.get_curr_plate() : nullptr;
|
||||
|
||||
struct Instance
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
bool selected{ false };
|
||||
};
|
||||
std::map<std::pair<int, int>, Instance> instances;
|
||||
if (plate != nullptr)
|
||||
for (const GLVolume* volume : m_volumes.volumes) {
|
||||
Instance& instance = instances[{ volume->object_idx(), volume->instance_idx() }];
|
||||
instance.box.merge(volume->transformed_convex_hull_bounding_box());
|
||||
instance.selected |= volume->selected;
|
||||
}
|
||||
|
||||
// A gizmo that hides the other objects shows them all again when it closes.
|
||||
const bool keep_hidden = m_gizmos.is_hiding_instances();
|
||||
bool changed = false;
|
||||
bool any_hidden = false;
|
||||
std::vector<bool> shown_again(m_volumes.volumes.size(), false);
|
||||
for (size_t i = 0; i < m_volumes.volumes.size(); ++i) {
|
||||
GLVolume* volume = m_volumes.volumes[i];
|
||||
const Instance* instance = plate != nullptr ? &instances[{ volume->object_idx(), volume->instance_idx() }] : nullptr;
|
||||
if (instance != nullptr && !instance->selected && instance->box.defined && !plate->get_bounding_box().overlap(instance->box)) {
|
||||
changed |= volume->is_active;
|
||||
volume->is_active = false;
|
||||
volume->on_hidden_plate = true;
|
||||
} else if (volume->on_hidden_plate && !keep_hidden) {
|
||||
volume->is_active = true;
|
||||
volume->on_hidden_plate = false;
|
||||
shown_again[i] = true;
|
||||
changed = true;
|
||||
}
|
||||
any_hidden |= volume->on_hidden_plate;
|
||||
}
|
||||
|
||||
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Volume))
|
||||
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters) {
|
||||
const int i = SceneRaycaster::decode_id(SceneRaycaster::EType::Volume, item->get_id());
|
||||
if (0 <= i && i < int(m_volumes.volumes.size()) && (m_volumes.volumes[i]->on_hidden_plate || shown_again[i]))
|
||||
item->set_active(shown_again[i]);
|
||||
}
|
||||
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Bed))
|
||||
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters)
|
||||
item->set_active(!hide || SceneRaycaster::decode_id(SceneRaycaster::EType::Bed, item->get_id()) / PartPlate::GRABBER_COUNT ==
|
||||
plate_list.get_curr_plate_index());
|
||||
|
||||
m_inactive_plates_hidden = hide || any_hidden;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
|
||||
{
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
@@ -8914,7 +8987,7 @@ void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transfo
|
||||
PartPlate* curr_plate = plate_list.get_curr_plate();
|
||||
const Transform3d plate_view_matrix = m_design_canvas && curr_plate != nullptr ?
|
||||
Transform3d(view_matrix * Geometry::translation_transform(-curr_plate->get_origin())) : view_matrix;
|
||||
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
|
||||
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled, _hides_inactive_plates());
|
||||
}
|
||||
|
||||
BoundingBoxf3 GLCanvas3D::_current_plate_box() const
|
||||
@@ -9932,6 +10005,7 @@ void GLCanvas3D::_render_overlays()
|
||||
|
||||
//BBS: GUI refactor: GLToolbar
|
||||
_render_imgui_select_plate_toolbar();
|
||||
_render_plate_switcher();
|
||||
_render_return_toolbar();
|
||||
// BBS
|
||||
//_render_view_toolbar();
|
||||
@@ -10200,6 +10274,35 @@ void GLCanvas3D::_render_main_toolbar()
|
||||
m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
|
||||
}
|
||||
|
||||
// The scrollbar of the plate toolbars of Preview and Prepare, in units of the toolbar scale.
|
||||
static constexpr float PLATE_TOOLBAR_SCROLLBAR = 14.f;
|
||||
|
||||
// Pushes 4 colors and 1 style var.
|
||||
static void push_plate_toolbar_scrollbar_style(bool is_dark, float scrollbar_size)
|
||||
{
|
||||
const ImVec4 grab(0.77f, 0.77f, 0.77f, is_dark ? .6f : 1.f);
|
||||
// A background with opacity over the window's would make a second color.
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, grab);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
|
||||
}
|
||||
|
||||
// The border of a hovered plate in the plate toolbars, when it is not the current one.
|
||||
static ImVec4 plate_toolbar_hover_color(bool is_dark)
|
||||
{
|
||||
return ImVec4(0.67f, 0.67f, 0.67f, is_dark ? .6f : 1.f);
|
||||
}
|
||||
|
||||
// Not IsWindowHovered(), which is false while a button is held down and would drop the bar.
|
||||
static bool plate_toolbar_keeps_scrollbar(bool shown, float scrollbar_size)
|
||||
{
|
||||
const ImVec2 pos = ImGui::GetWindowPos();
|
||||
return ImGui::IsMouseHoveringRect(pos, pos + ImGui::GetWindowSize() + ImVec2(shown ? scrollbar_size : 0.f, 0.f), !shown) ||
|
||||
(ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow());
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: GLToolbar adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
|
||||
void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
@@ -10330,16 +10433,15 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
int item_count = m_sel_plate_toolbar.m_items.size() + (m_sel_plate_toolbar.show_stats_item ? 1 : 0);
|
||||
float window_height_calc = (item_count * (button_height + (margin_size + button_margin) * 2.0f) + (item_count - 1) * ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().WindowPadding.y * 2.0f);
|
||||
bool show_scroll = m_sel_plate_toolbar.is_display_scrollbar && (window_height_calc > window_height_max);
|
||||
float scrollbar_size = 14.0f * f_scale;
|
||||
float scrollbar_size = PLATE_TOOLBAR_SCROLLBAR * f_scale;
|
||||
float window_height = std::min(window_height_calc, window_height_max);
|
||||
float window_width = button_width + (margin_size + button_margin + ImGui::GetStyle().WindowPadding.x) * 2 + (show_scroll ? scrollbar_size : 0);
|
||||
|
||||
ImVec4 window_bg = m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f);
|
||||
ImVec4 button_active = ImGuiWrapper::COL_ORCA; // ORCA: Use orca color for selected sliced plate border
|
||||
ImVec4 button_hover = ImVec4(0.67f, 0.67f, 0.67, m_is_dark ? .6f : 1.0f);
|
||||
ImVec4 button_hover = plate_toolbar_hover_color(m_is_dark);
|
||||
ImVec4 orca_active = m_is_dark ? ImGuiWrapper::COL_ORCA_DARK : ImGuiWrapper::COL_ORCA;
|
||||
ImVec4 orca_hover = m_is_dark ? ImGuiWrapper::COL_ORCA_HOVER_DARK : ImGuiWrapper::COL_ORCA_HOVER;
|
||||
ImVec4 scroll_col = ImVec4(0.77f, 0.77f, 0.77f, m_is_dark ? .6f : 1.0f);
|
||||
ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
|
||||
ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
|
||||
float button_radius = 1.0f * f_scale;
|
||||
@@ -10347,14 +10449,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
//use white text as the background switch to black
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, m_is_dark ? ImVec4(.9f, .9f, .9f, 1) : ImVec4(.3f, .3f, .3f, 1)); // ORCA Plate number text > Add support for dark mode
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, window_bg);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f)); // ORCA using background color with opacity creates a second color. This prevents secondary color
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, button_active);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, button_hover);
|
||||
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar_size);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, show_scroll ? (6.f * f_scale) : (button_radius + margin_size + frame_padding + ImGui::GetStyle().WindowPadding.x));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, button_radius + margin_size);
|
||||
@@ -10625,18 +10723,109 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
ImGui::PopStyleColor(8);
|
||||
ImGui::PopStyleVar(7);
|
||||
|
||||
//ORCA ImGui::IsWindowHovered() returns false when left_down events on buttons that causes scrollbar disappears for a short time
|
||||
auto win_pos = ImGui::GetWindowPos();
|
||||
bool is_win_hovered = ImGui::IsMouseHoveringRect(win_pos, win_pos + ImVec2(window_width + (show_scroll ? scrollbar_size : 0), window_height), !show_scroll);
|
||||
|
||||
// Also show scrollbar visible and continue to capture mouse position
|
||||
const bool is_scrollbar_active_drag = GImGui != nullptr && ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow();
|
||||
|
||||
m_sel_plate_toolbar.is_display_scrollbar = is_win_hovered || is_scrollbar_active_drag;
|
||||
m_sel_plate_toolbar.is_display_scrollbar = plate_toolbar_keeps_scrollbar(show_scroll, scrollbar_size);
|
||||
|
||||
imgui.end();
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_plate_switcher()
|
||||
{
|
||||
Plater* plater = wxGetApp().plater();
|
||||
PartPlateList& plate_list = plater->get_partplate_list();
|
||||
const int plate_count = plate_list.get_plate_count();
|
||||
if (!_hides_inactive_plates() || plate_count < 2 || m_gizmos.get_current_type() != GLGizmosManager::Undefined)
|
||||
return;
|
||||
|
||||
// The thumbnails are the ones of the Preview plate toolbar.
|
||||
const unsigned version = plater->plate_toolbar_image_version();
|
||||
if (m_plate_switcher_version != version || m_plate_switcher_items.size() != size_t(plate_count)) {
|
||||
// Only the current plate is edited here; the others are redrawn when their thumbnails go invalid.
|
||||
for (int i = 0; i < plate_count && !plater->is_gcode_3mf(); ++i)
|
||||
plater->update_plate_thumbnails(i, i == plate_list.get_curr_plate_index());
|
||||
m_plate_switcher_items.clear();
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
m_plate_switcher_items.push_back(std::make_unique<IMToolbarItem>());
|
||||
m_plate_switcher_items.back()->set_thumbnail(plate_list.get_plate(i)->thumbnail_data);
|
||||
}
|
||||
m_plate_switcher_version = version;
|
||||
}
|
||||
|
||||
float sc = get_scale();
|
||||
#ifdef WIN32
|
||||
sc *= (float) get_dpi_for_window(wxGetApp().GetTopWindow()) / (float) DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
ImGuiWrapper& imgui = *wxGetApp().imgui();
|
||||
const ImVec2 btn_size = ImVec2(72.f, 72.f) * sc;
|
||||
const ImVec2 padding = ImVec2(8.f, 8.f) * sc;
|
||||
const float spacing = 8.f * sc;
|
||||
const float rounding = 4.f * sc;
|
||||
const float scrollbar = PLATE_TOOLBAR_SCROLLBAR * sc;
|
||||
const float top = std::max(m_main_toolbar.get_height(), m_gizmos.get_scaled_total_height()) + CANVAS_TOOLBAR_MARGIN * sc;
|
||||
const float content_h = plate_count * (btn_size.y + spacing) - spacing + 2.f * padding.y;
|
||||
const float max_h = std::max(btn_size.y + 2.f * padding.y, float(get_canvas_size().get_height()) - top - (canvas_corner_height() + CANVAS_TOOLBAR_MARGIN) * sc);
|
||||
// As in the Preview plate toolbar, the scrollbar shows only while the mouse is over the column.
|
||||
const bool scroll = m_plate_switcher_scrollbar && content_h > max_h;
|
||||
|
||||
const ImVec4 text_color = m_is_dark ? ImVec4(.9f, .9f, .9f, 1.f) : ImVec4(.3f, .3f, .3f, 1.f);
|
||||
const ImVec4 empty_color = ImVec4(text_color.x, text_color.y, text_color.z, .45f);
|
||||
const ImVec4 hover = plate_toolbar_hover_color(m_is_dark);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
|
||||
// Concentric with the thumbnails; tighter around the scrollbar, as in the Preview plate toolbar.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, scroll ? 6.f * sc : rounding + padding.x);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f));
|
||||
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar);
|
||||
|
||||
imgui.set_next_window_pos(CANVAS_TOOLBAR_MARGIN * sc, top, ImGuiCond_Always, 0.f, 0.f);
|
||||
imgui.set_next_window_size(btn_size.x + 2.f * padding.x + (scroll ? scrollbar : 0.f), std::min(content_h, max_h), ImGuiCond_Always);
|
||||
imgui.begin(std::string("##PlateSwitcher"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoCollapse | (scroll ? 0 : ImGuiWindowFlags_NoScrollbar));
|
||||
|
||||
const int curr_plate = plate_list.get_curr_plate_index();
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
const ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
|
||||
const ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
const bool current = i == curr_plate;
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImGui::PushID(i);
|
||||
const bool clicked = ImGui::InvisibleButton("##plate", btn_size);
|
||||
ImGui::PopID();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
draw_list->AddRectFilled(pos, pos + btn_size, plate_bg, rounding);
|
||||
if (ImTextureID texture = m_plate_switcher_items[i]->texture_id)
|
||||
draw_list->AddImageRounded(texture, pos, pos + btn_size, ImVec2(0.f, 1.f), ImVec2(1.f, 0.f), IM_COL32_WHITE, rounding);
|
||||
if (plate->empty())
|
||||
draw_list->AddRectFilled(pos, pos + btn_size, plate_dim, rounding);
|
||||
if (current || hovered) {
|
||||
const float border = 2.f * sc;
|
||||
draw_list->AddRect(pos - ImVec2(border, border) * .5f, pos + btn_size + ImVec2(border, border) * .5f,
|
||||
ImGui::GetColorU32(current ? (hovered ? ImGuiWrapper::COL_ORCA_HOVER : ImGuiWrapper::COL_ORCA) : hover),
|
||||
rounding + border * .5f, 0, border);
|
||||
}
|
||||
draw_list->AddText(pos + ImVec2(4.f, 2.f) * sc, ImGui::GetColorU32(plate->empty() ? empty_color : text_color), std::to_string(i + 1).c_str());
|
||||
|
||||
if (clicked && !current)
|
||||
plater->CallAfter([i]() {
|
||||
wxGetApp().plater()->select_plate(i);
|
||||
wxGetApp().plater()->deselect_all();
|
||||
});
|
||||
if (hovered) {
|
||||
const std::string name = plate->get_plate_name();
|
||||
imgui.tooltip(_u8L("Plate") + " " + std::to_string(i + 1) + (name.empty() ? "" : " (" + name + ")"), ImGui::GetFontSize() * 20.f);
|
||||
}
|
||||
}
|
||||
|
||||
m_plate_switcher_scrollbar = plate_toolbar_keeps_scrollbar(scroll, scrollbar);
|
||||
imgui.end();
|
||||
ImGui::PopStyleColor(5);
|
||||
ImGui::PopStyleVar(5);
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: GLToolbar adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
|
||||
void GLCanvas3D::_render_assemble_view_toolbar() const
|
||||
@@ -10743,7 +10932,7 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
|
||||
ImVec2 btn_size = ImVec2(CANVAS_TOOLBAR_BUTTON, CANVAS_TOOLBAR_BUTTON) * sc;
|
||||
ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (CANVAS_TOOLBAR_MARGIN * sc), CANVAS_TOOLBAR_MARGIN * sc);
|
||||
ImVec2 spacing = ImVec2(6.f, 6.f) * sc;
|
||||
ImVec2 spacing = ImVec2(CANVAS_TOOLBAR_SPACING, CANVAS_TOOLBAR_SPACING) * sc;
|
||||
ImVec2 padding = ImVec2(CANVAS_TOOLBAR_PADDING, CANVAS_TOOLBAR_PADDING) * sc;
|
||||
Vec2i32 pos = {
|
||||
m_canvas_toolbar_pos[0] + margin.x,
|
||||
@@ -10946,6 +11135,17 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
[]{wxGetApp().toggle_show_plate_gridlines();}
|
||||
);
|
||||
|
||||
create_menu_item( _utf8(L("Focus mode")),
|
||||
m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas, // work only on prepare
|
||||
wxGetApp().focus_mode(),
|
||||
[this]{
|
||||
wxGetApp().toggle_focus_mode();
|
||||
if (wxGetApp().focus_mode())
|
||||
zoom_to_plate(REQUIRES_ZOOM_TO_CUR_PLATE);
|
||||
m_dirty = true;
|
||||
}
|
||||
);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
create_menu_item( _utf8(L("Labels")),
|
||||
|
||||
@@ -660,6 +660,10 @@ private:
|
||||
mutable GLToolbar m_main_toolbar;
|
||||
mutable GLToolbar m_separator_toolbar;
|
||||
mutable IMToolbar m_sel_plate_toolbar;
|
||||
// Thumbnails of the plate switcher (Focus mode), refreshed when the plater's version changes.
|
||||
std::vector<std::unique_ptr<IMToolbarItem>> m_plate_switcher_items;
|
||||
unsigned m_plate_switcher_version{ unsigned(-1) };
|
||||
bool m_plate_switcher_scrollbar{ false };
|
||||
mutable GLToolbar m_assemble_view_toolbar;
|
||||
mutable IMReturnToolbar m_return_toolbar;
|
||||
mutable Vec2i32 m_canvas_toolbar_pos = {140, 5};
|
||||
@@ -778,6 +782,8 @@ private:
|
||||
//BBS:add plate related logic
|
||||
mutable std::vector<int> m_hover_volume_idxs;
|
||||
std::vector<int> m_hover_plate_idxs;
|
||||
// Some volume or plate raycaster may still be hidden with the inactive plates.
|
||||
bool m_inactive_plates_hidden{ false };
|
||||
// IMEX ghost hover state (plate-owned transparent ghost volumes).
|
||||
int m_hover_ghost_head { -1 }; // physical head index, -1 when not hovering a ghost
|
||||
int m_hover_ghost_plate { -1 }; // plate index for the hovered ghost, -1 when none
|
||||
@@ -1527,6 +1533,9 @@ private:
|
||||
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
//BBS: add part plate related logic
|
||||
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
|
||||
bool _hides_inactive_plates() const;
|
||||
// Returns whether what the scene shows changed.
|
||||
bool _update_inactive_plates_visibility();
|
||||
// The current plate's box (XY, at z = 0); in the Design tab, its own bed's.
|
||||
BoundingBoxf3 _current_plate_box() const;
|
||||
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
|
||||
@@ -1565,6 +1574,8 @@ private:
|
||||
void _render_gizmos_overlay();
|
||||
void _render_main_toolbar();
|
||||
void _render_imgui_select_plate_toolbar();
|
||||
// Prepare: picks the plate to show while the inactive ones are hidden.
|
||||
void _render_plate_switcher();
|
||||
void _render_assemble_view_toolbar() const;
|
||||
void _render_return_toolbar() const;
|
||||
void _render_canvas_toolbar();
|
||||
|
||||
+149
-8
@@ -41,6 +41,7 @@
|
||||
#include <boost/filesystem/exception.hpp>
|
||||
#include <boost/locale/encoding_utf.hpp>
|
||||
#include <boost/log/detail/native_typeof.hpp>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
@@ -325,6 +326,13 @@ namespace GUI {
|
||||
|
||||
class MainFrame;
|
||||
|
||||
// The OrcaCloud agent behind the shared NetworkAgent, or null when networking/OrcaCloud is not
|
||||
// available (e.g. stealth mode or the network plugin still loading).
|
||||
static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent* agent)
|
||||
{
|
||||
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
|
||||
}
|
||||
|
||||
void start_ping_test()
|
||||
{
|
||||
return;
|
||||
@@ -2262,7 +2270,9 @@ void GUI_App::init_networking_callbacks()
|
||||
if (return_code == 5) {
|
||||
GUI::wxGetApp().CallAfter([this, provider = event.provider] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: login expired";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: the desktop lost its session on its own. Leave the webview's
|
||||
// own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
if (msg_dlg.ShowModal() == wxOK) {
|
||||
return;
|
||||
@@ -5238,7 +5248,7 @@ void GUI_App::post_logout_to_webview(const std::string& provider)
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/)
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/, bool user_initiated)
|
||||
{
|
||||
if (m_agent && m_agent->is_user_login(provider)) {
|
||||
m_agent->user_logout(true, provider);
|
||||
@@ -5251,6 +5261,21 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
|
||||
}
|
||||
|
||||
if (provider == ORCA_CLOUD_PROVIDER) {
|
||||
// Invalidate any handoff still in flight, so a token minted for the signed-out
|
||||
// account is never loaded into the pane.
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
|
||||
// An explicit user sign-out also ends the webview's own cloud session (guide §8).
|
||||
// A session the desktop lost on its own must NOT do this: a 2FA user would otherwise
|
||||
// loop through the code prompt on every desktop re-login.
|
||||
if (user_initiated) {
|
||||
if (auto orca = get_orca_cloud_agent(m_agent)) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->cloud_signed_out(from_u8(orca->build_web_session_signout_url()));
|
||||
}
|
||||
}
|
||||
|
||||
/* delete old user settings */
|
||||
bool transfer_preset_changes = false;
|
||||
wxString header = _L("Some presets are modified.") + "\n" +
|
||||
@@ -5372,7 +5397,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_logout";
|
||||
request_user_logout();
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_orca_login_info") == 0) {
|
||||
@@ -5387,7 +5412,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_bambu_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_bambu_logout";
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_orca_login_or_register") == 0) {
|
||||
@@ -5396,7 +5421,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_orca_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_orca_logout";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_modeldepot") == 0) {
|
||||
@@ -5408,6 +5433,20 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_openproject") == 0) {
|
||||
this->request_open_project({});
|
||||
}
|
||||
else if (command_str.compare("homepage_open_cloud") == 0) {
|
||||
// Deferred: set_cloud_mode() may create a second native webview, which must
|
||||
// not run inside this webview's script-message callback.
|
||||
CallAfter([this]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_close_cloud") == 0) {
|
||||
CallAfter([]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(false);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_recent_projects") == 0) {
|
||||
if (mainframe) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
@@ -5680,7 +5719,9 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
if (!provider.empty() && m_agent->is_user_login(provider)) {
|
||||
if (std::chrono::steady_clock::now() - m_last_401_error_time > 30s) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: a 401 means the desktop session was rejected. Leave the
|
||||
// webview's own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
|
||||
if (!m_show_http_error_msgdlg) {
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
@@ -5824,6 +5865,93 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
|
||||
}*/
|
||||
}
|
||||
|
||||
wxString GUI_App::cloud_webview_handoff()
|
||||
{
|
||||
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
|
||||
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
|
||||
// post request_web_session, which is what opens the desktop login dialog.
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return {};
|
||||
return from_u8(orca->build_web_session_start_url());
|
||||
}
|
||||
|
||||
void GUI_App::request_cloud_web_session(const std::string& state)
|
||||
{
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
// Only the newest request counts: a newer state supersedes any mint or login still in flight.
|
||||
m_cloud_handoff_state = state;
|
||||
const uint64_t gen = ++m_cloud_handoff_gen;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca || !orca->is_user_login()) {
|
||||
// Desktop signed out: open the desktop login dialog and stop here. On success
|
||||
// on_user_login_handle reloads the Start URL, which makes the page request again; on
|
||||
// cancel nothing happens (the guide's "on cancel, do nothing"). The newest state is
|
||||
// already stored, so a repeat request while the dialog is open is dropped rather than
|
||||
// stacking another dialog.
|
||||
if (!login_dlg || !login_dlg->IsShown())
|
||||
request_login(true, ORCA_CLOUD_PROVIDER);
|
||||
return;
|
||||
}
|
||||
|
||||
mint_cloud_web_session(state, gen);
|
||||
}
|
||||
|
||||
void GUI_App::mint_cloud_web_session(const std::string& state, uint64_t gen)
|
||||
{
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// The endpoint call blocks on network I/O (and possibly a cross-process token refresh), so
|
||||
// it must not run on the UI thread. Hand off to a worker and marshal the token back.
|
||||
boost::thread([this, orca, state, gen]() {
|
||||
std::string token_hash;
|
||||
std::string error_code;
|
||||
unsigned int http_code = 0;
|
||||
orca->request_web_session_handoff(token_hash, error_code, http_code);
|
||||
|
||||
if (is_closing())
|
||||
return;
|
||||
|
||||
if (token_hash.empty()) {
|
||||
// No token: 401 after retry, 409/429/503 or a transport error. The page owns its own
|
||||
// "Sign-in link expired"/"Try again" UI; the desktop stays silent. Never log the body
|
||||
// (it may echo account data), only the status/error code.
|
||||
BOOST_LOG_TRIVIAL(warning) << "cloud web session handoff not minted - http_code=" << http_code
|
||||
<< " error_code=" << error_code;
|
||||
return;
|
||||
}
|
||||
|
||||
GUI::wxGetApp().CallAfter([this, state, gen, token_hash]() {
|
||||
on_cloud_web_session_minted(state, gen, token_hash);
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void GUI_App::on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash)
|
||||
{
|
||||
if (is_closing())
|
||||
return;
|
||||
// Drop a result for a superseded request (a newer request_web_session, login or sign-out).
|
||||
if (gen != m_cloud_handoff_gen || state != m_cloud_handoff_state)
|
||||
return;
|
||||
if (token_hash.empty())
|
||||
return;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// Load the Complete URL in the SAME webview that posted request_web_session, because the
|
||||
// state lives in that webview's session storage (guide §3/§7).
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->load_cloud_url(from_u8(orca->build_web_session_complete_url(token_hash, state)));
|
||||
}
|
||||
|
||||
void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
{
|
||||
if (!m_agent) { return; }
|
||||
@@ -5879,6 +6007,16 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
if (app_config->get("sync_user_preset") == "true") {
|
||||
start_sync_user_preset();
|
||||
}
|
||||
|
||||
// Re-seed the embedded OrcaCloud pane if it is already open, so it picks up the new session
|
||||
// without waiting for the next open. On a fresh login or account switch this invalidates any
|
||||
// in-flight handoff for the previous account and loads the Start URL with the new user_id, so
|
||||
// the page drops the old account and re-requests (guide §8).
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
if (home->cloud_mode_active())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
}
|
||||
|
||||
|
||||
@@ -8012,8 +8150,11 @@ void GUI_App::on_stealth_mode_enter()
|
||||
{
|
||||
stop_sync_user_preset();
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: on_stealth_mode_enter";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
// Stealth mode forces a desktop logout, but the user did not choose "Sign out": guide §8 ties
|
||||
// ?signout=1 to the explicit desktop sign-out only. Leave the cloud webview's own session
|
||||
// alone, so it is re-checked (and re-seeded with the right account) on the next open.
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
home->SendCloudProvidersInfo();
|
||||
}
|
||||
|
||||
@@ -361,6 +361,11 @@ private:
|
||||
bool m_side_popup_status{false};
|
||||
bool m_show_http_error_msgdlg{false};
|
||||
std::chrono::steady_clock::time_point m_last_401_error_time;
|
||||
// OrcaCloud webview session handoff: the newest request_web_session state from the cloud pane
|
||||
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
|
||||
// newest request counts").
|
||||
std::string m_cloud_handoff_state;
|
||||
uint64_t m_cloud_handoff_gen { 0 };
|
||||
bool m_show_error_msgdlg{false};
|
||||
wxString m_info_dialog_content;
|
||||
HttpServer m_http_server;
|
||||
@@ -431,6 +436,8 @@ public:
|
||||
|
||||
bool show_plate_gridlines() const { return app_config->get_bool("show_plate_gridlines"); }
|
||||
void toggle_show_plate_gridlines() const { app_config->set_bool("show_plate_gridlines", !show_plate_gridlines()); }
|
||||
bool focus_mode() const { return app_config->get_bool("focus_mode"); }
|
||||
void toggle_focus_mode() const { app_config->set_bool("focus_mode", !focus_mode()); }
|
||||
|
||||
bool show_canvas_zoom_button() const { return app_config->get_bool("show_canvas_zoom_button"); }
|
||||
void toggle_canvas_zoom_button() const { app_config->set_bool("show_canvas_zoom_button", !show_canvas_zoom_button()); }
|
||||
@@ -540,7 +547,11 @@ public:
|
||||
|
||||
void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
// `user_initiated` distinguishes a user sign-out from the desktop losing its session on its
|
||||
// own (refresh failed, 401). Only an explicit sign-out also ends the cloud webview's own
|
||||
// session; an involuntary one leaves the webview alone (guide §8). Defaults to the safe
|
||||
// direction: a new caller that forgets the flag will not silently sign the webview out.
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER, bool user_initiated = false);
|
||||
void post_logout_to_webview(const std::string& provider);
|
||||
int request_user_unbind(std::string dev_id, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
std::string handle_web_request(std::string cmd);
|
||||
@@ -556,6 +567,15 @@ public:
|
||||
void on_update_machine_list(wxCommandEvent& evt);
|
||||
void on_user_login(wxCommandEvent &evt);
|
||||
void on_user_login_handle(wxCommandEvent& evt);
|
||||
// OrcaCloud webview session handoff (guide §5/§7). cloud_webview_handoff builds the cloud
|
||||
// pane's Start URL (with the desktop user_id while signed in). request_cloud_web_session is
|
||||
// called from the pane when the page asks for a session; it opens the desktop login dialog if
|
||||
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
|
||||
// in the same webview.
|
||||
wxString cloud_webview_handoff();
|
||||
void request_cloud_web_session(const std::string& state);
|
||||
void mint_cloud_web_session(const std::string& state, uint64_t gen);
|
||||
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
|
||||
void enable_user_preset_folder(bool enable);
|
||||
|
||||
// BBS
|
||||
|
||||
@@ -2400,7 +2400,7 @@ void MenuFactory::append_menu_item_drop(wxMenu* menu)
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
else {
|
||||
return (std::abs(plater()->get_view3D_canvas3D()->get_selection().get_bounding_box().min.z()) > -SINKING_Z_THRESHOLD);
|
||||
return plater()->get_view3D_canvas3D()->get_selection().can_drop();
|
||||
} //disable if model is on the bed / not in View3D
|
||||
}, m_parent);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
|
||||
#include <slic3r/GUI/AnimatedTexture.hpp>
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/CameraUtils.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
|
||||
@@ -57,24 +57,6 @@ const double GizmoObjectManipulation::mm_to_in = 0.0393700787;
|
||||
const double GizmoObjectManipulation::oz_to_g = 28.34952;
|
||||
const double GizmoObjectManipulation::g_to_oz = 0.035274;
|
||||
|
||||
// Helper function to be used by drop to bed button. Returns lowest point of this
|
||||
// volume in world coordinate system.
|
||||
static double get_volume_min_z(const GLVolume* volume)
|
||||
{
|
||||
const Transform3f& world_matrix = volume->world_matrix().cast<float>();
|
||||
|
||||
// need to get the ModelVolume pointer
|
||||
const ModelObject* mo = wxGetApp().model().objects[volume->composite_id.object_id];
|
||||
const ModelVolume* mv = mo->volumes[volume->composite_id.volume_id];
|
||||
const TriangleMesh& hull = mv->get_convex_hull();
|
||||
|
||||
float min_z = std::numeric_limits<float>::max();
|
||||
for (const stl_vertex& vert : hull.its.vertices) {
|
||||
min_z = std::min(min_z, Vec3f::UnitZ().dot(world_matrix * vert));
|
||||
}
|
||||
return min_z;
|
||||
}
|
||||
|
||||
GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas)
|
||||
: m_glcanvas(glcanvas), m_alignment_helper(glcanvas)
|
||||
{
|
||||
@@ -295,12 +277,14 @@ void GizmoObjectManipulation::update_reset_buttons_visibility()
|
||||
{
|
||||
const Selection& selection = m_glcanvas.get_selection();
|
||||
|
||||
// Assembly view has its own selection and transformations, which drop_selection() doesn't update
|
||||
m_enable_drop_to_bed = m_glcanvas.get_canvas_type() == GLCanvas3D::ECanvasType::CanvasView3D && selection.can_drop();
|
||||
|
||||
if (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()) {
|
||||
const GLVolume * volume = selection.get_first_volume();
|
||||
|
||||
Vec3d rotation;
|
||||
Vec3d scale;
|
||||
double min_z = 0.;
|
||||
|
||||
if (selection.is_single_full_instance()) {
|
||||
rotation = volume->get_instance_rotation();
|
||||
@@ -309,12 +293,10 @@ void GizmoObjectManipulation::update_reset_buttons_visibility()
|
||||
else {
|
||||
rotation = volume->get_volume_rotation();
|
||||
scale = volume->get_volume_scaling_factor();
|
||||
min_z = get_volume_min_z(volume);
|
||||
}
|
||||
m_show_clear_rotation = !rotation.isApprox(m_init_rotation);
|
||||
m_show_reset_0_rotation = !rotation.isApprox(Vec3d::Zero());
|
||||
m_show_clear_scale = (m_cache.scale / 100.0f - Vec3d::Ones()).norm() > 0.001;
|
||||
m_show_drop_to_bed = (std::abs(min_z) > EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -771,6 +753,11 @@ void GizmoObjectManipulation::reset_align_state()
|
||||
|
||||
void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, std::string window_name, float x, float y, float bottom_limit)
|
||||
{
|
||||
float scale = m_glcanvas.get_scale();
|
||||
#ifdef WIN32
|
||||
scale *= (float)get_dpi_for_window(wxGetApp().GetTopWindow()) / (float)DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
// BBS: GUI refactor: move gizmo to the right
|
||||
if (abs(last_move_input_window_width) > 0.01f) {
|
||||
if (x + last_move_input_window_width > m_glcanvas.get_canvas_size().get_width()) {
|
||||
@@ -842,7 +829,7 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
// Rotation
|
||||
float unit_size = imgui_wrapper->calc_text_size(MAX_SIZE).x + space_size;
|
||||
// Four align buttons sit under each axis' input, a space before the next axis, so the inputs widen to hold them.
|
||||
const float align_icon_size = 16.f * m_glcanvas.get_scale(); // ORCA use 16px icons
|
||||
const float align_icon_size = 16.f * scale;
|
||||
const float align_button_size = align_icon_size + 2.f * style.FramePadding.x; // ORCA
|
||||
if (show_align) {
|
||||
unit_size = std::max(unit_size, 3.f * align_button_size);
|
||||
@@ -1072,6 +1059,17 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
|
||||
GLGizmoUtils::render_tooltip_button(imgui_wrapper, m_glcanvas, m_shortcuts_move, x, y);
|
||||
|
||||
// ORCA use existing function for drop and show button on bottom bar to keep gizmo compact
|
||||
imgui_wrapper->disabled_begin(!m_enable_drop_to_bed);
|
||||
ImGui::SameLine(0, space_size * 1.5f);
|
||||
if (imgui_wrapper->button(_L("Drop"))){
|
||||
wxGetApp().plater()->drop_selection();
|
||||
UpdateAndShow(true);
|
||||
}
|
||||
imgui_wrapper->disabled_end();
|
||||
if(ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
imgui_wrapper->tooltip(_L("Move the selection along Z so that its lowest point touches the bed."), ImGui::GetFontSize() * 20.0f);
|
||||
|
||||
ImGui::SameLine();
|
||||
GLGizmoUtils::begin_right_aligned_buttons({ _L("Done") });
|
||||
if (imgui_wrapper->button(_L("Done"))) {
|
||||
|
||||
@@ -103,7 +103,7 @@ public:
|
||||
bool m_show_reset_0_rotation{false};
|
||||
bool m_show_clear_rotation { false };
|
||||
bool m_show_clear_scale { false };
|
||||
bool m_show_drop_to_bed { false };
|
||||
bool m_enable_drop_to_bed { false };
|
||||
enum class RotateType { None, Relative, Absolute
|
||||
};
|
||||
RotateType m_last_rotate_type{RotateType::None}; // 0:no input 1:relative 2:absolute
|
||||
|
||||
@@ -23,27 +23,23 @@ IMToolbarItem::~IMToolbarItem()
|
||||
}
|
||||
|
||||
|
||||
bool IMToolbarItem::generate_texture()
|
||||
void IMToolbarItem::set_thumbnail(const ThumbnailData& thumbnail)
|
||||
{
|
||||
GLint last_texture;
|
||||
unsigned m_image_texture{ 0 };
|
||||
unsigned char* pixels = (unsigned char*)(&image_data[0]);
|
||||
if (!thumbnail.is_valid())
|
||||
return;
|
||||
|
||||
GLint last_texture;
|
||||
GLuint id = (GLuint)(intptr_t)texture_id;
|
||||
glsafe(::glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture));
|
||||
glsafe(::glGenTextures(1, &m_image_texture));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_image_texture));
|
||||
if (id == 0)
|
||||
glsafe(::glGenTextures(1, &id));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, id));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
|
||||
glsafe(::glPixelStorei(GL_UNPACK_ROW_LENGTH, 0));
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels));
|
||||
|
||||
// Store our identifier
|
||||
texture_id = (ImTextureID)(intptr_t)m_image_texture;
|
||||
|
||||
// Restore state
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, thumbnail.width, thumbnail.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, thumbnail.pixels.data()));
|
||||
texture_id = (ImTextureID)(intptr_t)id;
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, last_texture));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IMToolbar::del_all_item()
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#define DEFAULT_TOOLBAR_BUTTON_HEIGHT 80.0f
|
||||
|
||||
namespace Slic3r {
|
||||
struct ThumbnailData;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class IMToolbarItem
|
||||
@@ -33,11 +35,9 @@ public:
|
||||
SliceState slice_state;
|
||||
|
||||
ImTextureID texture_id { 0 };
|
||||
std::vector<unsigned char> image_data;
|
||||
unsigned int image_width;
|
||||
unsigned int image_height;
|
||||
|
||||
bool generate_texture();
|
||||
// Uploads a valid thumbnail as the texture; the item keeps no copy of its pixels.
|
||||
void set_thumbnail(const ThumbnailData& thumbnail);
|
||||
~IMToolbarItem();
|
||||
};
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
|
||||
});
|
||||
add_with_icon("obj_drop", L("Drop to Bed"), L("Object"), "toolbar_flatten", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->get_selection().can_drop(); }, [](Plater* p) { p->drop_selection(); });
|
||||
});
|
||||
add("obj_fit_volume", L("Scale to Fit Print Volume"), L("Object"), [](const std::string&) {
|
||||
return object_op(
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include <ios>
|
||||
#include <iomanip>
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
@@ -6154,6 +6155,14 @@ int PartPlateList::select_plate(int index)
|
||||
//wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT));
|
||||
}
|
||||
|
||||
// With the other plates hidden, the view follows the selected plate.
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && m_plater->is_view3D_shown() && wxGetApp().focus_mode()) {
|
||||
Camera& camera = m_plater->get_camera();
|
||||
Vec3d target = camera.get_target();
|
||||
target.head<2>() = m_plate_list[index]->get_bounding_box().center().head<2>();
|
||||
camera.set_target(target);
|
||||
}
|
||||
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = m_plate_list[index]->get_plate_name();
|
||||
@@ -7063,7 +7072,7 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
|
||||
|
||||
/*rendering related functions*/
|
||||
//render
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome, bool hide_inactive)
|
||||
{
|
||||
const std::lock_guard<std::mutex> local_lock(m_plates_mutex);
|
||||
std::vector<PartPlate*>::iterator it = m_plate_list.begin();
|
||||
@@ -7078,7 +7087,7 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
|
||||
load_icon_textures();
|
||||
for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) {
|
||||
int current_index = (*it)->get_index();
|
||||
if (only_current && (current_index != m_current_plate))
|
||||
if ((only_current || hide_inactive) && (current_index != m_current_plate))
|
||||
continue;
|
||||
if (current_index == m_current_plate) {
|
||||
PartPlate::HeightLimitMode height_mode = (only_current)?PartPlate::HEIGHT_LIMIT_NONE:m_height_limit_mode;
|
||||
@@ -7539,7 +7548,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
|
||||
return ret;
|
||||
}
|
||||
|
||||
int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info, int plate_idx)
|
||||
int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info, int plate_idx, bool with_statistics)
|
||||
{
|
||||
int ret = 0;
|
||||
|
||||
@@ -7613,6 +7622,8 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
}
|
||||
//parse filament info
|
||||
plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result());
|
||||
if (with_statistics)
|
||||
plate_data_item->statistics = gcode_statistics_json(*m_plate_list[i]->get_slice_result());
|
||||
|
||||
// Record mixed (virtual) filaments actually used on this plate.
|
||||
// Source is ToolOrdering::used_mixed_filaments (slots that appeared in
|
||||
|
||||
@@ -1008,7 +1008,7 @@ public:
|
||||
|
||||
/*rendering related functions*/
|
||||
void on_change_color_mode(bool is_dark) { m_is_dark = is_dark; }
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false, bool hide_inactive = false);
|
||||
void set_render_option(bool bedtype_texture, bool plate_settings);
|
||||
void set_render_cali(bool value = true) { render_cali_logo = value; }
|
||||
void render_hover_tooltip() const;
|
||||
@@ -1070,7 +1070,7 @@ public:
|
||||
/* load/store releted functions, with_gcode = true and plate_idx = -1, export all gcode
|
||||
* if with_gcode = true and specify plate_idx, export plate_idx gcode only
|
||||
*/
|
||||
int store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info = true, int plate_idx = -1);
|
||||
int store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info = true, int plate_idx = -1, bool with_statistics = false);
|
||||
int load_from_3mf_structure(PlateDataPtrs& plate_data_list, int filament_count = 1);
|
||||
//load gcode files
|
||||
int load_gcode_files();
|
||||
|
||||
+37
-13
@@ -436,6 +436,7 @@ void Plater::show_illegal_characters_warning(wxWindow* parent)
|
||||
void Plater::mark_plate_toolbar_image_dirty()
|
||||
{
|
||||
m_b_plate_toolbar_image_dirty = true;
|
||||
++m_plate_toolbar_image_version;
|
||||
}
|
||||
|
||||
bool Plater::is_plate_toolbar_image_dirty() const
|
||||
@@ -6070,6 +6071,8 @@ void Sidebar::on_bed_type_change(BedType bed_type)
|
||||
* - Map key encoding:
|
||||
* - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary)
|
||||
* - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary)
|
||||
* - Generic (non-Bambu) units are not four slots wide: their trays index by a cumulative lane
|
||||
* count across units instead of ams_id * 4 + slot_id
|
||||
* - The 0x10000 flag indicates the main/right extruder
|
||||
* - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.)
|
||||
*
|
||||
@@ -6135,13 +6138,26 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
return std::string();
|
||||
};
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
|
||||
auto list = obj->GetFilaSystem()->GetAmsList();
|
||||
for (auto ams : list) {
|
||||
int ams_id = std::stoi(ams.first);
|
||||
int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right
|
||||
// Bambu units are four slots wide; a generic unit reports its own lane count and its trays
|
||||
// carry a cumulative lane index. The AMS map is keyed by string id, so count preceding units
|
||||
// numerically to keep this the same index the parser and the agent use.
|
||||
int generic_tray_base = 0;
|
||||
if (!is_bbl_vendor) {
|
||||
for (const auto& other : list) {
|
||||
if (std::stoi(other.first) < ams_id)
|
||||
generic_tray_base += static_cast<int>(other.second->GetTrays().size());
|
||||
}
|
||||
}
|
||||
for (auto tray : ams.second->GetTrays()) {
|
||||
int slot_id = std::stoi(tray.first);
|
||||
filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id),
|
||||
const int tray_index = is_bbl_vendor ? ams_id * 4 + slot_id : generic_tray_base + slot_id;
|
||||
filament_ams_list.emplace(extruder + tray_index,
|
||||
build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id)));
|
||||
}
|
||||
}
|
||||
@@ -18268,16 +18284,20 @@ wxString Plater::get_project_name()
|
||||
|
||||
void Plater::update_all_plate_thumbnails(bool force_update)
|
||||
{
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++) {
|
||||
PartPlate* plate = get_partplate_list().get_plate(i);
|
||||
ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i};
|
||||
if (force_update || !plate->thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho);
|
||||
}
|
||||
if (force_update || !plate->no_light_thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
}
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++)
|
||||
update_plate_thumbnails(i, force_update);
|
||||
}
|
||||
|
||||
void Plater::update_plate_thumbnails(int plate_idx, bool force_update)
|
||||
{
|
||||
PartPlate* plate = get_partplate_list().get_plate(plate_idx);
|
||||
ThumbnailsParams thumbnail_params = { {}, false, true, true, true, plate_idx};
|
||||
if (force_update || !plate->thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho);
|
||||
}
|
||||
if (force_update || !plate->no_light_thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19564,7 +19584,10 @@ void Plater::export_gcode_3mf(bool export_all)
|
||||
int plate_idx = get_partplate_list().get_curr_plate_index();
|
||||
if (export_all)
|
||||
plate_idx = PLATE_ALL_IDX;
|
||||
export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, plate_idx); // BBS: silence
|
||||
SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence
|
||||
if (wxGetApp().app_config->get_bool("export_statistics"))
|
||||
strategy = strategy | SaveStrategy::WithStatistics;
|
||||
export_3mf(output_path, strategy, plate_idx);
|
||||
|
||||
RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager();
|
||||
|
||||
@@ -20418,7 +20441,8 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
|
||||
//BBS: add bbs 3mf logic
|
||||
PlateDataPtrs plate_data_list;
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx);
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx,
|
||||
strategy & SaveStrategy::WithStatistics);
|
||||
|
||||
// BBS: backup
|
||||
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
|
||||
@@ -434,6 +434,7 @@ public:
|
||||
bool is_new_project_and_check_state() { return m_new_project_and_check_state; }
|
||||
wxString get_project_name();
|
||||
void update_all_plate_thumbnails(bool force_update = false);
|
||||
void update_plate_thumbnails(int plate_idx, bool force_update);
|
||||
void update_obj_preview_thumbnail(ModelObject *, int obj_idx, int vol_idx, std::vector<Slic3r::ColorRGBA> colors, int camera_view_angle_type);
|
||||
void invalid_all_plate_thumbnails();
|
||||
void force_update_all_plate_thumbnails();
|
||||
@@ -1071,6 +1072,8 @@ public:
|
||||
void mark_plate_toolbar_image_dirty();
|
||||
bool is_plate_toolbar_image_dirty() const;
|
||||
void clear_plate_toolbar_image_dirty();
|
||||
// Bumped each time the plate thumbnails go stale, so each consumer can track it on its own.
|
||||
unsigned plate_toolbar_image_version() const { return m_plate_toolbar_image_version; }
|
||||
|
||||
private:
|
||||
struct priv;
|
||||
@@ -1095,6 +1098,7 @@ private:
|
||||
int m_valid_plates_count { 0 };
|
||||
int m_check_status = 0; // 0 not check, 1 check success, 2 check failed
|
||||
bool m_b_plate_toolbar_image_dirty{ true };
|
||||
unsigned m_plate_toolbar_image_version{ 0 };
|
||||
|
||||
void suppress_snapshots();
|
||||
void allow_snapshots();
|
||||
|
||||
@@ -645,7 +645,7 @@ wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxStr
|
||||
combobox->SetSelection(current_region);
|
||||
return;
|
||||
} else {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
config->set("region", region.ToStdString());
|
||||
auto area = config->get_country_code();
|
||||
if (agent) {
|
||||
@@ -2312,6 +2312,13 @@ void PreferencesDialog::create_items()
|
||||
auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets");
|
||||
g_sizer->Add(item_show_unsupported);
|
||||
|
||||
auto item_export_statistics = create_item_checkbox(_L("Export statistics with G-code"),
|
||||
_L("If enabled, exporting a G-code file also writes its statistics as JSON next to it, as <name>.statistics.json, "
|
||||
"and an exported sliced 3MF file holds them inside: print time, filament and feature totals, "
|
||||
"and the same per layer and per object."),
|
||||
"export_statistics");
|
||||
g_sizer->Add(item_export_statistics);
|
||||
|
||||
//// DEVELOPER > Experimental Features
|
||||
g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND);
|
||||
|
||||
@@ -2481,7 +2488,7 @@ wxBoxSizer* PreferencesDialog::create_debug_page()
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
std::string country_code = config->get_country_code();
|
||||
if (agent) {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
agent->set_country_code(country_code);
|
||||
}
|
||||
ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"), ConfirmBeforeSendDialog::VisibleButtons::ONLY_CONFIRM); // ORCA VisibleButtons instead ButtonStyle
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "slic3r/GUI/CalibrationWizardPage.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
@@ -34,14 +36,12 @@
|
||||
#include <wx/image.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/statbox.h>
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/wupdlock.h>
|
||||
#include <wx/menu.h>
|
||||
#include <wx/odcombo.h>
|
||||
@@ -69,6 +69,7 @@
|
||||
#include "MsgDialog.hpp"
|
||||
#include "ParamsDialog.hpp"
|
||||
#include "FilamentPickerDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
@@ -84,6 +85,24 @@
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static FilamentColorPickerValue filament_color_picker_value(const DynamicPrintConfig& config, std::size_t index)
|
||||
{
|
||||
FilamentColorPickerValue value;
|
||||
if (const auto* primary = config.opt<ConfigOptionStrings>("filament_colour"); primary && index < primary->values.size())
|
||||
value.primary = primary->values[index];
|
||||
if (const auto* multi = config.opt<ConfigOptionStrings>("filament_multi_colour"); multi && index < multi->values.size()) {
|
||||
std::istringstream stream(multi->values[index]);
|
||||
for (std::string color; stream >> color;)
|
||||
value.colors.push_back(std::move(color));
|
||||
}
|
||||
if (value.colors.empty() && !value.primary.empty())
|
||||
value.colors.push_back(value.primary);
|
||||
if (const auto* types = config.opt<ConfigOptionStrings>("filament_colour_type"); types && index < types->values.size())
|
||||
value.gradient = value.colors.size() > 1 && types->values[index] == "0";
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
#define BORDER_W 10
|
||||
|
||||
// ---------------------------------
|
||||
@@ -911,31 +930,37 @@ PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset
|
||||
FilamentColor fila_color = get_cur_color_info();
|
||||
|
||||
// Show filament picker dialog
|
||||
FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type);
|
||||
FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type,
|
||||
filament_color_picker_value(m_preset_bundle->project_config, m_filament_idx));
|
||||
|
||||
if (!dialog.IsDataLoaded()) {
|
||||
// If FilamentPicker fails, fallback to default color picker
|
||||
show_default_color_picker();
|
||||
} else if (dialog.ShowModal() == wxID_OK) {
|
||||
// Get selected filament color data
|
||||
FilamentColor fila_color = dialog.GetSelectedFilamentColor();
|
||||
|
||||
// Check if we have valid color data
|
||||
if (!fila_color.m_colors.empty()) {
|
||||
// Convert to storage format
|
||||
std::vector<std::string> colors;
|
||||
for (const wxColour& color : fila_color.m_colors) {
|
||||
colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
}
|
||||
|
||||
bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR);
|
||||
this->sync_colour_config(colors, is_gradient);
|
||||
} else if (dialog.ShowModal() == wxID_OK && dialog.HasSelectionChanged()) {
|
||||
if (const auto& custom = dialog.GetCustomColorSelection()) {
|
||||
const auto value = filament_color_picker_result(*custom);
|
||||
sync_colour_config(value.colors, value.gradient);
|
||||
} else {
|
||||
// Fallback to basic color if no FilamentColor data
|
||||
wxColour selected_color = dialog.GetSelectedColour();
|
||||
if (selected_color.IsOk()) {
|
||||
std::vector<std::string> color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
this->sync_colour_config(color, false);
|
||||
// Get selected filament color data
|
||||
FilamentColor fila_color = dialog.GetSelectedFilamentColor();
|
||||
|
||||
// Check if we have valid color data
|
||||
if (!fila_color.m_colors.empty()) {
|
||||
// Convert to storage format
|
||||
std::vector<std::string> colors;
|
||||
for (const wxColour& color : fila_color.m_colors) {
|
||||
colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
}
|
||||
|
||||
bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR);
|
||||
this->sync_colour_config(colors, is_gradient);
|
||||
} else {
|
||||
// Fallback to basic color if no FilamentColor data
|
||||
wxColour selected_color = dialog.GetSelectedColour();
|
||||
if (selected_color.IsOk()) {
|
||||
std::vector<std::string> color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
this->sync_colour_config(color, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1091,30 +1116,8 @@ bool PlaterPresetComboBox::switch_to_tab()
|
||||
|
||||
void PlaterPresetComboBox::change_extruder_color()
|
||||
{
|
||||
// get current color
|
||||
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
|
||||
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
|
||||
wxColour clr(colors->values[m_filament_idx]);
|
||||
if (!clr.IsOk())
|
||||
clr = wxColour(0, 0, 0); // Don't set alfa to transparence
|
||||
|
||||
auto data = new wxColourData();
|
||||
data->SetChooseFull(1);
|
||||
data->SetColour(clr);
|
||||
|
||||
wxColourDialog dialog(this, data);
|
||||
dialog.CenterOnParent();
|
||||
if (dialog.ShowModal() == wxID_OK)
|
||||
{
|
||||
colors->values[m_filament_idx] = dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
|
||||
DynamicPrintConfig cfg_new = *cfg;
|
||||
cfg_new.set_key_value("filament_colour", colors);
|
||||
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new);
|
||||
this->update();
|
||||
wxGetApp().plater()->on_config_change(cfg_new);
|
||||
}
|
||||
// The Linux sidebar menu edits the same ordered project color as its swatch.
|
||||
show_default_color_picker();
|
||||
}
|
||||
|
||||
void PlaterPresetComboBox::show_add_menu()
|
||||
@@ -1608,19 +1611,12 @@ FilamentColor PlaterPresetComboBox::get_cur_color_info()
|
||||
|
||||
void PlaterPresetComboBox::show_default_color_picker()
|
||||
{
|
||||
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
|
||||
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
|
||||
wxColour current_clr(colors->values[m_filament_idx]);
|
||||
if (!current_clr.IsOk())
|
||||
current_clr = wxColour(0, 0, 0); // Don't set alfa to transparence
|
||||
|
||||
m_clrData.SetColour(current_clr);
|
||||
|
||||
wxColourData data = show_sys_picker_dialog(this, m_clrData);
|
||||
if (m_clrData.GetColour() != data.GetColour()) {
|
||||
std::vector<std::string> color = {data.GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
m_clrData.SetColour(data.GetColour());
|
||||
sync_colour_config(color, false);
|
||||
const auto value = filament_color_picker_value(wxGetApp().preset_bundle->project_config, m_filament_idx);
|
||||
const auto initial = filament_color_picker_initial(value);
|
||||
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(value), clr_picker);
|
||||
if (dialog.ShowModal() == wxID_OK && filament_color_picker_changed(initial, dialog.selection())) {
|
||||
const auto selected = filament_color_picker_result(*dialog.selection());
|
||||
sync_colour_config(selected.colors, selected.gradient);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/clrpicker.h>
|
||||
@@ -201,7 +200,6 @@ public:
|
||||
|
||||
// BBS
|
||||
wxButton* clr_picker { nullptr };
|
||||
wxColourData m_clrData;
|
||||
|
||||
wxColor get_color() { return m_color; }
|
||||
|
||||
|
||||
@@ -558,15 +558,37 @@ void Selection::center()
|
||||
return;
|
||||
}
|
||||
|
||||
double Selection::get_drop_min_z() const
|
||||
{
|
||||
if (!m_valid)
|
||||
return 0.;
|
||||
if (m_mode == Volume)
|
||||
return get_bounding_box().min.z();
|
||||
|
||||
double min_z = DBL_MAX;
|
||||
for (const auto& [obj_idx, inst_idxs] : m_cache.content) {
|
||||
if (obj_idx >= (int)m_model->objects.size())
|
||||
continue; // wipe tower
|
||||
for (int inst_idx : inst_idxs)
|
||||
min_z = std::min(min_z, m_model->objects[obj_idx]->get_instance_min_z(inst_idx));
|
||||
}
|
||||
return min_z == DBL_MAX ? 0. : min_z;
|
||||
}
|
||||
|
||||
bool Selection::can_drop() const
|
||||
{
|
||||
return std::abs(get_drop_min_z()) > -SINKING_Z_THRESHOLD;
|
||||
}
|
||||
|
||||
void Selection::drop()
|
||||
{
|
||||
if (std::abs(this->get_bounding_box().min.z()) < -SINKING_Z_THRESHOLD) {
|
||||
return; // shouldnt happen, but better check anyways, already checked in append_menu_item_drop()
|
||||
}
|
||||
const double min_z = get_drop_min_z();
|
||||
if (std::abs(min_z) < -SINKING_Z_THRESHOLD)
|
||||
return; // already checked in can_drop()
|
||||
|
||||
wxGetApp().plater()->take_snapshot(L("Move object"));
|
||||
|
||||
this->move_to_center(Vec3d(0, 0, -this->get_bounding_box().min.z()));
|
||||
this->move_to_center(Vec3d(0, 0, -min_z));
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& volume = *(*m_volumes)[i];
|
||||
|
||||
@@ -248,6 +248,9 @@ public:
|
||||
void clone(int numbers = 1);
|
||||
void center();
|
||||
void drop();
|
||||
bool can_drop() const;
|
||||
// Lowest world Z that drop() moves to the bed; whole objects count model parts only, as auto drop does.
|
||||
double get_drop_min_z() const;
|
||||
void center_plate(const int plate_idx);
|
||||
void set_printable(bool printable);
|
||||
bool get_auto_drop() const;
|
||||
|
||||
+831
-255
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
#ifndef slic3r_StatusPanel_hpp_
|
||||
#define slic3r_StatusPanel_hpp_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include "DeviceManager.hpp"
|
||||
#include "MonitorPage.hpp"
|
||||
@@ -66,6 +68,7 @@
|
||||
#include "StagedBuild.hpp"
|
||||
|
||||
class StepIndicator;
|
||||
class wxChoice;
|
||||
|
||||
#define COMMAND_TIMEOUT 5
|
||||
|
||||
@@ -140,28 +143,36 @@ class ExtruderImage : public wxWindow
|
||||
ScalableBitmap *m_left_extruder_active_empty;
|
||||
ScalableBitmap *m_left_extruder_unactive_filled;
|
||||
ScalableBitmap *m_left_extruder_unactive_empty;
|
||||
|
||||
ScalableBitmap *m_right_extruder_active_filled;
|
||||
ScalableBitmap *m_right_extruder_active_empty;
|
||||
ScalableBitmap *m_right_extruder_unactive_filled;
|
||||
ScalableBitmap *m_right_extruder_unactive_empty;
|
||||
|
||||
|
||||
ScalableBitmap *m_extruder_single_nozzle_empty_load;
|
||||
ScalableBitmap *m_extruder_single_nozzle_empty_unload;
|
||||
ScalableBitmap *m_extruder_single_nozzle_filled_load;
|
||||
ScalableBitmap *m_extruder_single_nozzle_filled_unload;
|
||||
|
||||
|
||||
ExtruderState m_left_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
ExtruderState m_right_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
ExtruderState m_single_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
std::vector<ExtruderState> m_multi_extruder_states;
|
||||
bool m_generic_nozzle_display{false};
|
||||
|
||||
public:
|
||||
void update(int nozzle_num, int nozzle_id);
|
||||
void update(ExtruderState single_state);
|
||||
void update(ExtruderState right_state, ExtruderState left_state);
|
||||
|
||||
void update(ExtruderState state, int idx);
|
||||
|
||||
void msw_rescale();
|
||||
void updateGenericSize();
|
||||
void setExtruderCount(int nozzle_num);
|
||||
void setGenericNozzleDisplay(bool enabled);
|
||||
void setExtruderUsed(std::string loc);
|
||||
void setExtruderUsed(int nozzle_idx);
|
||||
void paintEvent(wxPaintEvent &evt);
|
||||
|
||||
void render(wxDC &dc);
|
||||
@@ -495,6 +506,8 @@ protected:
|
||||
std::vector<ExtruderImage *> m_extruderImage;
|
||||
|
||||
SwitchBoard * m_nozzle_btn_panel;
|
||||
wxChoice* m_generic_nozzle_selector{nullptr};
|
||||
int m_generic_nozzle_selector_count{0};
|
||||
|
||||
wxStaticText * m_text_tasklist_caption;
|
||||
|
||||
@@ -507,10 +520,23 @@ protected:
|
||||
wxBoxSizer * m_misc_ctrl_sizer;
|
||||
StaticBox* m_fan_panel;
|
||||
StaticLine * m_line_nozzle;
|
||||
TempInput* m_tempCtrl_nozzle;
|
||||
wxWindowID m_nozzle_temp_control_id{wxID_ANY};
|
||||
wxWindow* m_temp_nozzle_parent{nullptr};
|
||||
wxBoxSizer* m_temp_nozzle_sizer{nullptr};
|
||||
size_t m_temp_nozzle_active_count{0};
|
||||
TempInput* m_tempCtrl_nozzle{nullptr};
|
||||
int m_temp_nozzle_timeout{ 0 };
|
||||
TempInput* m_tempCtrl_nozzle_deputy;
|
||||
TempInput* m_tempCtrl_nozzle_deputy{nullptr};
|
||||
int m_temp_nozzle_deputy_timeout{ 0 };
|
||||
|
||||
std::vector<TempInput*> m_tempCtrl_nozzles;
|
||||
std::vector<int> m_temp_nozzle_timeouts;
|
||||
|
||||
// Last (dev_id, device toolhead count, preset nozzle count) we warned about, to avoid log spam.
|
||||
std::string m_last_mismatch_dev_id;
|
||||
int m_last_mismatch_device_count{-1};
|
||||
int m_last_mismatch_preset_count{-1};
|
||||
|
||||
TempInput * m_tempCtrl_bed;
|
||||
int m_temp_bed_timeout {0};
|
||||
TempInput * m_tempCtrl_chamber;
|
||||
@@ -550,6 +576,7 @@ protected:
|
||||
|
||||
/* AMS control box <-> live nozzle-rack panel toggle (rack printers only) */
|
||||
SwitchBoard* m_ams_rack_switch{ nullptr };
|
||||
MultiSwitchButton* m_ams_nozzle_switch{ nullptr };
|
||||
|
||||
AMSControl* m_ams_control;
|
||||
StaticBox* m_ams_control_box;
|
||||
@@ -624,6 +651,9 @@ public:
|
||||
|
||||
wxBoxSizer *create_temp_axis_group(wxWindow *parent);
|
||||
wxBoxSizer *create_temp_control(wxWindow *parent);
|
||||
TempInput* create_nozzle_temp_control(wxWindow *parent, wxWindowID id);
|
||||
void set_temp_input_colors(TempInput* temp_ctrl);
|
||||
void ensure_nozzle_temp_controls(size_t count);
|
||||
wxBoxSizer *create_misc_control(wxWindow *parent);
|
||||
wxBoxSizer *create_axis_control(wxWindow *parent);
|
||||
wxPanel *create_bed_control(wxWindow *parent);
|
||||
@@ -654,6 +684,7 @@ private:
|
||||
friend class MonitorPanel;
|
||||
void wire_controls();
|
||||
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
|
||||
void sync_nozzle_temp_controls(size_t count);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
|
||||
@@ -801,6 +832,7 @@ protected:
|
||||
void update_cloud_subtask(MachineObject *obj);
|
||||
void update_sdcard_subtask(MachineObject *obj);
|
||||
void update_temp_ctrl(MachineObject *obj);
|
||||
void check_extruder_count_mismatch(MachineObject* obj);
|
||||
void update_misc_ctrl(MachineObject *obj);
|
||||
void update_ams(MachineObject* obj);
|
||||
void update_rack(MachineObject* obj);
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "ColorDecomposeDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <wx/weakref.h>
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
#include "Widgets/StaticLine.hpp"
|
||||
@@ -32,9 +37,7 @@
|
||||
#include <mutex>
|
||||
#include <wx/button.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcbuffer.h>
|
||||
#include <wx/dialog.h>
|
||||
@@ -509,30 +512,6 @@ static wxString auto_mix_mode_label(TextureAutoMixMode mode)
|
||||
_L("One-click RYBW auto-mix");
|
||||
}
|
||||
|
||||
static wxPoint constrained_dialog_position(wxWindow* anchor, const wxSize& dialog_size)
|
||||
{
|
||||
if (!anchor)
|
||||
return wxDefaultPosition;
|
||||
|
||||
wxSize size = dialog_size;
|
||||
if (size.x <= 0 || size.y <= 0)
|
||||
size = wxSize(anchor->FromDIP(450), anchor->FromDIP(350));
|
||||
|
||||
wxPoint pos = anchor->ClientToScreen(wxPoint(0, anchor->GetSize().y));
|
||||
wxRect display_rect;
|
||||
int display_idx = wxDisplay::GetFromPoint(pos);
|
||||
if (display_idx != wxNOT_FOUND)
|
||||
display_rect = wxDisplay(display_idx).GetClientArea();
|
||||
else
|
||||
display_rect = wxDisplay().GetClientArea();
|
||||
|
||||
pos.x = std::clamp(pos.x, display_rect.GetLeft(),
|
||||
std::max(display_rect.GetLeft(), display_rect.GetRight() - size.x));
|
||||
pos.y = std::clamp(pos.y, display_rect.GetTop(),
|
||||
std::max(display_rect.GetTop(), display_rect.GetBottom() - size.y));
|
||||
return pos;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FilamentSelectPopup
|
||||
// ============================================================
|
||||
@@ -653,24 +632,22 @@ public:
|
||||
auto on_add_filament = m_on_add_filament;
|
||||
wxWindow* popup_parent = GetParent();
|
||||
wxWindow* color_anchor = m_dialog_anchor ? m_dialog_anchor : popup_parent;
|
||||
wxWeakRef<FilamentSelectPopup> popup(this);
|
||||
m_closing_from_action = true;
|
||||
Dismiss();
|
||||
wxColourData cd;
|
||||
cd.SetChooseFull(true);
|
||||
wxColourDialog dlg(popup_parent, &cd);
|
||||
auto move_color_dialog = [&dlg, color_anchor]() {
|
||||
dlg.Move(constrained_dialog_position(color_anchor, dlg.GetBestSize()));
|
||||
};
|
||||
dlg.Bind(wxEVT_SHOW, [move_color_dialog](wxShowEvent& e) mutable {
|
||||
e.Skip();
|
||||
if (e.IsShown())
|
||||
move_color_dialog();
|
||||
});
|
||||
move_color_dialog();
|
||||
if (dlg.ShowModal() == wxID_OK) {
|
||||
wxColour clr = dlg.GetColourData().GetColour();
|
||||
if (on_add_filament) on_add_filament(clr);
|
||||
std::optional<wxColour> selected;
|
||||
{
|
||||
// Capture the stable mapping row before the transient popup is dismissed.
|
||||
ColorPickerDialog dlg(popup_parent, ColorRGBA(0.f, 0.f, 0.f, 1.f), {}, false, color_anchor);
|
||||
if (popup) popup->Dismiss();
|
||||
// Dismissal can delete the popup. Only copied locals are used below.
|
||||
if (dlg.ShowModal() == wxID_OK && dlg.selection()) {
|
||||
const auto& color = std::get<ColorRGBA>(*dlg.selection());
|
||||
selected = wxColour(static_cast<unsigned char>(std::lround(color.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(color.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(color.b() * 255.f)));
|
||||
}
|
||||
}
|
||||
if (selected && on_add_filament) on_add_filament(*selected);
|
||||
});
|
||||
|
||||
m_content->SetSizer(outer);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "WebGuideDialog.hpp"
|
||||
#include "CloudProvider.hpp"
|
||||
#include "ConfigWizard.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -450,6 +451,16 @@ void GuideFrame::on_profile_loaded()
|
||||
SaveProfileData();
|
||||
const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll;
|
||||
m_profile_ready = true;
|
||||
notify_profile_loaded();
|
||||
}
|
||||
|
||||
// The data is often ready before the loading page has run its scripts, and a
|
||||
// call into a page without HandleStudio is lost, so the message waits for both.
|
||||
void GuideFrame::notify_profile_loaded()
|
||||
{
|
||||
if (!m_profile_ready || !m_page_ready)
|
||||
return;
|
||||
json res;
|
||||
res["command"] = "userguide_profile_load_finish";
|
||||
res["sequence_id"] = "10001";
|
||||
@@ -545,7 +556,11 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
|
||||
wxString strCmd = j["command"];
|
||||
BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnScriptMessage;Command:" << strCmd;
|
||||
|
||||
if (strCmd == "close_page") {
|
||||
if (strCmd == "userguide_loading_page_ready") {
|
||||
m_page_ready = true;
|
||||
notify_profile_loaded();
|
||||
}
|
||||
else if (strCmd == "close_page") {
|
||||
this->EndModal(wxID_CANCEL);
|
||||
}
|
||||
if (strCmd == "user_clause") {
|
||||
@@ -755,7 +770,7 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
|
||||
if (wxGetApp().is_user_login()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: user_logout on user_guide_finish";
|
||||
// agent->user_logout();
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ public:
|
||||
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
|
||||
void init_guide_paths();
|
||||
void on_profile_loaded();
|
||||
void notify_profile_loaded();
|
||||
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
|
||||
bool BuildProfileDataFromPresetBundle();
|
||||
bool BuildProfileDataFromVendors();
|
||||
@@ -142,6 +143,10 @@ private:
|
||||
|
||||
//First Load
|
||||
bool bFirstComplete{false};
|
||||
// The loading page gets userguide_profile_load_finish once both are true;
|
||||
// every instance of it reports in, so the flag is never reset.
|
||||
bool m_profile_ready{false};
|
||||
bool m_page_ready{false};
|
||||
// Set once in the destructor. Read through `this` by the loading thread
|
||||
// (joined before `this` dies) and captured as the shared_ptr by CallAfter
|
||||
// lambdas so they don't touch `this` after the object is freed.
|
||||
|
||||
@@ -36,12 +36,17 @@
|
||||
#include <wx/textdlg.h>
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/url.h>
|
||||
#include <wx/uri.h>
|
||||
|
||||
#include <slic3r/GUI/Widgets/WebView.hpp>
|
||||
#include <slic3r/GUI/Widgets/WebViewHostDialog.hpp>
|
||||
#include <wx/webview.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <webkit2/webkit2.h>
|
||||
#endif
|
||||
#include <wx/infobar.h>
|
||||
|
||||
namespace pt = boost::property_tree;
|
||||
@@ -55,6 +60,13 @@ namespace GUI {
|
||||
|
||||
#define LOGIN_INFO_UPDATE_TIMER_ID 10002
|
||||
|
||||
// OrcaCloud is a native pane of its own: the site refuses to be framed
|
||||
// (x-frame-options: DENY / CSP frame-ancestors 'none').
|
||||
static const char *const kCloudUrl = "https://cloud.orcaslicer.com";
|
||||
// #LeftBoard in resources/web/homepage/css/home.css is a 262px column plus a 1px right
|
||||
// border; the homepage keeps that width as the nav column while the cloud pane is open.
|
||||
static const int kLeftNavWidth = 263;
|
||||
|
||||
// The home page's strings (resources/web/homepage), keyed by their data-i18n and T() ids.
|
||||
static json home_page_strings()
|
||||
{
|
||||
@@ -136,7 +148,11 @@ WebViewPanel::WebViewPanel(wxWindow *parent)
|
||||
m_reset_on_show = WebView::NeedsRecreateOnShow();
|
||||
SetSizer(topsizer);
|
||||
|
||||
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
// The homepage shell is the left nav column; when OrcaCloud opens, its page lives in a
|
||||
// second native pane beside it (the site refuses to be framed).
|
||||
m_content_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_content_sizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
topsizer->Add(m_content_sizer, wxSizerFlags().Expand().Proportion(1));
|
||||
|
||||
// Log backend information
|
||||
/* m_browser->GetUserAgent() may lead crash
|
||||
@@ -292,12 +308,282 @@ void WebViewPanel::create_browser()
|
||||
m_browser->Hide();
|
||||
}
|
||||
|
||||
// The cloud page gets its own native view, created on first use and kept alive afterwards
|
||||
// so its session and scroll position survive hiding.
|
||||
void WebViewPanel::create_cloud_browser()
|
||||
{
|
||||
m_cloud_browser = WebView::CreateWebView(this, wxEmptyString);
|
||||
|
||||
#ifdef __linux__
|
||||
// The pane's web session is a cookie (guide §3), and WebKitGTK keeps cookies only in memory
|
||||
// unless a persistent store is set up, so without this the sign-in is lost on every restart
|
||||
// (and on every reset_browser). Each wxWebView has its own context, so give the cloud pane its
|
||||
// own store instead of sharing PrinterWebView's. Mirrors PrinterWebView::create_browser.
|
||||
if (auto* wv = static_cast<WebKitWebView*>(m_cloud_browser->GetNativeBackend())) {
|
||||
const auto cookies_path = boost::filesystem::path(data_dir() + "/cache/cloud_cookies.db");
|
||||
auto* cookie_manager = webkit_web_context_get_cookie_manager(webkit_web_view_get_context(wv));
|
||||
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookies_path.c_str(),
|
||||
WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Webview events are command events, so they would otherwise propagate up to the
|
||||
// panel's handlers, which drive the homepage view. Keep the cloud pane's events here.
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NAVIGATING, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_LOADED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_TITLE_CHANGED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_ERROR, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
|
||||
m_cloud_browser->Hide();
|
||||
if (m_content_sizer)
|
||||
m_content_sizer->Add(m_cloud_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
}
|
||||
|
||||
// The scheme://host[:port] origin of an http(s) URL, lower-cased and with the default port
|
||||
// dropped, or empty for any other scheme or a malformed URL. Comparing origins (not a string
|
||||
// prefix) is what keeps a lookalike host such as "https://cloud.orcaslicer.com.evil.com" out.
|
||||
static wxString http_origin_of(const wxString& url)
|
||||
{
|
||||
const wxURI uri(url);
|
||||
if (!uri.HasScheme() || !uri.HasServer())
|
||||
return {};
|
||||
const wxString scheme = uri.GetScheme().Lower();
|
||||
if (scheme != "http" && scheme != "https")
|
||||
return {};
|
||||
wxString origin = scheme + "://" + uri.GetServer().Lower();
|
||||
const wxString port = uri.GetPort();
|
||||
if (!port.IsEmpty() && port != (scheme == "https" ? "443" : "80"))
|
||||
origin += ":" + port;
|
||||
return origin;
|
||||
}
|
||||
|
||||
// Whether a NAVIGATING event will replace the pane's top-level document rather than load a
|
||||
// subframe. Edge and macOS report the target frame; WebKitGTK does not (IsTargetMainFrame() is
|
||||
// always false there), so a user-gesture navigation stands in: a page's own embedded frames load
|
||||
// without a gesture and stay put, while a user following a link to another site does not.
|
||||
static bool cloud_navigation_is_top_level(const wxWebViewEvent& evt)
|
||||
{
|
||||
#ifdef __WXGTK__
|
||||
return evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER;
|
||||
#else
|
||||
return evt.IsTargetMainFrame();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Sends an off-site navigation to the system browser instead of embedding it. Deferred: this runs
|
||||
// inside a native webview navigation callback, so launching must not re-enter it.
|
||||
static void open_cloud_url_externally(const wxString& url)
|
||||
{
|
||||
if (url.empty())
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(info) << "cloud pane: opening externally " << WebView::RedactUrl(url).ToUTF8().data();
|
||||
wxGetApp().CallAfter([url] { wxLaunchDefaultBrowser(url); });
|
||||
}
|
||||
|
||||
// Consumed on the cloud view so it never reaches the homepage handlers. {web} pages stay in the
|
||||
// pane; every other site (OAuth providers included) opens in the system browser (guide §3).
|
||||
void WebViewPanel::OnCloudWebViewEvent(wxWebViewEvent& evt)
|
||||
{
|
||||
const wxEventType event_type = evt.GetEventType();
|
||||
if (event_type == wxEVT_WEBVIEW_NAVIGATING) {
|
||||
const wxString url = evt.GetURL();
|
||||
// Guide §3: keep every {web} page in the pane and open other sites (OAuth providers
|
||||
// included) in the system browser. Only top-level navigations are diverted, and
|
||||
// should_divert_cloud_url() excludes same-origin URLs and internal schemes.
|
||||
if (should_divert_cloud_url(url) && cloud_navigation_is_top_level(evt)) {
|
||||
evt.Veto();
|
||||
open_cloud_url_externally(url);
|
||||
}
|
||||
} else if (event_type == wxEVT_WEBVIEW_NEWWINDOW) {
|
||||
const wxString url = evt.GetURL();
|
||||
if (!url.empty()) {
|
||||
if (is_cloud_origin(url))
|
||||
load_cloud_url(url);
|
||||
else
|
||||
open_cloud_url_externally(url);
|
||||
}
|
||||
evt.Veto();
|
||||
} else if (event_type == wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED) {
|
||||
on_cloud_script_message(evt.GetString().utf8_string());
|
||||
} else if (event_type == wxEVT_WEBVIEW_LOADED || event_type == wxEVT_WEBVIEW_ERROR) {
|
||||
// A Start URL deferred behind a sign-out (see load_cloud_after_signout).
|
||||
if (!m_cloud_after_signout_url.IsEmpty()) {
|
||||
const wxString next = std::exchange(m_cloud_after_signout_url, wxString());
|
||||
load_cloud_url(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The page asks for a web session by posting JSON through window.wx; only the newest request
|
||||
// counts (tracked in GUI_App). Nothing here touches the UI directly: the desktop login dialog and
|
||||
// the network mint must not run on the webview's message-dispatch stack.
|
||||
void WebViewPanel::on_cloud_script_message(const std::string& body)
|
||||
{
|
||||
const json msg = json::parse(body, nullptr, false);
|
||||
if (msg.is_discarded() || !msg.is_object() || msg.value("command", std::string()) != "request_web_session")
|
||||
return;
|
||||
|
||||
std::string state;
|
||||
if (msg.contains("data") && msg["data"].is_object())
|
||||
state = msg["data"].value("state", std::string());
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
wxGetApp().CallAfter([state = std::move(state)] { wxGetApp().request_cloud_web_session(state); });
|
||||
}
|
||||
|
||||
void WebViewPanel::apply_cloud_layout(bool cloud_on)
|
||||
{
|
||||
if (!m_content_sizer)
|
||||
return;
|
||||
|
||||
// Cloud mode pins the homepage to the nav column width; otherwise it fills the panel.
|
||||
if (m_browser) {
|
||||
if (wxSizerItem* item = m_content_sizer->GetItem(m_browser))
|
||||
item->SetProportion(cloud_on ? 0 : 1);
|
||||
// kLeftNavWidth is in DIP: the web view's CSS px are DIP, so scale to physical pixels.
|
||||
m_browser->SetMinSize(cloud_on ? m_browser->FromDIP(wxSize(kLeftNavWidth, -1)) : wxDefaultSize);
|
||||
}
|
||||
Layout();
|
||||
}
|
||||
|
||||
void WebViewPanel::load_cloud_url(const wxString& url)
|
||||
{
|
||||
if (url.empty())
|
||||
return;
|
||||
// Track the origin of whatever the pane is told to load (Start URL, Complete URL, sign-out or
|
||||
// the plain cloud fallback), so navigation policy has the right origin from the first load on.
|
||||
const wxString origin = http_origin_of(url);
|
||||
if (!origin.IsEmpty())
|
||||
m_cloud_base_url = origin;
|
||||
if (!m_cloud_browser)
|
||||
return;
|
||||
// Raw LoadURL: WebView::LoadUrl re-serialises through wxURI, which must not alter the
|
||||
// fragment that carries the one-time handoff token. Log only the redacted URL.
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(url).ToUTF8().data();
|
||||
m_cloud_browser->LoadURL(url);
|
||||
}
|
||||
|
||||
bool WebViewPanel::is_cloud_origin(const wxString& url) const
|
||||
{
|
||||
const wxString origin = http_origin_of(url);
|
||||
return !origin.IsEmpty() && !m_cloud_base_url.IsEmpty() && origin == m_cloud_base_url;
|
||||
}
|
||||
|
||||
bool WebViewPanel::should_divert_cloud_url(const wxString& url) const
|
||||
{
|
||||
const wxString origin = http_origin_of(url);
|
||||
// Unknown cloud origin or a non-web scheme: never divert (the pane must keep working).
|
||||
if (origin.IsEmpty() || m_cloud_base_url.IsEmpty())
|
||||
return false;
|
||||
return origin != m_cloud_base_url;
|
||||
}
|
||||
|
||||
// Explicit sign-out ends the pane's own web session. That session is a cookie JS cannot clear
|
||||
// (HttpOnly, guide §3/§8) and ClearHistory() is a no-op on WebKitGTK, so drop the pane's stored
|
||||
// data natively. wx 3.3.2 ignores the type mask on every backend (any non-zero mask clears
|
||||
// everything) and needs an invalid `since` to delete cookies, so the default "all" call is used.
|
||||
void WebViewPanel::clear_cloud_storage()
|
||||
{
|
||||
if (m_cloud_browser)
|
||||
m_cloud_browser->ClearBrowsingData(wxWEBVIEW_BROWSING_DATA_ALL);
|
||||
}
|
||||
|
||||
void WebViewPanel::load_cloud_after_signout(const wxString& url)
|
||||
{
|
||||
if (m_cloud_pending_signout && !m_cloud_signout_url.IsEmpty()) {
|
||||
// The pane did not exist when the user signed out, so its persisted session must be
|
||||
// cleared before the sign-out page runs and `url` loads once that navigation finishes
|
||||
// (see OnCloudWebViewEvent).
|
||||
m_cloud_pending_signout = false;
|
||||
m_cloud_after_signout_url = url;
|
||||
clear_cloud_storage();
|
||||
load_cloud_url(m_cloud_signout_url);
|
||||
return;
|
||||
}
|
||||
load_cloud_url(url);
|
||||
}
|
||||
|
||||
void WebViewPanel::schedule_cloud_load(const wxString& url)
|
||||
{
|
||||
if (!m_cloud_browser) {
|
||||
load_cloud_after_signout(url);
|
||||
return;
|
||||
}
|
||||
// CreateWebView queues the "wx" handler add on the view (macOS/Linux); queue the load behind
|
||||
// it so the page's first scripts already see the bridge (guide §3).
|
||||
m_cloud_browser->CallAfter([this, url]() { load_cloud_after_signout(url); });
|
||||
}
|
||||
|
||||
void WebViewPanel::cloud_signed_out(const wxString& signout_url)
|
||||
{
|
||||
if (signout_url.empty())
|
||||
return;
|
||||
m_cloud_signout_url = signout_url;
|
||||
// The next Start URL must re-seed the pane (it now reflects the signed-out state), so forget
|
||||
// the previous seed regardless of which branch below runs.
|
||||
m_cloud_seed_url.clear();
|
||||
if (m_cloud_browser) {
|
||||
m_cloud_pending_signout = false;
|
||||
// Run the sign-out page (ends the server-side session), then clear the pane's stored data.
|
||||
load_cloud_url(signout_url);
|
||||
clear_cloud_storage();
|
||||
} else {
|
||||
// Pane never opened this run: clear the persisted session on its first open.
|
||||
m_cloud_pending_signout = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
{
|
||||
if (on) {
|
||||
// load_cloud_url records the origin from whatever it loads, including the plain cloud
|
||||
// fallback used when `url` is empty.
|
||||
if (!m_cloud_browser) {
|
||||
create_cloud_browser();
|
||||
m_cloud_seed_url = url;
|
||||
schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url);
|
||||
} else if (!url.empty() && url != m_cloud_seed_url) {
|
||||
// Re-seed only when the Start URL actually changed — a login, an account switch or a
|
||||
// sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged
|
||||
// Start URL re-runs the page's auth check on every open, which flashes "Signing you
|
||||
// in…" and re-requests a session (guide §10: mint when the page asks, not per open).
|
||||
// The pane is kept alive, so an unchanged open just shows the page it already holds.
|
||||
m_cloud_seed_url = url;
|
||||
load_cloud_after_signout(url);
|
||||
}
|
||||
m_cloud_browser->Show();
|
||||
} else if (m_cloud_browser) {
|
||||
// Closing keeps the pane alive with its session, so re-opening does not have to sign in
|
||||
// again (guide §10). Nothing is cleared here.
|
||||
m_cloud_browser->Hide();
|
||||
}
|
||||
apply_cloud_layout(on && m_cloud_browser != nullptr);
|
||||
}
|
||||
|
||||
void WebViewPanel::reset_browser()
|
||||
{
|
||||
// Rebuild both views the same way, keeping the cloud pane's open/closed state.
|
||||
const bool cloud_on = m_cloud_browser && m_cloud_browser->IsShown();
|
||||
|
||||
m_browser->Destroy(); // also removes it from the sizer
|
||||
create_browser();
|
||||
GetSizer()->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
Layout();
|
||||
m_content_sizer->Insert(size_t(0), m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
|
||||
if (m_cloud_browser) {
|
||||
m_cloud_browser->Destroy();
|
||||
m_cloud_browser = nullptr;
|
||||
create_cloud_browser();
|
||||
schedule_cloud_load(wxString(kCloudUrl));
|
||||
m_cloud_browser->Show(cloud_on);
|
||||
// The rebuilt pane holds no seed, so the next open loads the Start URL again.
|
||||
m_cloud_seed_url.clear();
|
||||
}
|
||||
|
||||
apply_cloud_layout(cloud_on);
|
||||
}
|
||||
|
||||
bool WebViewPanel::Show(bool show)
|
||||
@@ -647,7 +933,7 @@ void WebViewPanel::update_mode()
|
||||
*/
|
||||
void WebViewPanel::OnNavigationRequest(wxWebViewEvent& evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
const wxString &url = evt.GetURL();
|
||||
if (url.StartsWith("File://") || url.StartsWith("file://")) {
|
||||
if (!url.Contains("/web/homepage/index.html")) {
|
||||
@@ -693,7 +979,7 @@ void WebViewPanel::OnNavigationComplete(wxWebViewEvent& evt)
|
||||
{
|
||||
m_browser->Show();
|
||||
Layout();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
if (wxGetApp().get_mode() == comDevelop)
|
||||
wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'");
|
||||
UpdateState();
|
||||
@@ -712,6 +998,9 @@ void WebViewPanel::OnDocumentLoaded(wxWebViewEvent& evt)
|
||||
if (wxGetApp().get_mode() == comDevelop)
|
||||
wxLogMessage("%s", "Document loaded; url='" + evt.GetURL() + "'");
|
||||
}
|
||||
// A reloaded homepage loses its cloud-mode class; restore it if the pane is open.
|
||||
if (m_cloud_browser && m_cloud_browser->IsShown())
|
||||
RunScript("SetCloudMode(true)");
|
||||
UpdateState();
|
||||
SendCloudProvidersInfo();
|
||||
}
|
||||
@@ -727,7 +1016,7 @@ void WebViewPanel::OnTitleChanged(wxWebViewEvent &evt)
|
||||
*/
|
||||
void WebViewPanel::OnNewWindow(wxWebViewEvent& evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
wxString flag = " (other)";
|
||||
|
||||
if (evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "wx/fs_mem.h"
|
||||
#include "wx/stdpaths.h"
|
||||
#include <wx/panel.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/tbarbase.h>
|
||||
#include "wx/textctrl.h"
|
||||
#include <wx/timer.h>
|
||||
@@ -114,14 +115,62 @@ public:
|
||||
|
||||
void update_mode();
|
||||
|
||||
// Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the
|
||||
// cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide
|
||||
// whether it already has a session; empty falls back to the plain cloud page.
|
||||
void set_cloud_mode(bool on, const wxString& url = wxString());
|
||||
|
||||
// True while the OrcaCloud pane exists and is visible.
|
||||
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
|
||||
|
||||
// Loads `url` in the cloud pane — the webview that owns the session handoff. Raw LoadURL (no
|
||||
// wxURI re-serialisation) so the Complete URL's #fragment stays intact; never logged.
|
||||
void load_cloud_url(const wxString& url);
|
||||
|
||||
// Clears the cloud pane's own session on an explicit desktop sign-out. If the pane has not
|
||||
// been created yet, the sign-out is remembered and applied when it is first opened.
|
||||
void cloud_signed_out(const wxString& signout_url);
|
||||
|
||||
bool Show(bool show = true) override;
|
||||
private:
|
||||
void create_browser();
|
||||
void create_cloud_browser();
|
||||
void reset_browser();
|
||||
// Consumes the cloud pane's webview events so they stay off the homepage handlers.
|
||||
void OnCloudWebViewEvent(wxWebViewEvent& evt);
|
||||
// True when `url` has the same scheme/host/port as the cloud origin ({web}).
|
||||
bool is_cloud_origin(const wxString& url) const;
|
||||
// True when a navigation to `url` must leave the pane: an http(s) URL on another origin than
|
||||
// the (known) cloud origin. False for non-web schemes and while the origin is unknown.
|
||||
bool should_divert_cloud_url(const wxString& url) const;
|
||||
// Parses a request_web_session message and defers it to GUI_App.
|
||||
void on_cloud_script_message(const std::string& body);
|
||||
// Loads `url`, first clearing a lazily-required sign-out (see cloud_signed_out).
|
||||
void load_cloud_after_signout(const wxString& url);
|
||||
// First load of a freshly created pane: deferred so CreateWebView's queued "wx" handler add
|
||||
// runs first and the page sees window.wx at load time (guide §3).
|
||||
void schedule_cloud_load(const wxString& url);
|
||||
// Clears the pane's stored web data (its cookie-backed session) on an explicit sign-out.
|
||||
void clear_cloud_storage();
|
||||
// Re-applies the content sizer's proportions and min sizes for the cloud state.
|
||||
void apply_cloud_layout(bool cloud_on);
|
||||
|
||||
wxWebView* m_browser;
|
||||
wxWebView* m_cloud_browser{nullptr};
|
||||
wxBoxSizer* m_content_sizer{nullptr};
|
||||
wxString m_home_url;
|
||||
bool m_reset_on_show{false};
|
||||
// {web} origin (scheme://host[:port]) of the loaded pane. Set from every load, including the
|
||||
// plain cloud fallback, so navigation policy can keep {web} pages in the pane and send other
|
||||
// sites to the system browser.
|
||||
wxString m_cloud_base_url;
|
||||
// Start URL the pane was last seeded with. An open whose URL matches this is an ordinary
|
||||
// re-open (nothing changed), so it only shows the pane instead of reloading it (guide §10).
|
||||
wxString m_cloud_seed_url;
|
||||
// Sign-out clear the pane must apply once it exists / next loads.
|
||||
wxString m_cloud_signout_url;
|
||||
wxString m_cloud_after_signout_url;
|
||||
bool m_cloud_pending_signout{false};
|
||||
wxButton * m_button_stop;
|
||||
wxTextCtrl *m_url;
|
||||
#if !BBL_RELEASE_TO_PUBLIC
|
||||
|
||||
@@ -244,7 +244,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
|
||||
m_sizer_ams_body->Add(m_sizer_ams_area_right, wxALIGN_CENTER, 0);
|
||||
|
||||
m_sizer_body->Add(m_sizer_ams_items, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
|
||||
m_sizer_ams_gap = m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
|
||||
m_sizer_body->Add(m_sizer_ams_body, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(m_sizer_down_road, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(m_sizer_ams_option, 0, wxEXPAND, 0);
|
||||
@@ -371,7 +371,7 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else{
|
||||
else if (m_total_ext_count == 1){
|
||||
for (auto id : m_item_ids[MAIN_EXTRUDER_ID]){
|
||||
if (id == ams_id){
|
||||
return true;
|
||||
@@ -379,6 +379,10 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AMSControl::AmsSelectedSwitch(wxCommandEvent& event) {
|
||||
@@ -519,7 +523,7 @@ void AMSControl::msw_rescale()
|
||||
m_button_ams_setting_press.msw_rescale();
|
||||
m_button_ams_setting->SetBitmap(m_button_ams_setting_normal.bmp());
|
||||
|
||||
m_extruder->msw_rescale();
|
||||
if (m_extruder) m_extruder->msw_rescale();
|
||||
|
||||
if (m_button_extruder_feed) m_button_extruder_feed->Rescale(); // ORCA
|
||||
if (m_button_extruder_back) m_button_extruder_back->Rescale(); // ORCA
|
||||
@@ -603,6 +607,8 @@ void AMSControl::CreateAms()
|
||||
|
||||
|
||||
void AMSControl::ClearAms() {
|
||||
m_sizer_ams_items->Clear(false);
|
||||
|
||||
m_simplebook_ams_right->DeleteAllPages();
|
||||
m_simplebook_ams_left->DeleteAllPages();
|
||||
m_simplebook_ams_right->DestroyChildren();
|
||||
@@ -613,7 +619,7 @@ void AMSControl::ClearAms() {
|
||||
m_simplebook_ams_left->Refresh();
|
||||
|
||||
for (auto it : m_ams_preview_list) {
|
||||
delete it.second;
|
||||
if (it.second) it.second->Destroy();
|
||||
}
|
||||
m_ams_preview_list.clear();
|
||||
m_ext_image_list.clear();
|
||||
@@ -624,12 +630,181 @@ void AMSControl::ClearAms() {
|
||||
m_ams_item_list.clear();
|
||||
m_sizer_prv_right->Clear();
|
||||
m_sizer_prv_left->Clear();
|
||||
m_item_ids = { {}, {} };
|
||||
|
||||
if (m_nozzle_book) {
|
||||
m_nozzle_book->Destroy();
|
||||
m_nozzle_book = nullptr;
|
||||
} else {
|
||||
for (auto &pane : m_nozzle_panes) {
|
||||
if (pane.preview_panel) pane.preview_panel->Destroy();
|
||||
if (pane.ams_book) pane.ams_book->Destroy();
|
||||
if (pane.page) pane.page->Destroy();
|
||||
}
|
||||
}
|
||||
m_item_ids.assign(std::max(m_total_ext_count, 2), {});
|
||||
m_nozzle_panes.clear();
|
||||
m_active_nozzle_id = 0;
|
||||
m_current_ams.clear();
|
||||
m_current_show_ams_left.clear();
|
||||
m_current_show_ams_right.clear();
|
||||
pair_id.clear();
|
||||
}
|
||||
|
||||
void AMSControl::restore_legacy_ams_layout()
|
||||
{
|
||||
m_sizer_ams_items->Clear(false);
|
||||
m_sizer_ams_items->SetOrientation(wxHORIZONTAL);
|
||||
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
|
||||
m_sizer_ams_items->Add(m_panel_prv_left, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
m_sizer_ams_items->Add(m_panel_prv_right, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
|
||||
m_panel_prv_left->Show();
|
||||
m_panel_prv_right->Show();
|
||||
m_simplebook_ams_left->Show();
|
||||
m_simplebook_ams_right->Show();
|
||||
m_panel_down_road->Show();
|
||||
m_sizer_body->Show(m_sizer_ams_body, true);
|
||||
m_sizer_option_mid->Show(m_extruder, true);
|
||||
m_down_road->SetSingleSideLayout(false);
|
||||
m_sizer_ams_gap->SetProportion(1);
|
||||
m_sizer_ams_gap->SetBorder(FromDIP(10));
|
||||
m_down_road->SetNozzleCount(m_total_ext_count);
|
||||
}
|
||||
|
||||
bool AMSControl::use_multi_nozzle_layout() const
|
||||
{
|
||||
const bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
return is_bbl ? m_total_ext_count > 2 : m_nozzle_book != nullptr;
|
||||
}
|
||||
|
||||
void AMSControl::CreateAmsMultiNozzle(const std::string &series_name, const std::string &printer_type) {
|
||||
const size_t pane_count = static_cast<size_t>(std::max(m_total_ext_count, 0));
|
||||
m_nozzle_panes.resize(pane_count);
|
||||
m_active_nozzle_id = 0;
|
||||
|
||||
m_sizer_ams_items->Clear(false);
|
||||
m_sizer_ams_items->SetOrientation(wxVERTICAL);
|
||||
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
|
||||
m_sizer_body->Show(m_sizer_ams_body, false);
|
||||
m_sizer_ams_gap->SetProportion(0);
|
||||
m_sizer_ams_gap->SetBorder(0);
|
||||
|
||||
m_panel_prv_left->Hide();
|
||||
m_panel_prv_right->Hide();
|
||||
m_simplebook_ams_left->Hide();
|
||||
m_simplebook_ams_right->Hide();
|
||||
m_panel_down_road->Show();
|
||||
m_sizer_option_mid->Show(m_extruder, true);
|
||||
m_switcher->Hide();
|
||||
m_down_road->SetNozzleCount(1);
|
||||
m_down_road->SetSingleSideLayout(true, AMSPanelPos::LEFT_PANEL);
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_SINGLE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_extruder->updateNozzleNum(1, series_name);
|
||||
|
||||
m_nozzle_book = new wxSimplebook(m_amswin, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_nozzle_book->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
m_nozzle_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
m_sizer_ams_items->Add(m_nozzle_book, 0, wxALIGN_CENTER);
|
||||
|
||||
for (size_t nozzle_id = 0; nozzle_id < pane_count; ++nozzle_id) {
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
// A generic page is one H2C side. Keep the left-side geometry as the
|
||||
// canonical single-nozzle presentation; the road and AMS item code
|
||||
// still use the same side-specific paths as the legacy layout.
|
||||
pane.panel_pos = AMSPanelPos::LEFT_PANEL;
|
||||
pane.page = new wxPanel(m_nozzle_book, wxID_ANY);
|
||||
pane.page->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
pane.page->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
pane.page_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
pane.page->SetSizer(pane.page_sizer);
|
||||
|
||||
pane.preview_panel = new wxScrolledWindow(pane.page, wxID_ANY);
|
||||
pane.preview_panel->SetScrollRate(10, 0);
|
||||
pane.preview_panel->SetSize(AMS_ITEMS_PANEL_SIZE);
|
||||
pane.preview_panel->SetMinSize(AMS_ITEMS_PANEL_SIZE);
|
||||
pane.preview_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
|
||||
pane.preview_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
pane.preview_panel->SetSizer(pane.preview_sizer);
|
||||
|
||||
// Generic lane cards grow to their calculated width for 1-8 lanes. Keep the
|
||||
// book at the legacy minimum, but allow the page to expand to that width.
|
||||
pane.ams_book = new wxSimplebook(pane.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
|
||||
pane.ams_book->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
pane.ams_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
// wxALIGN_CENTER does not centre horizontally inside a HORIZONTAL sizer, so a book wider
|
||||
// than pane.ams_area's own minimum would sit at ams_area's left edge and overflow to the
|
||||
// right, shifting the whole card column off the shared down-road connector axis. A pair of
|
||||
// stretch spacers keeps the book centred on the pane (and thus on the connector) at any
|
||||
// book width; ams_area is expanded below so the spacers have room to work with.
|
||||
pane.ams_area = new wxBoxSizer(wxHORIZONTAL);
|
||||
pane.ams_area->AddStretchSpacer(1);
|
||||
pane.ams_area->Add(pane.ams_book, 0, wxALIGN_CENTER, 0);
|
||||
pane.ams_area->AddStretchSpacer(1);
|
||||
|
||||
pane.page_sizer->Add(pane.preview_panel, 0, wxALIGN_CENTER | wxTOP, FromDIP(5));
|
||||
pane.page_sizer->Add(pane.ams_area, 0, wxEXPAND | wxTOP, FromDIP(10));
|
||||
|
||||
m_nozzle_book->AddPage(pane.page, wxEmptyString, nozzle_id == 0);
|
||||
}
|
||||
|
||||
auto add_info = [this, series_name, printer_type, pane_count](AMSinfo info) {
|
||||
if (pane_count == 0) return;
|
||||
|
||||
info.nozzle_id = std::clamp(info.nozzle_id, 0, static_cast<int>(pane_count) - 1);
|
||||
AddAmsPreview(info, info.ams_type);
|
||||
if (info.ams_type == AMSModel::EXT_AMS && info.cans.size() == 1)
|
||||
AddAms({info}, series_name, printer_type, m_nozzle_panes[info.nozzle_id].panel_pos);
|
||||
else
|
||||
AddAms(info);
|
||||
};
|
||||
|
||||
for (const auto &info : m_ams_info)
|
||||
add_info(info);
|
||||
|
||||
for (auto &info : m_ext_info) {
|
||||
const int virtual_tray_id = std::atoi(info.ams_id.c_str());
|
||||
if (virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID &&
|
||||
virtual_tray_id >= VIRTUAL_TRAY_MAIN_ID - static_cast<int>(pane_count) + 1)
|
||||
info.nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
|
||||
add_info(info);
|
||||
}
|
||||
|
||||
for (auto &pane : m_nozzle_panes) {
|
||||
if (pane.ams_book && pane.ams_book->GetPageCount() > 0)
|
||||
pane.ams_book->SetSelection(0);
|
||||
if (pane.preview_sizer) pane.preview_sizer->Layout();
|
||||
if (pane.preview_panel) pane.preview_panel->FitInside();
|
||||
if (pane.ams_book) pane.ams_book->Layout();
|
||||
if (pane.page) pane.page->Layout();
|
||||
|
||||
if (!pane.current_ams.empty()) {
|
||||
auto preview = m_ams_preview_list.find(pane.current_ams);
|
||||
if (preview != m_ams_preview_list.end() && preview->second)
|
||||
preview->second->OnSelected();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_nozzle_book && pane_count > 0) {
|
||||
m_nozzle_book->SetSelection(0);
|
||||
m_current_ams = m_nozzle_panes[0].current_ams;
|
||||
if (!m_current_ams.empty())
|
||||
SwitchAms(m_current_ams);
|
||||
else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
}
|
||||
}
|
||||
m_amswin->Layout();
|
||||
m_amswin->Fit();
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std::string &printer_type)
|
||||
{
|
||||
restore_legacy_ams_layout();
|
||||
|
||||
std::vector<AMSinfo> single_info_left;
|
||||
std::vector<AMSinfo> single_info_right;
|
||||
|
||||
@@ -751,6 +926,8 @@ void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std
|
||||
|
||||
void AMSControl::CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type)
|
||||
{
|
||||
restore_legacy_ams_layout();
|
||||
|
||||
std::vector<int>m_item_nums{0,0};
|
||||
std::vector<AMSinfo> single_info;
|
||||
|
||||
@@ -979,6 +1156,17 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
if (m_ams_info[i].routes_to_main_extruder() != ams_info[i].routes_to_main_extruder()) {
|
||||
fresh = true;
|
||||
}
|
||||
if (m_ams_info[i].nozzle_id != ams_info[i].nozzle_id) {
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(m_ext_info.size()); ++i) {
|
||||
if (m_ext_info[i].ams_id != ext_info[i].ams_id ||
|
||||
m_ext_info[i].nozzle_id != ext_info[i].nozzle_id ||
|
||||
m_ext_info[i].ext_type != ext_info[i].ext_type) {
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
@@ -993,11 +1181,16 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
m_dev_id = dev_id;
|
||||
if (fresh){
|
||||
ClearAms();
|
||||
if (m_total_ext_count >= 2){
|
||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
||||
}else{
|
||||
CreateAmsSingleNozzle(series_name, printer_type);
|
||||
bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl) {
|
||||
if (m_total_ext_count == 1)
|
||||
CreateAmsSingleNozzle(series_name, printer_type);
|
||||
else if (m_total_ext_count == 2)
|
||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
||||
}
|
||||
else
|
||||
CreateAmsMultiNozzle(series_name, printer_type);
|
||||
|
||||
SetSize(wxSize(FromDIP(578), -1));
|
||||
SetMinSize(wxSize(FromDIP(578), -1));
|
||||
Layout();
|
||||
@@ -1070,7 +1263,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
}
|
||||
|
||||
/*update ams extruder*/
|
||||
if (m_extruder->updateNozzleNum(m_total_ext_count, series_name))
|
||||
if (m_total_ext_count <= 2 && m_extruder->updateNozzleNum(m_total_ext_count, series_name))
|
||||
{
|
||||
m_amswin->Layout();
|
||||
}
|
||||
@@ -1106,7 +1299,15 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
||||
{
|
||||
AMSPreview *ams_prv = nullptr;
|
||||
|
||||
if (info.routes_to_main_extruder())
|
||||
// Match AddAms: the generic multi-nozzle layout's preview panel is the visible one for
|
||||
// non-Bambu vendors at any extruder count, not only above two.
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, type);
|
||||
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
}
|
||||
else if (info.routes_to_main_extruder())
|
||||
{
|
||||
ams_prv = new AMSPreview(m_panel_prv_right, wxID_ANY, info, type);
|
||||
m_sizer_prv_right->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
@@ -1129,6 +1330,54 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
||||
void AMSControl::createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos) {
|
||||
auto ams_item = new AmsItem(parent, info, info.ams_type, pos);
|
||||
parent->InsertPage(idx, ams_item, wxEmptyString, true);
|
||||
|
||||
// Generic AMS cards may be wider than the legacy 264 DIP page. Propagate
|
||||
// that calculated width through the page and simplebook so 5-8 lanes are
|
||||
// laid out fully instead of being clipped by the old minimum width.
|
||||
if (info.ams_type == AMSModel::GENERIC_AMS) {
|
||||
const int required_width = ams_item->GetMinSize().x;
|
||||
if (required_width > 0) {
|
||||
const int page_width = std::max(parent->GetMinSize().x, required_width);
|
||||
parent->SetMinSize(wxSize(page_width, -1));
|
||||
if (auto *page = parent->GetParent()) {
|
||||
page->SetMinSize(wxSize(std::max(page->GetMinSize().x, page_width), -1));
|
||||
if (auto *nozzle_book = page->GetParent())
|
||||
nozzle_book->SetMinSize(wxSize(std::max(nozzle_book->GetMinSize().x, page_width), -1));
|
||||
}
|
||||
}
|
||||
// Above eight lanes, AmsItem is a fixed-minimum viewport over a wider
|
||||
// lane strip. Expand the viewport through its page's sizers rather
|
||||
// than propagating the strip's virtual width into the card/book.
|
||||
// Reset back to the centered fixed layout for eight or fewer lanes so a
|
||||
// later small unit is not left in expand mode.
|
||||
const bool generic_lane_layout = !wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (generic_lane_layout) {
|
||||
if (auto *page = parent->GetParent()) {
|
||||
auto pane = std::find_if(m_nozzle_panes.begin(), m_nozzle_panes.end(),
|
||||
[page](const NozzleAmsPane &candidate) { return candidate.page == page; });
|
||||
if (pane != m_nozzle_panes.end() && m_nozzle_book) {
|
||||
const bool expand_viewport = info.cans.size() > 8;
|
||||
if (auto *book_item = pane->ams_area->GetItem(pane->ams_book)) {
|
||||
book_item->SetProportion(expand_viewport ? 1 : 0);
|
||||
book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
}
|
||||
// Keep the centring spacers (items 0 and 2) elastic only in the centred layout;
|
||||
// in the >8-lane viewport they must collapse so the book can fill ams_area.
|
||||
if (auto *lead = pane->ams_area->GetItem(static_cast<size_t>(0)))
|
||||
lead->SetProportion(expand_viewport ? 0 : 1);
|
||||
if (auto *trail = pane->ams_area->GetItem(static_cast<size_t>(2)))
|
||||
trail->SetProportion(expand_viewport ? 0 : 1);
|
||||
if (auto *area_item = pane->page_sizer->GetItem(pane->ams_area))
|
||||
area_item->SetFlag(wxEXPAND | wxTOP);
|
||||
if (auto *nozzle_book_item = m_sizer_ams_items->GetItem(m_nozzle_book))
|
||||
nozzle_book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
if (auto *items_item = m_sizer_body->GetItem(m_sizer_ams_items))
|
||||
items_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
m_amswin->Layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ams_item->set_selection(idx);
|
||||
idx++;
|
||||
|
||||
@@ -1153,7 +1402,10 @@ AMSRoadShowMode AMSControl::findFirstMode(AMSPanelPos pos) {
|
||||
if (item->second->get_ams_model() == AMSModel::EXT_AMS && item->second->get_ext_type() == AMSModelOriginType::LITE_EXT) return AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
}
|
||||
else{
|
||||
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM) {
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
}
|
||||
else {
|
||||
for (auto ids : pair_id){
|
||||
if (ids.first == ams_id || ids.second == ams_id){
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
@@ -1172,8 +1424,13 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
wxPanel* book_panel = new wxPanel(parent);
|
||||
wxBoxSizer* book_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
book_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||
book_panel->SetSize(AMS_PANEL_SIZE);
|
||||
book_panel->SetMinSize(AMS_PANEL_SIZE);
|
||||
// Size the page to the AMS book it becomes, not to the legacy fixed width: an AMS card is
|
||||
// added directly as the book page (so it always spans the book), while a single external
|
||||
// spool is wrapped here. Matching the book keeps the Ext card centred on the same axis as
|
||||
// the AMS cards and the shared down-road connector anchored below them.
|
||||
const wxSize book_panel_size(std::max(parent->GetMinSize().x, AMS_PANEL_SIZE.x), AMS_PANEL_SIZE.y);
|
||||
book_panel->SetSize(book_panel_size);
|
||||
book_panel->SetMinSize(book_panel_size);
|
||||
|
||||
AmsItem* ams1 = nullptr, * ams2 = nullptr;
|
||||
ams1 = new AmsItem(book_panel, infos[0], infos[0].ams_type, pos);
|
||||
@@ -1200,6 +1457,14 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
//book_sizer->Add(ams1, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
book_sizer->Add(ams1, 0, wxLEFT, (book_panel->GetSize().x - ams1->GetSize().x) / 2);
|
||||
}
|
||||
else if (pos == AMSPanelPos::SINGLE_PANEL) {
|
||||
// Centre the single external-spool card explicitly, using the same fixed-margin
|
||||
// technique as the Lite-EXT branch above. Two stretch spacers centred the card
|
||||
// against the wrapper's own width rather than the AMS book, which left the card
|
||||
// (and the connector drawn below it) off the book/connector axis.
|
||||
book_sizer->Add(ams1, 0, wxLEFT,
|
||||
std::max(0, (book_panel_size.x - ams1->GetSize().x) / 2));
|
||||
}
|
||||
else{
|
||||
auto ext_image = new AMSExtImage(book_panel, pos, m_total_ext_count, false);
|
||||
book_sizer->Add(ams1, 0, wxLEFT, FromDIP(30));
|
||||
@@ -1228,7 +1493,20 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
|
||||
void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
|
||||
{
|
||||
if (m_total_ext_count > 1){
|
||||
// The generic multi-nozzle layout (non-Bambu vendors) owns the only visible book; a unit must
|
||||
// land there for any extruder count. Keying off m_total_ext_count instead sent 1- and 2-extruder
|
||||
// generic printers into the legacy simplebooks that CreateAmsMultiNozzle hides.
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
pane.item_ids.push_back(info.ams_id);
|
||||
m_item_ids[nozzle_id].push_back(info.ams_id);
|
||||
if (pane.current_ams.empty()) pane.current_ams = info.ams_id;
|
||||
createAms(pane.ams_book, pane.page_index, info, AMSPanelPos::SINGLE_PANEL);
|
||||
pane.ams_book->Fit();
|
||||
pane.ams_book->Layout();
|
||||
}
|
||||
else if (m_total_ext_count == 2){
|
||||
if (info.routes_to_main_extruder()){
|
||||
createAms(m_simplebook_ams_right, m_right_page_index, info, AMSPanelPos::RIGHT_PANEL);
|
||||
}
|
||||
@@ -1239,6 +1517,9 @@ void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
|
||||
else if (m_total_ext_count == 1){
|
||||
createAms(m_simplebook_ams_left, m_left_page_index, info, AMSPanelPos::LEFT_PANEL);
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
m_simplebook_ams_left->Layout();
|
||||
m_simplebook_ams_right->Layout();
|
||||
m_simplebook_ams_left->Refresh();
|
||||
@@ -1269,7 +1550,19 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
if (single_info.size() <= 0){
|
||||
return;
|
||||
}
|
||||
if (m_total_ext_count == 2) {
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
for (const auto &info : single_info) {
|
||||
pane.item_ids.push_back(info.ams_id);
|
||||
m_item_ids[nozzle_id].push_back(info.ams_id);
|
||||
}
|
||||
if (pane.current_ams.empty()) pane.current_ams = single_info.front().ams_id;
|
||||
createAmsPanel(pane.ams_book, pane.page_index, single_info, series_name, printer_type, AMSPanelPos::SINGLE_PANEL, m_total_ext_count);
|
||||
pane.ams_book->Fit();
|
||||
pane.ams_book->Layout();
|
||||
}
|
||||
else if (m_total_ext_count == 2) {
|
||||
if (single_info[0].routes_to_main_extruder()) {
|
||||
createAmsPanel(m_simplebook_ams_right, m_right_page_index, single_info, series_name, printer_type, AMSPanelPos::RIGHT_PANEL, m_total_ext_count);
|
||||
}
|
||||
@@ -1285,6 +1578,9 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
createAmsPanel(m_simplebook_ams_left, m_left_page_index, single_info, series_name, printer_type, AMSPanelPos::LEFT_PANEL, m_total_ext_count);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
m_simplebook_ams_left->Layout();
|
||||
m_simplebook_ams_right->Layout();
|
||||
@@ -1322,6 +1618,21 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos) {
|
||||
if (single_info.size() <= 0) return;
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
for (const auto &info : single_info) {
|
||||
auto *ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, info.ams_type);
|
||||
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
ams_prv->Bind(wxEVT_LEFT_DOWN, [this, ams_prv](wxMouseEvent &e) {
|
||||
SwitchAms(ams_prv->get_ams_id());
|
||||
e.Skip();
|
||||
});
|
||||
m_ams_preview_list[info.ams_id] = ams_prv;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
AMSPreview* ams_prv = nullptr;
|
||||
AMSPreview* ams_prv2 = nullptr;
|
||||
if (pos == AMSPanelPos::RIGHT_PANEL){
|
||||
@@ -1360,8 +1671,97 @@ void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos)
|
||||
}
|
||||
}
|
||||
|
||||
void AMSControl::SelectNozzle(int nozzle_id)
|
||||
{
|
||||
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
|
||||
if (m_active_nozzle_id == nozzle_id &&
|
||||
(!m_nozzle_book || m_nozzle_book->GetSelection() == nozzle_id))
|
||||
return;
|
||||
|
||||
m_active_nozzle_id = nozzle_id;
|
||||
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
|
||||
m_nozzle_book->SetSelection(nozzle_id);
|
||||
m_current_ams = m_nozzle_panes[nozzle_id].current_ams;
|
||||
if (!m_current_ams.empty()) {
|
||||
SwitchAms(m_current_ams);
|
||||
return;
|
||||
}
|
||||
if (m_down_road) {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
}
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
post_event(SimpleEvent(EVT_AMS_SWITCH));
|
||||
}
|
||||
|
||||
void AMSControl::SwitchAms(std::string ams_id)
|
||||
{
|
||||
if (use_multi_nozzle_layout()) {
|
||||
NozzleAmsPane *pane = nullptr;
|
||||
int nozzle_id = -1;
|
||||
for (int index = 0; index < static_cast<int>(m_nozzle_panes.size()); ++index) {
|
||||
auto &candidate = m_nozzle_panes[index];
|
||||
if (std::find(candidate.item_ids.begin(), candidate.item_ids.end(), ams_id) != candidate.item_ids.end()) {
|
||||
pane = &candidate;
|
||||
nozzle_id = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pane) return;
|
||||
|
||||
m_active_nozzle_id = nozzle_id;
|
||||
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
|
||||
m_nozzle_book->SetSelection(nozzle_id);
|
||||
|
||||
pane->current_ams = ams_id;
|
||||
m_current_ams = ams_id;
|
||||
m_current_select = ams_id;
|
||||
|
||||
for (const auto &id : pane->item_ids) {
|
||||
auto preview = m_ams_preview_list.find(id);
|
||||
if (preview == m_ams_preview_list.end() || !preview->second) continue;
|
||||
if (id == ams_id) preview->second->OnSelected();
|
||||
else preview->second->UnSelected();
|
||||
}
|
||||
|
||||
auto item = m_ams_item_list.find(ams_id);
|
||||
if (item != m_ams_item_list.end() && item->second) {
|
||||
if (pane->ams_book)
|
||||
pane->ams_book->SetSelection(item->second->get_selection());
|
||||
|
||||
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
if (item->second->get_can_count() == GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (item->second->get_ams_model() == AMSModel::N3S_AMS)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
else if (item->second->get_ams_model() == AMSModel::AMS_LITE ||
|
||||
(item->second->get_ams_model() == AMSModel::EXT_AMS &&
|
||||
item->second->get_ext_type() == AMSModelOriginType::LITE_EXT))
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
|
||||
if (m_down_road) {
|
||||
if (pane->panel_pos == AMSPanelPos::LEFT_PANEL) {
|
||||
m_down_road->UpdateLeft(1, road_mode);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
} else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, road_mode);
|
||||
}
|
||||
m_down_road->UpdatePassRoad(pane->panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
}
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
|
||||
post_event(SimpleEvent(EVT_AMS_SWITCH));
|
||||
return;
|
||||
}
|
||||
|
||||
if(ams_id == m_current_show_ams_left || ams_id == m_current_show_ams_right){return;}
|
||||
|
||||
bool is_in_right = IsAmsInRightPanel(ams_id);
|
||||
@@ -1432,13 +1832,17 @@ void AMSControl::SwitchAms(std::string ams_id)
|
||||
else {
|
||||
AMSRoadShowMode mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
|
||||
if (item->get_ams_model() == AMSModel::N3S_AMS)
|
||||
if (item->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (item->get_ams_model() == AMSModel::N3S_AMS)
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
|
||||
for (auto it : pair_id) {
|
||||
if (it.first == ams_id || it.second == ams_id) {
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
break;
|
||||
if (item->get_can_count() <= GENERIC_AMS_SLOT_NUM) {
|
||||
for (auto it : pair_id) {
|
||||
if (it.first == ams_id || it.second == ams_id) {
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos == AMSPanelPos::LEFT_PANEL ? m_down_road->UpdateLeft(m_total_ext_count, mode)
|
||||
@@ -1500,6 +1904,20 @@ void AMSControl::SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std
|
||||
AmsItem *item = nullptr;
|
||||
if (m_ams_item_list.find(ams_id) != m_ams_item_list.end()) { item = m_ams_item_list[ams_id]; }
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
|
||||
if (!m_extruder)
|
||||
return;
|
||||
|
||||
if (on_off && item)
|
||||
m_extruder->OnAmsLoading(true, 0, item->GetTagColr(slot_id));
|
||||
else
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (on_off && item) {
|
||||
auto col = item->GetTagColr(slot_id);
|
||||
m_extruder->OnAmsLoading(true, nozzle_id, col);
|
||||
@@ -1624,6 +2042,69 @@ void AMSControl::SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadTy
|
||||
else{
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(ams->get_nozzle_id(), 0, m_total_ext_count - 1);
|
||||
if (nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
if (ams->get_can_count() == GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
else if (ams->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (ams->get_ams_model() == AMSModel::N3S_AMS)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
else if (ams->get_ams_model() == AMSModel::AMS_LITE ||
|
||||
(ams->get_ams_model() == AMSModel::EXT_AMS && ams->get_ext_type() == AMSModelOriginType::LITE_EXT))
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
|
||||
if (can_index >= 0 && can_index < static_cast<int>(info.cans.size()) && m_down_road)
|
||||
m_down_road->SetPassRoadColour(pane.panel_pos == AMSPanelPos::LEFT_PANEL,
|
||||
info.cans[can_index].material_colour);
|
||||
if (m_down_road) {
|
||||
if (pane.panel_pos == AMSPanelPos::LEFT_PANEL) {
|
||||
m_down_road->UpdateLeft(1, road_mode);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
} else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, road_mode);
|
||||
}
|
||||
}
|
||||
|
||||
int road_length = 0;
|
||||
|
||||
if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_NONE) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP1) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (can_index >= 0 && can_index < info.cans.size())
|
||||
{
|
||||
m_down_road->SetPassRoadColour(left, info.cans[can_index].material_colour);
|
||||
|
||||
@@ -47,6 +47,20 @@ public:
|
||||
void on_retry();
|
||||
|
||||
protected:
|
||||
struct NozzleAmsPane
|
||||
{
|
||||
wxPanel* page{nullptr};
|
||||
wxBoxSizer* page_sizer{nullptr};
|
||||
wxScrolledWindow* preview_panel{nullptr};
|
||||
wxBoxSizer* preview_sizer{nullptr};
|
||||
wxSimplebook* ams_book{nullptr};
|
||||
wxBoxSizer* ams_area{nullptr};
|
||||
AMSPanelPos panel_pos{AMSPanelPos::LEFT_PANEL};
|
||||
std::vector<std::string> item_ids;
|
||||
std::string current_ams;
|
||||
int page_index{0};
|
||||
};
|
||||
|
||||
std::string m_current_ams;
|
||||
std::string m_current_slot_left;
|
||||
std::string m_current_slot_right;
|
||||
@@ -63,6 +77,9 @@ protected:
|
||||
|
||||
std::string m_dev_id;
|
||||
std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
|
||||
std::vector<NozzleAmsPane> m_nozzle_panes;
|
||||
wxSimplebook* m_nozzle_book{nullptr};
|
||||
int m_active_nozzle_id{0};
|
||||
std::vector<std::pair<std::string, std::string>> pair_id;
|
||||
|
||||
int m_total_ext_count = 1;
|
||||
@@ -103,6 +120,7 @@ protected:
|
||||
wxSimplebook *m_simplebook_ams_left{nullptr};
|
||||
wxSimplebook *m_simplebook_ams_right{ nullptr };
|
||||
wxSimplebook *m_simplebook_bottom{nullptr};
|
||||
wxSizerItem *m_sizer_ams_gap{nullptr};
|
||||
wxPanel *m_panel_down_road{ nullptr };
|
||||
int m_left_page_index = 0;
|
||||
int m_right_page_index = 0;
|
||||
@@ -156,6 +174,7 @@ public:
|
||||
void createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos);
|
||||
void createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSinfo> infos, const std::string &series_name, const std::string &printer_type, AMSPanelPos pos, int total_ext_num);
|
||||
AMSRoadShowMode findFirstMode(AMSPanelPos pos);
|
||||
void restore_legacy_ams_layout();
|
||||
|
||||
AMSModel m_ams_model{AMSModel::EXT_AMS};
|
||||
AMSModel m_ext_model{AMSModel::EXT_AMS};
|
||||
@@ -183,6 +202,7 @@ public:
|
||||
|
||||
void UpdatePassRoad(std::string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||
void CreateAms();
|
||||
void CreateAmsMultiNozzle(const std::string& series_name, const std::string& printer_type);
|
||||
void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
|
||||
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
|
||||
void ClearAms();
|
||||
@@ -210,6 +230,8 @@ public:
|
||||
void SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std::string slot_id);
|
||||
void SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||
void SwitchAms(std::string ams_id);
|
||||
void SelectNozzle(int nozzle_id);
|
||||
int GetActiveNozzleId() const { return m_active_nozzle_id; }
|
||||
|
||||
void msw_rescale();
|
||||
void on_filament_load(wxCommandEvent &event);
|
||||
@@ -234,6 +256,11 @@ public:
|
||||
private:
|
||||
std::string get_filament_id(const std::string& ams_id, const std::string& can_id);
|
||||
|
||||
// True when AMS units are laid out on the generic multi-nozzle book. Bambu
|
||||
// printers keep the legacy count-based single/double-nozzle layout at any
|
||||
// extruder count; other vendors use the book created for them.
|
||||
bool use_multi_nozzle_layout() const;
|
||||
|
||||
public:
|
||||
std::string m_current_select;
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
|
||||
#include "CalibUtils.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
@@ -76,6 +77,48 @@ namespace Slic3r { namespace GUI {
|
||||
//#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), 144)
|
||||
#define AMS_CANS_WINDOW_SIZE wxSize(FromDIP(264), FromDIP(174))
|
||||
#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), FromDIP(174))
|
||||
|
||||
int generic_ams_layout_width(wxWindow *window, int lane_count)
|
||||
{
|
||||
const int base_width = window->FromDIP(264);
|
||||
const int lane_width = window->FromDIP(52);
|
||||
const int lane_gap = window->FromDIP(4);
|
||||
if (lane_count <= 4)
|
||||
return base_width;
|
||||
|
||||
// Lane centers are distributed as width * (index + 1) / (count + 1).
|
||||
// Keep at least one card width plus the requested gap between centers.
|
||||
return std::max(base_width, (lane_count + 1) * (lane_width + lane_gap));
|
||||
}
|
||||
|
||||
std::vector<int> generic_ams_lane_centers(int lane_count, int width)
|
||||
{
|
||||
std::vector<int> centers;
|
||||
if (lane_count <= 0)
|
||||
return centers;
|
||||
|
||||
centers.reserve(lane_count);
|
||||
for (int lane = 0; lane < lane_count; ++lane)
|
||||
centers.push_back(width * (lane + 1) / (lane_count + 1));
|
||||
return centers;
|
||||
}
|
||||
|
||||
int generic_ams_preview_width(wxWindow *window, int lane_count)
|
||||
{
|
||||
const int base_width = window->FromDIP(52);
|
||||
const int lane_width = window->FromDIP(9);
|
||||
const int lane_gap = window->FromDIP(4);
|
||||
if (lane_count <= 0)
|
||||
return base_width;
|
||||
|
||||
return std::max(base_width, lane_count * lane_width + (lane_count + 1) * lane_gap);
|
||||
}
|
||||
|
||||
bool use_generic_ams_layout(AMSModel model)
|
||||
{
|
||||
return model == AMSModel::GENERIC_AMS &&
|
||||
(!wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor());
|
||||
}
|
||||
bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, bool humidity_flag)
|
||||
{
|
||||
if (!ams) return false;
|
||||
@@ -170,10 +213,14 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
|
||||
info.can_id = std::to_string(0);
|
||||
this->cans.clear();
|
||||
|
||||
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID))
|
||||
try {
|
||||
const int virtual_tray_id = std::stoi(tray.id);
|
||||
if (virtual_tray_id >= 0 && virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID)
|
||||
this->nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
|
||||
}
|
||||
catch (...) {
|
||||
this->nozzle_id = 0;
|
||||
else if (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
|
||||
this->nozzle_id = 1;
|
||||
}
|
||||
|
||||
if (tray.is_tray_info_ready()) {
|
||||
info.ctype = tray.ctype;
|
||||
@@ -856,7 +903,11 @@ AMSextruder::AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const
|
||||
|
||||
void AMSextruder::TurnOff()
|
||||
{
|
||||
m_left_extruder->TurnOff();
|
||||
if (m_left_extruder) m_left_extruder->TurnOff();
|
||||
if (m_right_extruder) m_right_extruder->TurnOff();
|
||||
for (auto *extruder : m_nozzle_extruders) {
|
||||
if (extruder) extruder->TurnOff();
|
||||
}
|
||||
}
|
||||
|
||||
void AMSextruder::create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, int nozzle_num)
|
||||
@@ -882,7 +933,7 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
|
||||
m_left_extruder->OnAmsLoading(load, col);
|
||||
if (load) m_current_colur_deputy = col;
|
||||
}
|
||||
else if (m_nozzle_num > 1){
|
||||
else if (m_nozzle_num == 2){
|
||||
if (nozzle_id == MAIN_EXTRUDER_ID) {
|
||||
m_right_extruder->OnAmsLoading(load, col);
|
||||
if (m_current_colur != col){
|
||||
@@ -896,6 +947,15 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (m_nozzle_num > 2) {
|
||||
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(m_nozzle_extruders.size())) {
|
||||
m_nozzle_extruders[nozzle_id]->OnAmsLoading(load, col);
|
||||
if (load) m_current_colur = col;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
}
|
||||
|
||||
/*return true if something is updated*/
|
||||
@@ -905,12 +965,26 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
|
||||
m_series_name = series_name;
|
||||
m_nozzle_num = nozzle_num;
|
||||
this->DestroyChildren();
|
||||
m_left_extruder = nullptr;
|
||||
m_right_extruder = nullptr;
|
||||
m_nozzle_extruders.clear();
|
||||
|
||||
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
if (m_nozzle_num == 1) {
|
||||
if (MachineObject::is_series_n(m_series_name)) {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
} else if (MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name)) {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
} else {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
|
||||
wxBoxSizer* m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
if (m_nozzle_num >= 2)
|
||||
m_left_extruder->setShowState(true);
|
||||
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
}
|
||||
else if (m_nozzle_num == 2)
|
||||
{
|
||||
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "left_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_left_extruder->setShowState(true);
|
||||
m_right_extruder->setShowState(true);
|
||||
@@ -918,25 +992,17 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
|
||||
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(2));
|
||||
m_bitmap_sizer->AddSpacer(2);
|
||||
}
|
||||
else if (m_nozzle_num > 2) {
|
||||
for (int nozzle_id = 0; nozzle_id < m_nozzle_num; ++nozzle_id) {
|
||||
auto *extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
extruder->setShowState(true);
|
||||
m_nozzle_extruders.push_back(extruder);
|
||||
m_bitmap_sizer->Add(extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MachineObject::is_series_n(m_series_name))
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
}
|
||||
else if(MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name))
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
|
||||
m_left_extruder->setShowState(true);
|
||||
m_right_extruder->setShowState(false);
|
||||
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(3));
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
SetSizer(m_bitmap_sizer);
|
||||
@@ -951,6 +1017,9 @@ void AMSextruder::msw_rescale()
|
||||
//m_amsSextruder->msw_rescale();
|
||||
if (m_left_extruder) m_left_extruder->msw_rescale();
|
||||
if (m_right_extruder) m_right_extruder->msw_rescale();
|
||||
for (auto *extruder : m_nozzle_extruders) {
|
||||
if (extruder) extruder->msw_rescale();
|
||||
}
|
||||
Layout();
|
||||
Update();
|
||||
Refresh();
|
||||
@@ -2123,7 +2192,10 @@ AMSRoadUpPart::AMSRoadUpPart(wxWindow* parent, wxWindowID id, AMSinfo info, AMSM
|
||||
m_ams_model = model;
|
||||
|
||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||
create(parent, id, pos, wxSize(FromDIP(264), FromDIP(34)));
|
||||
const int width = use_generic_ams_layout(m_ams_model)
|
||||
? generic_ams_layout_width(this, static_cast<int>(m_amsinfo.cans.size()))
|
||||
: FromDIP(264);
|
||||
create(parent, id, pos, wxSize(width, FromDIP(34)));
|
||||
}
|
||||
else{
|
||||
create(parent, id, pos, wxSize(FromDIP(78), FromDIP(34)));
|
||||
@@ -2235,6 +2307,38 @@ void AMSRoadUpPart::doRender(wxDC& dc)
|
||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||
dc.SetBrush(wxBrush(*wxTRANSPARENT_BRUSH));
|
||||
|
||||
if (use_generic_ams_layout(m_ams_model)) {
|
||||
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
|
||||
if (lane_count <= 0)
|
||||
return;
|
||||
|
||||
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
|
||||
const int height = FromDIP(21);
|
||||
for (const int lane_center : lane_centers)
|
||||
dc.DrawLine(lane_center, 0, lane_center, height);
|
||||
|
||||
if (lane_centers.size() > 1)
|
||||
dc.DrawLine(lane_centers.front(), height, lane_centers.back(), height);
|
||||
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
|
||||
|
||||
if (m_load_step != AMSPassRoadSTEP::AMS_ROAD_STEP_NONE &&
|
||||
m_load_slot_index >= 0 && m_load_slot_index < lane_count) {
|
||||
const int lane_center = lane_centers[m_load_slot_index];
|
||||
dc.SetPen(wxPen(_get_diff_clr(this, m_amsinfo.cans[m_load_slot_index].material_colour), 4, wxPENSTYLE_SOLID));
|
||||
dc.DrawLine(lane_center, 0, lane_center, height);
|
||||
dc.DrawLine(std::min(lane_center, size.x / 2), height,
|
||||
std::max(lane_center, size.x / 2), height);
|
||||
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
|
||||
m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3)
|
||||
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
|
||||
}
|
||||
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(wxColour(194, 194, 194)));
|
||||
dc.DrawRectangle(size.x / 2 - FromDIP(14), height - FromDIP(5), FromDIP(28), FromDIP(10));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((m_ams_model == N3S_AMS || m_ams_model == EXT_AMS) && m_amsinfo.cans.size() != 4){
|
||||
dc.DrawLine(((float)size.x / 2), (0), ((float)size.x / 2), (size.y));
|
||||
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 || m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3){
|
||||
@@ -2360,6 +2464,25 @@ void AMSRoadDownPart::UpdateRight(int nozzle_num, AMSRoadShowMode mode)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::SetNozzleCount(int nozzle_num)
|
||||
{
|
||||
const int new_nozzle_num = std::max(1, nozzle_num);
|
||||
if (m_nozzle_num == new_nozzle_num) return;
|
||||
|
||||
m_nozzle_num = new_nozzle_num;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::SetSingleSideLayout(bool enabled, AMSPanelPos pos)
|
||||
{
|
||||
if (m_single_side_layout == enabled && m_single_side_pos == pos)
|
||||
return;
|
||||
|
||||
m_single_side_layout = enabled;
|
||||
m_single_side_pos = pos;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::OnVamsLoading(bool load, wxColour col /*= AMS_CONTROL_GRAY500*/)
|
||||
{
|
||||
/*m_vams_loading = load;
|
||||
@@ -2433,8 +2556,14 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
/*if (m_road_color.Alpha() == 0) { dc.SetPen(wxPen(*wxWHITE, m_passroad_width, wxPENSTYLE_SOLID)); }
|
||||
else { dc.SetPen(wxPen(m_road_color, m_passroad_width, wxPENSTYLE_SOLID)); }*/
|
||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||
|
||||
auto xpos = left_nozzle_pos.x;
|
||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE){
|
||||
if (m_single_side_layout) {
|
||||
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
|
||||
}
|
||||
else if (
|
||||
(m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE ||
|
||||
m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE)){
|
||||
auto length = 50;
|
||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE)
|
||||
length = -13;
|
||||
@@ -2465,6 +2594,9 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE:
|
||||
dc.DrawLine(left_nozzle_pos.x, 0, left_nozzle_pos.x, size.y);
|
||||
break;
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||
// Generic N-lane geometry is intentionally left unimplemented.
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -2492,23 +2624,41 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
dc.DrawLine(left_nozzle_pos.x, (size.y / 2), left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||
dc.DrawLine(left_nozzle_pos.x + FromDIP(145), 0, left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||
break;
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||
// Generic N-lane geometry is intentionally left unimplemented.
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_single_side_layout) {
|
||||
if (m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
|
||||
m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3) {
|
||||
dc.SetPen(wxPen(_get_diff_clr(this, m_road_color[1]), 4, wxPENSTYLE_SOLID));
|
||||
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE){
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
|
||||
}
|
||||
const auto &nozzle_pos = m_single_side_layout && m_single_side_pos == AMSPanelPos::RIGHT_PANEL
|
||||
? right_nozzle_pos
|
||||
: left_nozzle_pos;
|
||||
dc.DrawLine(nozzle_pos.x, (size.y / 2), nozzle_pos.x, (size.y));
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
/*dc.DrawLine(FromDIP(left_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(left_nozzle_pos.x), FromDIP(size.y));
|
||||
dc.DrawLine(FromDIP(right_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(right_nozzle_pos.x), FromDIP(size.y));*/
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
|
||||
dc.DrawLine((right_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y));
|
||||
}
|
||||
else {
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
|
||||
}
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
|
||||
// Generic N Nozzles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2533,10 +2683,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
dc.DrawLine(xpos, size.y / 2, xpos, size.y);*/
|
||||
int x = left_nozzle_pos.x;
|
||||
int len = m_right_road_length;
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
// Keep the single-nozzle path anchored at the left nozzle.
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
x = right_nozzle_pos.x;
|
||||
len = len - 14;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||
@@ -2544,10 +2700,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
else{
|
||||
int x = left_nozzle_pos.x;
|
||||
int len = m_right_road_length;
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
// Keep the single-nozzle path anchored at the left nozzle.
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
x = right_nozzle_pos.x;
|
||||
len = len - 14;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||
@@ -2571,7 +2733,7 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP step) {
|
||||
if (m_nozzle_num >= 2){
|
||||
if (m_nozzle_num == 2){
|
||||
if (pos == AMSPanelPos::LEFT_PANEL){
|
||||
if (m_left_road_length == len && m_pass_road_left_step == step){ return; }
|
||||
m_left_road_length = len;;
|
||||
@@ -2583,7 +2745,7 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
||||
m_pass_road_right_step = step;
|
||||
}
|
||||
}
|
||||
else{
|
||||
else if (m_nozzle_num == 1){
|
||||
if (pos == AMSPanelPos::LEFT_PANEL) {
|
||||
if (m_left_road_length == len && m_pass_road_left_step == step && m_right_road_length == -1) { return; }
|
||||
m_left_road_length = len;
|
||||
@@ -2597,6 +2759,9 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
||||
m_pass_road_right_step = step;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@@ -2615,7 +2780,10 @@ AMSPreview::AMSPreview(wxWindow* parent, wxWindowID id, AMSinfo amsinfo, AMSMode
|
||||
{
|
||||
wxWindow::Create(parent, id, pos);
|
||||
if (itemType == AMSModel::GENERIC_AMS || itemType == AMSModel::AMS_LITE || itemType == AMSModel::N3F_AMS) {
|
||||
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
|
||||
if (itemType == AMSModel::GENERIC_AMS && use_generic_ams_layout(itemType))
|
||||
create(parent, id, pos, wxSize(generic_ams_preview_width(this, static_cast<int>(amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y));
|
||||
else
|
||||
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
|
||||
}
|
||||
else {
|
||||
create(parent, id, pos, AMS_PREV_SINGLE_SIZE);
|
||||
@@ -2650,7 +2818,13 @@ void AMSPreview::UpdateInfo(AMSinfo amsinfo)
|
||||
m_amsinfo = amsinfo;
|
||||
m_ams_item_type = amsinfo.ams_type;
|
||||
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
|
||||
const wxSize generic_size(generic_ams_preview_width(this, static_cast<int>(m_amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y);
|
||||
SetSize(generic_size);
|
||||
SetMinSize(generic_size);
|
||||
SetMaxSize(generic_size);
|
||||
}
|
||||
else if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
|
||||
SetMinSize(AMS_PREV_FOUR_SIZE);
|
||||
SetMaxSize(AMS_PREV_FOUR_SIZE);
|
||||
} else {
|
||||
@@ -2758,8 +2932,41 @@ void AMSPreview::doRender(wxDC &dc)
|
||||
dc.DrawRoundedRectangle(0, 0, size.x, size.y, FromDIP(3));
|
||||
|
||||
auto left = 0;
|
||||
//four slot
|
||||
if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
||||
// Generic AMS previews use the same normalized lane centers as the card row and connector.
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
|
||||
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
|
||||
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
|
||||
const int cube_width = FromDIP(9);
|
||||
const int cube_height = FromDIP(14);
|
||||
|
||||
for (int lane = 0; lane < lane_count; ++lane) {
|
||||
const Caninfo &can = m_amsinfo.cans[lane];
|
||||
const int cube_left = lane_centers[lane] - cube_width / 2;
|
||||
const int cube_top = (size.y - cube_height) / 2;
|
||||
wxRect rect(cube_left, cube_top, cube_width, cube_height);
|
||||
|
||||
if (can.material_colour.Alpha() == 0) {
|
||||
if (wxGetApp().dark_mode())
|
||||
dc.DrawBitmap(m_ts_bitmap_cube_dark.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube_dark.GetBmpHeight()) / 2);
|
||||
else
|
||||
dc.DrawBitmap(m_ts_bitmap_cube.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube.GetBmpHeight()) / 2);
|
||||
}
|
||||
else if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
dc.SetPen(wxPen(wxColor(0, 0, 0)));
|
||||
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1),
|
||||
rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
|
||||
}
|
||||
else {
|
||||
wxColour color = can.material_colour;
|
||||
change_the_opacity(color);
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
dc.SetBrush(wxBrush(color));
|
||||
dc.DrawRoundedRectangle(rect, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
// four slot
|
||||
else if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
||||
left = FromDIP(8);
|
||||
for (std::vector<Caninfo>::iterator iter = m_amsinfo.cans.begin(); iter != m_amsinfo.cans.end(); iter++) {
|
||||
|
||||
@@ -3158,10 +3365,19 @@ AmsItem::AmsItem(wxWindow *parent,AMSinfo info, AMSModel model, AMSPanelPos pos
|
||||
m_panel_pos = pos;
|
||||
|
||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_CANS_WINDOW_SIZE);
|
||||
SetSize(AMS_CANS_WINDOW_SIZE);
|
||||
SetMinSize(AMS_CANS_WINDOW_SIZE);
|
||||
SetMaxSize(AMS_CANS_WINDOW_SIZE);
|
||||
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||
const int width = use_generic_ams_layout(m_ams_model) && lane_count <= 8
|
||||
? generic_ams_layout_width(this, lane_count)
|
||||
: FromDIP(264);
|
||||
const wxSize generic_size(width, FromDIP(174));
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, generic_size);
|
||||
SetSize(generic_size);
|
||||
SetMinSize(generic_size);
|
||||
// For large lane counts the card is a viewport, not the lane strip. Let
|
||||
// its parent allocate the available width; the scrolled child retains
|
||||
// the wider virtual content size.
|
||||
if (!(use_generic_ams_layout(m_ams_model) && lane_count > 8))
|
||||
SetMaxSize(generic_size);
|
||||
}
|
||||
else{
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_SINGLE_CAN_SIZE);
|
||||
@@ -3187,12 +3403,43 @@ void AmsItem::create(wxWindow *parent)
|
||||
if (m_ams_model != AMSModel::AMS_LITE) {
|
||||
sizer_can = new wxBoxSizer(wxHORIZONTAL);
|
||||
sizer_item = new wxBoxSizer(wxVERTICAL);
|
||||
auto it = m_info.cans.begin();
|
||||
for (; it != m_info.cans.end(); it++) {
|
||||
AddCan(*it, m_can_count, m_info.cans.size(), sizer_can);
|
||||
const bool generic_layout = use_generic_ams_layout(m_ams_model);
|
||||
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||
const int content_width = generic_layout ? generic_ams_layout_width(this, lane_count) : 0;
|
||||
const bool scroll_lanes = generic_layout && lane_count > 8;
|
||||
const auto lane_centers = generic_layout
|
||||
? generic_ams_lane_centers(lane_count, content_width)
|
||||
: std::vector<int>();
|
||||
|
||||
m_can_parent = this;
|
||||
if (scroll_lanes) {
|
||||
m_can_scroll = new wxScrolledWindow(this, wxID_ANY);
|
||||
m_can_scroll->SetScrollRate(FromDIP(10), 0);
|
||||
m_can_scroll->SetMinSize(wxSize(FromDIP(264), FromDIP(143)));
|
||||
m_can_scroll->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||
m_can_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
m_can_scroll->SetSizer(m_can_scroll_sizer);
|
||||
m_can_parent = m_can_scroll;
|
||||
}
|
||||
|
||||
int previous_right = 0;
|
||||
for (int can_index = 0; can_index < lane_count; ++can_index) {
|
||||
if (generic_layout) {
|
||||
const int left = lane_centers[can_index] - FromDIP(26);
|
||||
sizer_can->AddSpacer(std::max(0, left - previous_right));
|
||||
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
|
||||
previous_right = left + FromDIP(52);
|
||||
}
|
||||
else {
|
||||
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
|
||||
}
|
||||
m_can_count++;
|
||||
}
|
||||
it = m_info.cans.begin();
|
||||
if (generic_layout)
|
||||
sizer_can->AddSpacer(std::max(0, content_width - previous_right));
|
||||
if (generic_layout)
|
||||
sizer_can->SetMinSize(wxSize(content_width, -1));
|
||||
|
||||
//auto road_panel = new wxWindow(this, wxID_ANY);
|
||||
//auto road_panel = new wxPanel(this, wxID_ANY);
|
||||
//road_panel->SetSize(AMS_CAN_ROAD_SIZE);
|
||||
@@ -3207,11 +3454,18 @@ void AmsItem::create(wxWindow *parent)
|
||||
sizer_item->Add(m_ext_image, 0, wxALIGN_CENTER, 0);
|
||||
}
|
||||
}
|
||||
m_panel_road = new AMSRoadUpPart(this, wxID_ANY, m_info, m_ams_model);
|
||||
m_panel_road = new AMSRoadUpPart(m_can_parent, wxID_ANY, m_info, m_ams_model);
|
||||
|
||||
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
||||
if (scroll_lanes) {
|
||||
m_can_scroll_sizer->Add(sizer_can, 0, wxALIGN_LEFT, 0);
|
||||
m_can_scroll_sizer->Add(m_panel_road, 0, wxALIGN_LEFT, 0);
|
||||
sizer_item->Add(m_can_scroll, 1, wxEXPAND);
|
||||
}
|
||||
else {
|
||||
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
||||
}
|
||||
|
||||
SetSizer(sizer_item);
|
||||
}
|
||||
@@ -3235,13 +3489,17 @@ void AmsItem::create(wxWindow *parent)
|
||||
|
||||
Layout();
|
||||
Fit();
|
||||
if (m_can_scroll) {
|
||||
m_can_scroll->Layout();
|
||||
m_can_scroll->FitInside();
|
||||
}
|
||||
Thaw();
|
||||
//Refresh();
|
||||
}
|
||||
|
||||
void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* sizer)
|
||||
{
|
||||
auto amscan = new wxWindow(this, wxID_ANY);
|
||||
auto amscan = new wxWindow(m_can_parent ? m_can_parent : this, wxID_ANY);
|
||||
|
||||
amscan->SetSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||
amscan->SetMinSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||
@@ -3322,7 +3580,8 @@ void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* size
|
||||
amscan->Layout();
|
||||
amscan->Fit();
|
||||
|
||||
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, FromDIP(5));
|
||||
const int margin = use_generic_ams_layout(m_ams_model) ? 0 : FromDIP(5);
|
||||
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, margin);
|
||||
|
||||
m_can_lib_list[caninfo.can_id] = m_panel_lib;
|
||||
//m_can_road_list[caninfo.can_id] = m_panel_road;
|
||||
@@ -3447,6 +3706,10 @@ void AmsItem::UpdateInfo(AMSinfo info)
|
||||
}
|
||||
|
||||
Layout();
|
||||
if (m_can_scroll) {
|
||||
m_can_scroll->Layout();
|
||||
m_can_scroll->FitInside();
|
||||
}
|
||||
}
|
||||
|
||||
void AmsItem::SetDefSelectCan()
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <wx/string.h>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" // Orca: DevFilaSwitch::SwitchPos for inlet-aware AMS placement
|
||||
|
||||
@@ -96,6 +97,7 @@ enum class AMSRoadShowMode : int {
|
||||
AMS_ROAD_MODE_SINGLE,
|
||||
AMS_ROAD_MODE_SINGLE_N3S,
|
||||
AMS_ROAD_MODE_AMS_LITE,
|
||||
AMS_ROAD_MODE_GENERIC,
|
||||
AMS_ROAD_MODE_NONE
|
||||
};
|
||||
|
||||
@@ -480,6 +482,7 @@ public:
|
||||
//AMSextruderImage *m_amsSextruder{nullptr};
|
||||
AMSextruderImage* m_left_extruder = nullptr;
|
||||
AMSextruderImage* m_right_extruder = nullptr;
|
||||
std::vector<AMSextruderImage*> m_nozzle_extruders;
|
||||
AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
|
||||
~AMSextruder();
|
||||
|
||||
@@ -681,6 +684,8 @@ public:
|
||||
// void Update(AMSRoadDownPartMode nozzle, AMSRoadShowMode left_mode, AMSRoadShowMode right_mode, int left_len, int right_len);
|
||||
void UpdateLeft(int nozzle_num, AMSRoadShowMode mode);
|
||||
void UpdateRight(int nozzle_num, AMSRoadShowMode mode);
|
||||
void SetNozzleCount(int nozzle_num);
|
||||
void SetSingleSideLayout(bool enabled, AMSPanelPos pos = AMSPanelPos::LEFT_PANEL);
|
||||
|
||||
void OnVamsLoading(bool load, wxColour col = AMS_CONTROL_GRAY500);
|
||||
void SetPassRoadColour(bool left, wxColour col);
|
||||
@@ -698,6 +703,8 @@ private:
|
||||
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||
bool m_selected = {false};
|
||||
bool m_single_side_layout = {false};
|
||||
AMSPanelPos m_single_side_pos = {AMSPanelPos::LEFT_PANEL};
|
||||
|
||||
int m_left_road_length = {-1};
|
||||
int m_right_road_length = {-1};
|
||||
@@ -864,6 +871,9 @@ private:
|
||||
|
||||
std::map<std::string, AMSLib*> m_can_lib_list;
|
||||
//std::map<std::string, AMSRoad*> m_can_road_list;
|
||||
wxScrolledWindow* m_can_scroll = { nullptr };
|
||||
wxBoxSizer* m_can_scroll_sizer = { nullptr };
|
||||
wxWindow* m_can_parent = { nullptr };
|
||||
AMSRoadUpPart* m_panel_road = { nullptr };
|
||||
std::map<std::string, AMSrefresh*> m_can_refresh_list;
|
||||
AMSHumidity* m_humidity = { nullptr };
|
||||
|
||||
@@ -420,6 +420,14 @@ ExtruderBadge::ExtruderBadge(wxWindow* parent) : wxPanel(parent)
|
||||
|
||||
void ExtruderBadge::SetExtruderInfo(int extruder_id, const std::string& diameter, const NozzleVolumeType& volume_type)
|
||||
{
|
||||
// The badge renders exactly two extruders (left/right). A generic non-BBL printer with
|
||||
// N > 2 can report an out-of-range id here (see MultiNozzleStatusTable::UpdateRackInfo);
|
||||
// reject it rather than writing past the two-element lists.
|
||||
if (extruder_id < 0 || extruder_id >= static_cast<int>(m_diameter_list.size())) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": out-of-range extruder_id " << extruder_id;
|
||||
return;
|
||||
}
|
||||
|
||||
m_diameter_list[extruder_id] = diameter;
|
||||
m_volume_type_list[extruder_id] = volume_type;
|
||||
|
||||
@@ -867,14 +875,21 @@ void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr<DevNozzleRack> rack)
|
||||
bool has_right = false;
|
||||
for (auto& elem : nozzles_in_extruder) {
|
||||
auto& nozzle = elem.second;
|
||||
int extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
||||
if (nozzle.AtRightExtruder())
|
||||
has_right = true;
|
||||
|
||||
int extruder_id{};
|
||||
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
||||
if (nozzle.AtRightExtruder())
|
||||
has_right = true;
|
||||
}
|
||||
else
|
||||
extruder_id = nozzle.GetExtruderId();
|
||||
|
||||
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow);
|
||||
|
||||
m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type);
|
||||
}
|
||||
// TODO: Update for N extruders
|
||||
m_badge->SetExtruderValid(has_right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,9 @@ void TempInput::SetFinish()
|
||||
wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH);
|
||||
event.SetInt(temp_type);
|
||||
event.SetString(wxString::Format("%d", m_input_type));
|
||||
// N-extruder temp controls all share TEMP_OF_NORMAL_TYPE, so the string payload above can't
|
||||
// tell them apart; carry widget identity so the listener can find which one fired.
|
||||
event.SetEventObject(this);
|
||||
wxPostEvent(this->GetParent(), event);
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +444,12 @@ bool WebView::DownloadAndInstallWebViewRuntime()
|
||||
return DownloadAndInstallWV2RT() == 0;
|
||||
}
|
||||
#endif
|
||||
wxString WebView::RedactUrl(const wxString& url)
|
||||
{
|
||||
const size_t cut = url.find_first_of(wxString("?#"));
|
||||
return cut == wxString::npos ? url : url.Left(cut);
|
||||
}
|
||||
|
||||
void WebView::LoadUrl(wxWebView * webView, wxString const &url)
|
||||
{
|
||||
auto url2 = url;
|
||||
@@ -451,7 +457,7 @@ void WebView::LoadUrl(wxWebView * webView, wxString const &url)
|
||||
url2.Replace("\\", "/");
|
||||
#endif
|
||||
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << url2.ToUTF8();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << RedactUrl(url2).ToUTF8();
|
||||
webView->LoadURL(url2);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ public:
|
||||
#endif
|
||||
static void LoadUrl(wxWebView * webView, wxString const &url);
|
||||
|
||||
// Returns the URL truncated at the first '?' or '#', so a query/fragment carrying secrets
|
||||
// (e.g. a session-handoff token) is never written to a log.
|
||||
static wxString RedactUrl(const wxString& url);
|
||||
|
||||
static bool RunScript(wxWebView * webView, wxString const & msg);
|
||||
|
||||
// Marks "wx" as registered so CreateWebView's deferred add skips the duplicate.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/object.h>
|
||||
#include <wx/popupwin.h>
|
||||
@@ -630,43 +630,6 @@ std::vector<std::vector<std::string>> read_color_pack(std::vector<std::string> c
|
||||
return color_info;
|
||||
}
|
||||
|
||||
wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data)
|
||||
{
|
||||
wxColourData data = clr_data;
|
||||
data.SetChooseFull(true);
|
||||
|
||||
// Load custom colors from config (support both "r,g,b,a" and "#RRGGBB" formats)
|
||||
std::vector<std::string> colors = Slic3r::GUI::wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < (int)colors.size(); i++) {
|
||||
wxColour c;
|
||||
if (colors[i].find(',') != std::string::npos)
|
||||
c = string_to_wxColor(colors[i]);
|
||||
else
|
||||
c = wxColour(colors[i]);
|
||||
if (c.IsOk())
|
||||
data.SetCustomColour(i, c);
|
||||
}
|
||||
|
||||
wxColourDialog dialog(parent, &data);
|
||||
dialog.SetTitle(_L("Please choose the filament color"));
|
||||
|
||||
if (dialog.ShowModal() == wxID_OK) {
|
||||
data = dialog.GetColourData();
|
||||
|
||||
// Save custom colors to config (use RGBA string format for consistency)
|
||||
std::vector<std::string> colors;
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
wxColour custom_clr = data.GetCustomColour(i);
|
||||
if (custom_clr.IsOk())
|
||||
colors[i] = color_to_string(custom_clr);
|
||||
}
|
||||
Slic3r::GUI::wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height,
|
||||
const std::vector<wxColour> *ramp){
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user