Compare commits

...
Author SHA1 Message Date
SoftFever e4142db820 Let texture bakes create a chosen number of mixed colours (#16320)
# Description

Follow-up to #16242, which turned texture colour mixes into mixed
filament slots but created a slot for every possible mix, even when only
previewing. A new Mixed colors setting (default 8) caps how many mixed
filaments a bake adds. They are picked from the texture's own colours,
and only the ones the bake actually paints with are created. Previews no
longer touch the project's filaments, both previews show a mix in the
colour its slot will have, and the bake paints each mix with the slot it
got, which went wrong whenever the project already held mixed slots.

It also improves the colour preview: the green paint highlight no longer
covers the colours, and the colours a bake writes stay visible in the
gizmo.

Only texture displacement changes. Models without a colour layer behave
as before, and there is no change to project or profile formats.

# Screenshots/Recordings/Graphs

**Before**



https://github.com/user-attachments/assets/554a0d0d-cd9f-4095-840e-ca764dde6d52

**After**



https://github.com/user-attachments/assets/7fe10431-8c3f-4759-bd3f-78b4f7a12fc7



## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-10 12:00:39 +08:00
SoftFever d629922060 Keep the paint highlight off every texture preview between strokes 2026-10-10 11:44:09 +08:00
Kris Austin 4a20168742 Fix loading a multi-toolhead 3MF that has no filament_self_index (#16331) 2026-10-09 22:07:33 -03:00
Kris Austin 1466c0e57f Fix rare hang in mcut mesh booleans (#16330) 2026-10-09 21:55:06 -03:00
Kris Austin cd02116242 ci: replace the Docker tag action with git commands (#16327) 2026-10-09 19:17:16 -03:00
Kris Austin 35bac4cb69 Remove unused OpenCSG, GLEW and GLU dependencies (#16318) 2026-10-09 16:50:19 -03:00
0f3e8fbf27 Add center of mass markers to Prepare and Preview (#16291)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
Co-authored-by: Kris Austin <kris.austin@gmail.com>
2026-10-09 16:48:48 -03:00
SoftFever 901b14a723 Merge branch 'main' into feature/texture-color-number 2026-10-10 02:46:28 +08:00
SoftFever b8665b69b0 Merge branch 'main' into feature/texture-color-number 2026-10-09 22:58:10 +08:00
SoftFever 7d141bd691 Keep the texture displacement gizmo out of the assemble view 2026-10-09 22:26:19 +08:00
SoftFever 10787dd59d Show the model's painted colours in the texture displacement gizmo 2026-10-09 22:25:36 +08:00
SoftFever 1fb5da4148 Keep the paint highlight off the texture colour preview between strokes
The green highlight and tint no longer cover a colour preview, and return while a stroke is
painted. The Fast view also keeps the other parts of a multi-part object.
2026-10-09 22:24:40 +08:00
SoftFever 0473da4ef8 Let texture bakes create a chosen number of mixed colours
The new Mixed colors setting caps how many mixed filaments a bake adds. They are picked from the
texture's colours, and only the ones the bake paints with are created. Previewing no longer creates
filament slots, both previews show a mix in its slot's colour, and the bake paints each mix with the
slot it actually got.
2026-10-09 22:23:07 +08:00
82 changed files with 3072 additions and 67921 deletions
+3 -6
View File
@@ -707,12 +707,9 @@ jobs:
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
- name: Deploy Ubuntu release - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: rickstaa/action-create-tag@v1 run: |
with: git -c user.name="${GITHUB_ACTOR}" -c user.email="${GITHUB_ACTOR}@users.noreply.github.com" tag -f -a nightly-builds "${GITHUB_SHA}" -m nightly-builds
tag: "nightly-builds" git push -f origin refs/tags/nightly-builds
tag_exists_error: false
force_push_tag: true
message: "nightly-builds"
- name: Deploy Ubuntu OrcaSlicer_profile_validator release - name: Deploy Ubuntu OrcaSlicer_profile_validator release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
+2 -2
View File
@@ -156,7 +156,7 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends \ sudo apt-get install -y --no-install-recommends \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0 libopengl0 libgl1 libegl1 libwebkit2gtk-4.1-0
- uses: actions/setup-python@v6 - uses: actions/setup-python@v6
with: with:
@@ -224,7 +224,7 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends \ sudo apt-get install -y --no-install-recommends \
xvfb xdotool imagemagick openbox mesa-utils \ xvfb xdotool imagemagick openbox mesa-utils \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0 libopengl0 libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Run the parity harness - name: Run the parity harness
run: | run: |
-359
View File
@@ -1,359 +0,0 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
# PrusaSlicer specifics:
# This file is backported from CMake 3.15 distribution to behave uniformly
# across all versions of CMake. It explicitly adds GLEW_STATIC compile
# definition to static targets which is needed to prevent link errors.
#[=======================================================================[.rst:
FindGLEW
--------
Find the OpenGL Extension Wrangler Library (GLEW)
Input Variables
^^^^^^^^^^^^^^^
The following variables may be set to influence this module’s behavior:
``GLEW_USE_STATIC_LIBS``
to find and create :prop_tgt:`IMPORTED` target for static linkage.
``GLEW_VERBOSE``
to output a detailed log of this module.
Imported Targets
^^^^^^^^^^^^^^^^
This module defines the following :ref:`Imported Targets <Imported Targets>`:
``GLEW::glew``
The GLEW shared library.
``GLEW::glew_s``
The GLEW static library, if ``GLEW_USE_STATIC_LIBS`` is set to ``TRUE``.
``GLEW::GLEW``
Duplicates either ``GLEW::glew`` or ``GLEW::glew_s`` based on availability.
Result Variables
^^^^^^^^^^^^^^^^
This module defines the following variables:
``GLEW_INCLUDE_DIRS``
include directories for GLEW
``GLEW_LIBRARIES``
libraries to link against GLEW
``GLEW_SHARED_LIBRARIES``
libraries to link against shared GLEW
``GLEW_STATIC_LIBRARIES``
libraries to link against static GLEW
``GLEW_FOUND``
true if GLEW has been found and can be used
``GLEW_VERSION``
GLEW version
``GLEW_VERSION_MAJOR``
GLEW major version
``GLEW_VERSION_MINOR``
GLEW minor version
``GLEW_VERSION_MICRO``
GLEW micro version
#]=======================================================================]
include(FindPackageHandleStandardArgs)
if(APPLE)
find_package(OpenGL QUIET)
if(OpenGL_FOUND)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Found OpenGL Framework.")
message(STATUS "FindGLEW: OPENGL_LIBRARIES: ${OPENGL_LIBRARIES}")
endif()
else()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: could not find GLEW library.")
endif()
return()
endif()
endif()
function(__glew_set_find_library_suffix shared_or_static)
if((UNIX AND NOT APPLE) AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".so")
elseif((UNIX AND NOT APPLE) AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
elseif(APPLE AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dylib;.so")
elseif(APPLE AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
elseif(WIN32 AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib")
elseif(WIN32 AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib;.a;.dll.a")
endif()
set(CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_FIND_LIBRARY_SUFFIXES}" PARENT_SCOPE)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: CMAKE_FIND_LIBRARY_SUFFIXES for ${shared_or_static}: ${CMAKE_FIND_LIBRARY_SUFFIXES}")
endif()
endfunction()
if(GLEW_VERBOSE)
if(DEFINED GLEW_USE_STATIC_LIBS)
message(STATUS "FindGLEW: GLEW_USE_STATIC_LIBS: ${GLEW_USE_STATIC_LIBS}.")
else()
message(STATUS "FindGLEW: GLEW_USE_STATIC_LIBS is undefined. Treated as FALSE.")
endif()
endif()
find_path(GLEW_INCLUDE_DIR GL/glew.h)
mark_as_advanced(GLEW_INCLUDE_DIR)
set(GLEW_INCLUDE_DIRS ${GLEW_INCLUDE_DIR})
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_INCLUDE_DIR: ${GLEW_INCLUDE_DIR}")
message(STATUS "FindGLEW: GLEW_INCLUDE_DIRS: ${GLEW_INCLUDE_DIRS}")
endif()
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "x64" OR "${CMAKE_GENERATOR}" MATCHES "Win64")
set(_arch "x64")
elseif("${CMAKE_GENERATOR_PLATFORM}" MATCHES "ARM64")
set(_arch "x64") # GLEW ships one header set; ARM64 uses the x64 import path
else()
set(_arch "Win32")
endif()
set(__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
__glew_set_find_library_suffix(SHARED)
find_library(GLEW_SHARED_LIBRARY_RELEASE
NAMES GLEW glew glew32
PATH_SUFFIXES lib lib64 libx32 lib/Release/${_arch}
PATHS ENV GLEW_ROOT)
find_library(GLEW_SHARED_LIBRARY_DEBUG
NAMES GLEWd glewd glew32d
PATH_SUFFIXES lib lib64
PATHS ENV GLEW_ROOT)
__glew_set_find_library_suffix(STATIC)
find_library(GLEW_STATIC_LIBRARY_RELEASE
NAMES GLEW glew glew32s
PATH_SUFFIXES lib lib64 libx32 lib/Release/${_arch}
PATHS ENV GLEW_ROOT)
find_library(GLEW_STATIC_LIBRARY_DEBUG
NAMES GLEWds GLEWd glewd glewds glew32ds
PATH_SUFFIXES lib lib64
PATHS ENV GLEW_ROOT)
set(CMAKE_FIND_LIBRARY_SUFFIXES ${__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES})
unset(__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES)
include(SelectLibraryConfigurations)
select_library_configurations(GLEW_SHARED)
select_library_configurations(GLEW_STATIC)
if(NOT GLEW_USE_STATIC_LIBS)
set(GLEW_LIBRARIES ${GLEW_SHARED_LIBRARY})
else()
set(GLEW_LIBRARIES ${GLEW_STATIC_LIBRARY})
endif()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY_RELEASE: ${GLEW_SHARED_LIBRARY_RELEASE}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY_RELEASE: ${GLEW_STATIC_LIBRARY_RELEASE}")
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY_DEBUG: ${GLEW_SHARED_LIBRARY_DEBUG}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY_DEBUG: ${GLEW_STATIC_LIBRARY_DEBUG}")
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY: ${GLEW_SHARED_LIBRARY}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY: ${GLEW_STATIC_LIBRARY}")
message(STATUS "FindGLEW: GLEW_LIBRARIES: ${GLEW_LIBRARIES}")
endif()
# Read version from GL/glew.h file
if(EXISTS "${GLEW_INCLUDE_DIR}/GL/glew.h")
file(STRINGS "${GLEW_INCLUDE_DIR}/GL/glew.h" _contents REGEX "^VERSION_.+ [0-9]+")
if(_contents)
string(REGEX REPLACE ".*VERSION_MAJOR[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MAJOR "${_contents}")
string(REGEX REPLACE ".*VERSION_MINOR[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MINOR "${_contents}")
string(REGEX REPLACE ".*VERSION_MICRO[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MICRO "${_contents}")
set(GLEW_VERSION "${GLEW_VERSION_MAJOR}.${GLEW_VERSION_MINOR}.${GLEW_VERSION_MICRO}")
endif()
endif()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_VERSION_MAJOR: ${GLEW_VERSION_MAJOR}")
message(STATUS "FindGLEW: GLEW_VERSION_MINOR: ${GLEW_VERSION_MINOR}")
message(STATUS "FindGLEW: GLEW_VERSION_MICRO: ${GLEW_VERSION_MICRO}")
message(STATUS "FindGLEW: GLEW_VERSION: ${GLEW_VERSION}")
endif()
find_package_handle_standard_args(GLEW
REQUIRED_VARS GLEW_INCLUDE_DIRS GLEW_LIBRARIES
VERSION_VAR GLEW_VERSION)
if(NOT GLEW_FOUND)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: could not find GLEW library.")
endif()
return()
endif()
if(NOT TARGET GLEW::glew AND NOT GLEW_USE_STATIC_LIBS)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::glew imported target.")
endif()
add_library(GLEW::glew UNKNOWN IMPORTED)
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(GLEW_SHARED_LIBRARY_RELEASE)
set_property(TARGET GLEW::glew
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::glew
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_SHARED_LIBRARY_RELEASE}")
endif()
if(GLEW_SHARED_LIBRARY_DEBUG)
set_property(TARGET GLEW::glew
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::glew
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_SHARED_LIBRARY_DEBUG}")
endif()
elseif(NOT TARGET GLEW::glew_s AND GLEW_USE_STATIC_LIBS)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::glew_s imported target.")
endif()
add_library(GLEW::glew_s UNKNOWN IMPORTED)
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
set_target_properties(GLEW::glew_s PROPERTIES INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(GLEW_STATIC_LIBRARY_RELEASE)
set_property(TARGET GLEW::glew_s
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::glew_s
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_STATIC_LIBRARY_RELEASE}")
endif()
if(GLEW_STATIC_LIBRARY_DEBUG)
set_property(TARGET GLEW::glew_s
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::glew_s
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_STATIC_LIBRARY_DEBUG}")
endif()
endif()
if(NOT TARGET GLEW::GLEW)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::GLEW imported target.")
endif()
add_library(GLEW::GLEW UNKNOWN IMPORTED)
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(TARGET GLEW::glew)
if(GLEW_SHARED_LIBRARY_RELEASE)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_SHARED_LIBRARY_RELEASE}")
endif()
if(GLEW_SHARED_LIBRARY_DEBUG)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_SHARED_LIBRARY_DEBUG}")
endif()
elseif(TARGET GLEW::glew_s)
if(GLEW_STATIC_LIBRARY_RELEASE)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_STATIC_LIBRARY_RELEASE}"
INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
endif()
if(GLEW_STATIC_LIBRARY_DEBUG AND GLEW_USE_STATIC_LIBS)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_STATIC_LIBRARY_DEBUG}"
INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
endif()
elseif(GLEW_VERBOSE)
message(WARNING "FindGLEW: no `GLEW::glew` or `GLEW::glew_s` target was created. Something went wrong in FindGLEW target creation.")
endif()
endif()
-3
View File
@@ -379,10 +379,8 @@ include(Boost/Boost.cmake)
include(Cereal/Cereal.cmake) include(Cereal/Cereal.cmake)
include(Qhull/Qhull.cmake) include(Qhull/Qhull.cmake)
include(GLEW/GLEW.cmake)
include(GLFW/GLFW.cmake) include(GLFW/GLFW.cmake)
include(OpenCSG/OpenCSG.cmake)
set(SLVS_PKG "") set(SLVS_PKG "")
if (SLIC3R_CAD) if (SLIC3R_CAD)
include(SLVS/SLVS.cmake) include(SLVS/SLVS.cmake)
@@ -478,7 +476,6 @@ set(_dep_list
dep_Draco dep_Draco
dep_NLopt dep_NLopt
dep_OpenVDB dep_OpenVDB
dep_OpenCSG
${SLVS_PKG} ${SLVS_PKG}
dep_OpenCV dep_OpenCV
dep_Eigen dep_Eigen
-2
View File
@@ -1,6 +1,4 @@
orcaslicer_add_cmake_project(EXPAT orcaslicer_add_cmake_project(EXPAT
# GIT_REPOSITORY https://github.com/nigels-com/glew.git
# GIT_TAG 3a8eff7 # 2.1.0
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/expat SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/expat
) )
-14
View File
@@ -1,14 +0,0 @@
# We have to check for OpenGL to compile GLEW
set(OpenGL_GL_PREFERENCE "LEGACY") # to prevent a nasty warning by cmake
find_package(OpenGL QUIET REQUIRED)
orcaslicer_add_cmake_project(
GLEW
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/glew
CMAKE_ARGS
-DGLEW_USE_EGL=OFF
)
if (MSVC)
add_debug_dep(dep_GLEW)
endif ()
-44
View File
@@ -1,44 +0,0 @@
cmake_minimum_required(VERSION 3.0)
project(GLEW)
find_package(OpenGL REQUIRED)
# Allow parent project to control EGL usage.
# Default to OFF since OrcaSlicer forces GDK_BACKEND=x11 (using GLX contexts).
# GLEW must use glXGetProcAddressARB (GLX) to match wxWidgets GL canvas.
# Using EGL function loading with GLX contexts causes rendering failures.
option(GLEW_USE_EGL "Use EGL instead of GLX for OpenGL function loading" OFF)
if(GLEW_USE_EGL)
message(STATUS "Building GLEW with EGL support")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DGLEW_EGL")
else()
message(STATUS "Building GLEW with GLX support")
endif()
add_library(GLEW src/glew.c)
target_include_directories(GLEW PRIVATE include/)
target_link_libraries(GLEW PUBLIC OpenGL::GL)
if (NOT BUILD_SHARED_LIBS)
target_compile_definitions(GLEW PUBLIC GLEW_STATIC)
endif ()
include(GNUInstallDirs)
install(
FILES
${PROJECT_SOURCE_DIR}/include/GL/glew.h
${PROJECT_SOURCE_DIR}/include/GL/wglew.h
${PROJECT_SOURCE_DIR}/include/GL/glxew.h
${PROJECT_SOURCE_DIR}/include/GL/eglew.h
DESTINATION
${CMAKE_INSTALL_INCLUDEDIR}/GL
)
install(TARGETS GLEW GLEW
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
-73
View File
@@ -1,73 +0,0 @@
The OpenGL Extension Wrangler Library
Copyright (C) 2002-2007, Milan Ikits <milan ikits[]ieee org>
Copyright (C) 2002-2007, Marcelo E. Magallon <mmagallo[]debian org>
Copyright (C) 2002, Lev Povalahev
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name of the author may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
Mesa 3-D graphics library
Version: 7.0
Copyright (C) 1999-2007 Brian Paul All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
BRIAN PAUL BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copyright (c) 2007 The Khronos Group Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and/or associated documentation files (the
"Materials"), to deal in the Materials without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Materials, and to
permit persons to whom the Materials are furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Materials.
THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
-251
View File
@@ -1,251 +0,0 @@
# GLEW - The OpenGL Extension Wrangler Library
The OpenGL Extension Wrangler Library (GLEW) is a cross-platform open-source C/C++ extension loading library. GLEW provides efficient run-time mechanisms for determining which OpenGL extensions are supported on the target platform. OpenGL core and extension functionality is exposed in a single header file. GLEW has been tested on a variety of operating systems, including Windows, Linux, Mac OS X, FreeBSD, Irix, and Solaris.
![](http://glew.sourceforge.net/glew.png)
http://glew.sourceforge.net/
https://github.com/nigels-com/glew
[![Build Status](https://travis-ci.org/nigels-com/glew.svg?branch=master)](https://travis-ci.org/nigels-com/glew)
[![Gitter](https://badges.gitter.im/nigels-com/glew.svg)](https://gitter.im/nigels-com/glew?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Download](https://img.shields.io/sourceforge/dm/glew.svg)](https://sourceforge.net/projects/glew/files/latest/download)
## Table of Contents
* [Downloads](#downloads)
* [Recent snapshots](#recent-snapshots)
* [Build](#build)
* [Linux and Mac](#linux-and-mac)
* [Using GNU Make](#using-gnu-make)
* [Install build tools](#install-build-tools)
* [Build](#build-1)
* [Linux EGL](#linux-egl)
* [Linux mingw-w64](#linux-mingw-w64)
* [Using cmake](#using-cmake)
* [Install build tools](#install-build-tools-1)
* [Build](#build-2)
* [Windows](#windows)
* [Visual Studio](#visual-studio)
* [MSYS/Mingw](#msysmingw)
* [MSYS2/Mingw-w64](#msys2mingw-w64)
* [glewinfo](#glewinfo)
* [Code Generation](#code-generation)
* [Authors](#authors)
* [Contributions](#contributions)
* [Copyright and Licensing](#copyright-and-licensing)
## Downloads
Current release is [2.1.0](https://sourceforge.net/projects/glew/files/glew/2.1.0/).
[(Change Log)](http://glew.sourceforge.net/log.html)
Sources available as
[ZIP](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0.zip/download) or
[TGZ](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0.tgz/download).
Windows binaries for [32-bit and 64-bit](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0-win32.zip/download).
### Recent snapshots
Snapshots may contain new features, bug-fixes or new OpenGL extensions ahead of tested, official releases.
[glew-20200115.tgz](https://sourceforge.net/projects/glew/files/glew/snapshots/glew-20200115.tgz/download) *GLEW 2.2.0 RC3: fixes*
[glew-20190928.tgz](https://sourceforge.net/projects/glew/files/glew/snapshots/glew-20190928.tgz/download) *GLEW 2.2.0 RC2: New extensions, bug fixes*
## Build
It is highly recommended to build from a tgz or zip release snapshot.
The code generation workflow is a complex brew of gnu make, perl and python, that works best on Linux or Mac.
The code generation is known to work on Windows using [MSYS2](https://www.msys2.org/).
For most end-users of GLEW the official releases are the best choice, with first class support.
### Linux and Mac
#### Using GNU Make
GNU make is the primary build system for GLEW, historically.
It includes targets for building the sources and headers, for maintenance purposes.
##### Install build tools
Debian/Ubuntu/Mint: `$ sudo apt-get install build-essential libxmu-dev libxi-dev libgl-dev`
RedHat/CentOS/Fedora: `$ sudo yum install libXmu-devel libXi-devel libGL-devel`
FreeBSD: `# pkg install xorg lang/gcc git cmake gmake bash python perl5`
##### Build
$ make
$ sudo make install
$ make clean
Targets: `all, glew.lib (sub-targets: glew.lib.shared, glew.lib.static), glew.bin, clean, install, uninstall`
Variables: `SYSTEM=linux-clang, GLEW_DEST=/usr/local, STRIP=`
_Note: you may need to call `make` in the **auto** folder first_
##### Linux EGL
$ sudo apt install libegl1-mesa-dev
$ make SYSTEM=linux-egl
##### Linux mingw-w64
$ sudo apt install mingw-w64
$ make SYSTEM=linux-mingw32
$ make SYSTEM=linux-mingw64
#### Using cmake
The cmake build is mostly contributer maintained.
Due to the multitude of use cases this is maintained on a _best effort_ basis.
Pull requests are welcome.
*CMake 2.8.12 or higher is required.*
##### Install build tools
Debian/Ubuntu/Mint: `$ sudo apt-get install build-essential libxmu-dev libxi-dev libgl-dev cmake git`
RedHat/CentOS/Fedora: `$ sudo yum install libXmu-devel libXi-devel libGL-devel cmake git`
##### Build
$ cd build
$ cmake ./cmake
$ make -j4
| Target | Description |
| ---------- | ----------- |
| glew | Build the glew shared library. |
| glew_s | Build the glew static library. |
| glewinfo | Build the `glewinfo` executable (requires `BUILD_UTILS` to be `ON`). |
| visualinfo | Build the `visualinfo` executable (requires `BUILD_UTILS` to be `ON`). |
| install | Install all enabled targets into `CMAKE_INSTALL_PREFIX`. |
| clean | Clean up build artifacts. |
| all | Build all enabled targets (default target). |
| Variables | Description |
| --------------- | ----------- |
| BUILD_UTILS | Build the `glewinfo` and `visualinfo` executables. |
| GLEW_REGAL | Build in Regal mode. |
| BUILD_FRAMEWORK | Build as MacOSX Framework. Setting `CMAKE_INSTALL_PREFIX` to `/Library/Frameworks` is recommended. |
### Windows
#### Visual Studio
Use the provided Visual Studio project file in build/vc15/
Projects for vc6, vc10, vc12 and vc14 are also provided
#### MSYS/Mingw
Available from [Mingw](http://www.mingw.org/)
Requirements: bash, make, gcc
$ mingw32-make
$ mingw32-make install
$ mingw32-make install.all
Alternative toolchain: `SYSTEM=mingw-win32`
#### MSYS2/Mingw-w64
Available from [Msys2](http://msys2.github.io/) and/or [Mingw-w64](http://mingw-w64.org/)
Requirements: bash, make, gcc
$ pacman -S gcc make mingw-w64-i686-gcc mingw-w64-x86_64-gcc
$ make
$ make install
$ make install.all
Alternative toolchain: `SYSTEM=msys, SYSTEM=msys-win32, SYSTEM=msys-win64`
## glewinfo
`glewinfo` is a command-line tool useful for inspecting the capabilities of an
OpenGL implementation and GLEW support for that. Please include `glewinfo.txt`
with bug reports, as appropriate.
---------------------------
GLEW Extension Info
---------------------------
GLEW version 2.0.0
Reporting capabilities of pixelformat 3
Running on a Intel(R) HD Graphics 3000 from Intel
OpenGL version 3.1.0 - Build 9.17.10.4229 is supported
GL_VERSION_1_1: OK
---------------
GL_VERSION_1_2: OK
---------------
glCopyTexSubImage3D: OK
glDrawRangeElements: OK
glTexImage3D: OK
glTexSubImage3D: OK
...
## Code Generation
A Unix or Mac environment is needed for building GLEW from scratch to
include new extensions, or customize the code generation. The extension
data is regenerated from the top level source directory with:
make extensions
An alternative to generating the GLEW sources from scratch is to
download a pre-generated (unsupported) snapshot:
https://sourceforge.net/projects/glew/files/glew/snapshots/
## Authors
GLEW is currently maintained by [Nigel Stewart](https://github.com/nigels-com)
with bug fixes, new OpenGL extension support and new releases.
GLEW was developed by [Milan Ikits](http://www.cs.utah.edu/~ikits/)
and [Marcelo Magallon](http://wwwvis.informatik.uni-stuttgart.de/~magallon/).
Aaron Lefohn, Joe Kniss, and Chris Wyman were the first users and also
assisted with the design and debugging process.
The acronym GLEW originates from Aaron Lefohn.
Pasi K&auml;rkk&auml;inen identified and fixed several problems with
GLX and SDL. Nate Robins created the `wglinfo` utility, to
which modifications were made by Michael Wimmer.
## Contributions
GLEW welcomes community contributions. Typically these are co-ordinated
via [Issues](https://github.com/nigels-com/glew/issues) or
[Pull Requests](https://github.com/nigels-com/glew/pulls) in the
GitHub web interface.
Be sure to mention platform and compiler toolchain details when filing
a bug report. The output of `glewinfo` can be quite useful for discussion
also.
Generally GLEW is usually released once a year, around the time of the Siggraph
computer graphics conference. If you're not using the current release
version of GLEW, be sure to check if the issue or bug is fixed there.
## Copyright and Licensing
GLEW is originally derived from the EXTGL project by Lev Povalahev.
The source code is licensed under the
[Modified BSD License](http://glew.sourceforge.net/glew.txt), the
[Mesa 3-D License](http://glew.sourceforge.net/mesa.txt) (MIT) and the
[Khronos License](http://glew.sourceforge.net/khronos.txt) (MIT).
The automatic code generation scripts are released under the
[GNU GPL](http://glew.sourceforge.net/gpl.txt).
-1
View File
@@ -1 +0,0 @@
2.2.0
-3051
View File
File diff suppressed because it is too large Load Diff
-26427
View File
File diff suppressed because it is too large Load Diff
-1831
View File
File diff suppressed because it is too large Load Diff
-1468
View File
File diff suppressed because it is too large Load Diff
-31949
View File
File diff suppressed because it is too large Load Diff
-101
View File
@@ -1,101 +0,0 @@
cmake_minimum_required(VERSION 3.0)
project(OpenCSG)
if (NOT BUILD_SHARED_LIBS)
set(GLEW_USE_STATIC_LIBS ON)
elseif (MSVC)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
find_package(OpenGL REQUIRED)
set(GLEW_VERBOSE ON)
find_package(GLEW 1.13.0 REQUIRED)
set(_srcfiles
src/area.cpp
src/batch.cpp
src/context.cpp
src/channelManager.cpp
src/frameBufferObject.cpp
src/frameBufferObjectExt.cpp
src/occlusionQuery.cpp
src/opencsgRender.cpp
src/openglHelper.cpp
src/pBufferTexture.cpp
src/primitive.cpp
src/primitiveHelper.cpp
src/renderGoldfeather.cpp
src/renderSCS.cpp
src/scissorMemo.cpp
src/settings.cpp
src/stencilManager.cpp
RenderTexture/RenderTexture.cpp
include/opencsg.h
src/opencsgConfig.h
src/area.h
src/batch.h
src/context.h
src/channelManager.h
src/frameBufferObject.h
src/frameBufferObjectExt.h
src/occlusionQuery.h
src/offscreenBuffer.h
src/opencsgRender.h
src/openglHelper.h
src/pBufferTexture.h
src/primitiveHelper.h
src/scissorMemo.h
src/settings.h
src/stencilManager.h
)
add_library(opencsg ${_srcfiles})
target_include_directories(opencsg PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
target_include_directories(opencsg PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>)
target_link_libraries(opencsg PRIVATE GLEW::GLEW OpenGL::GL)
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
VERSION 1.4.2
COMPATIBILITY AnyNewerVersion
)
install(TARGETS opencsg
EXPORT ${PROJECT_NAME}Targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
export(EXPORT ${PROJECT_NAME}Targets
FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
NAMESPACE ${PROJECT_NAME}:: )
set(ConfigPackageLocation ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(EXPORT ${PROJECT_NAME}Targets
FILE
"${PROJECT_NAME}Config.cmake"
NAMESPACE
${PROJECT_NAME}::
DESTINATION
${ConfigPackageLocation}
)
install(
FILES
${PROJECT_SOURCE_DIR}/include/opencsg.h
DESTINATION
${CMAKE_INSTALL_INCLUDEDIR}/opencsg
)
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
DESTINATION
${ConfigPackageLocation}
)
-17
View File
@@ -1,17 +0,0 @@
orcaslicer_add_cmake_project(OpenCSG
# GIT_REPOSITORY https://github.com/floriankirsch/OpenCSG.git
# GIT_TAG 83e274457b46c9ad11a4ee599203250b1618f3b9 #v1.4.2
URL https://github.com/floriankirsch/OpenCSG/archive/refs/tags/opencsg-1-4-2-release.zip
URL_HASH SHA256=51afe0db79af8386e2027d56d685177135581e0ee82ade9d7f2caff8deab5ec5
PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in ./CMakeLists.txt
DEPENDS dep_GLEW
)
if (TARGET ${ZLIB_PKG})
add_dependencies(dep_OpenCSG ${ZLIB_PKG})
endif()
if (MSVC)
add_debug_dep(dep_OpenCSG)
endif ()
+1 -1
View File
@@ -191,7 +191,7 @@ public:
tail->next = std::move(p); tail->next = std::move(p);
tail = new_tail; tail = new_tail;
} }
data_cond.notify_one(); disrupt_wait_for_data();
} }
void wait_and_pop(T& value) void wait_and_pop(T& value)
+172
View File
@@ -0,0 +1,172 @@
# Center of mass markers — High Level Design
## Purpose and scope
The "Center of mass" item of the canvas toolbar menu, in the bottom left corner of the 3D view, marks
where the mass of the plate, of each object instance and of each body of an assembly is centered,
in Prepare and in Preview. It helps judge how parts will rest on the plate, for instance whether a
tall or leaning part could tip. 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
`show_center_of_mass`, off by default. The assembly view and the Design tab have no markers.
Three kinds of marker share one shape, a sphere whose octants alternate between two colors:
- each plate, black and white, for everything on it;
- each object instance, light blue and white;
- each body of an assembly, red and yellow;
- in Preview, the supports and raft of each object instance, green and black.
A click on a marker opens a box beside it with the weight and volume of what it stands for, where its
center lies in that thing's bounding box and the size of the box, and its moments of inertia about
axes through the center parallel to x, y and z.
An assembly is an object of several parts or with negative volumes. Its bodies are the connected
solids its parts make once united, the bodies the separated infills option centers its infill on
(see separated-infills.md): parts that overlap or touch are one body, parts apart are separate
bodies. An object of one body, and every object of a single part, has no body markers, as its object
marker says it all.
Mass is volume times density, the `filament_density` of the filament that prints it, or 1.245 g/cm³
(`DEFAULT_FILAMENT_DENSITY`) for a filament without one. Prepare has the model only, so it takes each
part as a solid of the density of its filament, the part's own or else its object's. It reads each
filament's density from the filament's selected preset, edits not yet saved included, as slicing does:
the plater's own config holds the values of the filament edited last only. Preview has
what will be printed, so its markers come from the toolpaths, whose mass depends on walls, infill
and flow as well. There the solid markers are for what is printed up to the top layer the layer
slider shows: for the plate with brim, raft and supports, where the weight rests at that point of
the print; for an object or a body, its own extrusions. Each has a faded twin for the same at the
end of the print, so the slider shows the weight moving toward where it ends, and at the top layer
the two meet. In Prepare the plate has the model alone, as brim, raft and supports exist only once
sliced.
## Prepare: from the meshes
An object of one part takes the mass properties of its mesh at unit density, times its density, from
`its_mass_properties()`, which handles a mesh in a single pass. Each triangle and a fixed vertex of
the mesh span a tetrahedron whose signed volume is `V = a · (b × c) / 6`, with `a`, `b`, `c` taken
relative to that vertex. By the divergence theorem these volumes add up to the volume of a closed
mesh, their volume-weighted centroids to its center of mass, and their second moments
`V (a aᵀ + b bᵀ + c cᵀ + s sᵀ) / 20`, with `s = a + b + c`, to its own. Shells facing inward, such as
a cavity, subtract themselves, and flipping every triangle changes nothing. The sums are kept in
double precision and relative to a vertex of the mesh rather than the origin, which keeps them exact
for meshes far from it. The result keeps the spread of the mass about its center,
`(x - c)(x - c)ᵀ` averaged over the mass, from which the moments of inertia follow.
The CGAL routines that look alike do not compute this. `CGAL::centroid` weighs tetrahedra by their
unsigned volume, so it fails on cavities and on any shell that is not star-shaped from the fan's
apex; over triangles it returns the centroid of the surface, and over points the average of the
vertices, which depends on the tessellation. `CGAL::barycenter` with the signed volumes as weights
gives the same answer, but only after copying every tetrahedron into a vector of weighted points,
and takes two and a half times as long.
A mesh's result is in its own coordinates. Each `GLVolume` maps it to the world with its world
matrix `M`, and weighs it by the volume times the absolute determinant of that matrix: a center of
mass moves with any affine map, and the spread becomes `L S Lᵀ` for the linear part `L` of `M`, so no
mesh is ever transformed. The `GLVolume`'s matrices, rather than the
model's, let the markers follow an object while it is dragged, before the model is updated. Results
are cached by `ModelVolume` id; a `ModelVolume` takes a new id whenever its mesh changes, which is
the rule `reload_scene()` relies on to rebuild a `GLVolume`'s geometry, so a cached result never
outlives its mesh.
An assembly's parts overlap or touch, which the mesh formula would count twice, so `solid_bodies()`
slices them instead, in the object's coordinates. It cuts the height into 500 slabs, 100 while a part
is dragged, slices every part and negative volume at the middle of each slab, unites the parts and
cuts the negative volumes away, and links the islands of neighboring slabs that overlap into bodies
with `connected_bodies()`. Each island adds a prism of the slab's thickness at the density: its
area, and its first and second moments of area, from the same sums over the outline as the area,
with the slab's height for z. Where parts of different densities overlap, the later volume of the object counts, as
slicing clips every part by the parts after it; each part then weighs the region it prints, which is
credited to the island holding it. Each body also keeps the convex hull of its islands and the height
they span, whose corners, once transformed, give its bounding box, tight while the instance turns
about z only. The object is the sum of its bodies, its box that of its parts, as the object's size
shows it, and each plate the sum of the object instances `PartPlateList::find_instance()` puts on it,
so that an instance on no plate counts in none. The bodies are cached by `ModelObject` id with the volumes, types, densities and
transformations they were sliced from.
## Preview: from the toolpaths
`GCodeProcessor` sums the masses while it processes the G-code, in the same pass that builds the
moves, and leaves them in `GCodeProcessorResult`; `GCodeViewer` keeps a copy of them when it loads a
result. Nothing is stored per move.
Each extrusion weighs the volume of filament its E extrudes times the density of the filament that
extrudes it, so a print of several materials weighs each as it is. Flow ratio, line widths, ironing
and purging into infill all count
as printed. Its mass spreads evenly along the segment the bead's center runs, half the layer height
below the nozzle, in the frame of the stored moves: plate offset added, Z offset removed. Such a
segment from `a` to `b` adds `m (a + b) / 2` to the moments and `m (a² + a b + b²) / 3` to the second
moments along each axis, and its box widened by half the bead's height to the bounding box; not by
half its width, which the processor only estimates, so that a box runs along the walls' center lines. Arcs are already split into segments by the processor. Walls, infill, top and bottom surfaces, ironing
and gap fill make the parts. The brim and the support roles, the raft among them, count only in what
the plate prints. The skirt, the prime tower and custom G-code count nowhere.
The plate takes every extrusion, so it needs nothing more. The objects and bodies need the sliced
objects, which the G-code does not describe, so the G-code export hands the processor a locator
built from the `Print`; G-code opened from a file, or from a project sliced earlier, has no `Print`
behind it, and so shows the plate alone. Object labels would not do: profiles turn the four kinds
Orca writes on and off in every combination, and none of them tells the bodies apart.
The locator numbers the object instances and, for each assembly, the bodies of every instance. It
takes the bodies `PrintObject::prepare_infill()` found for separated infills, or, when that option
did not need them, links the islands (`Layer::lslices`) of neighboring layers into bodies with the
same `connected_bodies()`. For each extrusion of a part, it finds the first layer printed at or above
its height, as spiral vase rises through each layer, and the island holding it with an
`IslandLocator`, the one `solid_bodies()` credits its regions with: by the island's box, widened by
1 mm for walls reaching past it, with a polygon test only where boxes overlap, and the nearest
outline where none holds the point. The boxes of one layer's islands say nothing of the other
instances, so an instance whose widened box reaches another's, as copies placed side by side do,
tests the outlines alone, and outside them the nearest outline of all such instances wins. The island
gives both the instance and the body. The island found last is tried first, as extrusions mostly follow each other on one
island. Brim, raft and supports lie outside the islands. The brim counts in the plate only; a support
or raft extrusion goes to the instance whose footprint, the box of its widened islands, holds it, the
one whose center is nearest among several, or else the nearest footprint, as supports stand below and
around their object.
Each mass holds, for each layer id, the running total of what is printed up to that layer, the last
of which is the faded marker's, so the solid marker for any slider position is a single lookup. The layer ids are those
the moves carry, which are also the layers of libvgcode and of the slider; in a print by object they
follow the order of printing, so the solid markers show the objects printed so far as they are.
## Drawing
`smooth_sphere()` with a resolution divisible by four leaves every triangle within one octant, so it
splits into two models drawn with the `gouraud_light` shader in each kind's two colors. The radius is
9 pixels for the plate, 7 for the objects, 6 for the supports and 5 for the bodies, scaled like the canvas toolbar for the
display's DPI and kept constant on screen through the camera's inverse zoom. They are drawn in that
order, so that markers at one place show as rings. The faded markers are the same spheres at 40%
opacity, drawn before all the solid ones, which show over them where both meet.
The centers usually lie inside the objects, so the markers are drawn without the depth test and show
through the objects and anything in front of them. Back face culling keeps the far half of a sphere
from covering the near one. They are drawn after the ambient occlusion pass, which would otherwise
darken them as the surface behind them, and before FXAA, which smooths their edges.
The markers are part of the cached scene, so toggling them, or changing a filament's density while
they are shown, marks the scene dirty, and moving the layer slider redraws the scene with the solid
markers where they belong. In Prepare they are hidden while any gizmo other than Move, Rotate, Scale
and Lay on face is open, since the others work on the surface a marker would cover, and a hidden
object has no markers.
## Details
The markers drawn last are kept, and a left click is tested against them before it selects: each
center and a point a radius to its right are projected to the screen, and the click hits a marker
within that distance. The solid markers are tested before the faded ones and the smaller kinds
before the larger, the order in which they cover each other. A hit opens the details of that marker
and keeps the click from changing the selection; a click anywhere else closes them. The box is an
ImGui window beside the marker, redrawn with the overlay from the markers of the last scene, so it
follows a dragged object, and in Preview the layer slider. It closes when its marker is gone, or when
the number of markers of its kind changes, as then it may stand for something else.
Its title says what the marker stands for: the plate, an object, an assembly or a part, the body of
an assembly. The G-code export lists the object instances for the processor, marking assemblies, as
it hands it the locator.
Each marker carries its sums: mass, volume, first moments and the second moments about the origin
along each axis, `Σ m x²`, `Σ m y²` and `Σ m z²`, which add up from parts to objects to plates. The
moment of inertia about the axis through the center parallel to x is then
`m (σy² + σz²)`, with `σ² = Σ m x² / m - c²` along each axis, and likewise for y and z. Masses are
kept in mg, volume times density in g/cm³, and shown in g, volumes in cm³ and moments of inertia in
g·mm². In Preview the box tells the finished print from what is printed up to the layer shown, the
two weighing differently, and both are placed in the bounding box of everything the marker holds
by the end.
+2 -2
View File
@@ -18,8 +18,8 @@ each body on its own (see Octree infill).
## Bodies ## Bodies
`PrintObject::prepare_infill()` groups the islands of every layer (`lslices`) `PrintObject::prepare_infill()` groups the islands of every layer (`lslices`)
into 3D connected bodies before bridges are detected, so bridge anchors and into 3D connected bodies with `connected_bodies()` before bridges are detected,
printed infill share one origin. Islands on adjacent layers belong to one body so bridge anchors and printed infill share one origin. Islands on adjacent layers belong to one body
when their slices overlap. Parts that touch or overlap form one body. Separate when their slices overlap. Parts that touch or overlap form one body. Separate
parts, disconnected islands of one mesh, and interleaved parts that never touch, parts, disconnected islands of one mesh, and interleaved parts that never touch,
such as chain links, each form their own. Every island stores the index of its such as chain links, each form their own. Every island stores the index of its
@@ -29,13 +29,11 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64]; uniform vec3 palette_rgb[64];
uniform int palette_count; uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image) uniform bool pure_only; // match against single filaments only (flat-colour image)
// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament // The entry's two filaments, equal for a single filament - only so a mix can be told apart. An entry's
// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's // palette_rgb is already the colour it prints in (for a mix, its mixed filament slot's).
// colour up.
uniform int palette_a[64]; uniform int palette_a[64];
uniform int palette_b[64]; uniform int palette_b[64];
uniform vec3 filament_rgb[16]; uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform int filament_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex; uniform bool has_color_tex;
uniform bool volume_mirrored; uniform bool volume_mirrored;
@@ -210,31 +208,16 @@ int nearest_palette_entry(vec3 rgb)
best = i; best = i;
} }
} }
// The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so // The same bias make_palette_quantizer() applies: a mix is an interleave, so it is only worth taking
// it is only worth taking when it beats the nearest single filament by a visible step. Without it // when it beats the nearest single filament by a visible step - otherwise the preview shows mixes
// this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries // where the bake prints a single filament. Compared on the distances rather than their squares, so
// against 30 mixes - while the bake picked a single filament for most of them, so the preview // the margin means the same thing as it does on the CPU (up to CIE76 against CIEDE2000, the
// interleaved the whole wall where the bake interleaves only patches. Compared on the distances // approximation already noted above).
// rather than their squares, so the threshold means the same thing as it does on the CPU (up to if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < prefer_pure_de)
// CIE76 against CIEDE2000, the approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0)
best = best_pure; best = best_pure;
return best; return best;
} }
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
// per print layer, so there is nothing left to interleave here.
vec3 printed_color(int index)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
}
void main() void main()
{ {
if (any(lessThan(clipping_planes_dots, ZERO))) if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -354,16 +337,11 @@ void main()
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour - // Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as // Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected. // the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb; vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
// tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb));
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
} }
@@ -88,13 +88,11 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64]; uniform vec3 palette_rgb[64];
uniform int palette_count; uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image) uniform bool pure_only; // match against single filaments only (flat-colour image)
// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament // The entry's two filaments, equal for a single filament - only so a mix can be told apart. An entry's
// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's // palette_rgb is already the colour it prints in (for a mix, its mixed filament slot's).
// colour up.
uniform int palette_a[64]; uniform int palette_a[64];
uniform int palette_b[64]; uniform int palette_b[64];
uniform vec3 filament_rgb[16]; uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform int filament_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex; uniform bool has_color_tex;
uniform bool volume_mirrored; uniform bool volume_mirrored;
@@ -276,31 +274,16 @@ int nearest_palette_entry(vec3 rgb)
best = i; best = i;
} }
} }
// The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so // The same bias make_palette_quantizer() applies: a mix is an interleave, so it is only worth taking
// it is only worth taking when it beats the nearest single filament by a visible step. Without it // when it beats the nearest single filament by a visible step - otherwise the preview shows mixes
// this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries // where the bake prints a single filament. Compared on the distances rather than their squares, so
// against 30 mixes - while the bake picked a single filament for most of them, so the preview // the margin means the same thing as it does on the CPU (up to CIE76 against CIEDE2000, the
// interleaved the whole wall where the bake interleaves only patches. Compared on the distances // approximation already noted above).
// rather than their squares, so the threshold means the same thing as it does on the CPU (up to if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < prefer_pure_de)
// CIE76 against CIEDE2000, the approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0)
best = best_pure; best = best_pure;
return best; return best;
} }
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
// per print layer, so there is nothing left to interleave here.
vec3 printed_color(int index)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
}
void main() void main()
{ {
if (any(lessThan(clipping_planes_dots, ZERO))) if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -442,16 +425,11 @@ void main()
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour - // Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as // Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected. // the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb; vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
// tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb));
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
} }
-1
View File
@@ -2,5 +2,4 @@
#add_subdirectory(openvdb) #add_subdirectory(openvdb)
# add_subdirectory(meshboolean) # add_subdirectory(meshboolean)
add_subdirectory(its_neighbor_index) add_subdirectory(its_neighbor_index)
# add_subdirectory(opencsg)
#add_subdirectory(aabb-evaluation) #add_subdirectory(aabb-evaluation)
-30
View File
@@ -1,30 +0,0 @@
cmake_minimum_required(VERSION 3.0)
project(OpenCSG-example)
add_executable(opencsg_example WIN32
main.cpp
Engine.hpp Engine.cpp
ShaderCSGDisplay.hpp ShaderCSGDisplay.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/Jobs/Job.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/ProgressStatusBar.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/I18N.hpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/I18N.cpp)
find_package(wxWidgets 3.1 REQUIRED COMPONENTS core base gl html)
find_package(OpenGL REQUIRED)
find_package(GLEW REQUIRED)
find_package(OpenCSG REQUIRED)
include(${wxWidgets_USE_FILE})
target_link_libraries(opencsg_example libslic3r)
target_include_directories(opencsg_example PRIVATE ${wxWidgets_INCLUDE_DIRS})
target_compile_definitions(opencsg_example PRIVATE ${wxWidgets_DEFINITIONS})
slic3r_remap_configs(OpenCSG::opencsg RelWithDebInfo Release)
target_link_libraries(opencsg_example ${wxWidgets_LIBRARIES}
OpenCSG::opencsg
GLEW::GLEW
OpenGL::GL
#-lXrandr -lXext -lX11
)
-495
View File
@@ -1,495 +0,0 @@
#include "Engine.hpp"
#include <libslic3r/Utils.hpp>
#include <libslic3r/SLAPrint.hpp>
#include <GL/glew.h>
#include <boost/log/trivial.hpp>
#ifndef NDEBUG
#define HAS_GLSAFE
#endif
#ifdef HAS_GLSAFE
extern void glAssertRecentCallImpl(const char *file_name, unsigned int line, const char *function_name);
inline void glAssertRecentCall() { glAssertRecentCallImpl(__FILE__, __LINE__, __FUNCTION__); }
#define glsafe(cmd) do { cmd; glAssertRecentCallImpl(__FILE__, __LINE__, __FUNCTION__); } while (false)
#define glcheck() do { glAssertRecentCallImpl(__FILE__, __LINE__, __FUNCTION__); } while (false)
void glAssertRecentCallImpl(const char *file_name, unsigned int line, const char *function_name)
{
GLenum err = glGetError();
if (err == GL_NO_ERROR)
return;
const char *sErr = 0;
switch (err) {
case GL_INVALID_ENUM: sErr = "Invalid Enum"; break;
case GL_INVALID_VALUE: sErr = "Invalid Value"; break;
// be aware that GL_INVALID_OPERATION is generated if glGetError is executed between the execution of glBegin and the corresponding execution of glEnd
case GL_INVALID_OPERATION: sErr = "Invalid Operation"; break;
case GL_STACK_OVERFLOW: sErr = "Stack Overflow"; break;
case GL_STACK_UNDERFLOW: sErr = "Stack Underflow"; break;
case GL_OUT_OF_MEMORY: sErr = "Out Of Memory"; break;
default: sErr = "Unknown"; break;
}
BOOST_LOG_TRIVIAL(error) << "OpenGL error in " << file_name << ":" << line << ", function " << function_name << "() : " << (int)err << " - " << sErr;
assert(false);
}
#else
inline void glAssertRecentCall() { }
#define glsafe(cmd) cmd
#define glcheck()
#endif
namespace Slic3r { namespace GL {
Scene::Scene() = default;
Scene::~Scene() = default;
void CSGDisplay::render_scene()
{
GLfloat color[] = {1.f, 1.f, 0.f, 0.f};
glsafe(::glColor4fv(color));
if (m_csgsettings.is_enabled()) {
OpenCSG::render(m_scene_cache.primitives_csg);
glDepthFunc(GL_EQUAL);
}
for (auto& p : m_scene_cache.primitives_csg) p->render();
if (m_csgsettings.is_enabled()) glDepthFunc(GL_LESS);
for (auto& p : m_scene_cache.primitives_free) p->render();
glFlush();
}
void Scene::set_print(std::unique_ptr<SLAPrint> &&print)
{
m_print = std::move(print);
// Notify displays
call(&Listener::on_scene_updated, m_listeners, *this);
}
BoundingBoxf3 Scene::get_bounding_box() const
{
return m_print->model().bounding_box();
}
void CSGDisplay::SceneCache::clear()
{
primitives_csg.clear();
primitives_free.clear();
primitives.clear();
}
std::shared_ptr<Primitive> CSGDisplay::SceneCache::add_mesh(const TriangleMesh &mesh)
{
auto p = std::make_shared<Primitive>();
p->load_mesh(mesh);
primitives.emplace_back(p);
primitives_free.emplace_back(p.get());
return p;
}
std::shared_ptr<Primitive> CSGDisplay::SceneCache::add_mesh(const TriangleMesh &mesh,
OpenCSG::Operation o,
unsigned c)
{
auto p = std::make_shared<Primitive>(o, c);
p->load_mesh(mesh);
primitives.emplace_back(p);
primitives_csg.emplace_back(p.get());
return p;
}
void IndexedVertexArray::push_geometry(float x, float y, float z, float nx, float ny, float nz)
{
assert(this->vertices_and_normals_interleaved_VBO_id == 0);
if (this->vertices_and_normals_interleaved_VBO_id != 0)
return;
if (this->vertices_and_normals_interleaved.size() + 6 > this->vertices_and_normals_interleaved.capacity())
this->vertices_and_normals_interleaved.reserve(next_highest_power_of_2(this->vertices_and_normals_interleaved.size() + 6));
this->vertices_and_normals_interleaved.emplace_back(nx);
this->vertices_and_normals_interleaved.emplace_back(ny);
this->vertices_and_normals_interleaved.emplace_back(nz);
this->vertices_and_normals_interleaved.emplace_back(x);
this->vertices_and_normals_interleaved.emplace_back(y);
this->vertices_and_normals_interleaved.emplace_back(z);
this->vertices_and_normals_interleaved_size = this->vertices_and_normals_interleaved.size();
}
void IndexedVertexArray::push_triangle(int idx1, int idx2, int idx3) {
assert(this->vertices_and_normals_interleaved_VBO_id == 0);
if (this->vertices_and_normals_interleaved_VBO_id != 0)
return;
if (this->triangle_indices.size() + 3 > this->vertices_and_normals_interleaved.capacity())
this->triangle_indices.reserve(next_highest_power_of_2(this->triangle_indices.size() + 3));
this->triangle_indices.emplace_back(idx1);
this->triangle_indices.emplace_back(idx2);
this->triangle_indices.emplace_back(idx3);
this->triangle_indices_size = this->triangle_indices.size();
}
void IndexedVertexArray::load_mesh(const TriangleMesh &mesh)
{
assert(triangle_indices.empty() && vertices_and_normals_interleaved_size == 0);
assert(quad_indices.empty() && triangle_indices_size == 0);
assert(vertices_and_normals_interleaved.size() % 6 == 0 && quad_indices_size == vertices_and_normals_interleaved.size());
this->vertices_and_normals_interleaved.reserve(this->vertices_and_normals_interleaved.size() + 3 * 3 * 2 * mesh.facets_count());
int vertices_count = 0;
for (size_t i = 0; i < mesh.facets_count(); ++i) {
const stl_facet &facet = mesh.stl.facet_start[i];
for (int j = 0; j < 3; ++j)
this->push_geometry(facet.vertex[j](0), facet.vertex[j](1), facet.vertex[j](2), facet.normal(0), facet.normal(1), facet.normal(2));
this->push_triangle(vertices_count, vertices_count + 1, vertices_count + 2);
vertices_count += 3;
}
}
void IndexedVertexArray::finalize_geometry()
{
assert(this->vertices_and_normals_interleaved_VBO_id == 0);
assert(this->triangle_indices_VBO_id == 0);
assert(this->quad_indices_VBO_id == 0);
if (!this->vertices_and_normals_interleaved.empty()) {
glsafe(
::glGenBuffers(1, &this->vertices_and_normals_interleaved_VBO_id));
glsafe(::glBindBuffer(GL_ARRAY_BUFFER,
this->vertices_and_normals_interleaved_VBO_id));
glsafe(
::glBufferData(GL_ARRAY_BUFFER,
GLsizeiptr(
this->vertices_and_normals_interleaved.size() *
4),
this->vertices_and_normals_interleaved.data(),
GL_STATIC_DRAW));
glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0));
this->vertices_and_normals_interleaved.clear();
}
if (!this->triangle_indices.empty()) {
glsafe(::glGenBuffers(1, &this->triangle_indices_VBO_id));
glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
this->triangle_indices_VBO_id));
glsafe(::glBufferData(GL_ELEMENT_ARRAY_BUFFER,
GLsizeiptr(this->triangle_indices.size() * 4),
this->triangle_indices.data(), GL_STATIC_DRAW));
glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
this->triangle_indices.clear();
}
if (!this->quad_indices.empty()) {
glsafe(::glGenBuffers(1, &this->quad_indices_VBO_id));
glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
this->quad_indices_VBO_id));
glsafe(::glBufferData(GL_ELEMENT_ARRAY_BUFFER,
GLsizeiptr(this->quad_indices.size() * 4),
this->quad_indices.data(), GL_STATIC_DRAW));
glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
this->quad_indices.clear();
}
}
void IndexedVertexArray::release_geometry()
{
if (this->vertices_and_normals_interleaved_VBO_id) {
glsafe(
::glDeleteBuffers(1,
&this->vertices_and_normals_interleaved_VBO_id));
this->vertices_and_normals_interleaved_VBO_id = 0;
}
if (this->triangle_indices_VBO_id) {
glsafe(::glDeleteBuffers(1, &this->triangle_indices_VBO_id));
this->triangle_indices_VBO_id = 0;
}
if (this->quad_indices_VBO_id) {
glsafe(::glDeleteBuffers(1, &this->quad_indices_VBO_id));
this->quad_indices_VBO_id = 0;
}
this->clear();
}
void IndexedVertexArray::render() const
{
assert(this->vertices_and_normals_interleaved_VBO_id != 0);
assert(this->triangle_indices_VBO_id != 0 ||
this->quad_indices_VBO_id != 0);
glsafe(::glBindBuffer(GL_ARRAY_BUFFER,
this->vertices_and_normals_interleaved_VBO_id));
glsafe(::glVertexPointer(3, GL_FLOAT, 6 * sizeof(float),
reinterpret_cast<const void *>(3 * sizeof(float))));
glsafe(::glNormalPointer(GL_FLOAT, 6 * sizeof(float), nullptr));
glsafe(::glEnableClientState(GL_VERTEX_ARRAY));
glsafe(::glEnableClientState(GL_NORMAL_ARRAY));
// Render using the Vertex Buffer Objects.
if (this->triangle_indices_size > 0) {
glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
this->triangle_indices_VBO_id));
glsafe(::glDrawElements(GL_TRIANGLES,
GLsizei(this->triangle_indices_size),
GL_UNSIGNED_INT, nullptr));
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
}
if (this->quad_indices_size > 0) {
glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
this->quad_indices_VBO_id));
glsafe(::glDrawElements(GL_QUADS, GLsizei(this->quad_indices_size),
GL_UNSIGNED_INT, nullptr));
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
}
glsafe(::glDisableClientState(GL_VERTEX_ARRAY));
glsafe(::glDisableClientState(GL_NORMAL_ARRAY));
glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0));
}
void IndexedVertexArray::clear() {
this->vertices_and_normals_interleaved.clear();
this->triangle_indices.clear();
this->quad_indices.clear();
vertices_and_normals_interleaved_size = 0;
triangle_indices_size = 0;
quad_indices_size = 0;
}
void IndexedVertexArray::shrink_to_fit() {
this->vertices_and_normals_interleaved.shrink_to_fit();
this->triangle_indices.shrink_to_fit();
this->quad_indices.shrink_to_fit();
}
void Volume::render()
{
glsafe(::glPushMatrix());
glsafe(::glMultMatrixd(m_trafo.get_matrix().data()));
m_geom.render();
glsafe(::glPopMatrix());
}
void Display::clear_screen()
{
glViewport(0, 0, GLsizei(m_size.x()), GLsizei(m_size.y()));
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
}
Display::~Display()
{
OpenCSG::freeResources();
}
void Display::set_active(long width, long height)
{
if (!m_initialized) {
glewInit();
m_initialized = true;
}
// gray background
glClearColor(0.9f, 0.9f, 0.9f, 1.0f);
// Enable two OpenGL lights
GLfloat light_diffuse[] = { 1.0f, 1.0f, 0.0f, 1.0f}; // White diffuse light
GLfloat light_position0[] = {-1.0f, -1.0f, -1.0f, 0.0f}; // Infinite light location
GLfloat light_position1[] = { 1.0f, 1.0f, 1.0f, 0.0f}; // Infinite light location
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
glLightfv(GL_LIGHT0, GL_POSITION, light_position0);
glEnable(GL_LIGHT0);
glLightfv(GL_LIGHT1, GL_DIFFUSE, light_diffuse);
glLightfv(GL_LIGHT1, GL_POSITION, light_position1);
glEnable(GL_LIGHT1);
glEnable(GL_LIGHTING);
glEnable(GL_NORMALIZE);
// Use depth buffering for hidden surface elimination
glEnable(GL_DEPTH_TEST);
glEnable(GL_STENCIL_TEST);
set_screen_size(width, height);
}
void Display::set_screen_size(long width, long height)
{
if (m_size.x() != width || m_size.y() != height)
m_camera->set_screen(width, height);
m_size = {width, height};
}
void Display::repaint()
{
clear_screen();
m_camera->view();
render_scene();
m_fps_counter.update();
swap_buffers();
}
void Controller::on_scene_updated(const Scene &scene)
{
const SLAPrint *print = scene.get_print();
if (!print) return;
auto bb = scene.get_bounding_box();
double d = std::max(std::max(bb.size().x(), bb.size().y()), bb.size().z());
m_wheel_pos = long(2 * d);
call_cameras(&Camera::set_zoom, m_wheel_pos);
call(&Display::on_scene_updated, m_displays, scene);
}
void Controller::on_scroll(long v, long d, MouseInput::WheelAxis /*wa*/)
{
m_wheel_pos += v / d;
call_cameras(&Camera::set_zoom, m_wheel_pos);
call(&Display::repaint, m_displays);
}
void Controller::on_moved_to(long x, long y)
{
if (m_left_btn) {
call_cameras(&Camera::rotate, (Vec2i32{x, y} - m_mouse_pos).cast<float>());
call(&Display::repaint, m_displays);
}
m_mouse_pos = {x, y};
}
void CSGDisplay::apply_csgsettings(const CSGSettings &settings)
{
using namespace OpenCSG;
bool needupdate = m_csgsettings.get_convexity() != settings.get_convexity();
m_csgsettings = settings;
setOption(AlgorithmSetting, m_csgsettings.get_algo());
setOption(DepthComplexitySetting, m_csgsettings.get_depth_algo());
setOption(DepthBoundsOptimization, m_csgsettings.get_optimization());
if (needupdate) {
for (OpenCSG::Primitive * p : m_scene_cache.primitives_csg)
if (p->getConvexity() > 1)
p->setConvexity(m_csgsettings.get_convexity());
}
}
void CSGDisplay::on_scene_updated(const Scene &scene)
{
const SLAPrint *print = scene.get_print();
if (!print) return;
m_scene_cache.clear();
for (const SLAPrintObject *po : print->objects()) {
const ModelObject *mo = po->model_object();
TriangleMesh msh = mo->raw_mesh();
sla::DrainHoles holedata = mo->sla_drain_holes;
for (const ModelInstance *mi : mo->instances) {
TriangleMesh mshinst = msh;
auto interior = po->hollowed_interior_mesh();
interior.transform(po->trafo().inverse());
mshinst.merge(interior);
mi->transform_mesh(&mshinst);
auto bb = mshinst.bounding_box();
auto center = bb.center().cast<float>();
mshinst.translate(-center);
m_scene_cache.add_mesh(mshinst, OpenCSG::Intersection,
m_csgsettings.get_convexity());
}
for (const sla::DrainHole &holept : holedata) {
TriangleMesh holemesh = sla::to_triangle_mesh(holept.to_mesh());
m_scene_cache.add_mesh(holemesh, OpenCSG::Subtraction, 1);
}
}
repaint();
}
void Camera::view()
{
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0.0, m_zoom, 0.0, /* eye is at (0,zoom,0) */
m_referene.x(), m_referene.y(), m_referene.z(),
0.0, 0.0, 1.0); /* up is in positive Y direction */
// TODO Could have been set in prevoius gluLookAt in first argument
glRotatef(m_rot.y(), 1.0, 0.0, 0.0);
glRotatef(m_rot.x(), 0.0, 0.0, 1.0);
if (m_clip_z > 0.) {
GLdouble plane[] = {0., 0., 1., m_clip_z};
glClipPlane(GL_CLIP_PLANE0, plane);
glEnable(GL_CLIP_PLANE0);
} else {
glDisable(GL_CLIP_PLANE0);
}
}
void PerspectiveCamera::set_screen(long width, long height)
{
// Setup the view of the CSG shape
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, width / double(height), .1, 200.0);
glMatrixMode(GL_MODELVIEW);
}
bool enable_multisampling(bool e)
{
if (!e) { glDisable(GL_MULTISAMPLE); return false; }
GLint is_ms_context;
glGetIntegerv(GL_SAMPLE_BUFFERS, &is_ms_context);
if (is_ms_context) { glEnable(GL_MULTISAMPLE); return true; }
else return false;
}
MouseInput::Listener::~Listener() = default;
void FpsCounter::update()
{
++m_frames;
TimePoint msec = Clock::now();
double seconds_window = to_sec(msec - m_window);
m_fps = 0.5 * m_fps + 0.5 * (m_frames / seconds_window);
if (to_sec(msec - m_last) >= m_resolution) {
m_last = msec;
for (auto &l : m_listeners) l(m_fps);
}
if (seconds_window >= m_window_size) {
m_frames = 0;
m_window = msec;
}
}
}} // namespace Slic3r::GL
-488
View File
@@ -1,488 +0,0 @@
#ifndef SLIC3R_OCSG_EXMP_ENGINE_HPP
#define SLIC3R_OCSG_EXMP_ENGINE_HPP
#include <vector>
#include <memory>
#include <chrono>
#include <libslic3r/Geometry.hpp>
#include <libslic3r/Model.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/SLA/Hollowing.hpp>
#include <opencsg/opencsg.h>
namespace Slic3r {
class SLAPrint;
namespace GL {
template<class T, class A = std::allocator<T>> using vector = std::vector<T, A>;
// remove empty weak pointers from a vector
template<class L> inline void cleanup(vector<std::weak_ptr<L>> &listeners) {
auto it = std::remove_if(listeners.begin(), listeners.end(),
[](auto &l) { return !l.lock(); });
listeners.erase(it, listeners.end());
}
// Call a class method on each element of a vector of objects (weak pointers)
// of the same type.
template<class F, class L, class...Args>
inline void call(F &&f, vector<std::weak_ptr<L>> &listeners, Args&&... args) {
for (auto &l : listeners)
if (auto p = l.lock()) ((p.get())->*f)(std::forward<Args>(args)...);
}
// A representation of a mouse input for the engine.
class MouseInput
{
public:
enum WheelAxis { waVertical, waHorizontal };
// Interface to implement if an object wants to receive notifications
// about mouse events.
class Listener {
public:
virtual ~Listener();
virtual void on_left_click_down() {}
virtual void on_left_click_up() {}
virtual void on_right_click_down() {}
virtual void on_right_click_up() {}
virtual void on_double_click() {}
virtual void on_scroll(long /*v*/, long /*delta*/, WheelAxis ) {}
virtual void on_moved_to(long /*x*/, long /*y*/) {}
};
private:
vector<std::weak_ptr<Listener>> m_listeners;
public:
virtual ~MouseInput() = default;
virtual void left_click_down()
{
call(&Listener::on_left_click_down, m_listeners);
}
virtual void left_click_up()
{
call(&Listener::on_left_click_up, m_listeners);
}
virtual void right_click_down()
{
call(&Listener::on_right_click_down, m_listeners);
}
virtual void right_click_up()
{
call(&Listener::on_right_click_up, m_listeners);
}
virtual void double_click()
{
call(&Listener::on_double_click, m_listeners);
}
virtual void scroll(long v, long d, WheelAxis wa)
{
call(&Listener::on_scroll, m_listeners, v, d, wa);
}
virtual void move_to(long x, long y)
{
call(&Listener::on_moved_to, m_listeners, x, y);
}
void add_listener(std::shared_ptr<Listener> listener)
{
m_listeners.emplace_back(listener);
cleanup(m_listeners);
}
};
// This is a stripped down version of Slic3r::IndexedVertexArray
class IndexedVertexArray {
public:
~IndexedVertexArray() { release_geometry(); }
// Vertices and their normals, interleaved to be used by void
// glInterleavedArrays(GL_N3F_V3F, 0, x)
vector<float> vertices_and_normals_interleaved;
vector<int> triangle_indices;
vector<int> quad_indices;
// When the geometry data is loaded into the graphics card as Vertex
// Buffer Objects, the above mentioned std::vectors are cleared and the
// following variables keep their original length.
size_t vertices_and_normals_interleaved_size{ 0 };
size_t triangle_indices_size{ 0 };
size_t quad_indices_size{ 0 };
// IDs of the Vertex Array Objects, into which the geometry has been loaded.
// Zero if the VBOs are not sent to GPU yet.
unsigned int vertices_and_normals_interleaved_VBO_id{ 0 };
unsigned int triangle_indices_VBO_id{ 0 };
unsigned int quad_indices_VBO_id{ 0 };
void push_geometry(float x, float y, float z, float nx, float ny, float nz);
inline void push_geometry(
double x, double y, double z, double nx, double ny, double nz)
{
push_geometry(float(x), float(y), float(z), float(nx), float(ny), float(nz));
}
inline void push_geometry(const Vec3d &p, const Vec3d &n)
{
push_geometry(p(0), p(1), p(2), n(0), n(1), n(2));
}
void push_triangle(int idx1, int idx2, int idx3);
void load_mesh(const TriangleMesh &mesh);
inline bool has_VBOs() const
{
return vertices_and_normals_interleaved_VBO_id != 0;
}
// Finalize the initialization of the geometry & indices,
// upload the geometry and indices to OpenGL VBO objects
// and shrink the allocated data, possibly relasing it if it has been
// loaded into the VBOs.
void finalize_geometry();
// Release the geometry data, release OpenGL VBOs.
void release_geometry();
void render() const;
// Is there any geometry data stored?
bool empty() const { return vertices_and_normals_interleaved_size == 0; }
void clear();
// Shrink the internal storage to tighly fit the data stored.
void shrink_to_fit();
};
// Try to enable or disable multisampling.
bool enable_multisampling(bool e = true);
class Volume {
IndexedVertexArray m_geom;
Geometry::Transformation m_trafo;
public:
void render();
void translation(const Vec3d &offset) { m_trafo.set_offset(offset); }
void rotation(const Vec3d &rot) { m_trafo.set_rotation(rot); }
void scale(const Vec3d &scaleing) { m_trafo.set_scaling_factor(scaleing); }
void scale(double s) { scale({s, s, s}); }
inline void load_mesh(const TriangleMesh &mesh)
{
m_geom.load_mesh(mesh);
m_geom.finalize_geometry();
}
};
// A primitive that can be used with OpenCSG rendering algorithms.
// Does a similar job to GLVolume.
class Primitive : public Volume, public OpenCSG::Primitive
{
public:
using OpenCSG::Primitive::Primitive;
Primitive() : OpenCSG::Primitive(OpenCSG::Intersection, 1) {}
void render() override { Volume::render(); }
};
// A simple representation of a camera in a 3D scene
class Camera {
protected:
Vec2f m_rot = {0., 0.};
Vec3d m_referene = {0., 0., 0.};
double m_zoom = 0.;
double m_clip_z = 0.;
public:
virtual ~Camera() = default;
virtual void view();
virtual void set_screen(long width, long height) = 0;
void set_rotation(const Vec2f &rotation) { m_rot = rotation; }
void rotate(const Vec2f &rotation) { m_rot += rotation; }
void set_zoom(double z) { m_zoom = z; }
void set_reference_point(const Vec3d &p) { m_referene = p; }
void set_clip_z(double z) { m_clip_z = z; }
};
// Reset a camera object
inline void reset(Camera &cam)
{
cam.set_rotation({0., 0.});
cam.set_zoom(0.);
cam.set_reference_point({0., 0., 0.});
cam.set_clip_z(0.);
}
// Specialization of a camera which shows in perspective projection
class PerspectiveCamera: public Camera {
public:
void set_screen(long width, long height) override;
};
// A simple counter of FPS. Subscribed objects will receive updates of the
// current fps.
class FpsCounter {
vector<std::function<void(double)>> m_listeners;
using Clock = std::chrono::high_resolution_clock;
using Duration = Clock::duration;
using TimePoint = Clock::time_point;
int m_frames = 0;
TimePoint m_last = Clock::now(), m_window = m_last;
double m_resolution = 0.1, m_window_size = 1.0;
double m_fps = 0.;
static double to_sec(Duration d)
{
return d.count() * double(Duration::period::num) / Duration::period::den;
}
public:
void update();
void add_listener(std::function<void(double)> lst)
{
m_listeners.emplace_back(lst);
}
void clear_listeners() { m_listeners = {}; }
void set_notification_interval(double seconds);
void set_measure_window_size(double seconds);
double get_notification_interval() const { return m_resolution; }
double get_mesure_window_size() const { return m_window_size; }
};
// Collection of the used OpenCSG library settings.
class CSGSettings {
public:
static const constexpr unsigned DEFAULT_CONVEXITY = 10;
private:
OpenCSG::Algorithm m_csgalg = OpenCSG::Algorithm::Automatic;
OpenCSG::DepthComplexityAlgorithm m_depth_algo = OpenCSG::NoDepthComplexitySampling;
OpenCSG::Optimization m_optim = OpenCSG::OptimizationDefault;
bool m_enable = true;
unsigned int m_convexity = DEFAULT_CONVEXITY;
public:
int get_algo() const { return int(m_csgalg); }
void set_algo(int alg)
{
if (alg < OpenCSG::Algorithm::AlgorithmUnused)
m_csgalg = OpenCSG::Algorithm(alg);
}
int get_depth_algo() const { return int(m_depth_algo); }
void set_depth_algo(int alg)
{
if (alg < OpenCSG::DepthComplexityAlgorithmUnused)
m_depth_algo = OpenCSG::DepthComplexityAlgorithm(alg);
}
int get_optimization() const { return int(m_optim); }
void set_optimization(int o)
{
if (o < OpenCSG::Optimization::OptimizationUnused)
m_optim = OpenCSG::Optimization(o);
}
void enable_csg(bool en = true) { m_enable = en; }
bool is_enabled() const { return m_enable; }
unsigned get_convexity() const { return m_convexity; }
void set_convexity(unsigned c) { m_convexity = c; }
};
// The scene is a wrapper around SLAPrint which holds the data to be visualized.
class Scene
{
std::unique_ptr<SLAPrint> m_print;
public:
// Subscribers will be notified if the model is changed. This might be a
// display which will have to load the meshes and repaint itself when
// the scene data changes.
// eg. We load a new 3mf through the UI, this will notify the controller
// associated with the scene and all the displays that the controller is
// connected with.
class Listener {
public:
virtual ~Listener() = default;
virtual void on_scene_updated(const Scene &scene) = 0;
};
Scene();
~Scene();
void set_print(std::unique_ptr<SLAPrint> &&print);
const SLAPrint * get_print() const { return m_print.get(); }
BoundingBoxf3 get_bounding_box() const;
void add_listener(std::shared_ptr<Listener> listener)
{
m_listeners.emplace_back(listener);
cleanup(m_listeners);
}
private:
vector<std::weak_ptr<Listener>> m_listeners;
};
// The basic Display. This is almost just an interface but will do all the
// initialization and show the fps values. Overriding the render_scene is
// needed to show the scene content. The specific method of displaying the
// scene is up the particular implementation (OpenCSG or other screen space
// boolean algorithms)
class Display : public Scene::Listener
{
protected:
Vec2i32 m_size;
bool m_initialized = false;
std::shared_ptr<Camera> m_camera;
FpsCounter m_fps_counter;
public:
explicit Display(std::shared_ptr<Camera> camera = nullptr)
: m_camera(camera ? camera : std::make_shared<PerspectiveCamera>())
{}
~Display() override;
std::shared_ptr<const Camera> get_camera() const { return m_camera; }
std::shared_ptr<Camera> get_camera() { return m_camera; }
void set_camera(std::shared_ptr<Camera> cam) { m_camera = cam; }
virtual void swap_buffers() = 0;
virtual void set_active(long width, long height);
virtual void set_screen_size(long width, long height);
Vec2i32 get_screen_size() const { return m_size; }
virtual void repaint();
bool is_initialized() const { return m_initialized; }
virtual void clear_screen();
virtual void render_scene() {}
template<class _FpsCounter> void set_fps_counter(_FpsCounter &&fpsc)
{
m_fps_counter = std::forward<_FpsCounter>(fpsc);
}
const FpsCounter &get_fps_counter() const { return m_fps_counter; }
FpsCounter &get_fps_counter() { return m_fps_counter; }
};
// Special dispaly using OpenCSG for rendering the scene.
class CSGDisplay : public Display {
protected:
CSGSettings m_csgsettings;
// Cache the renderable primitives. These will be fetched when the scene
// is modified.
struct SceneCache {
vector<std::shared_ptr<Primitive>> primitives;
vector<Primitive *> primitives_free;
vector<OpenCSG::Primitive *> primitives_csg;
void clear();
std::shared_ptr<Primitive> add_mesh(const TriangleMesh &mesh);
std::shared_ptr<Primitive> add_mesh(const TriangleMesh &mesh,
OpenCSG::Operation op,
unsigned covexity);
} m_scene_cache;
public:
// Receive or apply the new settings.
const CSGSettings & get_csgsettings() const { return m_csgsettings; }
void apply_csgsettings(const CSGSettings &settings);
void render_scene() override;
void on_scene_updated(const Scene &scene) override;
};
// The controller is a hub which dispatches mouse events to the connected
// displays. It keeps track of the mouse wheel position, the states whether
// the mouse is being held, dragged, etc... All the connected displays will
// mirror the camera movement (if there is more than one display).
class Controller : public std::enable_shared_from_this<Controller>,
public MouseInput::Listener,
public Scene::Listener
{
long m_wheel_pos = 0;
Vec2i32 m_mouse_pos, m_mouse_pos_rprev, m_mouse_pos_lprev;
bool m_left_btn = false, m_right_btn = false;
std::shared_ptr<Scene> m_scene;
vector<std::weak_ptr<Display>> m_displays;
// Call a method of Camera on all the cameras of the attached displays
template<class F, class...Args>
void call_cameras(F &&f, Args&&... args) {
for (std::weak_ptr<Display> &l : m_displays)
if (auto disp = l.lock()) if (auto cam = disp->get_camera())
(cam.get()->*f)(std::forward<Args>(args)...);
}
public:
// Set the scene that will be controlled.
void set_scene(std::shared_ptr<Scene> scene)
{
m_scene = scene;
m_scene->add_listener(shared_from_this());
}
const Scene * get_scene() const { return m_scene.get(); }
void add_display(std::shared_ptr<Display> disp)
{
m_displays.emplace_back(disp);
cleanup(m_displays);
}
void remove_displays() { m_displays = {}; }
void on_scene_updated(const Scene &scene) override;
void on_left_click_down() override { m_left_btn = true; }
void on_left_click_up() override { m_left_btn = false; }
void on_right_click_down() override { m_right_btn = true; }
void on_right_click_up() override { m_right_btn = false; }
void on_scroll(long v, long d, MouseInput::WheelAxis wa) override;
void on_moved_to(long x, long y) override;
void move_clip_plane(double z) { call_cameras(&Camera::set_clip_z, z); }
};
}} // namespace Slic3r::GL
#endif // SLIC3R_OCSG_EXMP_ENGINE_HPP
-63
View File
@@ -1,63 +0,0 @@
#include "ShaderCSGDisplay.hpp"
#include "libslic3r/SLAPrint.hpp"
#include <GL/glew.h>
namespace Slic3r { namespace GL {
void ShaderCSGDisplay::add_mesh(const TriangleMesh &mesh)
{
auto v = std::make_shared<CSGVolume>();
v->load_mesh(mesh);
m_volumes.emplace_back(v);
}
void ShaderCSGDisplay::render_scene()
{
GLfloat color[] = {1.f, 1.f, 0.f, 0.f};
glColor4fv(color);
glDepthFunc(GL_LESS);
for (auto &v : m_volumes) v->render();
glFlush();
}
void ShaderCSGDisplay::on_scene_updated(const Scene &scene)
{
// TriangleMesh mesh = print->objects().front()->hollowed_interior_mesh();
// Look at CSGDisplay::on_scene_updated to see how its done there.
const SLAPrint *print = scene.get_print();
if (!print) return;
m_volumes.clear();
for (const SLAPrintObject *po : print->objects()) {
const ModelObject *mo = po->model_object();
TriangleMesh msh = mo->raw_mesh();
sla::DrainHoles holedata = mo->sla_drain_holes;
for (const ModelInstance *mi : mo->instances) {
TriangleMesh mshinst = msh;
auto interior = po->hollowed_interior_mesh();
interior.transform(po->trafo().inverse());
mshinst.merge(interior);
mi->transform_mesh(&mshinst);
auto bb = mshinst.bounding_box();
auto center = bb.center().cast<float>();
mshinst.translate(-center);
add_mesh(mshinst);
}
for (const sla::DrainHole &holept : holedata)
add_mesh(sla::to_triangle_mesh(holept.to_mesh()));
}
repaint();
}
}} // namespace Slic3r::GL
-27
View File
@@ -1,27 +0,0 @@
#ifndef SHADERCSGDISPLAY_HPP
#define SHADERCSGDISPLAY_HPP
#include "Engine.hpp"
namespace Slic3r { namespace GL {
class CSGVolume: public Volume
{
// Extend...
};
class ShaderCSGDisplay: public Display {
protected:
vector<std::shared_ptr<CSGVolume>> m_volumes;
void add_mesh(const TriangleMesh &mesh);
public:
void render_scene() override;
void on_scene_updated(const Scene &scene) override;
};
}}
#endif // SHADERCSGDISPLAY_HPP
-734
View File
@@ -1,734 +0,0 @@
#include <iostream>
#include <utility>
#include <memory>
#include "Engine.hpp"
#include "ShaderCSGDisplay.hpp"
#include <GL/glew.h>
#include <opencsg/opencsg.h>
// For compilers that support precompilation, includes "wx/wx.h".
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
#include <wx/slider.h>
#include <wx/tglbtn.h>
#include <wx/combobox.h>
#include <wx/spinctrl.h>
#include <wx/msgdlg.h>
#include <wx/glcanvas.h>
#include <wx/cmdline.h>
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/SLAPrint.hpp"
#include "slic3r/GUI/Jobs/Job.hpp"
#include "slic3r/GUI/ProgressStatusBar.hpp"
using namespace Slic3r::GL;
class Renderer {
protected:
wxGLCanvas *m_canvas;
std::shared_ptr<wxGLContext> m_context;
public:
Renderer(wxGLCanvas *c): m_canvas{c} {
auto ctx = new wxGLContext(m_canvas);
if (!ctx || !ctx->IsOK()) {
wxMessageBox("Could not create OpenGL context.", "Error",
wxOK | wxICON_ERROR);
return;
}
m_context.reset(ctx);
}
wxGLContext * context() { return m_context.get(); }
const wxGLContext * context() const { return m_context.get(); }
};
// Tell the CSGDisplay how to swap buffers and set the gl context.
class OCSGRenderer: public Renderer, public Slic3r::GL::CSGDisplay {
public:
OCSGRenderer(wxGLCanvas *c): Renderer{c} {}
void set_active(long w, long h) override
{
m_canvas->SetCurrent(*m_context);
Slic3r::GL::Display::set_active(w, h);
}
void swap_buffers() override { m_canvas->SwapBuffers(); }
};
// Tell the CSGDisplay how to swap buffers and set the gl context.
class ShaderCSGRenderer : public Renderer, public Slic3r::GL::ShaderCSGDisplay {
public:
ShaderCSGRenderer(wxGLCanvas *c): Renderer{c} {}
void set_active(long w, long h) override
{
m_canvas->SetCurrent(*m_context);
Slic3r::GL::Display::set_active(w, h);
}
void swap_buffers() override { m_canvas->SwapBuffers(); }
};
// The opengl rendering facility. Here we implement the rendering objects.
class Canvas: public wxGLCanvas
{
// One display is active at a time, the OCSGRenderer by default.
std::shared_ptr<Slic3r::GL::Display> m_display;
public:
template<class...Args>
Canvas(Args &&...args): wxGLCanvas(std::forward<Args>(args)...) {}
std::shared_ptr<Slic3r::GL::Display> get_display() const { return m_display; }
void set_display(std::shared_ptr<Slic3r::GL::Display> d) { m_display = d; }
};
// Enumerate possible mouse events, we will record them.
enum EEvents { LCLK_U, RCLK_U, LCLK_D, RCLK_D, DDCLK, SCRL, MV };
struct Event
{
EEvents type;
long a, b;
Event(EEvents t, long x = 0, long y = 0) : type{t}, a{x}, b{y} {}
};
// Create a special mouse input adapter, which can store (record) the received
// mouse signals into a file and play back the stored events later.
class RecorderMouseInput: public MouseInput {
std::vector<Event> m_events;
bool m_recording = false, m_playing = false;
public:
void left_click_down() override
{
if (m_recording) m_events.emplace_back(LCLK_D);
if (!m_playing) MouseInput::left_click_down();
}
void left_click_up() override
{
if (m_recording) m_events.emplace_back(LCLK_U);
if (!m_playing) MouseInput::left_click_up();
}
void right_click_down() override
{
if (m_recording) m_events.emplace_back(RCLK_D);
if (!m_playing) MouseInput::right_click_down();
}
void right_click_up() override
{
if (m_recording) m_events.emplace_back(RCLK_U);
if (!m_playing) MouseInput::right_click_up();
}
void double_click() override
{
if (m_recording) m_events.emplace_back(DDCLK);
if (!m_playing) MouseInput::double_click();
}
void scroll(long v, long d, WheelAxis wa) override
{
if (m_recording) m_events.emplace_back(SCRL, v, d);
if (!m_playing) MouseInput::scroll(v, d, wa);
}
void move_to(long x, long y) override
{
if (m_recording) m_events.emplace_back(MV, x, y);
if (!m_playing) MouseInput::move_to(x, y);
}
void save(std::ostream &stream)
{
for (const Event &evt : m_events)
stream << evt.type << " " << evt.a << " " << evt.b << std::endl;
}
void load(std::istream &stream)
{
m_events.clear();
while (stream.good()) {
int type; long a, b;
stream >> type >> a >> b;
m_events.emplace_back(EEvents(type), a, b);
}
}
void record(bool r) { m_recording = r; if (r) m_events.clear(); }
void play()
{
m_playing = true;
for (const Event &evt : m_events) {
switch (evt.type) {
case LCLK_U: MouseInput::left_click_up(); break;
case LCLK_D: MouseInput::left_click_down(); break;
case RCLK_U: MouseInput::right_click_up(); break;
case RCLK_D: MouseInput::right_click_down(); break;
case DDCLK: MouseInput::double_click(); break;
case SCRL: MouseInput::scroll(evt.a, evt.b, WheelAxis::waVertical); break;
case MV: MouseInput::move_to(evt.a, evt.b); break;
}
wxTheApp->Yield();
if (!m_playing)
break;
}
m_playing = false;
}
void stop() { m_playing = false; }
bool is_playing() const { return m_playing; }
};
// The top level frame of the application.
class MyFrame: public wxFrame
{
// Instantiate the 3D engine.
std::shared_ptr<Scene> m_scene; // Model
std::shared_ptr<Canvas> m_canvas; // Views store
std::shared_ptr<OCSGRenderer> m_ocsgdisplay; // View
std::shared_ptr<ShaderCSGRenderer> m_shadercsg_display; // Another view
std::shared_ptr<Controller> m_ctl; // Controller
// Add a status bar with progress indication.
std::shared_ptr<Slic3r::GUI::ProgressStatusBar> m_stbar;
RecorderMouseInput m_mouse;
// When loading a Model from 3mf and preparing it, we use a separate thread.
class SLAJob: public Slic3r::GUI::Job {
MyFrame *m_parent;
std::unique_ptr<Slic3r::SLAPrint> m_print;
std::string m_fname;
public:
SLAJob(MyFrame *frame, const std::string &fname)
: Slic3r::GUI::Job{frame->m_stbar}
, m_parent{frame}
, m_fname{fname}
{}
// Runs in separate thread
void process() override;
const std::string & get_project_fname() const { return m_fname; }
protected:
// Runs in the UI thread.
void finalize() override
{
m_parent->m_scene->set_print(std::move(m_print));
m_parent->m_stbar->set_status_text(
wxString::Format("Model %s loaded.", m_fname));
}
};
std::unique_ptr<SLAJob> m_ui_job;
// To keep track of the running average of measured fps values.
double m_fps_avg = 0.;
// We need the record button across methods
wxToggleButton *m_record_btn;
wxComboBox * m_alg_select;
wxComboBox * m_depth_select;
wxComboBox * m_optimization_select;
wxSpinCtrl * m_convexity_spin;
wxToggleButton *m_csg_toggle;
wxToggleButton *m_ms_toggle;
wxStaticText *m_fpstext;
CSGSettings m_csg_settings;
void read_csg_settings(const wxCmdLineParser &parser);
void set_renderer_algorithm(const wxString &alg);
void activate_canvas_display();
public:
MyFrame(const wxString & title,
const wxPoint & pos,
const wxSize & size,
const wxCmdLineParser &parser);
// Grab a 3mf and load (hollow it out) within the UI job.
void load_model(const std::string &fname) {
m_ui_job = std::make_unique<SLAJob>(this, fname);
m_ui_job->start();
}
// Load a previously stored mouse event log and play it back.
void play_back_mouse(const std::string &events_fname)
{
std::fstream stream(events_fname, std::fstream::in);
if (stream.good()) {
std::string model_name;
std::getline(stream, model_name);
load_model(model_name);
while (!m_ui_job->is_finalized())
wxTheApp->Yield();;
int w, h;
stream >> w >> h;
SetSize(w, h);
m_mouse.load(stream);
if (m_record_btn) m_record_btn->Disable();
m_mouse.play();
}
}
Canvas * canvas() { return m_canvas.get(); }
const Canvas * canvas() const { return m_canvas.get(); }
// Bind the canvas mouse events to a class implementing MouseInput interface
void bind_canvas_events(MouseInput &msinput);
double get_fps_average() const { return m_fps_avg; }
};
// Possible OpenCSG configuration values. Will be used on the command line and
// on the UI widgets.
static const std::vector<wxString> CSG_ALGS = {"Auto", "Goldfeather", "SCS", "EnricoShader"};
static const std::vector<wxString> CSG_DEPTH = {"Off", "OcclusionQuery", "On"};
static const std::vector<wxString> CSG_OPT = { "Default", "ForceOn", "On", "Off" };
inline long get_idx(const wxString &a, const std::vector<wxString> &v)
{
auto it = std::find(v.begin(), v.end(), a.ToStdString());
return it - v.begin();
};
class App : public wxApp {
MyFrame *m_frame = nullptr;
wxString m_fname;
public:
bool OnInit() override {
wxCmdLineParser parser(argc, argv);
parser.AddOption("p", "play", "play back file", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL);
parser.AddOption("a", "algorithm", "OpenCSG algorithm [Auto|Goldfeather|SCS]", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL);
parser.AddOption("d", "depth", "OpenCSG depth strategy [Off|OcclusionQuery|On]", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL);
parser.AddOption("o", "optimization", "OpenCSG optimization strategy [Default|ForceOn|On|Off]", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL);
parser.AddOption("c", "convexity", "OpenCSG convexity parameter for generic meshes", wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL);
parser.AddSwitch("", "disable-csg", "Disable csg rendering", wxCMD_LINE_PARAM_OPTIONAL);
parser.Parse();
bool is_play = parser.Found("play", &m_fname);
m_frame = new MyFrame("OrcaSlicer OpenCSG Demo", wxDefaultPosition, wxSize(1024, 768), parser);
if (is_play) {
Bind(wxEVT_IDLE, &App::Play, this);
m_frame->Show( true );
} else m_frame->Show( true );
return true;
}
void Play(wxIdleEvent &) {
Unbind(wxEVT_IDLE, &App::Play, this);
m_frame->play_back_mouse(m_fname.ToStdString());
m_frame->Destroy();
}
};
wxIMPLEMENT_APP(App);
void MyFrame::read_csg_settings(const wxCmdLineParser &parser)
{
wxString alg;
parser.Found("algorithm", &alg);
wxString depth;
parser.Found("depth", &depth);
wxString opt;
parser.Found("optimization", &opt);
long convexity = 1;
parser.Found("convexity", &convexity);
bool csg_off = parser.Found("disable-csg");
if (auto a = get_idx(alg, CSG_ALGS) < OpenCSG::AlgorithmUnused)
m_csg_settings.set_algo(OpenCSG::Algorithm(a));
if (auto a = get_idx(depth, CSG_DEPTH) < OpenCSG::DepthComplexityAlgorithmUnused)
m_csg_settings.set_depth_algo(OpenCSG::DepthComplexityAlgorithm(a));
if (auto a = get_idx(opt, CSG_OPT) < OpenCSG::OptimizationUnused)
m_csg_settings.set_optimization(OpenCSG::Optimization(a));
m_csg_settings.set_convexity(unsigned(convexity));
m_csg_settings.enable_csg(!csg_off);
if (m_ocsgdisplay) m_ocsgdisplay->apply_csgsettings(m_csg_settings);
}
void MyFrame::set_renderer_algorithm(const wxString &alg)
{
long alg_idx = get_idx(alg, CSG_ALGS);
if (alg_idx < 0 || alg_idx >= long(CSG_ALGS.size())) return;
// If there is a valid display in place, save its camera.
auto cam = m_canvas->get_display() ?
m_canvas->get_display()->get_camera() : nullptr;
if (alg == "EnricoShader") {
m_alg_select->SetSelection(int(alg_idx));
m_depth_select->Disable();
m_optimization_select->Disable();
m_csg_toggle->Disable();
m_ocsgdisplay.reset();
canvas()->set_display(nullptr);
m_shadercsg_display = std::make_shared<ShaderCSGRenderer>(canvas());
canvas()->set_display(m_shadercsg_display);
} else {
if (m_csg_settings.get_algo() > 0) m_depth_select->Enable(true);
m_alg_select->SetSelection(m_csg_settings.get_algo());
m_depth_select->SetSelection(m_csg_settings.get_depth_algo());
m_optimization_select->SetSelection(m_csg_settings.get_optimization());
m_convexity_spin->SetValue(int(m_csg_settings.get_convexity()));
m_csg_toggle->SetValue(m_csg_settings.is_enabled());
m_optimization_select->Enable();
m_csg_toggle->Enable();
m_shadercsg_display.reset();
canvas()->set_display(nullptr);
m_ocsgdisplay = std::make_shared<OCSGRenderer>(canvas());
m_ocsgdisplay->apply_csgsettings(m_csg_settings);
canvas()->set_display(m_ocsgdisplay);
}
if (cam)
m_canvas->get_display()->set_camera(cam);
m_ctl->remove_displays();
m_ctl->add_display(m_canvas->get_display());
m_canvas->get_display()->get_fps_counter().add_listener([this](double fps) {
m_fpstext->SetLabel(wxString::Format("fps: %.2f", fps));
m_fps_avg = 0.9 * m_fps_avg + 0.1 * fps;
});
if (IsShown()) {
activate_canvas_display();
m_canvas->get_display()->on_scene_updated(*m_scene);
}
}
void MyFrame::activate_canvas_display()
{
const wxSize ClientSize = m_canvas->GetClientSize();
m_canvas->get_display()->set_active(ClientSize.x, ClientSize.y);
enable_multisampling(m_ms_toggle->GetValue());
m_canvas->Bind(wxEVT_PAINT, [this](wxPaintEvent &) {
// This is required even though dc is not used otherwise.
wxPaintDC dc(m_canvas.get());
const wxSize csize = m_canvas->GetClientSize();
m_canvas->get_display()->set_screen_size(csize.x, csize.y);
m_canvas->get_display()->repaint();
});
m_canvas->Bind(wxEVT_SIZE, [this](wxSizeEvent &) {
const wxSize csize = m_canvas->GetClientSize();
m_canvas->get_display()->set_screen_size(csize.x, csize.y);
m_canvas->get_display()->repaint();
});
// Do the repaint continuously
m_canvas->Bind(wxEVT_IDLE, [this](wxIdleEvent &evt) {
m_canvas->get_display()->repaint();
evt.RequestMore();
});
bind_canvas_events(m_mouse);
}
MyFrame::MyFrame(const wxString &title, const wxPoint &pos, const wxSize &size,
const wxCmdLineParser &parser):
wxFrame(nullptr, wxID_ANY, title, pos, size)
{
wxMenu *menuFile = new wxMenu;
menuFile->Append(wxID_OPEN);
menuFile->Append(wxID_EXIT);
wxMenuBar *menuBar = new wxMenuBar;
menuBar->Append( menuFile, "&File" );
SetMenuBar( menuBar );
m_stbar = std::make_shared<Slic3r::GUI::ProgressStatusBar>(this);
m_stbar->embed(this);
SetStatusText( "Welcome to wxWidgets!" );
int attribList[] =
{WX_GL_RGBA, WX_GL_DOUBLEBUFFER,
// RGB channels each should be allocated with 8 bit depth. One
// should almost certainly get these bit depths by default.
WX_GL_MIN_RED, 8, WX_GL_MIN_GREEN, 8, WX_GL_MIN_BLUE, 8,
// Requesting an 8 bit alpha channel. Interestingly, the NVIDIA
// drivers would most likely work with some alpha plane, but
// glReadPixels would not return the alpha channel on NVIDIA if
// not requested when the GL context is created.
WX_GL_MIN_ALPHA, 8, WX_GL_DEPTH_SIZE, 8, WX_GL_STENCIL_SIZE, 8,
WX_GL_SAMPLE_BUFFERS, GL_TRUE, WX_GL_SAMPLES, 4, 0};
m_scene = std::make_shared<Scene>();
m_ctl = std::make_shared<Controller>();
m_ctl->set_scene(m_scene);
m_canvas = std::make_shared<Canvas>(this, wxID_ANY, attribList,
wxDefaultPosition, wxDefaultSize,
wxWANTS_CHARS | wxFULL_REPAINT_ON_RESIZE);
read_csg_settings(parser);
wxPanel *control_panel = new wxPanel(this);
auto controlsizer = new wxBoxSizer(wxHORIZONTAL);
auto slider_sizer = new wxBoxSizer(wxVERTICAL);
auto console_sizer = new wxBoxSizer(wxVERTICAL);
auto slider = new wxSlider(control_panel, wxID_ANY, 0, 0, 100,
wxDefaultPosition, wxDefaultSize,
wxSL_VERTICAL);
slider_sizer->Add(slider, 1, wxEXPAND);
m_ms_toggle = new wxToggleButton(control_panel, wxID_ANY, "Multisampling");
console_sizer->Add(m_ms_toggle, 0, wxALL | wxEXPAND, 5);
m_csg_toggle = new wxToggleButton(control_panel, wxID_ANY, "CSG");
m_csg_toggle->SetValue(true);
console_sizer->Add(m_csg_toggle, 0, wxALL | wxEXPAND, 5);
auto add_combobox = [control_panel, console_sizer]
(const wxString &label, const std::vector<wxString> &list)
{
auto widget = new wxComboBox(control_panel, wxID_ANY, list[0],
wxDefaultPosition, wxDefaultSize,
int(list.size()), list.data());
auto sz = new wxBoxSizer(wxHORIZONTAL);
sz->Add(new wxStaticText(control_panel, wxID_ANY, label), 0,
wxALL | wxALIGN_CENTER, 5);
sz->Add(widget, 1, wxALL | wxEXPAND, 5);
console_sizer->Add(sz, 0, wxEXPAND);
return widget;
};
auto add_spinctl = [control_panel, console_sizer]
(const wxString &label, int initial, int min, int max)
{
auto widget = new wxSpinCtrl(
control_panel, wxID_ANY,
wxString::Format("%d", initial),
wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, min, max,
initial);
auto sz = new wxBoxSizer(wxHORIZONTAL);
sz->Add(new wxStaticText(control_panel, wxID_ANY, label), 0,
wxALL | wxALIGN_CENTER, 5);
sz->Add(widget, 1, wxALL | wxEXPAND, 5);
console_sizer->Add(sz, 0, wxEXPAND);
return widget;
};
m_convexity_spin = add_spinctl("Convexity", CSGSettings::DEFAULT_CONVEXITY, 0, 100);
m_alg_select = add_combobox("Algorithm", CSG_ALGS);
m_depth_select = add_combobox("Depth Complexity", CSG_DEPTH);
m_optimization_select = add_combobox("Optimization", CSG_OPT);
m_fpstext = new wxStaticText(control_panel, wxID_ANY, "");
console_sizer->Add(m_fpstext, 0, wxALL, 5);
m_record_btn = new wxToggleButton(control_panel, wxID_ANY, "Record");
console_sizer->Add(m_record_btn, 0, wxALL | wxEXPAND, 5);
controlsizer->Add(slider_sizer, 0, wxEXPAND);
controlsizer->Add(console_sizer, 1, wxEXPAND);
control_panel->SetSizer(controlsizer);
auto sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->Add(m_canvas.get(), 1, wxEXPAND);
sizer->Add(control_panel, 0, wxEXPAND);
SetSizer(sizer);
wxString alg;
if (!parser.Found("algorithm", &alg)) alg = "Auto";
set_renderer_algorithm(alg);
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent &evt){
if (m_canvas) RemoveChild(m_canvas.get());
m_canvas.reset();
if (!m_mouse.is_playing()) evt.Skip();
else m_mouse.stop();
});
Bind(wxEVT_MENU, [this](wxCommandEvent &) {
wxFileDialog dlg(this, "Select project file", wxEmptyString,
wxEmptyString, "*.3mf", wxFD_OPEN|wxFD_FILE_MUST_EXIST);
if (dlg.ShowModal() == wxID_OK) load_model(dlg.GetPath().ToStdString());
}, wxID_OPEN);
Bind(wxEVT_MENU, [this](wxCommandEvent &) { Close(true); }, wxID_EXIT);
Bind(wxEVT_SHOW, [this](wxShowEvent &) {
activate_canvas_display();
});
Bind(wxEVT_SLIDER, [this, slider](wxCommandEvent &) {
m_ctl->move_clip_plane(double(slider->GetValue()));
});
m_ms_toggle->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &){
enable_multisampling(m_ms_toggle->GetValue());
m_canvas->get_display()->repaint();
});
m_csg_toggle->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &){
CSGSettings stt = m_ocsgdisplay->get_csgsettings();
stt.enable_csg(m_csg_toggle->GetValue());
m_ocsgdisplay->apply_csgsettings(stt);
});
m_alg_select->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) {
wxString alg = m_alg_select->GetValue();
int sel = m_alg_select->GetSelection();
m_csg_settings.set_algo(sel);
set_renderer_algorithm(alg);
});
m_depth_select->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) {
int sel = m_depth_select->GetSelection();
m_csg_settings.set_depth_algo(sel);
if (m_ocsgdisplay) m_ocsgdisplay->apply_csgsettings(m_csg_settings);
});
m_optimization_select->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) {
int sel = m_optimization_select->GetSelection();
m_csg_settings.set_optimization(sel);
if (m_ocsgdisplay) m_ocsgdisplay->apply_csgsettings(m_csg_settings);
});
m_convexity_spin->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent &) {
int c = m_convexity_spin->GetValue();
if (c > 0) {
m_csg_settings.set_convexity(unsigned(c));
if (m_ocsgdisplay) m_ocsgdisplay->apply_csgsettings(m_csg_settings);
}
});
m_record_btn->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &) {
if (!m_ui_job) {
m_stbar->set_status_text("No project loaded!");
return;
}
if (m_record_btn->GetValue()) {
if (auto c = m_canvas->get_display()->get_camera()) reset(*c);
m_ctl->on_scene_updated(*m_scene);
m_mouse.record(true);
} else {
m_mouse.record(false);
wxFileDialog dlg(this, "Select output file",
wxEmptyString, wxEmptyString, "*.events",
wxFD_SAVE|wxFD_OVERWRITE_PROMPT);
if (dlg.ShowModal() == wxID_OK) {
std::fstream stream(dlg.GetPath().ToStdString(),
std::fstream::out);
if (stream.good()) {
stream << m_ui_job->get_project_fname() << "\n";
wxSize winsize = GetSize();
stream << winsize.x << " " << winsize.y << "\n";
m_mouse.save(stream);
}
}
}
});
}
void MyFrame::bind_canvas_events(MouseInput &ms)
{
m_canvas->Bind(wxEVT_MOUSEWHEEL, [&ms](wxMouseEvent &evt) {
ms.scroll(evt.GetWheelRotation(), evt.GetWheelDelta(),
evt.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL ?
Slic3r::GL::MouseInput::waVertical :
Slic3r::GL::MouseInput::waHorizontal);
});
m_canvas->Bind(wxEVT_MOTION, [&ms](wxMouseEvent &evt) {
ms.move_to(evt.GetPosition().x, evt.GetPosition().y);
});
m_canvas->Bind(wxEVT_RIGHT_DOWN, [&ms](wxMouseEvent & /*evt*/) {
ms.right_click_down();
});
m_canvas->Bind(wxEVT_RIGHT_UP, [&ms](wxMouseEvent & /*evt*/) {
ms.right_click_up();
});
m_canvas->Bind(wxEVT_LEFT_DOWN, [&ms](wxMouseEvent & /*evt*/) {
ms.left_click_down();
});
m_canvas->Bind(wxEVT_LEFT_UP, [&ms](wxMouseEvent & /*evt*/) {
ms.left_click_up();
});
ms.add_listener(m_ctl);
}
void MyFrame::SLAJob::process()
{
using SlStatus = Slic3r::PrintBase::SlicingStatus;
Slic3r::DynamicPrintConfig cfg;
auto model = Slic3r::Model::read_from_file(m_fname, &cfg);
m_print = std::make_unique<Slic3r::SLAPrint>();
m_print->apply(model, cfg);
Slic3r::PrintBase::TaskParams params;
params.to_object_step = Slic3r::slaposHollowing;
m_print->set_task(params);
m_print->set_status_callback([this](const SlStatus &status) {
update_status(status.percent, status.text);
});
try {
m_print->process();
} catch(std::exception &e) {
update_status(0, wxString("Exception during processing: ") + e.what());
}
}
//int main() {}
-4
View File
@@ -23,15 +23,11 @@ RUN apt-get update && apt-get install -y \
libcairo2-dev \ libcairo2-dev \
libcurl4-openssl-dev \ libcurl4-openssl-dev \
libdbus-1-dev \ libdbus-1-dev \
libglew-dev \
libglu1-mesa-dev \
libglu1-mesa-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
libgstreamerd-3-dev \ libgstreamerd-3-dev \
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
libgstreamer-plugins-good1.0-dev \ libgstreamer-plugins-good1.0-dev \
libgtk-3-dev \ libgtk-3-dev \
libgtk-3-dev \
libsecret-1-dev \ libsecret-1-dev \
libsoup2.4-dev \ libsoup2.4-dev \
libssl3 \ libssl3 \
-2
View File
@@ -31,8 +31,6 @@ RUN apt-get update && apt-get install -y \
libcairo2-dev \ libcairo2-dev \
libcurl4-openssl-dev \ libcurl4-openssl-dev \
libdbus-1-dev \ libdbus-1-dev \
libglew-dev \
libglu1-mesa-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
libgstreamerd-3-dev \ libgstreamerd-3-dev \
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
@@ -55,21 +55,6 @@ modules:
url: https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-1.22.8.tar.xz url: https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-1.22.8.tar.xz
sha256: e305b9f07f52743ca481da0a4e0c76c35efd60adaf1b0694eb3bb021e2137e39 sha256: e305b9f07f52743ca481da0a4e0c76c35efd60adaf1b0694eb3bb021e2137e39
- name: glu
build-options:
cxxflags: -Wno-register
config-opts:
- --disable-static
sources:
- type: archive
url: https://ftp.osuosl.org/pub/blfs/conglomeration/glu/glu-9.0.2.tar.xz
sha256: 6e7280ff585c6a1d9dfcdf2fca489251634b3377bfc33c29e4002466a38d02d4
cleanup:
- /include
- /lib/*.a
- /lib/*.la
- /lib/pkgconfig
- name: kde-extra-cmake-modules - name: kde-extra-cmake-modules
buildsystem: cmake-ninja buildsystem: cmake-ninja
sources: sources:
@@ -217,12 +202,6 @@ modules:
sha256: a133ddc3d3c66143eba9035621db8e0bcf34dba1ee9514a9e23e96afd39fd57a sha256: a133ddc3d3c66143eba9035621db8e0bcf34dba1ee9514a9e23e96afd39fd57a
dest: external-packages/GLFW dest: external-packages/GLFW
# OpenCSG 1.4.2
- type: file
url: https://github.com/floriankirsch/OpenCSG/archive/refs/tags/opencsg-1-4-2-release.zip
sha256: 51afe0db79af8386e2027d56d685177135581e0ee82ade9d7f2caff8deab5ec5
dest: external-packages/OpenCSG
# SolveSpace libslvs (2D sketch constraint solver, Design tab) # SolveSpace libslvs (2D sketch constraint solver, Design tab)
- type: file - type: file
url: https://github.com/JacobStoren/SolveSpaceLib/archive/4d8704523e4bf212fadf5189f92484244f670fea.zip url: https://github.com/JacobStoren/SolveSpaceLib/archive/4d8704523e4bf212fadf5189f92484244f670fea.zip
-1
View File
@@ -11,7 +11,6 @@ export REQUIRED_DEV_PACKAGES=(
file file
gettext gettext
git git
glew
gst-plugins-good gst-plugins-good
gstreamer gstreamer
gtk3 gtk3
-1
View File
@@ -11,7 +11,6 @@ export REQUIRED_DEV_PACKAGES=(
file file
gettext gettext
git git
glew
gst-plugins-good gst-plugins-good
gstreamer gstreamer
gtk3 gtk3
+1 -2
View File
@@ -6,10 +6,9 @@ export REQUIRED_BUNDLES=(
c-basic c-basic
dev-utils dev-utils
devpkg-curl devpkg-curl
devpkg-glew
devpkg-glu
devpkg-gstreamer devpkg-gstreamer
devpkg-gtk3 devpkg-gtk3
devpkg-libglvnd
devpkg-libmspack devpkg-libmspack
devpkg-libsecret devpkg-libsecret
devpkg-openssl devpkg-openssl
+1 -1
View File
@@ -14,7 +14,7 @@ REQUIRED_DEV_PACKAGES=(
gstreamer1.0-gtk3 gstreamer1.0-gtk3
libcurl4-openssl-dev libcurl4-openssl-dev
libdbus-1-dev libdbus-1-dev
libglew-dev libgl-dev
libgstreamerd-3-dev libgstreamerd-3-dev
libgtk-3-dev libgtk-3-dev
libmspack-dev libmspack-dev
+1 -1
View File
@@ -22,7 +22,7 @@ REQUIRED_DEV_PACKAGES=(
libspnav-devel libspnav-devel
libtool libtool
m4 m4
mesa-libGLU-devel mesa-libGL-devel
ninja-build ninja-build
openssl-devel openssl-devel
perl-FindBin perl-FindBin
+1 -1
View File
@@ -18,7 +18,6 @@ REQUIRED_DEV_PACKAGES=(
dev-vcs/git dev-vcs/git
gui-libs/eglexternalplatform gui-libs/eglexternalplatform
kde-frameworks/extra-cmake-modules kde-frameworks/extra-cmake-modules
media-libs/glew
media-libs/gst-plugins-base:1.0 media-libs/gst-plugins-base:1.0
media-libs/gstreamer:1.0 media-libs/gstreamer:1.0
media-plugins/gst-plugins-gtk:1.0 media-plugins/gst-plugins-gtk:1.0
@@ -31,6 +30,7 @@ REQUIRED_DEV_PACKAGES=(
sys-devel/gettext sys-devel/gettext
sys-devel/m4 sys-devel/m4
virtual/libudev virtual/libudev
virtual/opengl
x11-libs/gtk+:3 x11-libs/gtk+:3
dev-util/pkgconf dev-util/pkgconf
dev-lang/yasm dev-lang/yasm
+1 -1
View File
@@ -21,7 +21,7 @@ REQUIRED_DEV_PACKAGES=(
libspnav-devel libspnav-devel
libtool libtool
m4 m4
glu-devel Mesa-libGL-devel
ninja-build ninja-build
openssl-devel openssl-devel
perl-FindBin-Real perl-FindBin-Real
-1
View File
@@ -189,7 +189,6 @@ else ()
target_link_libraries(OrcaSlicer ${CMAKE_DL_LIBS} -lstdc++ Threads::Threads pangoft2-1.0) target_link_libraries(OrcaSlicer ${CMAKE_DL_LIBS} -lstdc++ Threads::Threads pangoft2-1.0)
endif () endif ()
# Add the Slic3r GUI library, libcurl, OpenGL and GLU libraries.
if (SLIC3R_GUI) if (SLIC3R_GUI)
# target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES}) # target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES})
target_link_libraries(OrcaSlicer libslic3r_gui wxInspector::wxInspector) target_link_libraries(OrcaSlicer libslic3r_gui wxInspector::wxInspector)
@@ -344,7 +344,7 @@ TARGET_BIN="\$1"
if target_missing_runtime_library "\$TARGET_BIN" "libOpenGL.so.0" || ! has_host_runtime_library "libOpenGL.so.0"; then if target_missing_runtime_library "\$TARGET_BIN" "libOpenGL.so.0" || ! has_host_runtime_library "libOpenGL.so.0"; then
echo "Error: missing host OpenGL runtime library libOpenGL.so.0." >&2 echo "Error: missing host OpenGL runtime library libOpenGL.so.0." >&2
echo "On Ubuntu/Pop!_OS/Debian, install: libopengl0 and libglu1-mesa" >&2 echo "On Ubuntu/Pop!_OS/Debian, install: libopengl0" >&2
echo "On Arch/CachyOS, install: libglvnd" >&2 echo "On Arch/CachyOS, install: libglvnd" >&2
exit 1 exit 1
fi fi
+3
View File
@@ -411,6 +411,9 @@ void AppConfig::set_defaults()
if (get("show_overhang").empty()) if (get("show_overhang").empty())
set_bool("show_overhang", false); set_bool("show_overhang", false);
if (get("show_center_of_mass").empty())
set_bool("show_center_of_mass", false);
#ifdef _WIN32 #ifdef _WIN32
//#ifdef SUPPORT_3D_CONNEXION //#ifdef SUPPORT_3D_CONNEXION
+2
View File
@@ -112,6 +112,8 @@ set(lisbslic3r_sources
CommonDefs.hpp CommonDefs.hpp
Config.cpp Config.cpp
Config.hpp Config.hpp
ConnectedBodies.cpp
ConnectedBodies.hpp
ContourZ.cpp ContourZ.cpp
CustomGCode.cpp CustomGCode.cpp
CustomGCode.hpp CustomGCode.hpp
+1 -1
View File
@@ -13,7 +13,7 @@ namespace Slic3r { namespace csg {
// A CSGPartT should be an object that can provide at least a mesh + trafo and an // A CSGPartT should be an object that can provide at least a mesh + trafo and an
// associated csg operation. A collection of CSGPartT objects can then // associated csg operation. A collection of CSGPartT objects can then
// be interpreted as one model and used in various contexts. It can be assembled // be interpreted as one model and used in various contexts. It can be assembled
// with CGAL or OpenVDB, rendered with OpenCSG or provided to a ray-tracer to // with CGAL or OpenVDB or provided to a ray-tracer to
// deal with various parts of it according to the supported CSG types... // deal with various parts of it according to the supported CSG types...
// //
// A few simple templated interface functions are provided here and a default // A few simple templated interface functions are provided here and a default
+292
View File
@@ -0,0 +1,292 @@
#include "ConnectedBodies.hpp"
#include "AABBTreeIndirect.hpp"
#include "BoundingBox.hpp"
#include "ClipperUtils.hpp"
#include "ExPolygon.hpp"
#include "Geometry/ConvexHull.hpp"
#include "Point.hpp"
#include "Polygon.hpp"
#include "TriangleMesh.hpp"
#include "TriangleMeshSlicer.hpp"
#include "libslic3r.h"
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <functional>
#include <limits>
#include <utility>
#include <vector>
namespace Slic3r {
std::vector<std::vector<size_t>> connected_bodies(const std::vector<const ExPolygons *> &layers, size_t &count,
const std::function<void()> &throw_if_canceled)
{
// Union-find over the islands of all layers, numbered layer after layer.
std::vector<size_t> first(layers.size() + 1, 0);
for (size_t l = 0; l < layers.size(); ++l)
first[l + 1] = first[l] + layers[l]->size();
std::vector<size_t> parent(first.back());
for (size_t i = 0; i < parent.size(); ++i)
parent[i] = i;
const auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
std::vector<std::vector<BoundingBox>> boxes(layers.size());
for (size_t l = 0; l < layers.size(); ++l)
for (const ExPolygon &island : *layers[l])
boxes[l].emplace_back(get_extents(island));
for (size_t l = 0; l + 1 < layers.size(); ++l) {
if (throw_if_canceled)
throw_if_canceled();
// Index the smaller of the two layers, so that a fragmented layer is not scanned island by island.
size_t a_layer = l;
size_t b_layer = l + 1;
if (layers[a_layer]->size() < layers[b_layer]->size())
std::swap(a_layer, b_layer);
if (layers[b_layer]->empty())
continue;
using IslandTree = AABBTreeIndirect::Tree<2, coord_t>;
std::vector<AABBTreeIndirect::BoundingBoxWrapper> wrappers;
wrappers.reserve(boxes[b_layer].size());
for (size_t b = 0; b < boxes[b_layer].size(); ++b)
wrappers.emplace_back(b, boxes[b_layer][b]);
IslandTree tree;
tree.build_modify_input(wrappers);
for (size_t a = 0; a < boxes[a_layer].size(); ++a) {
const IslandTree::BoundingBox query(boxes[a_layer][a].min, boxes[a_layer][a].max);
AABBTreeIndirect::traverse(
tree, [&query](const IslandTree::Node &node) { return node.bbox.intersects(query); },
[&](const IslandTree::Node &node) {
// The tree's boxes are widened by an epsilon, and islands already joined need no clipping.
const size_t b = node.idx;
if (boxes[a_layer][a].overlap(boxes[b_layer][b]) && find(first[a_layer] + a) != find(first[b_layer] + b) &&
!intersection_ex((*layers[a_layer])[a], (*layers[b_layer])[b]).empty())
parent[find(first[a_layer] + a)] = find(first[b_layer] + b);
return true;
});
}
}
std::vector<size_t> body(parent.size(), std::numeric_limits<size_t>::max());
std::vector<std::vector<size_t>> out(layers.size());
count = 0;
for (size_t l = 0; l < layers.size(); ++l)
for (size_t i = 0; i < layers[l]->size(); ++i) {
size_t &b = body[find(first[l] + i)];
if (b == std::numeric_limits<size_t>::max())
b = count++;
out[l].emplace_back(b);
}
return out;
}
IslandLocator::IslandLocator(const ExPolygons &islands, coord_t margin) : m_islands(&islands), m_alone(islands.size(), true)
{
m_boxes.reserve(islands.size());
for (const ExPolygon &island : islands)
m_boxes.emplace_back(get_extents(island).inflated(margin));
// Sweep the boxes along x, so that only those reaching each other are compared.
std::vector<size_t> order(m_boxes.size());
for (size_t i = 0; i < order.size(); ++i)
order[i] = i;
std::sort(order.begin(), order.end(), [this](size_t l, size_t r) { return m_boxes[l].min.x() < m_boxes[r].min.x(); });
for (size_t a = 0; a < order.size(); ++a)
for (size_t b = a + 1; b < order.size() && m_boxes[order[b]].min.x() <= m_boxes[order[a]].max.x(); ++b)
if (m_boxes[order[a]].overlap(m_boxes[order[b]]))
m_alone[order[a]] = m_alone[order[b]] = false;
}
bool IslandLocator::holds(size_t island, const Point &point, bool strict) const
{
return m_boxes[island].contains(point) && ((m_alone[island] && !strict) || (*m_islands)[island].contains(point));
}
std::pair<int, double> IslandLocator::find(const Point &point, bool strict) const
{
int nearest = -1;
double distance = std::numeric_limits<double>::max();
for (size_t i = 0; i < m_boxes.size(); ++i)
if (m_boxes[i].contains(point)) {
if ((m_alone[i] && !strict) || (*m_islands)[i].contains(point))
return { int(i), 0. };
if (const double d = ((*m_islands)[i].point_projection(point) - point).cast<double>().squaredNorm(); d < distance) {
distance = d;
nearest = int(i);
}
}
return { nearest, distance };
}
// The area of polygons and their first and second moments of area, which holes, running clockwise, subtract.
struct AreaMoments
{
double area{ 0. };
Vec2d first{ Vec2d::Zero() };
// Of x^2, y^2 and xy.
Vec3d second{ Vec3d::Zero() };
void add(const Polygon &polygon)
{
if (polygon.points.size() < 3)
return;
Vec2d p1 = unscaled(polygon.points.back());
for (const Point &point : polygon.points) {
const Vec2d p2 = unscaled(point);
const double a = cross2(p1, p2);
area += a / 2.;
first += a / 6. * (p1 + p2);
second += a / 12. *
Vec3d(p1.x() * p1.x() + p1.x() * p2.x() + p2.x() * p2.x(), p1.y() * p1.y() + p1.y() * p2.y() + p2.y() * p2.y(),
p1.x() * p1.y() + p2.x() * p2.y() + 0.5 * (p1.x() * p2.y() + p2.x() * p1.y()));
p1 = p2;
}
}
};
// Mass, volume and the first and second moments of mass about the origin.
struct Moments
{
double mass{ 0. };
double volume{ 0. };
Vec3d first{ Vec3d::Zero() };
Matrix3d second{ Matrix3d::Zero() };
void add(const Moments &other)
{
mass += other.mass;
volume += other.volume;
first += other.first;
second += other.second;
}
};
BoundingBoxf3 SolidBody::bounding_box(const Transform3d &trafo) const
{
BoundingBoxf3 box;
for (const Point &point : hull.points)
for (const double z : { z_min, z_max })
box.merge(trafo * Vec3d(unscaled(point.x()), unscaled(point.y()), z));
return box;
}
std::vector<SolidBody> solid_bodies(const std::vector<MeshInPlace> &solids, const std::vector<double> &densities,
const std::vector<MeshInPlace> &negatives, size_t slabs)
{
assert(densities.size() == solids.size());
double z_min = std::numeric_limits<double>::max();
double z_max = std::numeric_limits<double>::lowest();
for (const auto &[mesh, trafo] : solids)
for (const stl_vertex &v : mesh->vertices) {
const double z = (trafo * v.cast<double>()).z();
z_min = std::min(z_min, z);
z_max = std::max(z_max, z);
}
if (z_min >= z_max || slabs == 0)
return {};
// Each slab sliced at its middle.
const double thickness = (z_max - z_min) / double(slabs);
std::vector<float> zs(slabs);
for (size_t k = 0; k < slabs; ++k)
zs[k] = float(z_min + (double(k) + 0.5) * thickness);
MeshSlicingParamsEx params;
const auto slice = [&zs, &params](const MeshInPlace &mesh) {
params.trafo = mesh.second;
return slice_mesh_ex(*mesh.first, zs, params);
};
std::vector<std::vector<ExPolygons>> slices;
for (const MeshInPlace &solid : solids)
slices.emplace_back(slice(solid));
std::vector<ExPolygons> cut(slabs);
for (const MeshInPlace &negative : negatives) {
std::vector<ExPolygons> slices_negative = slice(negative);
for (size_t k = 0; k < slabs; ++k)
append(cut[k], std::move(slices_negative[k]));
}
// The islands of each slab, and the moments of what each solid prints of them with its density.
const bool uniform = std::all_of(densities.begin(), densities.end(), [&densities](double d) { return d == densities.front(); });
std::vector<ExPolygons> islands(slabs);
std::vector<std::vector<Moments>> moments(slabs);
tbb::parallel_for(tbb::blocked_range<size_t>(0, slabs), [&](const tbb::blocked_range<size_t> &range) {
for (size_t k = range.begin(); k < range.end(); ++k) {
ExPolygons all;
for (const std::vector<ExPolygons> &solid : slices)
append(all, solid[k]);
islands[k] = diff_ex(union_ex(all), cut[k]);
moments[k].assign(islands[k].size(), {});
const double z = zs[k];
const auto add = [&](const ExPolygon &region, double density, size_t island) {
AreaMoments area;
area.add(region.contour);
for (const Polygon &hole : region.holes)
area.add(hole);
if (area.area <= 0.)
return;
// A prism of the slab's thickness.
Matrix3d second;
second << area.second.x(), area.second.z(), area.first.x() * z, area.second.z(), area.second.y(), area.first.y() * z,
area.first.x() * z, area.first.y() * z, area.area * (z * z + thickness * thickness / 12.);
moments[k][island].add({ density * area.area * thickness, area.area * thickness,
density * thickness * Vec3d(area.first.x(), area.first.y(), area.area * z), density * thickness * second });
};
if (uniform) {
for (size_t j = 0; j < islands[k].size(); ++j)
add(islands[k][j], densities.front(), j);
continue;
}
// A later solid prints where it overlaps an earlier one, and each region it prints lies in one island.
const IslandLocator locator(islands[k], 10);
ExPolygons later = cut[k];
for (size_t i = solids.size(); i-- > 0;)
if (!slices[i][k].empty()) {
for (const ExPolygon &region : diff_ex(slices[i][k], later))
if (const int island = locator.find(region.contour.points.front()).first; island >= 0)
add(region, densities[i], size_t(island));
later = union_ex(later, slices[i][k]);
}
}
});
std::vector<const ExPolygons *> layers;
layers.reserve(slabs);
for (const ExPolygons &layer : islands)
layers.emplace_back(&layer);
size_t count = 0;
const std::vector<std::vector<size_t>> bodies = connected_bodies(layers, count);
std::vector<Moments> sums(count);
std::vector<Points> outlines(count);
std::vector<SolidBody> out(count);
for (SolidBody &body : out) {
body.z_min = std::numeric_limits<double>::max();
body.z_max = std::numeric_limits<double>::lowest();
}
for (size_t k = 0; k < slabs; ++k)
for (size_t j = 0; j < islands[k].size(); ++j) {
const size_t body = bodies[k][j];
sums[body].add(moments[k][j]);
append(outlines[body], islands[k][j].contour.points);
out[body].z_min = std::min(out[body].z_min, zs[k] - 0.5 * thickness);
out[body].z_max = std::max(out[body].z_max, zs[k] + 0.5 * thickness);
}
for (size_t body = 0; body < count; ++body)
if (const Moments &sum = sums[body]; sum.mass > 0.) {
const Vec3d center = sum.first / sum.mass;
MassProperties &solid = out[body];
solid = { sum.mass, sum.volume, center, sum.second / sum.mass - center * center.transpose() };
out[body].hull = Geometry::convex_hull(std::move(outlines[body]));
}
return out;
}
} // namespace Slic3r
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Point.hpp"
#include "Polygon.hpp"
#include "TriangleMesh.hpp"
#include "libslic3r.h"
#include <admesh/stl.h>
#include <cstddef>
#include <functional>
#include <utility>
#include <vector>
namespace Slic3r {
// The connected body of each island of each layer: islands of adjacent layers whose slices overlap are one body.
// Bodies are numbered from 0 in the order of their first island.
std::vector<std::vector<size_t>> connected_bodies(const std::vector<const ExPolygons *> &layers, size_t &count,
const std::function<void()> &throw_if_canceled = nullptr);
// Finds the island of a layer that a point lies in, testing the polygons only where the boxes of several islands hold it.
class IslandLocator
{
public:
// The islands must outlive the locator. Their boxes are widened by the margin, for points reaching past an outline.
IslandLocator(const ExPolygons &islands, coord_t margin);
// Whether the island holds the point, or its box does where no other box reaches unless strict.
bool holds(size_t island, const Point &point, bool strict = false) const;
// The island holding the point as above, else the nearest one whose box holds it, with the squared distance to it; -1
// for none.
std::pair<int, double> find(const Point &point, bool strict = false) const;
const std::vector<BoundingBox> &boxes() const { return m_boxes; }
private:
const ExPolygons *m_islands;
std::vector<BoundingBox> m_boxes;
std::vector<bool> m_alone;
};
using MeshInPlace = std::pair<const indexed_triangle_set *, Transform3d>;
// A connected body of solids, with its outline seen from above as a convex hull and the height it spans.
struct SolidBody : MassProperties
{
Polygon hull;
double z_min{ 0. };
double z_max{ 0. };
// Its box once transformed, tight for a transformation that rotates about z only.
BoundingBoxf3 bounding_box(const Transform3d &trafo) const;
};
// Each connected body of the union of the solids less the negatives, sliced into slabs, each solid weighing its density.
// Where solids overlap, the later one counts, as slicing prints it.
std::vector<SolidBody> solid_bodies(const std::vector<MeshInPlace> &solids, const std::vector<double> &densities,
const std::vector<MeshInPlace> &negatives, size_t slabs);
} // namespace Slic3r
+54
View File
@@ -18,6 +18,7 @@
#include <utility> #include <utility>
#include "ColorDecomposeRecipe.hpp" #include "ColorDecomposeRecipe.hpp"
#include "Config.hpp"
#include "FilamentMixerModel.hpp" #include "FilamentMixerModel.hpp"
#include "LocalesUtils.hpp" #include "LocalesUtils.hpp"
@@ -427,6 +428,59 @@ std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_componen
return ratios; return ratios;
} }
std::string format_mixed_components(const std::vector<unsigned int> &components)
{
std::string out;
for (size_t i = 0; i < components.size(); ++i) {
if (i > 0)
out += ",";
out += std::to_string(components[i]);
}
return out;
}
std::string format_mixed_ratios(const std::vector<int> &weights)
{
int sum = std::accumulate(weights.begin(), weights.end(), 0);
if (sum <= 0)
sum = 100;
CNumericLocalesSetter c_locale_setter;
std::string out;
for (size_t i = 0; i < weights.size(); ++i) {
if (i > 0)
out += ",";
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", float(weights[i]) / float(sum));
out += buf;
}
return out;
}
int find_fixed_mixed_filament(const ConfigBase &project_config,
const std::vector<unsigned int> &components,
const std::vector<int> &weights)
{
const auto *is_mixed = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comps = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
if (is_mixed == nullptr || comps == nullptr || ratios == nullptr)
return -1;
// Created lazily with the first mixed slot, so an older project may not have it at all.
const auto *gradient = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
const std::string comp_str = format_mixed_components(components);
const std::string ratio_str = format_mixed_ratios(weights);
for (size_t i = 0; i < is_mixed->values.size(); ++i) {
if (!is_mixed->values[i] || i >= comps->values.size() || i >= ratios->values.size())
continue;
if (gradient != nullptr && i < gradient->values.size() && gradient->values[i])
continue;
if (comps->values[i] == comp_str && ratios->values[i] == ratio_str)
return int(i);
}
return -1;
}
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed) bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed)
{ {
for (unsigned char v : is_mixed) for (unsigned char v : is_mixed)
+18
View File
@@ -11,6 +11,8 @@
namespace Slic3r { namespace Slic3r {
class ConfigBase;
// Photoshop-style gradient curve control point in [0,1] x [0,1]. // Photoshop-style gradient curve control point in [0,1] x [0,1].
// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent // (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent
// overrides. NaN means "use the PCHIP-computed default", which is the case for plain // overrides. NaN means "use the PCHIP-computed default", which is the case for plain
@@ -94,6 +96,22 @@ std::vector<unsigned int> parse_mixed_components(const std::string &str);
// Normalizes so the sum equals 1.0. // Normalizes so the sum equals 1.0.
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components); std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components);
// The text a mixed slot stores in filament_mixed_components, e.g. {1, 3} → "1,3".
std::string format_mixed_components(const std::vector<unsigned int> &components);
// The text a mixed slot stores in filament_mixed_sublayer_ratios: the weights normalised to sum to 1,
// four decimals, e.g. {1, 2} → "0.3333,0.6667". A non-positive sum is read as 100.
std::string format_mixed_ratios(const std::vector<int> &weights);
// The 0-based index of the mixed slot in `project_config` that blends `components` (1-based physical
// filaments) in `weights` at a fixed ratio, or -1 when there is none. Matched on the stored text, as
// format_mixed_components() and format_mixed_ratios() write it. A gradient slot never matches: its
// ratio drifts from layer to layer, so it is not the blend asked for even where its stored ratios
// are the same.
int find_fixed_mixed_filament(const ConfigBase &project_config,
const std::vector<unsigned int> &components,
const std::vector<int> &weights);
// Returns true if any element in is_mixed is true. // Returns true if any element in is_mixed is true.
// ConfigOptionBools stores values as std::vector<unsigned char>. // ConfigOptionBools stores values as std::vector<unsigned char>.
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed); bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed);
+152
View File
@@ -24,6 +24,7 @@
#include "Polygon.hpp" #include "Polygon.hpp"
#include "Polyline.hpp" #include "Polyline.hpp"
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "ConnectedBodies.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "enum_bitmask.hpp" #include "enum_bitmask.hpp"
#include "libslic3r.h" #include "libslic3r.h"
@@ -2590,6 +2591,156 @@ WipeTowerType GCode::wipe_tower_type()
return WipeTowerType::Type2; return WipeTowerType::Type2;
} }
// Numbers the object instances and the connected bodies of the instances of several, for the processor to find those an
// extrusion lies in.
static void set_mass_locator(GCodeProcessor &processor, const Print &print)
{
struct Object
{
const PrintObject *object;
int first_instance;
// No bodies for an object of one.
size_t bodies_count;
int first_body;
std::vector<coordf_t> print_zs;
// Per layer, the body of each island and a locator whose boxes are widened for walls reaching past them.
std::vector<std::vector<size_t>> bodies;
std::vector<IslandLocator> islands;
// Per instance, whether its widened box reaches another's, so that the box of an island proves nothing.
std::vector<bool> crowded;
};
std::vector<Object> objects;
std::vector<GCodeProcessorResult::ObjectMass> object_masses;
int bodies_total = 0;
for (const PrintObject *object : print.objects()) {
const auto layers = object->layers();
if (layers.empty())
continue;
// Bodies for assemblies only, as the Prepare tab counts them: those separated infills found, if it needed them.
const ModelVolumePtrs &volumes = object->model_object()->volumes;
const bool assembly = std::count_if(volumes.begin(), volumes.end(), [](const ModelVolume *v) { return v->is_model_part(); }) > 1 ||
std::any_of(volumes.begin(), volumes.end(), [](const ModelVolume *v) { return v->is_negative_volume(); });
size_t count = 0;
std::vector<std::vector<size_t>> bodies;
if (assembly) {
count = object->separated_body_bboxes().size();
if (count > 0 && std::all_of(layers.begin(), layers.end(), [](const Layer *l) { return l->lslices_separated_component_ids.size() == l->lslices.size(); }))
for (const Layer *layer : layers)
bodies.emplace_back(layer->lslices_separated_component_ids);
else {
std::vector<const ExPolygons *> islands;
for (const Layer *layer : layers)
islands.emplace_back(&layer->lslices);
bodies = connected_bodies(islands, count);
}
}
if (count < 2) {
count = 0;
bodies.assign(layers.size(), {});
}
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;
bodies_total += int(count * object->instances().size());
for (const Layer *layer : layers) {
o.print_zs.emplace_back(layer->print_z);
o.islands.emplace_back(layer->lslices, scaled<coord_t>(1.));
}
}
if (objects.empty())
return;
std::vector<BoundingBox> boxes;
for (const Object &o : objects) {
BoundingBox box;
for (const IslandLocator &islands : o.islands)
for (const BoundingBox &island : islands.boxes())
box.merge(island);
for (const PrintInstance &instance : o.object->instances()) {
BoundingBox &moved = boxes.emplace_back(box);
moved.translate(instance.shift);
}
}
for (Object &o : objects)
for (size_t instance = 0; instance < o.object->instances().size(); ++instance) {
const size_t i = o.first_instance + instance;
o.crowded.emplace_back(false);
for (size_t j = 0; j < boxes.size() && !o.crowded.back(); ++j)
o.crowded.back() = j != i && boxes[i].overlap(boxes[j]);
}
struct Hit
{
size_t object{ 0 }, instance{ 0 }, layer{ 0 }, island{ 0 };
};
auto locate = [objects = std::move(objects), footprints = std::move(boxes),
last = std::optional<Hit>()](const Vec3d &point, bool support) mutable -> GCodeProcessor::MassLocation {
// Supports stand below and around their object: the instance whose footprint holds the point, the one whose center
// is nearest among several, else the nearest footprint.
if (support) {
const Point p(scaled(point.x()), scaled(point.y()));
int found = -1;
bool inside = false;
double best = std::numeric_limits<double>::max();
for (size_t i = 0; i < footprints.size(); ++i) {
const BoundingBox &box = footprints[i];
const double gap = Point((box.min - p).cwiseMax(p - box.max).cwiseMax(0)).cast<double>().squaredNorm();
const bool in = gap == 0.;
const double d = in ? (box.center() - p).cast<double>().squaredNorm() : gap;
if ((in && !inside) || (in == inside && d < best)) {
found = int(i);
inside = in;
best = d;
}
}
return { found, -1 };
}
constexpr double z_tolerance = 0.002;
const auto local = [&point, &objects](size_t object, size_t instance) {
return Point(Point(scaled(point.x()), scaled(point.y())) - objects[object].object->instances()[instance].shift);
};
const auto location = [&objects, &last](const Hit &hit) {
last = hit;
const Object &o = objects[hit.object];
return GCodeProcessor::MassLocation{ o.first_instance + int(hit.instance),
o.bodies_count == 0 ? -1 : o.first_body + int(hit.instance * o.bodies_count + o.bodies[hit.layer][hit.island]) };
};
// A point lies on the first layer at or above it, as spiral vase rises through each layer.
// Extrusions mostly follow each other on one island.
if (last) {
const Object &o = objects[last->object];
if (point.z() <= o.print_zs[last->layer] + z_tolerance &&
(last->layer == 0 || point.z() > o.print_zs[last->layer - 1] + z_tolerance) &&
o.islands[last->layer].holds(last->island, local(last->object, last->instance), o.crowded[last->instance]))
return location(*last);
}
// Outside the islands of instances crowding each other, the nearest outline.
std::optional<Hit> nearest;
double distance = std::numeric_limits<double>::max();
for (size_t object = 0; object < objects.size(); ++object) {
const Object &o = objects[object];
const auto z = std::lower_bound(o.print_zs.begin(), o.print_zs.end(), point.z() - z_tolerance);
if (z == o.print_zs.end())
continue;
const size_t layer = size_t(z - o.print_zs.begin());
for (size_t instance = 0; instance < o.object->instances().size(); ++instance) {
const auto [island, d] = o.islands[layer].find(local(object, instance), o.crowded[instance]);
if (island < 0)
continue;
const Hit hit{ object, instance, layer, size_t(island) };
if (d == 0. || !o.crowded[instance])
return location(hit);
if (d < distance) {
distance = d;
nearest = hit;
}
}
}
return nearest ? location(*nearest) : GCodeProcessor::MassLocation{};
};
processor.set_mass_locator(std::move(locate), std::move(object_masses));
}
void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb) void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb)
{ {
PROFILE_CLEAR(); PROFILE_CLEAR();
@@ -3112,6 +3263,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// modifies m_silent_time_estimator_enabled // modifies m_silent_time_estimator_enabled
DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled, DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled,
print.get_layered_nozzle_group_result()); print.get_layered_nozzle_group_result());
set_mass_locator(m_processor, print);
const bool is_bbl_printers = print.is_BBL_printer(); const bool is_bbl_printers = print.is_BBL_printer();
const bool skip_config_block = print.config().gcode_skip_config_block; const bool skip_config_block = print.config().gcode_skip_config_block;
const WipeTowerType wipe_tower_type = print.wipe_tower_type(); const WipeTowerType wipe_tower_type = print.wipe_tower_type();
+76 -1
View File
@@ -89,7 +89,6 @@ static const float DEFAULT_TRAVEL_ACCELERATION = 1250.0f;
static const size_t MIN_EXTRUDERS_COUNT = 5; static const size_t MIN_EXTRUDERS_COUNT = 5;
static const float DEFAULT_FILAMENT_DIAMETER = 1.75f; static const float DEFAULT_FILAMENT_DIAMETER = 1.75f;
static const int DEFAULT_FILAMENT_HRC = 0; static const int DEFAULT_FILAMENT_HRC = 0;
static const float DEFAULT_FILAMENT_DENSITY = 1.245f;
static const float DEFAULT_FILAMENT_COST = 29.99f; static const float DEFAULT_FILAMENT_COST = 29.99f;
static const int DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE = 0; static const int DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE = 0;
static const Slic3r::Vec3f DEFAULT_EXTRUDER_OFFSET = Slic3r::Vec3f::Zero(); static const Slic3r::Vec3f DEFAULT_EXTRUDER_OFFSET = Slic3r::Vec3f::Zero();
@@ -2604,6 +2603,10 @@ void GCodeProcessorResult::reset() {
lock(); lock();
moves.clear(); moves.clear();
plate_mass = {};
object_masses.clear();
body_masses.clear();
support_masses.clear();
lines_ends.clear(); lines_ends.clear();
printable_area = Pointfs(); printable_area = Pointfs();
//BBS: add bed exclude area //BBS: add bed exclude area
@@ -3702,6 +3705,7 @@ void GCodeProcessor::reset()
m_g1_line_id = 0; m_g1_line_id = 0;
m_layer_id = 0; m_layer_id = 0;
m_cp_color.reset(); m_cp_color.reset();
m_mass_locator = nullptr;
m_producer = EProducer::Unknown; m_producer = EProducer::Unknown;
@@ -3841,6 +3845,7 @@ void GCodeProcessor::process_buffer(const std::string &buffer)
void GCodeProcessor::finalize(bool post_process) void GCodeProcessor::finalize(bool post_process)
{ {
m_result.z_offset = m_z_offset; m_result.z_offset = m_z_offset;
finalize_object_masses();
// update width/height of wipe moves // update width/height of wipe moves
for (GCodeProcessorResult::MoveVertex& move : m_result.moves) { for (GCodeProcessorResult::MoveVertex& move : m_result.moves) {
@@ -5469,6 +5474,9 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
m_seams_detector.set_first_vertex(m_result.moves.back().position - m_extruder_offsets[filament_id] - plate_offset); m_seams_detector.set_first_vertex(m_result.moves.back().position - m_extruder_offsets[filament_id] - plate_offset);
} }
if (type == EMoveType::Extrude)
add_object_mass(filament_id, area_filament_cross_section * delta_pos[E]);
// store move // store move
store_move_vertex(type); store_move_vertex(type);
} }
@@ -7277,6 +7285,73 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
} }
} }
void GCodeProcessorResult::ObjectMass::add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer)
{
box.merge(extent);
if (printed_up_to_layer.size() <= layer)
printed_up_to_layer.resize(layer + 1);
printed_up_to_layer[layer].add(sum);
}
void GCodeProcessor::add_object_mass(int filament_id, float volume)
{
// Skirt, prime tower and custom G-code belong to no object.
const ExtrusionRole role = m_extrusion_role;
if (volume <= 0.f || role == erNone || role == erSkirt || role == erWipeTower || role == erCustom || role == erMixed)
return;
const bool has_density = size_t(filament_id) < m_result.filament_densities.size() && m_result.filament_densities[filament_id] > 0.f;
const double mass = double(volume) * (has_density ? m_result.filament_densities[filament_id] : DEFAULT_FILAMENT_DENSITY);
// In the frame of the stored moves, the bead's center half its height below the nozzle, from the move's start to its end.
const Vec3d half_height = 0.5 * double(m_height) * Vec3d::UnitZ();
const Vec3d offset = Vec3d(m_x_offset, m_y_offset, -m_z_offset) - half_height + m_extruder_offsets[filament_id].cast<double>();
const Vec3d start = Vec3d(m_start_position[X], m_start_position[Y], m_start_position[Z]) + offset;
const Vec3d end = Vec3d(m_end_position[X], m_end_position[Y], m_end_position[Z]) + offset;
// The second moments of a uniform segment.
const GCodeProcessorResult::ObjectMass::Sum sum{ mass, double(volume), 0.5 * mass * (start + end),
mass / 3. * (start.cwiseProduct(start) + start.cwiseProduct(end) + end.cwiseProduct(end)) };
// Of the bead's center line and its height, as its width is only estimated. Merged, as a wall along an axis is flat.
BoundingBoxf3 extent;
extent.merge(start.cwiseMin(end) - half_height);
extent.merge(start.cwiseMax(end) + half_height);
const bool part = role != erBrim && !is_support(role);
const size_t layer = std::max<unsigned int>(1, m_layer_id) - 1;
m_result.plate_mass.add(sum, extent, layer);
// The brim belongs to the plate alone.
if (role == erBrim || !m_mass_locator)
return;
const auto add = [&sum, &extent, layer](std::vector<GCodeProcessorResult::ObjectMass> &masses, int index) {
if (index < 0)
return;
if (masses.size() <= size_t(index))
masses.resize(index + 1);
masses[index].add(sum, extent, layer);
};
// At the nozzle's height, which the layers print at.
const MassLocation location = m_mass_locator(0.5 * (start + end) + half_height, !part);
if (part) {
add(m_result.object_masses, location.object);
add(m_result.body_masses, location.body);
} else
add(m_result.support_masses, location.object);
}
void GCodeProcessor::finalize_object_masses()
{
const auto accumulate = [](GCodeProcessorResult::ObjectMass &object) {
for (size_t i = 1; i < object.printed_up_to_layer.size(); ++i)
object.printed_up_to_layer[i].add(object.printed_up_to_layer[i - 1]);
};
accumulate(m_result.plate_mass);
for (GCodeProcessorResult::ObjectMass &object : m_result.object_masses)
accumulate(object);
for (GCodeProcessorResult::ObjectMass &body : m_result.body_masses)
accumulate(body);
for (GCodeProcessorResult::ObjectMass &support : m_result.support_masses)
accumulate(support);
}
void GCodeProcessor::set_extrusion_role(ExtrusionRole role) void GCodeProcessor::set_extrusion_role(ExtrusionRole role)
{ {
m_used_filaments.process_role_cache(this); m_used_filaments.process_role_cache(this);
+62
View File
@@ -3,6 +3,7 @@
#include "libslic3r/CommonDefs.hpp" #include "libslic3r/CommonDefs.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Polygon.hpp" #include "libslic3r/Polygon.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/ArcFitter.hpp" #include "libslic3r/ArcFitter.hpp"
@@ -35,6 +36,9 @@ namespace Slic3r {
class Print; class Print;
// For a filament whose density is not set, in g/cm³.
inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
// slice warnings enum strings // slice warnings enum strings
#define NOZZLE_HRC_CHECKER "the_actual_nozzle_hrc_smaller_than_the_required_nozzle_hrc" #define NOZZLE_HRC_CHECKER "the_actual_nozzle_hrc_smaller_than_the_required_nozzle_hrc"
#define BED_TEMP_TOO_HIGH_THAN_FILAMENT "bed_temperature_too_high_than_filament" #define BED_TEMP_TOO_HIGH_THAN_FILAMENT "bed_temperature_too_high_than_filament"
@@ -270,9 +274,44 @@ class Print;
std::vector<std::string> params; // extra msg info std::vector<std::string> params; // extra msg info
}; };
// Material extruded for the plate, one object instance or one connected body of it, for their centers of mass.
struct ObjectMass
{
struct Sum
{
double mass{ 0. };
double volume{ 0. };
Vec3d moment{ Vec3d::Zero() };
// Of the mass about the origin along each axis, the sums of m x^2, m y^2 and m z^2.
Vec3d second{ Vec3d::Zero() };
void add(const Sum &other)
{
mass += other.mass;
volume += other.volume;
moment += other.moment;
second += other.second;
}
};
// Everything printed up to each layer id, the plate's with brim, raft and supports, and the box it fills.
std::vector<Sum> printed_up_to_layer;
BoundingBoxf3 box;
// Of an object, whether it is an assembly.
bool assembly{ false };
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);
};
std::string filename; std::string filename;
unsigned int id; unsigned int id;
std::vector<MoveVertex> moves; std::vector<MoveVertex> moves;
ObjectMass plate_mass;
// One per object instance, and one per connected body of the instances of several, when the sliced objects were at hand.
std::vector<ObjectMass> object_masses;
std::vector<ObjectMass> body_masses;
// One per object instance, of its supports and raft.
std::vector<ObjectMass> support_masses;
// Positions of ends of lines of the final G-code this->filename after TimeProcessor::post_process() finalizes the G-code. // Positions of ends of lines of the final G-code this->filename after TimeProcessor::post_process() finalizes the G-code.
std::vector<size_t> lines_ends; std::vector<size_t> lines_ends;
Pointfs printable_area; Pointfs printable_area;
@@ -360,6 +399,10 @@ class Print;
filename = std::forward<Other>(other).filename; filename = std::forward<Other>(other).filename;
id = std::forward<Other>(other).id; id = std::forward<Other>(other).id;
moves = std::forward<Other>(other).moves; moves = std::forward<Other>(other).moves;
plate_mass = std::forward<Other>(other).plate_mass;
object_masses = std::forward<Other>(other).object_masses;
body_masses = std::forward<Other>(other).body_masses;
support_masses = std::forward<Other>(other).support_masses;
lines_ends = std::forward<Other>(other).lines_ends; lines_ends = std::forward<Other>(other).lines_ends;
printable_area = std::forward<Other>(other).printable_area; printable_area = std::forward<Other>(other).printable_area;
bed_exclude_area = std::forward<Other>(other).bed_exclude_area; bed_exclude_area = std::forward<Other>(other).bed_exclude_area;
@@ -1099,6 +1142,15 @@ class Print;
}; };
#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING
// The object instance and the connected body of an instance of several that a point lies in, -1 for none.
struct MassLocation
{
int object{ -1 };
int body{ -1 };
};
// For a support, the object instance only.
using MassLocator = std::function<MassLocation(const Vec3d &point, bool support)>;
private: private:
CommandProcessor m_command_processor; CommandProcessor m_command_processor;
GCodeReader m_parser; GCodeReader m_parser;
@@ -1126,6 +1178,7 @@ class Print;
bool m_skippable{false}; bool m_skippable{false};
SkipType m_skippable_type{SkipType::stNone}; SkipType m_skippable_type{SkipType::stNone};
int m_object_label_id{-1}; int m_object_label_id{-1};
MassLocator m_mass_locator;
float m_print_z{0.0f}; float m_print_z{0.0f};
std::vector<float> m_remaining_volume; std::vector<float> m_remaining_volume;
ExtruderTemps m_filament_nozzle_temp; ExtruderTemps m_filament_nozzle_temp;
@@ -1280,6 +1333,13 @@ class Print;
const std::vector<std::set<int>>& unprintable_filament_types ); const std::vector<std::set<int>>& unprintable_filament_types );
void apply_config(const PrintConfig& config); void apply_config(const PrintConfig& config);
void set_print(Print* print) { m_print = print; } void set_print(Print* print) { m_print = print; }
// Locates extrusions in the objects and bodies it numbers, those objects listed beforehand.
void set_mass_locator(MassLocator locator, std::vector<GCodeProcessorResult::ObjectMass> objects)
{
m_mass_locator = std::move(locator);
m_result.support_masses.assign(objects.size(), {});
m_result.object_masses = std::move(objects);
}
// Hand the nozzle grouping context to the estimator BEFORE the streaming replay, so the // Hand the nozzle grouping context to the estimator BEFORE the streaming replay, so the
// per-slot machine-limit resolution can follow the active nozzle. Null is fine (slot 0). // per-slot machine-limit resolution can follow the active nozzle. Null is fine (slot 0).
void initialize_from_context(const std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase>& nozzle_group_result) { void initialize_from_context(const std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase>& nozzle_group_result) {
@@ -1534,6 +1594,8 @@ class Print;
//BBS: different path_type is only used for arc move //BBS: different path_type is only used for arc move
void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false); void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false);
void add_object_mass(int filament_id, float volume);
void finalize_object_masses();
void set_extrusion_role(ExtrusionRole role); void set_extrusion_role(ExtrusionRole role);
// Resolve the SKIPPABLE_TYPE payload to a SkipType. // Resolve the SKIPPABLE_TYPE payload to a SkipType.
+5 -3
View File
@@ -5381,13 +5381,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
bool process_multi_extruder = false; bool process_multi_extruder = false;
std::vector<int> filament_variant_index; std::vector<int> filament_variant_index;
size_t extruder_variant_count; size_t extruder_variant_count;
if (!config.option<ConfigOptionInts>("filament_self_index")) { // A config loaded over the full defaults has a one-entry index even when the file has none.
std::vector<int>& filament_self_indice = config.option<ConfigOptionInts>("filament_self_index", true)->values; ConfigOptionInts* filament_self_index_opt = config.option<ConfigOptionInts>("filament_self_index", true);
if (filament_self_index_opt->size() < num_filaments) {
std::vector<int>& filament_self_indice = filament_self_index_opt->values;
filament_self_indice.resize(num_filaments); filament_self_indice.resize(num_filaments);
for (int index = 0; index < num_filaments; index++) for (int index = 0; index < num_filaments; index++)
filament_self_indice[index] = index + 1; filament_self_indice[index] = index + 1;
} }
std::vector<int> filament_self_indice = std::move(config.option<ConfigOptionInts>("filament_self_index")->values); std::vector<int> filament_self_indice = std::move(filament_self_index_opt->values);
// ORCA: Initialize filament_extruder_variant for backward compatibility with old 3mf files // ORCA: Initialize filament_extruder_variant for backward compatibility with old 3mf files
// that don't have this option saved or have it with default single-element value // that don't have this option saved or have it with default single-element value
ConfigOptionStrings* filament_extruder_variant_opt = config.option<ConfigOptionStrings>("filament_extruder_variant"); ConfigOptionStrings* filament_extruder_variant_opt = config.option<ConfigOptionStrings>("filament_extruder_variant");
+13 -62
View File
@@ -12,6 +12,7 @@
#include "BoundingBox.hpp" #include "BoundingBox.hpp"
#include "ClipperUtils.hpp" #include "ClipperUtils.hpp"
#include "ConnectedBodies.hpp"
#include "Geometry.hpp" #include "Geometry.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "Layer.hpp" #include "Layer.hpp"
@@ -747,69 +748,19 @@ void PrintObject::prepare_infill()
for (Layer *layer : m_layers) for (Layer *layer : m_layers)
layer->lslices_separated_component_ids.clear(); layer->lslices_separated_component_ids.clear();
if (needs_separated_components) { if (needs_separated_components) {
const size_t nl = m_layers.size(); std::vector<const ExPolygons *> islands;
std::vector<size_t> offset(nl + 1, 0); // Orca: flat index of the first island of each layer islands.reserve(m_layers.size());
for (size_t i = 0; i < nl; ++ i) for (const Layer *layer : m_layers)
offset[i + 1] = offset[i] + m_layers[i]->lslices.size(); islands.emplace_back(&layer->lslices);
const size_t nreg = offset[nl]; size_t bodies = 0;
// Orca: Union-find over every (layer, island). std::vector<std::vector<size_t>> ids = connected_bodies(islands, bodies, [this]() { m_print->throw_if_canceled(); });
std::vector<size_t> parent(nreg); // Orca: Merge the bounding boxes of the islands of each body.
for (size_t i = 0; i < nreg; ++ i) parent[i] = i; m_separated_body_bboxes.assign(bodies, BoundingBox());
auto find = [&parent](size_t x) { for (size_t i = 0; i < m_layers.size(); ++ i) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
};
auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; };
// Orca: Index the smaller of two consecutive layers instead of scanning every
// pair of islands. The tree prunes distant boxes on fragmented models; exact
// polygon intersections still decide connectivity for the remaining candidates.
for (size_t i = 0; i + 1 < nl; ++ i) {
m_print->throw_if_canceled();
size_t layer_a = i, layer_b = i + 1;
if (m_layers[layer_a]->lslices.size() < m_layers[layer_b]->lslices.size())
std::swap(layer_a, layer_b);
const Layer *la = m_layers[layer_a], *lb = m_layers[layer_b];
if (lb->lslices.empty())
continue;
using IslandTree = AABBTreeIndirect::Tree<2, coord_t>;
std::vector<AABBTreeIndirect::BoundingBoxWrapper> bboxes;
bboxes.reserve(lb->lslices.size());
for (size_t b = 0; b < lb->lslices.size(); ++ b)
bboxes.emplace_back(b, lb->lslices_bboxes[b]);
IslandTree tree;
tree.build_modify_input(bboxes);
for (size_t a = 0; a < la->lslices.size(); ++ a) {
const IslandTree::BoundingBox query(la->lslices_bboxes[a].min, la->lslices_bboxes[a].max);
AABBTreeIndirect::traverse(tree,
[&query](const IslandTree::Node &node) { return node.bbox.intersects(query); },
[&](const IslandTree::Node &node) {
const size_t b = node.idx;
// Orca: Tree boxes include an epsilon, so retain the original box
// filter. Already-connected islands cannot change the partition
// and need no further polygon intersection.
if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) &&
find(offset[layer_a] + a) != find(offset[layer_b] + b) &&
! intersection_ex(la->lslices[a], lb->lslices[b]).empty())
unite(offset[layer_a] + a, offset[layer_b] + b);
return true;
});
}
}
// Orca: Number the bodies by their first island and merge the bounding boxes of their islands.
std::vector<size_t> body_of_root(nreg, size_t(-1));
for (size_t i = 0; i < nl; ++ i) {
Layer *layer = m_layers[i]; Layer *layer = m_layers[i];
layer->lslices_separated_component_ids.resize(layer->lslices.size()); for (size_t a = 0; a < layer->lslices.size(); ++ a)
for (size_t a = 0; a < layer->lslices.size(); ++ a) { m_separated_body_bboxes[ids[i][a]].merge(layer->lslices_bboxes[a]);
size_t &body = body_of_root[find(offset[i] + a)]; layer->lslices_separated_component_ids = std::move(ids[i]);
if (body == size_t(-1)) {
body = m_separated_body_bboxes.size();
m_separated_body_bboxes.emplace_back();
}
m_separated_body_bboxes[body].merge(layer->lslices_bboxes[a]);
layer->lslices_separated_component_ids[a] = body;
}
} }
} }
+14
View File
@@ -247,6 +247,20 @@ void smooth_height_pixels(std::vector<uint8_t> &pixels, int width, int height, f
} }
} // namespace } // namespace
bool height_texture_has_color(const TextureDisplacementLayer &layer)
{
if (layer.empty())
return false;
{
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
const auto it = g_decoded_texture_cache.entries.find(layer.image_data.get());
if (it != g_decoded_texture_cache.entries.end() && it->second.first.lock() == layer.image_data)
return it->second.second.has_color();
}
// Not decoded yet. Decoding caches the raw image, so this happens once per image.
return decode_height_texture(layer).has_color();
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer) DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{ {
DecodedHeightTexture result; DecodedHeightTexture result;
+17 -5
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_TextureDisplacement_hpp_ #ifndef slic3r_TextureDisplacement_hpp_
#define slic3r_TextureDisplacement_hpp_ #define slic3r_TextureDisplacement_hpp_
#include <cmath>
#include <cstddef> #include <cstddef>
#include <Eigen/Core> #include <Eigen/Core>
#include <cstdint> #include <cstdint>
@@ -387,6 +388,10 @@ struct TextureDisplacementOptions
// image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a // image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a
// photograph or gradient in mixes. Off forces single filaments everywhere. // photograph or gradient in mixes. Off forces single filaments everywhere.
bool color_mix_enabled = true; bool color_mix_enabled = true;
// The most mixes the palette may offer. Every mix a bake paints with becomes a mixed filament slot,
// so this is also the most slots one bake can add. The mixes themselves are picked from the
// texture's colours, those that improve the match the most coming first.
int color_mix_count = 8;
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes - // Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail // this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide. // finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
@@ -396,7 +401,7 @@ struct TextureDisplacementOptions
{ {
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border, ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
v2_relocate, color_mix_enabled, color_despeckle); v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
} }
}; };
@@ -480,6 +485,10 @@ struct DecodedHeightTexture
// DecodedHeightTexture if image_data is empty or is not a PNG at all. // DecodedHeightTexture if image_data is empty or is not a PNG at all.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer); DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
bool height_texture_has_color(const TextureDisplacementLayer &layer);
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour". // Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
// //
// Deliberately a callback rather than a function here: matching a colour to a filament is a // Deliberately a callback rather than a function here: matching a colour to a filament is a
@@ -494,12 +503,14 @@ using ColorQuantizeFn = std::function<int(const Vec3f &)>;
// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job. // interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job.
struct PrintableColor struct PrintableColor
{ {
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the colour its mixed filament slot shows
int a = 0; // filament index int a = 0; // filament index
int b = 0; // the second filament; == a for a pure entry int b = 0; // the second filament; == a for a pure entry
int num = 1; // a's share of the interleave, out of `den` int num = 1; // a's share of the interleave, out of `den`
int den = 1; int den = 1;
bool is_mix() const { return a != b; } bool is_mix() const { return a != b; }
// a's share in percent, the form a mixed filament slot is created from.
int a_percent() const { return int(std::lround(100.0 * double(num) / double(den))); }
}; };
// Everything needed to colour a mesh, captured on the main thread and handed to a job. An empty // Everything needed to colour a mesh, captured on the main thread and handed to a job. An empty
@@ -762,9 +773,10 @@ struct TextureColorRequest
float min_color_region_mm2 = 0.5f; float min_color_region_mm2 = 0.5f;
// Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned // Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned
// mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from // mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from
// the texture". The +1 is not arbitrary - it lines up with EnforcerBlockerType, where 0 is NONE // the texture". The +1 lines up with EnforcerBlockerType, where 0 is NONE ("use the volume's own
// ("use the volume's own filament") and 1..16 are Extruder1..16, so the caller can hand these // filament"): where every palette entry is a filament, these go straight to a TriangleSelector. A
// straight to a TriangleSelector without a second mapping table. // palette with mixes maps each index to the mix's filament slot first (see
// GLGizmoTextureDisplacement::palette_filaments()).
std::vector<uint8_t> *out_triangle = nullptr; std::vector<uint8_t> *out_triangle = nullptr;
}; };
+34
View File
@@ -1495,6 +1495,40 @@ float its_volume(const indexed_triangle_set &its)
return volume; return volume;
} }
MassProperties MassProperties::transformed(const Transform3d &trafo) const
{
const Matrix3d linear = trafo.linear();
const double scale = std::abs(linear.determinant());
return { mass * scale, volume * scale, trafo * center, linear * spread * linear.transpose() };
}
MassProperties its_mass_properties(const indexed_triangle_set &its)
{
if (its.indices.empty())
return {};
// Signed tetrahedra fanned from a mesh vertex, not the origin, to keep the sums precise far from it.
const Vec3d p0 = its.vertices.front().cast<double>();
double volume6 = 0.;
Vec3d moment24 = Vec3d::Zero();
Matrix3d second120 = Matrix3d::Zero();
for (const stl_triangle_vertex_indices &face : its.indices) {
const Vec3d a = its.vertices[face(0)].cast<double>() - p0;
const Vec3d b = its.vertices[face(1)].cast<double>() - p0;
const Vec3d c = its.vertices[face(2)].cast<double>() - p0;
const Vec3d s = a + b + c;
const double v = a.dot(b.cross(c));
volume6 += v;
moment24 += v * s;
second120 += v * (a * a.transpose() + b * b.transpose() + c * c.transpose() + s * s.transpose());
}
if (volume6 == 0.)
return {};
const Vec3d center = moment24 / (4. * volume6);
const double volume = std::abs(volume6) / 6.;
return { volume, volume, p0 + center, second120 / (20. * volume6) - center * center.transpose() };
}
float its_average_edge_length(const indexed_triangle_set &its) float its_average_edge_length(const indexed_triangle_set &its)
{ {
if (its.indices.empty()) if (its.indices.empty())
+15
View File
@@ -9,6 +9,7 @@
#include <array> #include <array>
#include <cereal/specialize.hpp> #include <cereal/specialize.hpp>
#include <functional> #include <functional>
#include <utility>
#include <vector> #include <vector>
#include "BoundingBox.hpp" #include "BoundingBox.hpp"
#include "Line.hpp" #include "Line.hpp"
@@ -324,6 +325,20 @@ inline stl_normal its_unnormalized_normal(const indexed_triangle_set &its,
} }
float its_volume(const indexed_triangle_set &its); float its_volume(const indexed_triangle_set &its);
// Mass, volume and center of mass of a solid, and the mean over its mass of (x - center)(x - center)^T, from which its
// moments of inertia about axes through the center follow.
struct MassProperties
{
double mass{ 0. };
double volume{ 0. };
Vec3d center{ Vec3d::Zero() };
Matrix3d spread{ Matrix3d::Zero() };
// Under an affine map, which scales mass and volume by its determinant.
MassProperties transformed(const Transform3d &trafo) const;
};
// The solid a closed mesh bounds at unit density, whichever way its faces turn; nothing for a zero volume.
MassProperties its_mass_properties(const indexed_triangle_set &its);
float its_average_edge_length(const indexed_triangle_set &its); float its_average_edge_length(const indexed_triangle_set &its);
void its_merge(indexed_triangle_set &A, const indexed_triangle_set &B); void its_merge(indexed_triangle_set &A, const indexed_triangle_set &B);
+12
View File
@@ -1441,6 +1441,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
wxGetApp().plater()->schedule_background_process(); wxGetApp().plater()->schedule_background_process();
return; return;
} }
m_plate_mass = gcode_result.plate_mass;
m_object_masses = gcode_result.object_masses;
m_body_masses = gcode_result.body_masses;
m_support_masses = gcode_result.support_masses;
// convert data from PrusaSlicer format to libvgcode format. // convert data from PrusaSlicer format to libvgcode format.
// Belt printers: when the designed (upright) view is active, back-transform // Belt printers: when the designed (upright) view is active, back-transform
@@ -1876,6 +1880,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data) void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data)
{ {
m_loaded_as_preview = true; m_loaded_as_preview = true;
m_plate_mass = {};
m_object_masses.clear();
m_body_masses.clear();
m_support_masses.clear();
m_move_type_counts.fill(0); m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times) for (auto& move_type_times : m_move_type_times)
@@ -1955,6 +1963,10 @@ void GCodeViewer::reset()
m_move_type_distances.fill(0.0f); m_move_type_distances.fill(0.0f);
m_print_statistics.reset(); m_print_statistics.reset();
m_custom_gcode_per_print_z = std::vector<CustomGCode::Item>(); m_custom_gcode_per_print_z = std::vector<CustomGCode::Item>();
m_plate_mass = {};
m_object_masses.clear();
m_body_masses.clear();
m_support_masses.clear();
m_left_extruder_filament.clear(); m_left_extruder_filament.clear();
m_right_extruder_filament.clear(); m_right_extruder_filament.clear();
m_sequential_view.gcode_window.reset(); m_sequential_view.gcode_window.reset();
+8
View File
@@ -260,6 +260,10 @@ private:
GCodeProcessorResult::SettingsIds m_settings_ids; GCodeProcessorResult::SettingsIds m_settings_ids;
std::vector<CustomGCode::Item> m_custom_gcode_per_print_z; std::vector<CustomGCode::Item> m_custom_gcode_per_print_z;
GCodeProcessorResult::ObjectMass m_plate_mass;
std::vector<GCodeProcessorResult::ObjectMass> m_object_masses;
std::vector<GCodeProcessorResult::ObjectMass> m_body_masses;
std::vector<GCodeProcessorResult::ObjectMass> m_support_masses;
bool m_contained_in_bed{ true }; bool m_contained_in_bed{ true };
mutable bool m_no_render_path { false }; mutable bool m_no_render_path { false };
@@ -343,6 +347,10 @@ public:
std::vector<float> get_layers_times() const { return m_viewer.get_layers_estimated_times(); } std::vector<float> get_layers_times() const { return m_viewer.get_layers_estimated_times(); }
const std::array<size_t,2> &get_layers_z_range() const { return m_viewer.get_layers_view_range(); } const std::array<size_t,2> &get_layers_z_range() const { return m_viewer.get_layers_view_range(); }
const GCodeProcessorResult::ObjectMass& get_plate_mass() const { return m_plate_mass; }
const std::vector<GCodeProcessorResult::ObjectMass>& get_object_masses() const { return m_object_masses; }
const std::vector<GCodeProcessorResult::ObjectMass>& get_body_masses() const { return m_body_masses; }
const std::vector<GCodeProcessorResult::ObjectMass>& get_support_masses() const { return m_support_masses; }
size_t get_vertices_count() const { return m_viewer.get_vertices_count(); } size_t get_vertices_count() const { return m_viewer.get_vertices_count(); }
size_t get_layers_count() const { return m_viewer.get_layers_count(); } size_t get_layers_count() const { return m_viewer.get_layers_count(); }
// ORCA: realistic view. Changes whenever the toolpaths casting shadows do. // ORCA: realistic view. Changes whenever the toolpaths casting shadows do.
+338
View File
@@ -85,7 +85,9 @@
#include "3DScene.hpp" #include "3DScene.hpp"
#include "BackgroundSlicingProcess.hpp" #include "BackgroundSlicingProcess.hpp"
#include "CameraUtils.hpp" #include "CameraUtils.hpp"
#include "GLModel.hpp"
#include "GLShader.hpp" #include "GLShader.hpp"
#include "libslic3r/ConnectedBodies.hpp"
#include "GUI.hpp" #include "GUI.hpp"
#include "Tab.hpp" #include "Tab.hpp"
#include "GUI_Preview.hpp" #include "GUI_Preview.hpp"
@@ -136,6 +138,7 @@
#include <tbb/spin_mutex.h> #include <tbb/spin_mutex.h>
#include <boost/functional/hash.hpp> #include <boost/functional/hash.hpp>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
@@ -1000,6 +1003,325 @@ void GLCanvas3D::Labels::render(const std::vector<const ModelInstance*>& sorted_
} }
} }
// The sums a solid adds to a marker.
static GCodeProcessorResult::ObjectMass::Sum mass_sum(const MassProperties& solid)
{
return { solid.mass, solid.volume, solid.mass * solid.center,
solid.mass * (solid.spread.diagonal() + solid.center.cwiseProduct(solid.center)) };
}
// On screen, of the plates, the objects, the supports and the bodies, each smaller than the one before, so that markers at
// one place still show.
static constexpr std::array<double, 4> marker_radii{ 9., 7., 6., 5. };
// As the canvas toolbar scales for the display's DPI.
static double marker_scale(const GLCanvas3D& canvas)
{
double scale = canvas.get_scale();
#ifdef WIN32
scale *= double(get_dpi_for_window(wxGetApp().GetTopWindow())) / double(DPI_DEFAULT);
#endif // WIN32
return scale;
}
GLCanvas3D::CenterOfMass::Markers GLCanvas3D::CenterOfMass::model_markers(const GLCanvas3D& canvas)
{
Markers markers;
if (canvas.get_model() == nullptr)
return markers;
struct Instance
{
Transform3d trafo;
std::vector<const GLVolume*> volumes;
};
std::map<int, std::map<int, Instance>> objects;
const ModelObjectPtrs& model_objects = canvas.get_model()->objects;
for (const GLVolume* volume : canvas.get_volumes().volumes) {
const int obj_idx = volume->object_idx();
const int vol_idx = volume->volume_idx();
if (!volume->is_active || volume->is_wipe_tower || obj_idx < 0 || obj_idx >= int(model_objects.size()) || vol_idx < 0 ||
vol_idx >= int(model_objects[obj_idx]->volumes.size()))
continue;
Instance& instance = objects[obj_idx][volume->instance_idx()];
instance.trafo = volume->get_instance_transformation().get_matrix();
instance.volumes.emplace_back(volume);
}
// From the filament presets, as the plater config holds the values of the last filament edited only.
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
std::vector<double> filament_densities;
for (const std::string& name : preset_bundle.filament_presets)
filament_densities.emplace_back(preset_bundle.filaments.find_preset(name, true)->config.opt_float("filament_density", 0));
const auto density = [&filament_densities](const ModelVolume& volume) {
const size_t filament = size_t(std::max(1, volume.extruder_id()));
const double density = filament <= filament_densities.size() ? filament_densities[filament - 1] : 0.;
return density > 0. ? density : double(DEFAULT_FILAMENT_DENSITY);
};
// One per plate, of the instances on it.
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
std::map<int, Marker> plates;
std::map<size_t, MassProperties> meshes;
std::map<size_t, Bodies> bodies;
for (const auto& [obj_idx, instances] : objects) {
const ModelObject& object = *model_objects[obj_idx];
// An assembly is sliced, so that its overlapping parts are united and its negative volumes cut away, in the
// order of its volumes, as the later one prints where two overlap.
std::vector<const GLVolume*> volumes = instances.begin()->second.volumes;
std::sort(volumes.begin(), volumes.end(), [](const GLVolume* l, const GLVolume* r) { return l->volume_idx() < r->volume_idx(); });
std::vector<MeshInPlace> solids;
std::vector<double> densities;
std::vector<MeshInPlace> negatives;
std::vector<Bodies::Volume> sliced;
for (const GLVolume* volume : volumes) {
const ModelVolume& model_volume = *object.volumes[volume->volume_idx()];
if (!model_volume.is_model_part() && !model_volume.is_negative_volume())
continue;
const Transform3d trafo = volume->get_volume_transformation().get_matrix();
if (model_volume.is_model_part()) {
solids.emplace_back(&model_volume.mesh().its, trafo);
densities.emplace_back(density(model_volume));
} else
negatives.emplace_back(&model_volume.mesh().its, trafo);
sliced.push_back({ model_volume.id().id, model_volume.is_negative_volume(), model_volume.is_model_part() ? densities.back() : 0., trafo });
}
const std::vector<SolidBody>* assembly = nullptr;
if (solids.size() > 1 || (!solids.empty() && !negatives.empty())) {
// Coarser while a part is dragged.
const size_t slabs = canvas.is_dragging() ? 100 : 500;
const auto cached = m_bodies.find(object.id().id);
const bool valid = cached != m_bodies.end() && cached->second.slabs >= slabs && cached->second.volumes == sliced;
Bodies& entry = bodies[object.id().id];
entry = valid ? std::move(cached->second) : Bodies{ std::move(sliced), slabs, solid_bodies(solids, densities, negatives, slabs) };
assembly = &entry.bodies;
}
for (const auto& [inst_idx, instance] : instances) {
// The box of its parts, which the object's size shows.
Marker object_marker;
object_marker.assembly = assembly != nullptr;
for (const GLVolume* volume : instance.volumes)
if (object.volumes[volume->volume_idx()]->is_model_part())
object_marker.box.merge(volume->transformed_convex_hull_bounding_box());
if (assembly != nullptr) {
std::vector<Marker> parts;
for (const SolidBody& body : *assembly)
if (body.mass > 0.) {
parts.push_back({ mass_sum(body.transformed(instance.trafo)), body.bounding_box(instance.trafo) });
object_marker.sum.add(parts.back().sum);
}
if (parts.size() > 1)
append(markers[mkBody], std::move(parts));
} else
for (const GLVolume* volume : instance.volumes) {
// The parts the object info's volume sums.
const ModelVolume& model_volume = *object.volumes[volume->volume_idx()];
if (!model_volume.is_model_part())
continue;
const auto [it, inserted] = meshes.try_emplace(model_volume.id().id);
if (inserted) {
const auto cached = m_meshes.find(it->first);
it->second = cached != m_meshes.end() ? cached->second : its_mass_properties(model_volume.mesh().its);
}
MassProperties part = it->second.transformed(volume->world_matrix());
part.mass *= density(model_volume);
object_marker.sum.add(mass_sum(part));
}
if (object_marker.sum.mass > 0.) {
if (const int plate = plate_list.find_instance(obj_idx, inst_idx); plate >= 0) {
plates[plate].sum.add(object_marker.sum);
plates[plate].box.merge(object_marker.box);
}
markers[mkObject].emplace_back(std::move(object_marker));
}
}
}
m_meshes = std::move(meshes);
m_bodies = std::move(bodies);
for (auto& [plate, marker] : plates)
markers[mkPlate].emplace_back(std::move(marker));
return markers;
}
void GLCanvas3D::CenterOfMass::render(GLCanvas3D& canvas)
{
m_drawn = {};
const bool preview = canvas.m_canvas_type == ECanvasType::CanvasPreview;
// The other gizmos work on the surface the marker would cover.
const GLGizmosManager::EType gizmo = canvas.get_gizmos_manager().get_current_type();
if (!wxGetApp().show_center_of_mass() || canvas.m_design_canvas ||
!(canvas.m_canvas_type == ECanvasType::CanvasView3D || (preview && canvas.m_render_preview)) ||
(gizmo != GLGizmosManager::Undefined && gizmo != GLGizmosManager::Move && gizmo != GLGizmosManager::Rotate &&
gizmo != GLGizmosManager::Scale && gizmo != GLGizmosManager::Flatten))
return;
GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
if (shader == nullptr)
return;
// Preview adds markers for what is printed up to the top layer shown.
if (preview) {
const GCodeViewer& gcode_viewer = canvas.get_gcode_viewer();
m_top_layer = gcode_viewer.get_layers_z_range()[1];
const auto add = [this](const GCodeProcessorResult::ObjectMass& mass, MarkerKind kind) {
if (const Sum total = mass.total(); total.mass > 0.)
m_drawn[0][kind].push_back({ total, mass.box, mass.assembly });
if (!mass.printed_up_to_layer.empty())
if (const Sum& sum = mass.printed_up_to_layer[std::min(m_top_layer, mass.printed_up_to_layer.size() - 1)]; sum.mass > 0.)
m_drawn[1][kind].push_back({ sum, mass.box, mass.assembly });
};
add(gcode_viewer.get_plate_mass(), mkPlate);
for (const GCodeProcessorResult::ObjectMass& object : gcode_viewer.get_object_masses())
add(object, mkObject);
for (const GCodeProcessorResult::ObjectMass& body : gcode_viewer.get_body_masses())
add(body, mkBody);
for (const GCodeProcessorResult::ObjectMass& support : gcode_viewer.get_support_masses())
add(support, mkSupport);
} else
m_drawn[0] = model_markers(canvas);
if (std::all_of(m_drawn.begin(), m_drawn.end(),
[](const Markers& markers) { return std::all_of(markers.begin(), markers.end(), [](const auto& kind) { return kind.empty(); }); }))
return;
if (!m_octants[0].is_initialized()) {
// A resolution divisible by 4 puts every triangle within one octant.
const GLModel::Geometry sphere = smooth_sphere(32, 1.f);
std::array<GLModel::Geometry, 2> octants;
for (size_t i = 0; i + 2 < sphere.indices_count(); i += 3) {
const std::array<unsigned int, 3> ids = { sphere.extract_index(i), sphere.extract_index(i + 1), sphere.extract_index(i + 2) };
const Vec3f c = sphere.extract_position_3(ids[0]) + sphere.extract_position_3(ids[1]) + sphere.extract_position_3(ids[2]);
GLModel::Geometry& octant = octants[c.x() * c.y() * c.z() > 0.f ? 0 : 1];
for (const unsigned int id : ids)
octant.add_vertex(sphere.extract_position_3(id), sphere.extract_normal_3(id));
const auto n = (unsigned int)octant.vertices_count();
octant.add_triangle(n - 3, n - 2, n - 1);
}
for (size_t i = 0; i < octants.size(); ++i)
m_octants[i].init_from(std::move(octants[i]));
}
const Camera& camera = wxGetApp().plater()->get_camera();
const Transform3d& view_matrix = camera.get_view_matrix();
const double scale = marker_scale(canvas) * camera.get_inv_zoom();
// Seen through the object it lies in; culling keeps the sphere's far half behind its near one.
glsafe(::glDisable(GL_DEPTH_TEST));
glsafe(::glEnable(GL_CULL_FACE));
shader->start_using();
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
shader->set_uniform("view_normal_matrix", (Matrix3d)view_matrix.matrix().block(0, 0, 3, 3));
shader->set_uniform("emission_factor", 0.1f);
const std::array<std::array<ColorRGBA, 2>, mkCount> colors = { {
{ ColorRGBA(0.1f, 0.1f, 0.1f, 1.f), ColorRGBA::WHITE() },
{ ColorRGBA(0x5A / 255.f, 0x9B / 255.f, 0xD4 / 255.f, 1.f), ColorRGBA::WHITE() },
{ ColorRGBA(0.f, 0.6f, 0.f, 1.f), ColorRGBA(0.1f, 0.1f, 0.1f, 1.f) },
{ ColorRGBA(0.7f, 0.f, 0.f, 1.f), ColorRGBA::YELLOW() },
} };
const auto draw = [&](const Markers& markers, float alpha) {
for (size_t kind = 0; kind < mkCount; ++kind)
for (const Marker& marker : markers[kind]) {
shader->set_uniform("view_model_matrix", view_matrix * Geometry::translation_transform(marker.center()) *
Geometry::scale_transform(marker_radii[kind] * scale));
for (size_t i = 0; i < m_octants.size(); ++i) {
ColorRGBA color = colors[kind][i];
color.a(alpha);
m_octants[i].set_color(color);
m_octants[i].render();
}
}
};
// Preview fades the finished parts' markers under those of what is printed so far.
draw(m_drawn[0], preview ? 0.4f : 1.f);
draw(m_drawn[1], 1.f);
shader->stop_using();
glsafe(::glEnable(GL_DEPTH_TEST));
}
bool GLCanvas3D::CenterOfMass::on_left_down(GLCanvas3D& canvas, const Vec2d& mouse)
{
const bool shown = m_picked.has_value();
const Camera& camera = wxGetApp().plater()->get_camera();
const double scale = marker_scale(canvas) * camera.get_inv_zoom();
m_picked.reset();
// In the order they cover each other: what is printed so far over the finished print, smaller kinds over larger ones.
for (size_t set = m_drawn.size(); set-- > 0 && !m_picked;)
for (size_t kind = mkCount; kind-- > 0 && !m_picked;)
for (size_t index = 0; index < m_drawn[set][kind].size(); ++index) {
const Vec3d center = m_drawn[set][kind][index].center();
const std::vector<Vec3d> ends = { center, center + marker_radii[kind] * scale * camera.get_dir_right() };
const Points screen = CameraUtils::project(camera, ends);
if ((screen[0].cast<double>() - mouse).norm() <= (screen[1] - screen[0]).cast<double>().norm()) {
m_picked = Pick{ set, kind, index, m_drawn[set][kind].size() };
break;
}
}
if (shown || m_picked)
canvas._set_overlay_as_dirty();
return m_picked.has_value();
}
void GLCanvas3D::CenterOfMass::render_details(GLCanvas3D& canvas)
{
if (!m_picked)
return;
const Pick& pick = *m_picked;
const std::vector<Marker>& markers = m_drawn[pick.set][pick.kind];
// Gone with the markers, or with what it stood for.
if (markers.size() != pick.count) {
m_picked.reset();
return;
}
const Marker& marker = markers[pick.index];
const Sum& sum = marker.sum;
const Vec3d center = marker.center();
// About the axes through the center, from how far the mass spreads along the two others.
const Vec3d spread = (sum.second / sum.mass - center.cwiseProduct(center)).cwiseMax(0.);
const Vec3d inertia = sum.mass * Vec3d(spread.y() + spread.z(), spread.x() + spread.z(), spread.x() + spread.y());
// Beside the marker.
const Point screen = CameraUtils::project(wxGetApp().plater()->get_camera(), center);
ImGuiWrapper& imgui = *wxGetApp().imgui();
imgui.set_next_window_pos(float(screen.x() + 2. * marker_radii[pick.kind] * marker_scale(canvas)), float(screen.y()), ImGuiCond_Always, 0.f, 0.5f);
const std::string title = pick.kind == mkPlate ? _u8L("Plate center of mass") :
pick.kind == mkBody ? _u8L("Part center of mass") :
pick.kind == mkSupport ? _u8L("Support center of mass") :
marker.assembly ? _u8L("Assembly center of mass") :
_u8L("Object center of mass");
bool open = true;
imgui.begin(title + "###center_of_mass", &open,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings);
if (ImGui::IsWindowAppearing())
imgui.set_requires_extra_frame();
if (canvas.get_canvas_type() == ECanvasType::CanvasPreview)
imgui.text(pick.set == 0 ? _u8L("Finished print") : (boost::format(_u8L("Printed up to layer %1%")) % (m_top_layer + 1)).str());
// Masses are in mg, volumes in mm³.
const auto xyz = [](const Vec3d& v, const char* format, const std::string& unit) {
return (boost::format(format) % v.x() % v.y() % v.z()).str() + " " + unit;
};
if (ImGui::BeginTable("##center_of_mass_details", 2)) {
const auto row = [](const std::string& label, const std::string& value) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORCA, label);
ImGui::TableSetColumnIndex(1);
ImGuiWrapper::text(value);
};
row(_u8L("Weight"), (boost::format("%.2f g") % (sum.mass / 1000.)).str());
row(_u8L("Volume"), (boost::format(u8"%.2f cm³") % (sum.volume / 1000.)).str());
if (marker.box.defined) {
row(_u8L("Center in bounding box"), xyz(center - marker.box.min, "X: %.2f, Y: %.2f, Z: %.2f", _u8L("mm")));
row(_u8L("Bounding box size"), xyz(marker.box.size(), "X: %.2f, Y: %.2f, Z: %.2f", _u8L("mm")));
}
row(_u8L("Moment of inertia"), xyz(inertia / 1000., "X: %.0f, Y: %.0f, Z: %.0f", u8"g·mm²"));
ImGui::EndTable();
}
imgui.end();
if (!open) {
m_picked.reset();
canvas._set_overlay_as_dirty();
}
}
void GLCanvas3D::Tooltip::set_text(const std::string& text) void GLCanvas3D::Tooltip::set_text(const std::string& text)
{ {
// If the mouse is inside an ImGUI dialog, then the tooltip is suppressed. // If the mouse is inside an ImGUI dialog, then the tooltip is suppressed.
@@ -2520,6 +2842,9 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
m_frame_profiler.mark("ssao"); m_frame_profiler.mark("ssao");
} }
// After the occlusion pass, which would shade it as the surface behind it.
m_center_of_mass.render(*this);
if (_is_fxaa_enabled()) { if (_is_fxaa_enabled()) {
_render_fxaa_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height())); _render_fxaa_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height()));
m_frame_profiler.mark("fxaa"); m_frame_profiler.mark("fxaa");
@@ -4513,6 +4838,12 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
return; return;
} }
// A click on a center of mass marker shows its details instead of selecting.
if (evt.LeftDown() && !mouse_in_layer_editing && m_center_of_mass.on_left_down(*this, pos.cast<double>())) {
m_mouse.ignore_left_up = true;
return;
}
bool any_gizmo_active = m_gizmos.get_current() != nullptr; bool any_gizmo_active = m_gizmos.get_current() != nullptr;
std::map<MouseButton, MouseAction> button_mappings; std::map<MouseButton, MouseAction> button_mappings;
@@ -9363,6 +9694,7 @@ void GLCanvas3D::_render_overlays()
}*/ }*/
} }
m_labels.render(sorted_instances); m_labels.render(sorted_instances);
m_center_of_mass.render_details(*this);
_render_3d_navigator(); _render_3d_navigator();
@@ -10350,6 +10682,12 @@ void GLCanvas3D::_render_canvas_toolbar()
[p]{p->show_view3D_labels(!p->are_view3D_labels_shown());} [p]{p->show_view3D_labels(!p->are_view3D_labels_shown());}
); );
create_menu_item( _utf8(L("Center of mass")),
m_canvas_type != ECanvasType::CanvasAssembleView && !m_design_canvas, // work on prepare and preview
wxGetApp().show_center_of_mass(),
[this]{wxGetApp().toggle_show_center_of_mass(); m_dirty = true;}
);
// Belt printers, G-code preview only: show the raw machine-frame G-code instead of // Belt printers, G-code preview only: show the raw machine-frame G-code instead of
// the designed (upright) view. This menu is the only place the toggle lives (plus // the designed (upright) view. This menu is the only place the toggle lives (plus
// its shortcut); the reload is deferred (CallAfter) so the preview is not rebuilt // its shortcut); the reload is deferred (CallAfter) so the preview is not rebuilt
+68
View File
@@ -2,6 +2,9 @@
#define slic3r_GLCanvas3D_hpp_ #define slic3r_GLCanvas3D_hpp_
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/ConnectedBodies.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "slic3r/GUI/3DScene.hpp" #include "slic3r/GUI/3DScene.hpp"
#include <cstdlib> #include <cstdlib>
#include <imgui.h> #include <imgui.h>
@@ -30,6 +33,7 @@
#include "Gizmos/GLGizmosManager.hpp" #include "Gizmos/GLGizmosManager.hpp"
#include "GUI_ObjectLayers.hpp" #include "GUI_ObjectLayers.hpp"
#include "GLSelectionRectangle.hpp" #include "GLSelectionRectangle.hpp"
#include "GLModel.hpp"
#include "MeshUtils.hpp" #include "MeshUtils.hpp"
#include "GCodeViewer.hpp" #include "GCodeViewer.hpp"
#include "Camera.hpp" #include "Camera.hpp"
@@ -477,6 +481,69 @@ class GLCanvas3D
void render(const std::vector<const ModelInstance*>& sorted_instances) const; void render(const std::vector<const ModelInstance*>& sorted_instances) const;
}; };
class CenterOfMass
{
using Sum = GCodeProcessorResult::ObjectMass::Sum;
enum MarkerKind : size_t { mkPlate, mkObject, mkSupport, mkBody, mkCount };
// A marker's mass and the box of what it stands for.
struct Marker
{
Sum sum;
BoundingBoxf3 box;
// Of an object, whether it is an assembly.
bool assembly{ false };
Vec3d center() const { return sum.moment / sum.mass; }
};
// The plates', each object instance's, its supports' and each body of an assembly's.
using Markers = std::array<std::vector<Marker>, mkCount>;
// The marker's two colors of alternating octants.
std::array<GLModel, 2> m_octants;
// Mass properties at unit density of each ModelVolume's mesh, by ModelVolume id, which a new mesh changes.
std::map<size_t, MassProperties> m_meshes;
// The connected bodies of each assembly in its own coordinates, by ModelObject id, with the volumes they were sliced from.
struct Bodies
{
struct Volume
{
size_t id;
bool negative;
double density;
Transform3d trafo;
bool operator==(const Volume& other) const
{
return id == other.id && negative == other.negative && density == other.density && trafo.matrix() == other.trafo.matrix();
}
};
std::vector<Volume> volumes;
size_t slabs{ 0 };
std::vector<SolidBody> bodies;
};
std::map<size_t, Bodies> m_bodies;
// The markers drawn last: of the finished print and, in Preview, of what is printed up to the top layer shown.
std::array<Markers, 2> m_drawn;
size_t m_top_layer{ 0 };
// The marker whose details are shown, with the number of its kind then.
struct Pick
{
size_t set;
size_t kind;
size_t index;
size_t count;
};
std::optional<Pick> m_picked;
Markers model_markers(const GLCanvas3D& canvas);
public:
void render(GLCanvas3D& canvas);
// Shows the details of the marker under the mouse, else hides them; whether it hit one.
bool on_left_down(GLCanvas3D& canvas, const Vec2d& mouse);
void render_details(GLCanvas3D& canvas);
};
class Tooltip class Tooltip
{ {
std::string m_text; std::string m_text;
@@ -733,6 +800,7 @@ private:
int m_selected_extruder; int m_selected_extruder;
Labels m_labels; Labels m_labels;
CenterOfMass m_center_of_mass;
Tooltip m_tooltip; Tooltip m_tooltip;
bool m_tooltip_enabled{ true }; bool m_tooltip_enabled{ true };
Slope m_slope; Slope m_slope;
+3
View File
@@ -435,6 +435,9 @@ public:
bool show_outline() const { return app_config->get_bool("show_outline"); } bool show_outline() const { return app_config->get_bool("show_outline"); }
void toggle_show_outline() const { app_config->set_bool("show_outline", !show_outline()); } void toggle_show_outline() const { app_config->set_bool("show_outline", !show_outline()); }
bool show_center_of_mass() const { return app_config->get_bool("show_center_of_mass"); }
void toggle_show_center_of_mass() const { app_config->set_bool("show_center_of_mass", !show_center_of_mass()); }
wxString get_inf_dialog_contect () {return m_info_dialog_content;}; wxString get_inf_dialog_contect () {return m_info_dialog_content;};
std::vector<std::string> split_str(std::string src, std::string separator); std::vector<std::string> split_str(std::string src, std::string separator);
+3 -1
View File
@@ -113,7 +113,7 @@ Vec3f GLGizmoPainterBase::get_tilt_up_direction() const
return build_plate_tilt_up_direction().cast<float>(); return build_plate_tilt_up_direction().cast<float>();
} }
void GLGizmoPainterBase::render_triangles(const Selection& selection) const void GLGizmoPainterBase::render_triangles(const Selection& selection, const ModelVolume* skip) const
{ {
auto* shader = wxGetApp().get_shader("mm_gouraud"); auto* shader = wxGetApp().get_shader("mm_gouraud");
if (!shader) if (!shader)
@@ -135,6 +135,8 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const
continue; continue;
++mesh_id; ++mesh_id;
if (mv == skip)
continue;
Transform3d trafo_matrix; Transform3d trafo_matrix;
if (m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView) { if (m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView) {
+8 -2
View File
@@ -48,6 +48,9 @@ public:
virtual ~TriangleSelectorGUI() = default; virtual ~TriangleSelectorGUI() = default;
virtual void render(ImGuiWrapper* imgui, const Transform3d& matrix); virtual void render(ImGuiWrapper* imgui, const Transform3d& matrix);
// The seed-fill contour alone, as render() last built it - for a gizmo that draws over the selector
// and has to put the contour back on top.
void render_paint_contour(const Transform3d& matrix);
//void render(const Transform3d& matrix) { this->render(nullptr, matrix); } //void render(const Transform3d& matrix) { this->render(nullptr, matrix); }
void set_wireframe_needed(bool need_wireframe) { m_need_wireframe = need_wireframe; } void set_wireframe_needed(bool need_wireframe) { m_need_wireframe = need_wireframe; }
bool get_wireframe_needed() { return m_need_wireframe; } bool get_wireframe_needed() { return m_need_wireframe; }
@@ -90,7 +93,6 @@ protected:
GLModel m_paint_contour; GLModel m_paint_contour;
void update_paint_contour(); void update_paint_contour();
void render_paint_contour(const Transform3d& matrix);
bool m_need_wireframe {false}; bool m_need_wireframe {false};
}; };
@@ -231,7 +233,8 @@ public:
bool on_mouse(const wxMouseEvent &mouse_event) override; bool on_mouse(const wxMouseEvent &mouse_event) override;
protected: protected:
virtual void render_triangles(const Selection& selection) const; // Draws every model part's selector, except `skip`'s when given.
virtual void render_triangles(const Selection& selection, const ModelVolume* skip = nullptr) const;
void render_cursor(); void render_cursor();
void render_cursor_circle(); void render_cursor_circle();
void render_cursor_sphere(const Transform3d& trafo) const; void render_cursor_sphere(const Transform3d& trafo) const;
@@ -328,6 +331,9 @@ protected:
TriangleSelector::ClippingPlane get_clipping_plane_in_volume_coordinates(const Transform3d &trafo) const; TriangleSelector::ClippingPlane get_clipping_plane_in_volume_coordinates(const Transform3d &trafo) const;
// True while a paint or erase stroke is under way.
bool is_painting() const { return m_button_down != Button::None; }
private: private:
std::vector<std::vector<ProjectedMousePosition>> get_projected_mouse_positions(const Vec2d &mouse_position, double resolution, const std::vector<Transform3d> &trafo_matrices) const; std::vector<std::vector<ProjectedMousePosition>> get_projected_mouse_positions(const Vec2d &mouse_position, double resolution, const std::vector<Transform3d> &trafo_matrices) const;
@@ -8,6 +8,7 @@
#include "ColorSpaceConvert.hpp" #include "ColorSpaceConvert.hpp"
#include "libslic3r/AABBTreeIndirect.hpp" #include "libslic3r/AABBTreeIndirect.hpp"
#include "libslic3r/Color.hpp" #include "libslic3r/Color.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "libslic3r/MeshBoolean.hpp" #include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
@@ -69,6 +70,7 @@
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <numeric>
#include <queue> #include <queue>
#include <set> #include <set>
#include <vector> #include <vector>
@@ -257,11 +259,54 @@ TriangleSelector::TriangleSplittingData remap_texture_paint_spatial(
// entry to fill. // entry to fill.
constexpr int PALETTE_LUT_EDGE = 24; constexpr int PALETTE_LUT_EDGE = 24;
// Ceiling on the printable palette, which bounds that fill cost (and the shader's uniform array). // The shaded preview shader's palette arrays. The palette itself stays within the paint mask's
// EnforcerBlockerType::ExtruderMax states, since every entry has to become a filament.
constexpr int PALETTE_MAX_ENTRIES = 64; constexpr int PALETTE_MAX_ENTRIES = 64;
// Ceiling on the filaments the palette's entries can refer to (the shaded preview shader's filament_rgb[]);
// mmu segmentation stops at Extruder16 anyway. // A mix is an interleave that only reads as its colour from a distance; up close it is stripes. So it
constexpr int PALETTE_MAX_FILAMENTS = 16; // is spent only where it beats the nearest single filament by this much (CIEDE2000). Two is about
// where a side-by-side difference stops being arguable; a margin of ten already turns most of a
// greyscale ramp - the shape a height texture actually traces - back into single filaments. The
// quantizer, the mix ranking and the shaded preview shader all apply it, so they agree on where a mix
// is used.
constexpr float PREFER_PURE_DE = 2.f;
// mix_targets(): the most pixels read per layer, and the histogram bins kept over all layers. Together
// they bound rank_mixes() to candidates x MIX_TARGET_BINS colour differences, the same order as filling
// the quantizer's lookup cube.
constexpr size_t MIX_TARGET_SAMPLES = size_t(1) << 20;
constexpr size_t MIX_TARGET_BINS = 256;
// rank_mixes() stops once the best remaining mix would improve the match by less than this, in
// CIEDE2000 averaged over every pixel of the colouring layers (see mix_targets()): a mix that only
// touches a few stray pixels is not worth a filament slot.
constexpr float MIN_MIX_GAIN = 0.05f;
// The project's mixed filament slots, one string each, as the palette cache compares them: anything
// that changes which of them a mix can reuse changes this.
std::vector<std::string> mixed_slot_signature(const DynamicPrintConfig &project_config)
{
std::vector<std::string> out;
const auto *is_mixed = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comps = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
const auto *gradient = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
if (is_mixed == nullptr || comps == nullptr || ratios == nullptr)
return out;
for (size_t i = 0; i < is_mixed->values.size(); ++i)
if (is_mixed->values[i])
out.push_back(std::to_string(i) + ':' + (i < comps->values.size() ? comps->values[i] : std::string()) + '|' +
(i < ratios->values.size() ? ratios->values[i] : std::string()) + '|' +
(gradient != nullptr && i < gradient->values.size() && gradient->values[i] ? "g" : ""));
return out;
}
// Whether two palettes would draw and print the same.
bool same_palette(const std::vector<PrintableColor> &l, const std::vector<PrintableColor> &r)
{
return std::equal(l.begin(), l.end(), r.begin(), r.end(), [](const PrintableColor &x, const PrintableColor &y) {
return x.a == y.a && x.b == y.b && x.num == y.num && x.den == y.den && x.rgb == y.rgb;
});
}
// sRGB (0..1) <-> CIELAB, D65. Exactly what the preview shader's srgb_to_lab() computes, so the CPU // sRGB (0..1) <-> CIELAB, D65. Exactly what the preview shader's srgb_to_lab() computes, so the CPU
// quantizer, the mixed-palette entries and the per-fragment preview all match in the same space. // quantizer, the mixed-palette entries and the per-fragment preview all match in the same space.
@@ -458,6 +503,11 @@ std::string GLGizmoTextureDisplacement::on_get_name() const
return _u8L("Texture displacement"); return _u8L("Texture displacement");
} }
bool GLGizmoTextureDisplacement::on_is_activable() const
{
return m_parent.get_canvas_type() != GLCanvas3D::CanvasAssembleView && GLGizmoPainterBase::on_is_activable();
}
void GLGizmoTextureDisplacement::on_shutdown() void GLGizmoTextureDisplacement::on_shutdown()
{ {
m_parent.toggle_model_objects_visibility(true); m_parent.toggle_model_objects_visibility(true);
@@ -465,10 +515,25 @@ void GLGizmoTextureDisplacement::on_shutdown()
m_shaded_preview_glmodel.reset(); m_shaded_preview_glmodel.reset();
m_paint_overlay_glmodel.reset(); m_paint_overlay_glmodel.reset();
m_paint_overlay_dirty = false; m_paint_overlay_dirty = false;
m_painted_colors = PaintedColors{};
m_painted_colors_key.clear();
m_painted_colors_glmodel.reset();
m_painted_colors_drawn_key.clear();
m_painted_colors_runs.clear();
m_seed_fill_last_mesh_id = -1; // a hover from this session must not count in the next
// Any preview still in flight is superseded: raising the shared counter makes it abort at its next // Any preview still in flight is superseded: raising the shared counter makes it abort at its next
// progress poll, and its completion handler then finds nothing to do. // progress poll, and its completion handler then finds nothing to do.
m_preview_generation->fetch_add(1); m_preview_generation->fetch_add(1);
m_preview_job_pending = false; m_preview_job_pending = false;
// The palette caches hold the last volume's images; a closed gizmo should not keep them alive.
m_palette_cache.clear();
m_palette_quantizer = nullptr;
m_palette_pure_quantizer = nullptr;
m_palette_filaments.clear();
m_palette_images.clear();
m_mix_ranking.reset();
m_palette_slots.clear();
m_palette_changed = false;
m_uvcheck_glmodel.reset(); m_uvcheck_glmodel.reset();
m_wireframe_overlay_glmodel.reset(); m_wireframe_overlay_glmodel.reset();
m_wireframe_overlay_vcount = 0; m_wireframe_overlay_vcount = 0;
@@ -527,22 +592,17 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glEnable(GL_DEPTH_TEST));
// Once anything is painted, m_preview_glmodel holds the true displaced result (same algorithm // Once anything is painted, m_preview_glmodel holds the true displaced result (same algorithm
// Bake uses). The untouched original topology (what render_triangles() draws) coincides // Bake uses). The untouched original topology (what render_triangles() draws) coincides exactly
// exactly with it everywhere except the painted/displaced area, so both are drawn: the real // with it everywhere except the displaced area, so both can be drawn: the real preview geometry
// preview geometry first, then the usual selection-highlight overlay with a small depth bias // first, then the selectors' highlight with a small depth bias so it wins the depth test on the
// so it wins the depth test on the coincident (unpainted) surface - keeping the familiar // coincident surface. Where the surface has actually been displaced, the raised preview geometry
// enforcer/blocker highlight for precise brush editing there. Where the surface has actually // legitimately occludes the flat highlight - that visible relief is itself the "this is painted"
// been displaced, the raised preview geometry legitimately occludes the flat overlay - that // indicator in that area.
// visible relief is itself the "this is painted" indicator in that area.
// //
// The shaded preview is different: it never actually moves geometry (it only shades), so // The shaded preview never actually moves geometry (it only shades), so its depth is identical to
// its depth is identical to the overlay's *everywhere*, not just in the unpainted area - the // the highlight's *everywhere*: the depth-biased opaque highlight would win the depth test across the
// depth-biased opaque overlay would win the depth test across the whole surface and hide the relief // whole surface and hide the shading entirely. So render_triangles() leaves the textured volume out
// shading entirely. So render_triangles() is skipped for it. What is *not* skipped is // there, and the translucent tint drawn further down is the paint feedback instead.
// render_paint_overlay(): leaving the shading as the only paint feedback meant a stroke that
// erased paint, or added it with no texture picked, changed nothing on screen until the whole
// preview rebuilt at stroke end - and in the true-displacement view the opaque overlay is hidden
// by the raised surface for the same reason. The translucent tint covers both cases.
// Coalesced shaded-preview rebuild from an in-progress UV island drag (see on_island_edited): done here, at // Coalesced shaded-preview rebuild from an in-progress UV island drag (see on_island_edited): done here, at
// most once per drawn frame, rather than synchronously in the UV canvas's mouse-move handler. // most once per drawn frame, rather than synchronously in the UV canvas's mouse-move handler.
if (m_use_shaded_preview && m_shaded_preview_dirty) { if (m_use_shaded_preview && m_shaded_preview_dirty) {
@@ -579,24 +639,34 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// Hide the real volume only when something is actually going to be drawn in its place; otherwise // Hide the real volume only when something is actually going to be drawn in its place; otherwise
// put it back. Getting this wrong leaves an invisible model, so it is decided once, here, rather // put it back. Getting this wrong leaves an invisible model, so it is decided once, here, rather
// than per branch below. // than per branch below.
ModelVolume *mv = texture_volume();
m_parent.toggle_model_objects_visibility(true); m_parent.toggle_model_objects_visibility(true);
if (use_shaded || use_true_preview) { if ((use_shaded || use_true_preview) && mv != nullptr)
if (ModelVolume *mv = texture_volume())
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(), m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
m_c->selection_info()->get_active_instance(), mv); m_c->selection_info()->get_active_instance(), mv);
}
// Over a preview, the paint feedback - the selectors' highlight in the Normal view, the tint in both -
// is drawn only while a mouse button is down: during a stroke, a fill click included. Between strokes
// it would cover the preview: the highlight buries it under a flat plane wherever the relief does not
// rise, and the tint washes it green. The debug view shows a captured stage rather than a preview, so
// it keeps the feedback.
const bool paint_feedback = is_painting() || m_debug_stage >= 0;
// Whether the textured volume's selector - and with it a fill tool's contour - is drawn this frame.
bool textured_selector_drawn = true;
if (use_shaded) { if (use_shaded) {
render_shaded_preview_mesh(); render_shaded_preview_mesh();
// The shaded mesh is the textured volume alone, so the other model parts are still the selectors' to draw.
render_triangles(selection, mv);
textured_selector_drawn = false;
} else if (use_true_preview) { } else if (use_true_preview) {
render_preview_mesh(); render_preview_mesh();
if (show_paint_overlay) {
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-1.0f, -1.0f)); glsafe(::glPolygonOffset(-1.0f, -1.0f));
render_triangles(selection); // Without paint feedback, only the other model parts: render_preview_mesh() draws the textured one.
render_triangles(selection, paint_feedback ? nullptr : mv);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
} textured_selector_drawn = paint_feedback;
} else { } else {
// render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the // render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the
// selector's colours), not an overlay on top of one - so it still has to run under a UV-check // selector's colours), not an overlay on top of one - so it still has to run under a UV-check
@@ -607,6 +677,10 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
render_triangles(selection); render_triangles(selection);
} }
// The model's colour paint, over whichever surface was drawn, left out where that surface's preview shows
// paint of its own: every layer's for the Normal mesh, the active layer's otherwise.
const bool painted_colors_drawn = show_paint_overlay && m_debug_stage < 0 && render_painted_colors(use_true_preview);
// Every other layer's paint, in muted grey, so all layers stay visible while one of them is edited. Drawn // Every other layer's paint, in muted grey, so all layers stay visible while one of them is edited. Drawn
// before the active layer's tint so that one reads on top where the two overlap. // before the active layer's tint so that one reads on top where the two overlap.
if (show_paint_overlay) if (show_paint_overlay)
@@ -615,10 +689,32 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// The translucent paint tint. Needed in the shaded view because the opaque highlight above is // The translucent paint tint. Needed in the shaded view because the opaque highlight above is
// skipped there, and in the true-displacement view because the displaced surface rises *above* // skipped there, and in the true-displacement view because the displaced surface rises *above*
// the undisplaced overlay geometry and hides it exactly where the relief is strongest - in both // the undisplaced overlay geometry and hides it exactly where the relief is strongest - in both
// cases leaving an erase stroke with no visible effect until the next full preview rebuild. // cases leaving an erase stroke with no visible effect until the next full preview rebuild. With no
if (show_paint_overlay && (use_shaded || use_true_preview)) // preview, the highlight is the selectors' own, except during a stroke over the colour paint: the paint
// there is not flushed yet, so the colours are still drawn over the stroke's highlight.
const bool preview_drawn = use_shaded || use_true_preview;
if (show_paint_overlay && (preview_drawn ? paint_feedback : painted_colors_drawn && is_painting()))
render_paint_overlay(m_paint_overlay_glmodel); render_paint_overlay(m_paint_overlay_glmodel);
// A fill tool's contour is drawn with the selectors, under the colour paint drawn since - which covers it
// on a steep face, where the paint's slope-scaled offset outruns the contour's fixed one. Put it back on top,
// at the depth its first draw stored, hence LEQUAL. Only where the selector was drawn this frame: drawing it
// is what rebuilds the contour. The tool test matters because the base keeps the last hovered mesh when the
// tool changes.
const bool fill_tool = m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL ||
(m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER);
const int textured_mesh_id = texture_volume_raycaster_index();
if (painted_colors_drawn && textured_selector_drawn && fill_tool && textured_mesh_id >= 0 &&
textured_mesh_id == m_seed_fill_last_mesh_id && size_t(textured_mesh_id) < m_triangle_selectors.size()) {
const ModelObject *mo = m_c->selection_info()->model_object();
GLint depth_func = GL_LESS;
glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &depth_func));
glsafe(::glDepthFunc(GL_LEQUAL));
m_triangle_selectors[size_t(textured_mesh_id)]->render_paint_contour(
mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix());
glsafe(::glDepthFunc(GLenum(depth_func)));
}
// The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane // The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane
// changes its selection in its own mouse handling, and a compare of a few ints per frame is free. // changes its selection in its own mouse handling, and a compare of a few ints per frame is free.
{ {
@@ -1495,10 +1591,9 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
shader->set_uniform("patch_center", m_shaded_patch_center); shader->set_uniform("patch_center", m_shaded_patch_center);
shader->set_uniform("patch_axis", m_shaded_patch_axis); shader->set_uniform("patch_axis", m_shaded_patch_axis);
// The filament palette the mesh's per-triangle indices refer to. Count 0 means "no layer is // The printable palette, in RGB for display and in Lab for the match. Uploaded rather than matched
// colouring", and the shader keeps the model's own colour for every fragment. // on the CPU because the quantization is per fragment here. Count 0 means "no layer is colouring",
// The printable palette, in RGB for display and in Lab for the match. Uploaded rather than // and the shader keeps the model's own colour for every fragment.
// matched on the CPU because the quantization is per fragment here.
const GLTexture *color_tex = get_layer_color_texture(*layer); const GLTexture *color_tex = get_layer_color_texture(*layer);
const int palette_count = const int palette_count =
(color_tex != nullptr) ? int(std::min(m_shaded_preview_palette.size(), size_t(PALETTE_MAX_ENTRIES))) : 0; (color_tex != nullptr) ? int(std::min(m_shaded_preview_palette.size(), size_t(PALETTE_MAX_ENTRIES))) : 0;
@@ -1506,24 +1601,17 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
shader->set_uniform("has_color_tex", color_tex != nullptr); shader->set_uniform("has_color_tex", color_tex != nullptr);
// A flat-colour image is matched against single filaments only, as the bake does. // A flat-colour image is matched against single filaments only, as the bake does.
shader->set_uniform("pure_only", color_tex != nullptr && analyze_texture_detail(*layer).flat_colors); shader->set_uniform("pure_only", color_tex != nullptr && analyze_texture_detail(*layer).flat_colors);
shader->set_uniform("prefer_pure_de", PREFER_PURE_DE);
for (int i = 0; i < palette_count; ++i) { for (int i = 0; i < palette_count; ++i) {
const PaletteEntry &e = m_shaded_preview_palette[size_t(i)]; const PaletteEntry &e = m_shaded_preview_palette[size_t(i)];
const std::string idx = "[" + std::to_string(i) + "]"; const std::string idx = "[" + std::to_string(i) + "]";
// An entry's colour is what it prints as: its filament's, or for a mix its mixed filament slot's.
shader->set_uniform(("palette_rgb" + idx).c_str(), e.rgb); shader->set_uniform(("palette_rgb" + idx).c_str(), e.rgb);
shader->set_uniform(("palette_lab" + idx).c_str(), srgb_to_lab(e.rgb)); shader->set_uniform(("palette_lab" + idx).c_str(), srgb_to_lab(e.rgb));
// How the entry prints: its filament, or for a mix the two it interleaves and in what ratio. // Only so the shader can tell a mix (a != b) from a single filament.
shader->set_uniform(("palette_a" + idx).c_str(), e.a); shader->set_uniform(("palette_a" + idx).c_str(), e.a);
shader->set_uniform(("palette_b" + idx).c_str(), e.b); shader->set_uniform(("palette_b" + idx).c_str(), e.b);
} }
// The filaments those indices refer to, and the interleave the shader resolves a mix with - the
// the mix's smooth average colour. m_palette_filaments is what m_shaded_preview_palette was built from.
const int filament_count =
(palette_count > 0) ? int(std::min(m_palette_filaments.size(), size_t(PALETTE_MAX_FILAMENTS))) : 0;
shader->set_uniform("filament_count", filament_count);
for (int i = 0; i < filament_count; ++i) {
const ColorRGBA &c = m_palette_filaments[size_t(i)];
shader->set_uniform(("filament_rgb[" + std::to_string(i) + "]").c_str(), Vec3f(c.r(), c.g(), c.b()));
}
if (color_tex != nullptr) { if (color_tex != nullptr) {
shader->set_uniform("color_tex", 1); shader->set_uniform("color_tex", 1);
glsafe(::glActiveTexture(GL_TEXTURE1)); glsafe(::glActiveTexture(GL_TEXTURE1));
@@ -1617,6 +1705,150 @@ void GLGizmoTextureDisplacement::rebuild_other_paint_overlay()
m_other_paint_glmodel.set_color(ColorRGBA(0.55f, 0.58f, 0.60f, 0.35f)); m_other_paint_glmodel.set_color(ColorRGBA(0.55f, 0.58f, 0.60f, 0.35f));
} }
std::vector<size_t> GLGizmoTextureDisplacement::PaintedColors::outside(const std::vector<bool> &excluded) const
{
std::vector<size_t> out;
out.reserve(source.size());
for (size_t i = 0; i < source.size(); ++i)
if (size_t(source[i]) >= excluded.size() || !excluded[size_t(source[i])])
out.push_back(i);
return out;
}
GLGizmoTextureDisplacement::PaintedColors GLGizmoTextureDisplacement::painted_colors(const TriangleMesh &mesh,
const TriangleSelector::TriangleSplittingData &paint)
{
PaintedColors out;
TriangleSelector selector(mesh);
selector.deserialize(paint, false);
for (const EnforcerBlockerType state : TriangleSelector::extract_used_facet_states(paint)) {
if (state == EnforcerBlockerType::NONE)
continue;
std::vector<int> source;
const indexed_triangle_set part = selector.get_facets_strict(state, &source);
// Every state comes back over the same vertex array, only the triangles differ.
if (out.facets.vertices.empty())
out.facets.vertices = part.vertices;
out.facets.indices.insert(out.facets.indices.end(), part.indices.begin(), part.indices.end());
out.source.insert(out.source.end(), source.begin(), source.end());
out.state.resize(out.facets.indices.size(), int(state));
}
return out;
}
void GLGizmoTextureDisplacement::rebuild_painted_colors(bool whole_stack)
{
const ModelVolume *mv = texture_volume();
const bool shown = mv != nullptr && any_layer_colors(*mv) && !mv->mmu_segmentation_facets.empty();
// The sub-triangles, keyed on what they were read from: the volume, its mesh and the paint.
std::string key;
if (shown)
key = std::to_string(mv->id().id) + ":" + std::to_string(reinterpret_cast<uintptr_t>(mv->mesh_ptr().get())) + ":" +
std::to_string(mv->mmu_segmentation_facets.timestamp());
if (key != m_painted_colors_key) {
m_painted_colors_key = std::move(key);
m_painted_colors = shown ? painted_colors(mv->mesh(), mv->mmu_segmentation_facets.get_data()) : PaintedColors{};
m_painted_colors_drawn_key.clear();
m_painted_colors_glmodel.reset();
m_painted_colors_runs.clear();
}
if (m_painted_colors.state.empty())
return;
// The part drawn, keyed on whose paint is left out and on that paint.
std::string drawn_key = m_painted_colors_key + (whole_stack ? std::string(":all") : ":" + std::to_string(m_active_layer_slot));
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers)
if (l.slot >= 0 && l.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
drawn_key += "|" + std::to_string(l.slot) + "@" + std::to_string(mv->texture_displacement_facet(l.slot).timestamp());
if (drawn_key == m_painted_colors_drawn_key)
return;
m_painted_colors_drawn_key = std::move(drawn_key);
m_painted_colors_glmodel.reset();
m_painted_colors_runs.clear();
// Whole model triangles, as the facets record what they touch: a triangle the paint only partly covers
// is left to the preview.
std::vector<bool> excluded(mv->mesh().its.indices.size(), false);
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) {
if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS) || (!whole_stack && l.slot != m_active_layer_slot))
continue;
for (const TriangleSelector::TriangleBitStreamMapping &m : mv->texture_displacement_facet(l.slot).get_data().triangles_to_split)
if (m.triangle_idx >= 0 && size_t(m.triangle_idx) < excluded.size())
excluded[size_t(m.triangle_idx)] = true;
}
// One model, its triangles in filament order (painted_colors() groups them), drawn a range per filament.
const std::vector<size_t> kept = m_painted_colors.outside(excluded);
GLModel::Geometry init_data;
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
init_data.reserve_vertices(kept.size() * 3);
init_data.reserve_indices(kept.size() * 3);
unsigned n = 0;
for (const size_t t : kept) {
const int state = m_painted_colors.state[t];
if (m_painted_colors_runs.empty() || m_painted_colors_runs.back().first != state)
m_painted_colors_runs.push_back({ state, { size_t(n), size_t(n) } });
for (int i = 0; i < 3; ++i)
init_data.add_vertex(m_painted_colors.facets.vertices[size_t(m_painted_colors.facets.indices[t][i])]);
init_data.add_triangle(n, n + 1, n + 2);
n += 3;
m_painted_colors_runs.back().second.second = size_t(n);
}
if (!init_data.is_empty())
m_painted_colors_glmodel.init_from(std::move(init_data));
}
bool GLGizmoTextureDisplacement::render_painted_colors(bool whole_stack)
{
rebuild_painted_colors(whole_stack);
const ModelObject *mo = m_c->selection_info()->model_object();
const ModelVolume *mv = texture_volume();
GLShaderProgram *shader = wxGetApp().get_shader("mm_gouraud");
if (mo == nullptr || mv == nullptr || shader == nullptr || !m_painted_colors_glmodel.is_initialized())
return false;
const Selection &selection = m_parent.get_selection();
const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
const Camera &camera = wxGetApp().plater()->get_camera();
const Transform3d &view_matrix = camera.get_view_matrix();
const Matrix3d normal_matrix = trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
const std::vector<ColorRGBA> colors = wxGetApp().plater()->get_extruders_colors();
shader->start_using();
// Set up as render_triangles() sets it up, so the colours are lit, clipped and slope-marked exactly as the
// neutral surface they cover.
const ClippingPlaneDataWrapper clp_data = get_clipping_plane_data();
shader->set_uniform("clipping_plane", clp_data.clp_dataf);
shader->set_uniform("z_range", clp_data.z_range);
shader->set_uniform("view_model_matrix", view_matrix * trafo_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
shader->set_uniform("view_normal_matrix", Matrix3d(view_matrix.matrix().block(0, 0, 3, 3) * normal_matrix));
shader->set_uniform("volume_world_matrix", trafo_matrix);
shader->set_uniform("volume_mirrored", trafo_matrix.matrix().determinant() < 0.);
shader->set_uniform("slope.actived", m_parent.is_using_slope());
shader->set_uniform("slope.volume_world_normal_matrix", Matrix3f(normal_matrix.cast<float>()));
shader->set_uniform("slope.normal_z", float(-std::cos(Geometry::deg2rad(m_highlight_by_angle_threshold_deg))));
shader->set_uniform("slope.up_direction", get_tilt_up_direction());
shader->set_uniform("show_wireframe", false);
// Pulled forward and without depth writes, as the tint is (see render_paint_overlay()). The tint is
// drawn after this, so it shows on top.
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-2.f, -2.f));
glsafe(::glDepthMask(GL_FALSE));
bool drawn = false;
for (const auto &[state, range] : m_painted_colors_runs)
if (state >= 1 && size_t(state) <= colors.size()) {
m_painted_colors_glmodel.set_color(adjust_color_for_rendering(colors[size_t(state - 1)]));
m_painted_colors_glmodel.render(range, shader);
drawn = true;
}
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
shader->stop_using();
return drawn;
}
void GLGizmoTextureDisplacement::rebuild_paint_overlay() void GLGizmoTextureDisplacement::rebuild_paint_overlay()
{ {
m_paint_overlay_glmodel.reset(); m_paint_overlay_glmodel.reset();
@@ -1671,11 +1903,12 @@ void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay)
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix); shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix()); shader->set_uniform("projection_matrix", camera.get_projection_matrix());
// Translucent, and pulled toward the camera so it wins the depth test against the coincident // Translucent, and pulled toward the camera so it wins the depth test against the coincident
// shaded surface. Depth writes are off: this is a tint, and letting it own the depth buffer would // shaded surface - and against the selectors' highlight at -1 in the Normal view, by the full depth
// make the wireframe and seam overlays drawn after it fight with geometry that is not really // unit OpenGL guarantees to tell apart. Depth writes are off: this is a tint, and letting it own the
// there. Blending is already enabled by render_painter_gizmo(). // depth buffer would make the wireframe and seam overlays drawn after it fight with geometry that is
// not really there. Blending is already enabled by render_painter_gizmo().
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-1.5f, -1.5f)); glsafe(::glPolygonOffset(-2.f, -2.f));
glsafe(::glDepthMask(GL_FALSE)); glsafe(::glDepthMask(GL_FALSE));
overlay.render(); overlay.render();
glsafe(::glDepthMask(GL_TRUE)); glsafe(::glDepthMask(GL_TRUE));
@@ -1991,6 +2224,9 @@ void GLGizmoTextureDisplacement::rebuild_preview()
// finishes after the job queued below - and, since the counter is shared with the worker, that // finishes after the job queued below - and, since the counter is shared with the worker, that
// job also notices mid-run and aborts rather than computing a result nobody will use. // job also notices mid-run and aborts rather than computing a result nobody will use.
m_preview_generation->fetch_add(1); m_preview_generation->fetch_add(1);
// Everything rebuilt from here on reads the current palette. Cleared before any of the early returns
// below, which would otherwise leave it set and re-run this every frame.
m_palette_changed = false;
update_uv_editor(); update_uv_editor();
rebuild_shaded_preview_mesh(); rebuild_shaded_preview_mesh();
rebuild_paint_overlay(); rebuild_paint_overlay();
@@ -2061,25 +2297,21 @@ void GLGizmoTextureDisplacement::queue_preview_job()
input.volume_to_world = texture_displacement_volume_to_world(*mv); input.volume_to_world = texture_displacement_volume_to_world(*mv);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data(); input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
// Captured here rather than read in the handler: get_extruders_colors() is main-thread state and // Captured here rather than read in the handler: the palette is main-thread state, and the preview
// the preview has to be grouped against the same palette it was computed with, not whatever is // has to be grouped against the same palette it was computed with, not whatever it is by the time
// loaded by the time it lands. // the result lands.
input.color = color_settings_for(*mv); input.color = color_settings_for(*mv);
// The filament list the result's indices refer to, captured with the job rather than read back // The result names a palette entry per triangle (index + 1), so these are the colours to draw it in:
// when it lands - loading a filament meanwhile must not recolour a preview computed against a // a single filament's own, or for a mix the colour its slot will show once a bake creates it.
// different list. std::vector<ColorRGBA> entry_colors;
// Every extruder, not the palette's physical-only list: the bake writes the filament it resolved entry_colors.reserve(input.color.palette.size());
// to, and a mix resolves to a *mixed filament slot*, which is an extruder past the physical ones. for (const PrintableColor &e : input.color.palette)
// Grouping against the shorter list dropped every triangle carrying such a slot out of the mesh entry_colors.emplace_back(e.rgb.x(), e.rgb.y(), e.rgb.z(), 1.f);
// entirely - the relief vanished and left only the few triangles that happened to print in a plain
// filament. The palette still has to be built from physical filaments alone (see filament_palette()),
// which is why these two are not the same list.
const std::vector<ColorRGBA> filaments = wxGetApp().plater()->get_extruders_colors();
m_preview_job_running = true; m_preview_job_running = true;
auto &worker = wxGetApp().plater()->get_ui_job_worker(); auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementPreviewJob>(std::move(input), generation, m_preview_generation, queue_job(worker, std::make_unique<TextureDisplacementPreviewJob>(std::move(input), generation, m_preview_generation,
[this, filaments](TextureDisplacementPreviewResult result, uint64_t result_generation) { [this, entry_colors](TextureDisplacementPreviewResult result, uint64_t result_generation) {
indexed_triangle_set its = std::move(result.mesh); indexed_triangle_set its = std::move(result.mesh);
m_preview_job_running = false; m_preview_job_running = false;
if (result_generation != m_preview_generation->load()) { if (result_generation != m_preview_generation->load()) {
@@ -2093,14 +2325,11 @@ void GLGizmoTextureDisplacement::queue_preview_job()
} else { } else {
m_preview_glmodel.reset(); m_preview_glmodel.reset();
m_preview_color_runs.clear(); m_preview_color_runs.clear();
if (result.triangle_color.size() == its.indices.size() && !filaments.empty()) { if (result.triangle_color.size() == its.indices.size() && !entry_colors.empty()) {
// Group by *filament*, not by palette entry: what the bake wrote is the resolved
// filament, interleaving already applied, so this shows the real banding rather
// than the flat average the eye will turn it into.
indexed_triangle_set sorted; indexed_triangle_set sorted;
sorted.vertices = its.vertices; sorted.vertices = its.vertices;
sorted.indices.reserve(its.indices.size()); sorted.indices.reserve(its.indices.size());
for (int want = 0; want <= int(filaments.size()); ++want) { for (int want = 0; want <= int(entry_colors.size()); ++want) {
const size_t first = sorted.indices.size(); const size_t first = sorted.indices.size();
for (size_t i = 0; i < its.indices.size(); ++i) for (size_t i = 0; i < its.indices.size(); ++i)
if (int(result.triangle_color[i]) == want) if (int(result.triangle_color[i]) == want)
@@ -2109,7 +2338,7 @@ void GLGizmoTextureDisplacement::queue_preview_job()
continue; continue;
m_preview_color_runs.push_back( m_preview_color_runs.push_back(
{ { first * 3, sorted.indices.size() * 3 }, { { first * 3, sorted.indices.size() * 3 },
want == 0 ? GLVolume::NEUTRAL_COLOR : filaments[size_t(want - 1)] }); want == 0 ? GLVolume::NEUTRAL_COLOR : entry_colors[size_t(want - 1)] });
} }
m_preview_glmodel.init_from(sorted); m_preview_glmodel.init_from(sorted);
} else { } else {
@@ -3366,6 +3595,9 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
const ModelObject *mo = m_c->selection_info()->model_object(); const ModelObject *mo = m_c->selection_info()->model_object();
m_triangle_selectors.clear(); m_triangle_selectors.clear();
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
// still on: a new set of selectors has none.
m_seed_fill_last_mesh_id = -1;
std::vector<ColorRGBA> ebt_colors; std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(GLVolume::NEUTRAL_COLOR); ebt_colors.push_back(GLVolume::NEUTRAL_COLOR);
@@ -4266,84 +4498,101 @@ TextureDisplacementFacetsData GLGizmoTextureDisplacement::facets_data_of(const M
bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv) bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv)
{ {
for (const TextureDisplacementLayer &layer : mv.texture_displacement_layers) for (const TextureDisplacementLayer &layer : mv.texture_displacement_layers)
if (layer.color_enabled && !layer.empty() && decode_height_texture(layer).has_color()) if (layer.color_enabled && !layer.empty() && height_texture_has_color(layer))
return true; return true;
return false; return false;
} }
void GLGizmoTextureDisplacement::bind_mixes_to_filament_slots(std::vector<PaletteEntry> &palette)
{
Sidebar *sidebar = &wxGetApp().plater()->sidebar();
if (sidebar == nullptr)
return;
for (PaletteEntry &e : palette) {
if (!e.is_mix())
continue;
// Components are 1-based in the config; the ratios are percentages summing to 100, which is the
// form create_mixed_filament_from_result() normalises from.
const int a_pct = int(std::lround(100.0 * double(e.num) / double(e.den)));
const int slot = sidebar->ensure_mixed_filament({ unsigned(e.a + 1), unsigned(e.b + 1) },
{ a_pct, 100 - a_pct });
if (slot >= 0) {
e.a = e.b = slot;
e.num = e.den = 1;
} else {
// No room for another slot. Collapse to the component that dominates the blend, which is what
// the old per-triangle path did on a surface it could not band anyway.
const int dominant = (e.num * 2 >= e.den) ? e.a : e.b;
e.a = e.b = dominant;
e.num = e.den = 1;
}
}
}
TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelVolume &mv) TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelVolume &mv)
{ {
TextureColorSettings out; TextureColorSettings out;
if (!any_layer_colors(mv)) if (!any_layer_colors(mv))
return out; // nothing is colouring: every colour path stays switched off return out; // nothing is colouring: every colour path stays switched off
out.palette = cached_palette(); out.palette = cached_palette();
out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false, PALETTE_MAX_ENTRIES); out.palette_pure = make_palette(m_palette_filaments, {});
// Done here rather than in cached_palette(): this runs when a preview or a bake is queued, off a
// user action, while that one is also touched from the render path - and creating filament slots
// there would mutate the project mid-frame.
bind_mixes_to_filament_slots(out.palette);
out.despeckle_passes = mv.texture_displacement_options.color_despeckle; out.despeckle_passes = mv.texture_displacement_options.color_despeckle;
return out; return out;
} }
const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDisplacement::cached_palette() const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDisplacement::cached_palette()
{ {
// Rebuilt only when the loaded filaments or the mixing setting actually change. The shaded preview // Rebuilt only when something it depends on actually changes. The shaded preview rebuilds on every
// rebuilds on every paint stroke and the subdivide preview on every slider frame, and filling the // paint stroke and the panel asks every frame, while ranking the mixes and filling the quantizer's
// quantizer's lookup cube for a 64-entry palette is tens of milliseconds - paying that per stroke // lookup cube each take tens of milliseconds - paying that per stroke is the difference between
// is the difference between painting that keeps up and painting that stutters. // painting that keeps up and painting that stutters.
//
// Two levels. The ranking depends only on the filaments and the images, so dragging the count, or a
// bake creating slots, re-picks from it without ranking again; the palette and its quantizers depend
// on that pick as well.
const ModelVolume *mv = texture_volume(); const ModelVolume *mv = texture_volume();
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled; const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
const int mix_count = mv != nullptr ? std::max(0, mv->texture_displacement_options.color_mix_count) : 0;
std::vector<ColorRGBA> filaments = filament_palette(); std::vector<ColorRGBA> filaments = filament_palette();
// Every mix costs a filament slot once they are bound to one, and the mask can name only so many // The images themselves rather than their addresses, so a freed and reallocated image can never
// states, so the palette has to leave room beside the physical filaments it already counts. // pass for the old one. Whether one has colour, and whether its colours are flat, follows from it.
const int cap = int(EnforcerBlockerType::ExtruderMax); std::vector<std::shared_ptr<std::vector<unsigned char>>> images;
if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing || if (mv != nullptr)
cap != m_palette_cap) { for (const TextureDisplacementLayer &layer : mv->texture_displacement_layers)
if (layer.color_enabled && !layer.empty())
images.push_back(layer.image_data);
const bool ranking_stale = filaments != m_palette_filaments || images != m_palette_images;
if (ranking_stale) {
m_palette_filaments = std::move(filaments); m_palette_filaments = std::move(filaments);
m_palette_images = std::move(images);
m_mix_ranking.reset();
}
if (mixing && !m_mix_ranking)
m_mix_ranking = rank_mixes(m_palette_filaments, mix_targets(mv->texture_displacement_layers),
int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
// Which mixes the project can still print: those it has a fixed slot for, plus as many new ones as
// there are free slots.
const PresetBundle &bundle = *wxGetApp().preset_bundle;
const int free_slots = std::max(0, int(EnforcerBlockerType::ExtruderMax) - int(bundle.filament_presets.size()));
const std::vector<std::string> slots = mixed_slot_signature(bundle.project_config);
if (ranking_stale || m_palette_cache.empty() || mixing != m_palette_mixing || mix_count != m_palette_mix_count ||
free_slots != m_palette_free_slots || slots != m_palette_slots) {
m_palette_mixing = mixing; m_palette_mixing = mixing;
m_palette_cap = cap; m_palette_mix_count = mix_count;
m_palette_cache = make_palette(m_palette_filaments, mixing, cap); m_palette_free_slots = free_slots;
m_palette_quantizer = make_palette_quantizer(m_palette_cache); m_palette_slots = slots;
std::vector<PaletteEntry> mixes;
if (mixing && m_mix_ranking)
mixes = pick_mixes(*m_mix_ranking, mix_count, free_slots, [&bundle](const PaletteEntry &e) {
return find_fixed_mixed_filament(bundle.project_config, {unsigned(e.a + 1), unsigned(e.b + 1)},
{e.a_percent(), 100 - e.a_percent()}) >= 0;
});
std::vector<PaletteEntry> palette = make_palette(m_palette_filaments, mixes);
// The previews keep what they were drawn with, so a palette that really changed under them -
// a filament or a mixed slot edited in the sidebar - has to send them round again.
m_palette_changed = m_palette_changed || (!m_palette_cache.empty() && !same_palette(palette, m_palette_cache));
m_palette_cache = std::move(palette);
m_palette_quantizer = nullptr;
m_palette_pure_quantizer = nullptr;
} }
return m_palette_cache; return m_palette_cache;
} }
std::pair<ColorQuantizeFn, ColorQuantizeFn> GLGizmoTextureDisplacement::palette_quantizers()
{
cached_palette();
if (!m_palette_quantizer) {
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
const bool has_mixes = m_palette_cache.size() > m_palette_filaments.size();
m_palette_pure_quantizer = has_mixes ? make_palette_quantizer(make_palette(m_palette_filaments, {})) : m_palette_quantizer;
}
return { m_palette_quantizer, m_palette_pure_quantizer };
}
std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette() std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
{ {
std::vector<ColorRGBA> all = wxGetApp().plater()->get_extruders_colors(); std::vector<ColorRGBA> all = wxGetApp().plater()->get_extruders_colors();
// Physical filaments only. The mixes this palette produces each become a mixed filament slot of // Physical filaments only. A mix bakes into a mixed filament slot of its own, and those slots are
// their own (see bind_mixes_to_filament_slots()), and those slots are extruders too - so taking the // extruders too - mixing them again would hand a blend components naming a virtual slot, where it
// list as it comes meant the next rebuild mixed *them* again, and handed components naming a // can only name physical ones ("Mixed filament has invalid or mismatched components"). Mixed slots
// virtual slot to a blend that can only name physical ones. That is what left entries reading // are kept after the physical ones, so filament i here is extruder i.
// "filament 1 plus nothing" and raised "Mixed filament has invalid or mismatched components".
const auto *is_mixed = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed"); const auto *is_mixed = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
std::vector<ColorRGBA> palette; std::vector<ColorRGBA> palette;
palette.reserve(all.size()); palette.reserve(all.size());
@@ -4357,42 +4606,209 @@ std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
return palette; return palette;
} }
std::vector<GLGizmoTextureDisplacement::MixTarget> GLGizmoTextureDisplacement::mix_targets(
const std::vector<TextureDisplacementLayer> &layers)
{
// Each layer's bins, weighted by their share of that layer's pixels.
std::vector<std::vector<MixTarget>> per_layer;
for (const TextureDisplacementLayer &layer : layers) {
if (!layer.color_enabled || layer.empty() || analyze_texture_detail(layer).flat_colors)
continue;
TextureDisplacementLayer raw = layer;
raw.smoothing = 0.f;
const DecodedHeightTexture tex = decode_height_texture(raw);
if (!tex.has_color())
continue;
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::make_palette( // 16 levels per channel, each bin keeping the mean of the colours that fell in it: a coarse grid
const std::vector<ColorRGBA> &filaments, bool mixing, int max_entries) // to gather on, without snapping every colour to a bin corner.
struct Bin
{
double r = 0., g = 0., b = 0.;
uint32_t n = 0;
};
std::vector<Bin> bins(size_t(16 * 16 * 16));
// Past the budget, one pixel from each run of `stride`, at an offset jittered by a fixed-seed
// generator. A fixed step would sample a lattice that a striped texture can line up with, so
// that only one of its colours is ever seen; jittered, stripes of any period or orientation are
// sampled in proportion, and the same image still always gives the same targets. The offset
// comes from the generator's high bits: a power-of-two LCG's low bits repeat every 2, 4, 8...
// steps, and the stride is a power of two for exactly the images large enough to need this.
const size_t npx = size_t(tex.width) * size_t(tex.height);
const size_t stride = std::max<size_t>(1, npx / MIX_TARGET_SAMPLES);
uint64_t state = 0x9E3779B97F4A7C15ull;
size_t sampled = 0;
for (size_t start = 0; start < npx; start += stride) {
state = state * 6364136223846793005ull + 1442695040888963407ull;
const size_t i = start + size_t((uint64_t(uint32_t(state >> 32)) * uint64_t(stride)) >> 32);
if (i >= npx)
break;
const uint8_t *px = &tex.rgb[i * 3];
Bin &bin = bins[size_t(px[0] >> 4) * 256 + size_t(px[1] >> 4) * 16 + size_t(px[2] >> 4)];
bin.r += px[0];
bin.g += px[1];
bin.b += px[2];
++bin.n;
++sampled;
}
std::vector<MixTarget> targets;
for (const Bin &bin : bins)
if (bin.n > 0) {
const double inv = 1. / (255. * double(bin.n));
targets.push_back({ srgb_to_lab(Vec3f(float(bin.r * inv), float(bin.g * inv), float(bin.b * inv))),
float(double(bin.n) / double(sampled)) });
}
per_layer.push_back(std::move(targets));
}
// The layers weigh the same and together 1, settled before the pruning below: what rank_mixes() sums
// over the kept bins is then a mean over every pixel of the colouring layers, with the pixels of a
// dropped bin counted as no better off.
std::vector<MixTarget> out;
for (std::vector<MixTarget> &targets : per_layer)
for (MixTarget &t : targets) {
t.weight /= float(per_layer.size());
out.push_back(t);
}
if (out.size() > MIX_TARGET_BINS) {
std::partial_sort(out.begin(), out.begin() + MIX_TARGET_BINS, out.end(),
[](const MixTarget &l, const MixTarget &r) { return l.weight > r.weight; });
out.resize(MIX_TARGET_BINS);
}
return out;
}
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::rank_mixes(
const std::vector<ColorRGBA> &filaments, const std::vector<MixTarget> &targets, int limit)
{
const int n = int(filaments.size());
if (n < 2 || targets.empty() || limit <= 0)
return {};
// Every pair at every short-cycle ratio, coloured as its slot will be.
std::vector<std::string> hex(filaments.size());
for (int i = 0; i < n; ++i)
hex[size_t(i)] = encode_color(filaments[size_t(i)]);
std::vector<PaletteEntry> candidates;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
for (int den = 2; den <= 6; ++den)
for (int num = 1; num < den; ++num) {
if (std::gcd(num, den) != 1)
continue; // 2/4 is 1/2, already there
PaletteEntry e{ Vec3f::Zero(), i, j, num, den };
ColorRGB blended;
if (!decode_color(blend_color_multi({ hex[size_t(i)], hex[size_t(j)] },
{ e.a_percent(), 100 - e.a_percent() }),
blended))
continue;
e.rgb = Vec3f(blended.r(), blended.g(), blended.b());
candidates.push_back(e);
}
// How far each target is from the nearest single filament, and from every candidate.
const size_t nt = targets.size(), nc = candidates.size();
std::vector<Vec3f> filament_lab(size_t(n), Vec3f::Zero());
for (int i = 0; i < n; ++i)
filament_lab[size_t(i)] = srgb_to_lab(Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()));
const auto de = [](const Vec3f &l, const Vec3f &r) { return DeltaE00(l.x(), l.y(), l.z(), r.x(), r.y(), r.z()); };
std::vector<float> pure_d(nt, std::numeric_limits<float>::max());
for (size_t t = 0; t < nt; ++t)
for (const Vec3f &lab : filament_lab)
pure_d[t] = std::min(pure_d[t], de(targets[t].lab, lab));
std::vector<float> dist(nc * nt);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nc), [&](const tbb::blocked_range<size_t> &range) {
for (size_t c = range.begin(); c < range.end(); ++c) {
const Vec3f lab = srgb_to_lab(candidates[c].rgb);
for (size_t t = 0; t < nt; ++t)
dist[c * nt + t] = de(targets[t].lab, lab);
}
});
// Greedy: each round takes the candidate that lowers the weighted error the most. A candidate only
// counts where it beats the single filament by PREFER_PURE_DE, since everywhere else the quantizer
// picks the filament anyway.
std::vector<float> current = pure_d;
std::vector<char> taken(nc, 0);
std::vector<PaletteEntry> out;
const auto counts = [&](size_t c, size_t t) {
const float d = dist[c * nt + t];
return d < current[t] && d <= pure_d[t] - PREFER_PURE_DE;
};
while (int(out.size()) < limit) {
size_t best = nc;
double best_gain = 0.;
for (size_t c = 0; c < nc; ++c) {
if (taken[c])
continue;
double gain = 0.;
for (size_t t = 0; t < nt; ++t)
if (counts(c, t))
gain += double(targets[t].weight) * double(current[t] - dist[c * nt + t]);
if (gain > best_gain) {
best_gain = gain;
best = c;
}
}
if (best == nc || best_gain < double(MIN_MIX_GAIN))
break;
taken[best] = 1;
out.push_back(candidates[best]);
for (size_t t = 0; t < nt; ++t)
if (counts(best, t))
current[t] = dist[best * nt + t];
}
return out;
}
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::pick_mixes(
const std::vector<PaletteEntry> &ranking, int count, int free_slots, const std::function<bool(const PaletteEntry &)> &reusable)
{ {
std::vector<PaletteEntry> out; std::vector<PaletteEntry> out;
const int n = int(filaments.size()); for (const PaletteEntry &e : ranking) {
for (int i = 0; i < n; ++i) if (int(out.size()) >= count)
out.push_back({ Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()),
i, i, 1, 1 });
if (!mixing || n < 2)
return out;
// How many intermediate steps each pair gets, chosen so the whole palette stays under
// PALETTE_MAX_ENTRIES. Fewer filaments means more room for mixes, which is also what you want:
// with two filaments the mixes are the only way to get anywhere, and with sixteen there is little
// point mixing at all. `den` is also the band/dither repeat, so a small one is a short pattern.
const int pairs = n * (n - 1) / 2;
int steps = 0;
for (int s = 5; s >= 1; --s)
if (n + pairs * s <= max_entries) {
steps = s;
break; break;
// Out of free slots, a later mix that already has one still fits.
if (reusable && reusable(e)) {
out.push_back(e);
} else if (free_slots > 0) {
out.push_back(e);
--free_slots;
}
} }
if (steps == 0)
return out; return out;
const int den = steps + 1; }
for (int i = 0; i < n; ++i) std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::make_palette(
for (int j = i + 1; j < n; ++j) { const std::vector<ColorRGBA> &filaments, const std::vector<PaletteEntry> &mixes)
const Vec3f lab_i = srgb_to_lab(Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b())); {
const Vec3f lab_j = srgb_to_lab(Vec3f(filaments[size_t(j)].r(), filaments[size_t(j)].g(), filaments[size_t(j)].b())); std::vector<PaletteEntry> out;
for (int k = 1; k <= steps; ++k) { out.reserve(filaments.size() + mixes.size());
// k/den of filament i, the rest of j - averaged in Lab, which is what the eye does for (int i = 0; i < int(filaments.size()); ++i)
// when the two are interleaved too finely to resolve. out.push_back({ Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()), i, i, 1, 1 });
const float t = float(k) / float(den); out.insert(out.end(), mixes.begin(), mixes.end());
out.push_back({ lab_to_srgb(lab_i * t + lab_j * (1.f - t)), i, j, k, den }); return out;
}
std::vector<int> GLGizmoTextureDisplacement::palette_filaments(const std::vector<PaletteEntry> &palette,
const std::vector<uint8_t> &triangle_color,
const std::function<int(const PaletteEntry &)> &slot_for_mix)
{
std::vector<char> used(palette.size(), 0);
for (const uint8_t v : triangle_color)
if (v > 0 && size_t(v) <= palette.size())
used[size_t(v) - 1] = 1;
std::vector<int> out(palette.size(), -1);
for (size_t i = 0; i < palette.size(); ++i) {
const PaletteEntry &e = palette[i];
if (!e.is_mix()) {
out[i] = e.a;
} else if (used[i]) {
const int slot = slot_for_mix ? slot_for_mix(e) : -1;
// No room for another slot: the component that dominates the blend is the nearest the print
// can come.
out[i] = slot >= 0 ? slot : (e.num * 2 >= e.den ? e.a : e.b);
} }
} }
return out; return out;
@@ -4434,15 +4850,7 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
best_pure = int(i); best_pure = int(i);
} }
} }
// A mix is an interleave that only reads as its colour from a distance; up close it is // A mix only where it clearly beats the nearest single filament, see PREFER_PURE_DE.
// stripes. So it is spent only where it buys a better match than the nearest single
// filament - but "better" was set at ten Delta E, which is not a visible step, it is a
// different colour. Measured over the whole cube that threshold turned 94% of the
// lookups that wanted a mix back into a pure filament, leaving 38%; along a greyscale
// ramp, the shape a height texture actually traces, it cut 80% to 66%. Two Delta E is
// about where a side-by-side difference stops being arguable, which is the right place
// to start paying for stripes.
constexpr float PREFER_PURE_DE = 2.f;
if (best_pure >= 0 && palette[size_t(best)].is_mix() && best_pure_d - best_d < PREFER_PURE_DE) if (best_pure >= 0 && palette[size_t(best)].is_mix() && best_pure_d - best_d < PREFER_PURE_DE)
best = best_pure; best = best_pure;
(*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best); (*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best);
@@ -4461,7 +4869,7 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh( TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks, const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams &params, const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams &params,
const std::vector<PrintableColor> &palette, const DisplacementProgressFn &progress, const TextureColorSettings &color_settings, const DisplacementProgressFn &progress,
BakeStageRecorder *debug) BakeStageRecorder *debug)
{ {
TextureDisplacementPrepareResult out; TextureDisplacementPrepareResult out;
@@ -4539,10 +4947,13 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// Colour boundaries need triangles of their own - the chord test cannot see them, since // Colour boundaries need triangles of their own - the chord test cannot see them, since
// the height field is perfectly smooth across a change of filament. // the height field is perfectly smooth across a change of filament.
ColorFieldSampler color; ColorFieldSampler color;
if (params.subdiv_color_edge_mm > 0.f && !palette.empty()) // A flat-colour layer is matched against single filaments only, as the bake does, so its
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(palette)); // boundaries are refined where the bake will actually change filament.
// Note the sampler is built on the *quantizer* alone - the refinement follows perceived if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty())
// colour, never the interleaving that realises a mix - the slicer does that per layer. color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette),
make_palette_quantizer(color_settings.palette_pure));
// The refinement follows perceived colour: a mix is one colour here, however the slicer
// interleaves its filaments layer by layer.
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down // "Min edge" is a feature-mode control (it is the floor the curvature test refines down
// to); in plain adaptive mode the target edge length is the only criterion, so the floor // to); in plain adaptive mode the target edge length is the only criterion, so the floor
// must not be allowed to silently override a target the user set below it. // must not be allowed to silently override a target the user set below it.
@@ -4830,9 +5241,9 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
// Same colour criterion Apply will use, so the previewed wireframe is the mesh that commits. // Same colour criterion Apply will use, so the previewed wireframe is the mesh that commits.
ColorFieldSampler color; ColorFieldSampler color;
if (m_subdivide_color_mm > 0.f && any_layer_colors(*mv)) { if (m_subdivide_color_mm > 0.f && any_layer_colors(*mv)) {
cached_palette(); // refreshes m_palette_quantizer if the filaments changed const auto [quantize, quantize_pure] = palette_quantizers();
color = make_combined_color_sampler(mv->mesh().its, mv->texture_displacement_layers, facets, color = make_combined_color_sampler(mv->mesh().its, mv->texture_displacement_layers, facets, quantize,
m_palette_quantizer); quantize_pure);
} }
its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm, its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000, int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
@@ -5344,6 +5755,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
return; return;
ModelVolume *mv = texture_volume(); ModelVolume *mv = texture_volume();
// The palette also follows project state nobody tells this gizmo about - the filaments and mixed
// slots in the sidebar - so it is checked once a frame, and a change re-runs the previews.
cached_palette();
if (m_palette_changed)
m_preview_params_dirty = true;
float scale = m_parent.get_scale(); float scale = m_parent.get_scale();
#ifdef WIN32 #ifdef WIN32
int dpi = get_dpi_for_window(wxGetApp().GetTopWindow()); int dpi = get_dpi_for_window(wxGetApp().GetTopWindow());
@@ -6127,6 +6544,15 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"of filaments can cover a photo or a gradient. An image of flat colors " "of filaments can cover a photo or a gradient. An image of flat colors "
"prints the same either way. Off uses one filament per area.")); "prints the same either way. Off uses one filament per area."));
if (opts.color_mix_enabled) { if (opts.color_mix_enabled) {
cached_palette(); // brings m_palette_filaments up to date
// Every mix can become a filament slot, and there are only so many beside the
// physical filaments.
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created."));
// After the slider, so a change shows in the same frame.
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"), ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str()); int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
} }
@@ -16,11 +16,15 @@
#include "libslic3r/Color.hpp" #include "libslic3r/Color.hpp"
#include <cstddef> #include <cstddef>
#include "libslic3r/TriangleSelector.hpp" #include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "admesh/stl.h"
#include <cstdint> #include <cstdint>
#include <functional>
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include <imgui.h> #include <imgui.h>
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional>
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp" #include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
#include <string> #include <string>
#include <vector> #include <vector>
@@ -58,7 +62,7 @@ public:
const TextureDisplacementFacetsData &masks, const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers, const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementPrepareParams &params, const TextureDisplacementPrepareParams &params,
const std::vector<PrintableColor> &palette, const TextureColorSettings &color_settings,
const DisplacementProgressFn &progress, const DisplacementProgressFn &progress,
// Optional step capture: receives the mesh // Optional step capture: receives the mesh
// after the remesh and after the refinement, // after the remesh and after the refinement,
@@ -70,22 +74,43 @@ public:
using PaletteEntry = PrintableColor; using PaletteEntry = PrintableColor;
// The printable palette: the loaded filaments (clamped to the sixteen mmu_segmentation_facets can // One colour mixes are meant to reach: a bin of the textures' colour histogram, in CIELAB, and how
// address), plus - when `mixing` - every pair of them at evenly spaced ratios. // much of the image it covers.
struct MixTarget
{
Vec3f lab = Vec3f::Zero();
float weight = 0.f;
};
// The colours worth mixing for: those of the colouring layers in `layers` whose image is not made of
// flat colours (TextureDetail::flat_colors - those print in single filaments only, so no mix could
// serve them). The layers weigh the same and, before only the heaviest bins are kept (which is what
// bounds rank_mixes()), together 1. Read from the image as imported, as analyze_texture_detail()
// does, so the Smoothing slider does not move the palette around.
static std::vector<MixTarget> mix_targets(const std::vector<TextureDisplacementLayer> &layers);
// Mixes of pairs of `filaments`, best first and at most `limit` of them. Each is the one that most
// improves the match to `targets` given those ranked before it, counted only where it beats the
// nearest single filament by the quantizer's prefer-pure margin - which is where the quantizer will
// actually pick it. Stops early once another mix would make no noticeable difference, so a texture
// the filaments already cover gets few mixes or none.
// //
// Mixes are averaged in **CIELAB**, not RGB and not subtractively: two filaments interleaved too // Ratios are the short-cycle ones (k/d for d up to 6): the slicer interleaves a mix layer by layer,
// finely to resolve are averaged by the eye, which is what a perceptual space models. Yellow and // and a long cycle prints as visible bands rather than as a colour. A mix's `rgb` is the colour its
// blue banded together read as a desaturated grey-green, and that is what the preview must promise // mixed filament slot will show (blend_color_multi(), as the sidebar computes it), so the match, the
// - blending them subtractively would show a green the printer cannot produce this way. // preview and the slot all agree on what the mix looks like.
// static std::vector<PaletteEntry> rank_mixes(const std::vector<ColorRGBA> &filaments,
// How many ratios depends on how many filaments there are, so the palette stays bounded: the const std::vector<MixTarget> &targets, int limit);
// quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen
// filaments there are already plenty of colours without mixing any of them. // The first `count` mixes of `ranking` the project can give a filament slot to. A mix `reusable`
// `max_entries` bounds the whole palette. It is normally the quantizer's own limit, but when the // reports as already having a fixed slot costs nothing; any other uses up one of `free_slots`, and
// mixes become filament slots it has to be the paint mask's instead: a mask can name only // is skipped once they run out - so the palette never offers a colour a bake could not print.
// EnforcerBlockerType::ExtruderMax states, and every mix now occupies one of them. static std::vector<PaletteEntry> pick_mixes(const std::vector<PaletteEntry> &ranking, int count, int free_slots,
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing, const std::function<bool(const PaletteEntry &)> &reusable);
int max_entries);
// The printable palette: the loaded filaments, entry i being filament i, followed by `mixes`.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments,
const std::vector<PaletteEntry> &mixes);
// Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a // Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a
// plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and // plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and
@@ -97,40 +122,49 @@ public:
// to a worker thread and outlives the palette it was built from. // to a worker thread and outlives the palette it was built from.
static ColorQuantizeFn make_palette_quantizer(const std::vector<PaletteEntry> &palette); static ColorQuantizeFn make_palette_quantizer(const std::vector<PaletteEntry> &palette);
// Turns a palette index plus a position into the filament to print there, interleaving the two // The filament (0-based) each entry of `palette` prints in, by palette index: a single filament is
// filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither // itself, a mix is the slot `slot_for_mix` returns for it. Only the mixes `triangle_color` actually
// uses (palette index + 1 per triangle, 0 for none) are asked for, since asking is what creates a
// slot. A mix that gets no slot (-1) prints in its dominant component; one nothing uses maps to -1.
//
// This is what the bake writes into the paint: a palette index is a filament only for the single
// filaments, while a mix's slot can sit anywhere among the project's mixed slots.
static std::vector<int> palette_filaments(const std::vector<PaletteEntry> &palette,
const std::vector<uint8_t> &triangle_color,
const std::function<int(const PaletteEntry &)> &slot_for_mix);
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer // Everything the jobs need to colour with, for the current volume: the palette, its single-filament
// height, despeckle. Empty when no layer is actually colouring. // part, and the despeckle passes. Empty when no layer is actually colouring. Read-only: no filament
// slot is created here, only when a bake commits (see palette_filaments()).
TextureColorSettings color_settings_for(const ModelVolume &mv); TextureColorSettings color_settings_for(const ModelVolume &mv);
// The printable palette for the current filaments and mixing setting, rebuilt only when either // The printable palette for the current volume and project, rebuilt only when what it depends on
// actually changes - see the definition for why that caching is not optional. // changes - see the definition for why that caching is not optional.
const std::vector<PaletteEntry> &cached_palette(); const std::vector<PaletteEntry> &cached_palette();
// Turns every mix in `palette` into a mixed filament slot and rewrites the entry to name that slot // The quantizers for the cached palette and for its single filaments alone (flat-colour images),
// as a plain filament, so nothing downstream has to know a mix is involved: is_mix() goes false and // filled on first use. Only the subdivide preview matches colours on this thread - the jobs build
// the resolver simply returns it. The per-layer interleaving then happens in the slicer, where it is // their own from the palette they capture - so a palette rebuild, such as every step of a count
// not limited by how fine the mesh is. Entries whose slot could not be created (the paint-state cap) // drag, costs no lookup cube unless that preview asks for one.
// fall back to the nearer of the two components. std::pair<ColorQuantizeFn, ColorQuantizeFn> palette_quantizers();
void bind_mixes_to_filament_slots(std::vector<PaletteEntry> &palette);
std::vector<PaletteEntry> m_palette_cache; std::vector<PaletteEntry> m_palette_cache;
std::vector<ColorRGBA> m_palette_filaments;
int m_palette_cap = 0; // the max_entries m_palette_cache was built with
bool m_palette_mixing = false;
ColorQuantizeFn m_palette_quantizer; ColorQuantizeFn m_palette_quantizer;
ColorQuantizeFn m_palette_pure_quantizer;
// Set when a rebuild changed the palette the previews were drawn with; cleared by rebuild_preview().
bool m_palette_changed = false;
// What the cache was built from.
std::vector<ColorRGBA> m_palette_filaments;
std::vector<std::shared_ptr<std::vector<unsigned char>>> m_palette_images; // the colouring layers' images
std::optional<std::vector<PaletteEntry>> m_mix_ranking; // rank_mixes(), computed on demand
bool m_palette_mixing = false;
int m_palette_mix_count = 0;
int m_palette_free_slots = 0;
std::vector<std::string> m_palette_slots; // the project's mixed slots
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address. // The loaded physical filaments, clamped to the states mmu_segmentation_facets can address.
static std::vector<ColorRGBA> filament_palette(); static std::vector<ColorRGBA> filament_palette();
// The Z band height, in mm. One print layer is the ideal, but the interleave is realised per
// *facet*: a band thinner than the mesh can resolve does not dither, it beats against the triangle
// grid and comes out as broad horizontal stripes - and since MMU segmentation reads facet colour,
// it does so in the print too, not only on screen. The refinement edge is chosen from the model's
// diagonal and knows nothing about the layer height, so the band is rounded up to a whole number of
// layers at least two facet rows tall: still exact on the printer, and representable by the mesh
// that has to carry it. Used by both the bake settings and the preview shader, so the two agree.
// The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet // The Normal preview's triangles, grouped by the palette entry they will print in. Colour is per facet
// and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer // and the palette is small, so the mesh is uploaded once with its index buffer
// sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex // sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex
// colour attribute, and so no change to GLModel's vertex layouts. // colour attribute, and so no change to GLModel's vertex layouts.
// //
@@ -148,6 +182,21 @@ public:
// and so whether the colour criterion and the mmu write ever run. // and so whether the colour criterion and the mmu write ever run.
static bool any_layer_colors(const ModelVolume &mv); static bool any_layer_colors(const ModelVolume &mv);
// The model's own colour paint (mmu_segmentation_facets) as the gizmo draws it over its surface: the
// sub-triangles painted in a filament, grouped by that filament. NONE - the volume's own filament - is
// left out, so those triangles keep the gizmo's neutral, as they do in the preview.
struct PaintedColors
{
indexed_triangle_set facets; // over the paint's whole vertex array
std::vector<int> source; // per triangle of `facets`: the model triangle it lies in
std::vector<int> state; // per triangle of `facets`: its filament state, 1-based
// The triangles of `facets` outside the model triangles `excluded` marks, in order. A model
// triangle past the end of `excluded` is not excluded.
std::vector<size_t> outside(const std::vector<bool> &excluded) const;
};
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
void render_painter_gizmo() override; void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/ // Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
@@ -157,6 +206,9 @@ public:
protected: protected:
void on_render_input_window(float x, float y, float bottom_limit) override; void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override; std::string on_get_name() const override;
// Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn
// with the main canvas's instance transform. The base alone would let the keyboard shortcut open it there.
bool on_is_activable() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
@@ -675,13 +727,9 @@ private:
bool m_shaded_preview_dirty = false; bool m_shaded_preview_dirty = false;
GLModel m_shaded_preview_glmodel; GLModel m_shaded_preview_glmodel;
// Translucent tint over the active layer's painted triangles, drawn on top of whichever preview // Translucent tint over the active layer's painted triangles: the paint feedback over a preview (see
// is showing. The base painter's own opaque paint highlight (render_triangles()) cannot be used // render_painter_gizmo()). Cheap (the painted patch only), translucent so the preview shows through,
// in either preview mode - it is coincident with the surface and simply covers it - so the only // and rebuilt live during a stroke.
// paint feedback the gizmo had was the relief itself, which meant erasing showed nothing at all
// until the stroke ended and the whole preview rebuilt. This is that feedback: cheap (the painted
// patch only), translucent (the preview stays visible through it) and rebuilt live during a
// stroke.
GLModel m_paint_overlay_glmodel; GLModel m_paint_overlay_glmodel;
// The islands selected in the UV editor, tinted on the model so the pane's selection can be seen // The islands selected in the UV editor, tinted on the model so the pane's selection can be seen
// in place. Rebuilt whenever the pane's selection differs from the one it was built for. // in place. Rebuilt whenever the pane's selection differs from the one it was built for.
@@ -699,6 +747,23 @@ private:
GLModel m_other_paint_glmodel; GLModel m_other_paint_glmodel;
std::string m_other_paint_key; std::string m_other_paint_key;
void rebuild_other_paint_overlay(); void rebuild_other_paint_overlay();
// The model's colour paint, drawn over the surface so the colours a bake wrote stay visible - the
// canvas draws no volume while a paint gizmo is open, and the selectors hold only displacement paint.
// Left out wherever the preview on screen shows paint of its own (`whole_stack`: every layer's, as the
// Normal preview does; otherwise the active layer's), since an opaque overlay there would hide that
// preview. Only while a layer colours: it is the colour workflow's result, and every other paint gizmo
// shows the model neutral.
//
// Two levels: the paint's sub-triangles, which take a selector over the whole mesh and change only with
// the paint itself, and the part drawn, which follows every flushed stroke.
PaintedColors m_painted_colors;
std::string m_painted_colors_key;
GLModel m_painted_colors_glmodel;
std::string m_painted_colors_drawn_key;
std::vector<std::pair<int, std::pair<size_t, size_t>>> m_painted_colors_runs; // filament state, index range
void rebuild_painted_colors(bool whole_stack);
// False when there was nothing to draw.
bool render_painted_colors(bool whole_stack);
// Whether render_shaded_preview_mesh() would actually draw something. Checked before the real volume // Whether render_shaded_preview_mesh() would actually draw something. Checked before the real volume
// is hidden: with no layer, no texture or no shader the shaded path draws nothing, and hiding the // is hidden: with no layer, no texture or no shader the shaded path draws nothing, and hiding the
// volume for it left the model invisible. // volume for it left the model invisible.
@@ -712,11 +777,9 @@ private:
int m_shaded_projection_mode = 0; int m_shaded_projection_mode = 0;
Vec3f m_shaded_patch_center = Vec3f::Zero(); Vec3f m_shaded_patch_center = Vec3f::Zero();
Vec3f m_shaded_patch_axis = Vec3f::UnitZ(); Vec3f m_shaded_patch_axis = Vec3f::UnitZ();
// The palette the fast preview's per-triangle filament indices were built against, captured when // The palette the fast preview matches each fragment against, captured with its mesh. Empty when the
// the mesh was. Empty when the active layer is not colouring, which is what tells the shader to // active layer is not colouring, which is what tells the shader to fall back to the model's own
// fall back to the model's own colour. Held rather than re-read at draw time so the indices baked // colour. Every entry carries the colour it prints in, so drawing it needs nothing else.
// into the mesh can never be resolved against a different set of filaments than they were computed
// from - loading a filament mid-session would otherwise recolour a stale preview at random.
std::vector<PaletteEntry> m_shaded_preview_palette; std::vector<PaletteEntry> m_shaded_preview_palette;
// GPU island drag: while an island is dragged in the UV editor, the displacement mesh is baked once (with // GPU island drag: while an island is dragged in the UV editor, the displacement mesh is baked once (with
@@ -4,6 +4,7 @@
#include <functional> #include <functional>
#include <utility> #include <utility>
#include <string> #include <string>
#include <vector>
#include "libslic3r/TextureDisplacement.hpp" #include "libslic3r/TextureDisplacement.hpp"
#include <exception> #include <exception>
#include <cstddef> #include <cstddef>
@@ -114,6 +115,17 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
if (volume == nullptr) if (volume == nullptr)
return; return;
// The filament each palette entry prints in. This is where a mix becomes a mixed filament slot,
// and only a mix the bake actually painted with: the preview never creates one, so the project
// gains only the slots this result needs. Done before anything below names them - adding a slot
// runs ModelVolume::update_extruder_count(), which clamps paint above the old filament count.
Sidebar &sidebar = plater->sidebar();
const std::vector<int> filament_of = GLGizmoTextureDisplacement::palette_filaments(
m_input.color.palette, m_triangle_color, [&sidebar](const PrintableColor &mix) {
return sidebar.ensure_mixed_filament({ unsigned(mix.a + 1), unsigned(mix.b + 1) },
{ mix.a_percent(), 100 - mix.a_percent() });
});
volume->set_mesh(std::move(m_result)); volume->set_mesh(std::move(m_result));
volume->set_new_unique_id(); volume->set_new_unique_id();
volume->calculate_convex_hull(); volume->calculate_convex_hull();
@@ -129,9 +141,11 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data(); const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
if (!existing.bitstream.empty()) if (!existing.bitstream.empty())
selector.deserialize(existing, false); selector.deserialize(existing, false);
for (size_t i = 0; i < m_triangle_color.size(); ++i) for (size_t i = 0; i < m_triangle_color.size(); ++i) {
if (m_triangle_color[i] > 0) const size_t entry = size_t(m_triangle_color[i]);
selector.set_facet(int(i), EnforcerBlockerType(m_triangle_color[i])); if (entry > 0 && entry <= filament_of.size() && filament_of[entry - 1] >= 0)
selector.set_facet(int(i), EnforcerBlockerType(filament_of[entry - 1] + 1));
}
volume->mmu_segmentation_facets.set(selector); volume->mmu_segmentation_facets.set(selector);
} }
@@ -56,8 +56,9 @@ private:
TriangleMesh m_result; TriangleMesh m_result;
// What the bake spent, for the message it leaves behind when the budget capped the detail. // What the bake spent, for the message it leaves behind when the budget capped the detail.
TextureBakeStats m_stats; TextureBakeStats m_stats;
// Per triangle of m_result: the filament to print it in, as an EnforcerBlockerType value // Per triangle of m_result: the palette entry to print it in, as its index + 1 (0 = leave alone).
// (0 = leave alone). Empty unless a layer asked for colour. See TextureColorRequest. // Empty unless a layer asked for colour. See TextureColorRequest; finalize() turns these into
// filaments.
std::vector<uint8_t> m_triangle_color; std::vector<uint8_t> m_triangle_color;
std::function<void()> m_on_finished; std::function<void()> m_on_finished;
}; };
@@ -59,7 +59,7 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
if (m_input.run_prepare && !m_input.options.pipeline_v2) { if (m_input.run_prepare && !m_input.options.pipeline_v2) {
const TextureDisplacementPrepareResult prepared = const TextureDisplacementPrepareResult prepared =
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params, GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
m_input.color.palette, m_input.color,
// Preparation is roughly half the run; the bake // Preparation is roughly half the run; the bake
// takes the progress bar from there. // takes the progress bar from there.
[&report](int pct) { return report(1 + pct / 2); }, [&report](int pct) { return report(1 + pct / 2); },
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
// idle loop. // idle loop.
int last_reported = 1; int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers, m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.params, m_input.color.palette, m_input.params, m_input.color,
[&ctl, &status, &last_reported](int percent) { [&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled()) if (ctl.was_canceled())
return false; return false;
@@ -26,13 +26,13 @@ struct TextureDisplacementPreviewInput
// Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake // Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake
// is and the two cannot disagree. See build_texture_displacement(). // is and the two cannot disagree. See build_texture_displacement().
Transform3d volume_to_world = Transform3d::Identity(); Transform3d volume_to_world = Transform3d::Identity();
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle // Empty unless a layer is colouring, in which case the preview reports the palette entry per
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included. // triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
TextureColorSettings color; TextureColorSettings color;
}; };
// A preview result: the displaced mesh, and - when the input carried a palette - one filament index // A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
// per triangle (an EnforcerBlockerType value; 0 means "no colour from the texture"). // triangle (its index + 1; 0 means "no colour from the texture").
struct TextureDisplacementPreviewResult struct TextureDisplacementPreviewResult
{ {
indexed_triangle_set mesh; indexed_triangle_set mesh;
+9 -50
View File
@@ -5048,35 +5048,15 @@ static bool create_mixed_filament_from_result(
is_mixed_opt->values[new_idx] = true; is_mixed_opt->values[new_idx] = true;
} }
std::string comp_str;
for (size_t i = 0; i < result.components.size(); ++i) {
if (i > 0) comp_str += ",";
comp_str += std::to_string(result.components[i]);
}
{ {
auto* comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); auto* comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
while (comp_opt->values.size() <= new_idx) comp_opt->values.push_back(std::string{}); while (comp_opt->values.size() <= new_idx) comp_opt->values.push_back(std::string{});
comp_opt->values[new_idx] = comp_str; comp_opt->values[new_idx] = format_mixed_components(result.components);
}
int ratio_sum = 0;
for (int r : result.ratios) ratio_sum += r;
if (ratio_sum <= 0) ratio_sum = 100;
std::string ratio_str;
{
CNumericLocalesSetter c_locale_setter;
for (size_t i = 0; i < result.ratios.size(); ++i) {
if (i > 0) ratio_str += ",";
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", (float)result.ratios[i] / ratio_sum);
ratio_str += buf;
}
} }
{ {
auto* ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"); auto* ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
while (ratios_opt->values.size() <= new_idx) ratios_opt->values.push_back(std::string{}); while (ratios_opt->values.size() <= new_idx) ratios_opt->values.push_back(std::string{});
ratios_opt->values[new_idx] = ratio_str; ratios_opt->values[new_idx] = format_mixed_ratios(result.ratios);
} }
if (!project_config.option("filament_mixed_gradient")) if (!project_config.option("filament_mixed_gradient"))
@@ -5136,36 +5116,12 @@ int Sidebar::ensure_mixed_filament(const std::vector<unsigned int> &components,
return -1; return -1;
if (p->combos_filament.size() < 2) if (p->combos_filament.size() < 2)
return -1; return -1;
if (std::accumulate(ratios.begin(), ratios.end(), 0) <= 0)
// Normalise the way create_mixed_filament_from_result() stores them, so the comparison below sees
// the same text the config holds rather than two spellings of one blend.
int ratio_sum = 0;
for (const int r : ratios)
ratio_sum += r;
if (ratio_sum <= 0)
return -1; return -1;
std::string comp_str, ratio_str; if (const int existing = find_fixed_mixed_filament(wxGetApp().preset_bundle->project_config, components, ratios);
{ existing >= 0)
CNumericLocalesSetter c_locale_setter; return existing;
for (size_t i = 0; i < components.size(); ++i) {
if (i > 0) { comp_str += ","; ratio_str += ","; }
comp_str += std::to_string(components[i]);
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", float(ratios[i]) / float(ratio_sum));
ratio_str += buf;
}
}
const auto &project_config = wxGetApp().preset_bundle->project_config;
const auto *is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
if (is_mixed_opt != nullptr && comp_opt != nullptr && ratios_opt != nullptr)
for (size_t i = 0; i < is_mixed_opt->values.size(); ++i)
if (is_mixed_opt->values[i] && i < comp_opt->values.size() && i < ratios_opt->values.size() &&
comp_opt->values[i] == comp_str && ratios_opt->values[i] == ratio_str)
return int(i);
if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)) if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax))
return -1; return -1;
@@ -21230,6 +21186,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config)
opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id") { opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id") {
update_scheduled = true; update_scheduled = true;
} }
// Orca: the center of mass markers weigh the parts by it.
else if (opt_key == "filament_density" && wxGetApp().show_center_of_mass())
p->view3D->get_canvas3d()->set_as_dirty();
} }
if (bed_shape_changed) if (bed_shape_changed)
+4 -3
View File
@@ -292,11 +292,12 @@ public:
// Mixed-color filament sidebar section // Mixed-color filament sidebar section
void add_mixed_filament(); void add_mixed_filament();
// The filament slot that blends `components` (1-based physical filament indices) in `ratios` // The filament slot that blends `components` (1-based physical filament indices) in `ratios`
// (percentages), creating it when no existing mixed slot already describes that blend. Returns the // (percentages) at a fixed ratio, creating it when no existing fixed mixed slot already describes
// 0-based filament index, or -1 when the paint-state cap leaves no room for another one. // that blend (see find_fixed_mixed_filament()). Returns the 0-based filament index, or -1 when the
// paint-state cap leaves no room for another one.
// //
// Exists so a feature that needs a blend can ask for one without going through the modal dialog: // Exists so a feature that needs a blend can ask for one without going through the modal dialog:
// the texture displacement gizmo turns each mix in its palette into a slot, which is what moves the // a texture displacement bake turns each mix it painted with into a slot, which is what moves the
// interleaving from its own paint mask to the slicer, where it happens per layer. // interleaving from its own paint mask to the slicer, where it happens per layer.
int ensure_mixed_filament(const std::vector<unsigned int> &components, const std::vector<int> &ratios); int ensure_mixed_filament(const std::vector<unsigned int> &components, const std::vector<int> &ratios);
void edit_mixed_filament(size_t idx); void edit_mixed_filament(size_t idx);
+233 -12
View File
@@ -6,10 +6,12 @@
#include <catch2/matchers/catch_matchers.hpp> #include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp> #include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "test_helpers.hpp" #include "test_helpers.hpp"
@@ -18,6 +20,7 @@
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
#include <fstream> #include <fstream>
#include <initializer_list>
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include <sstream> #include <sstream>
@@ -99,6 +102,22 @@ TEST_CASE("Reserved keyword detection reports every offending line", "[GCodeProc
namespace { namespace {
void process_gcode(const std::string &gcode, GCodeProcessorResult &result)
{
FullPrintConfig config;
config.gcode_flavor.value = gcfMarlinFirmware;
// s_IsBBLPrinter selects the "; FEATURE: " role tags the G-code uses.
const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter;
const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; });
GCodeProcessor::s_IsBBLPrinter = true;
ScopedTemporaryFile temp(".gcode");
std::ofstream(temp.string()) << gcode;
GCodeProcessor processor;
processor.apply_config(config);
processor.process_file(temp.string());
result = std::move(processor.extract_result());
}
// Closed outer-wall squares, each after a fast travel and before an inner-wall move, so the processor // Closed outer-wall squares, each after a fast travel and before an inner-wall move, so the processor
// records seams and inserts actual speed moves. virtual_moves adds a VG1 move after each square. // records seams and inserts actual speed moves. virtual_moves adds a VG1 move after each square.
void process_squares(int squares, GCodeProcessorResult &result, bool virtual_moves = false) void process_squares(int squares, GCodeProcessorResult &result, bool virtual_moves = false)
@@ -114,18 +133,42 @@ void process_squares(int squares, GCodeProcessorResult &result, bool virtual_mov
if (virtual_moves) if (virtual_moves)
gcode << "VG1 X20 Y30 F12000\n"; gcode << "VG1 X20 Y30 F12000\n";
} }
FullPrintConfig config; process_gcode(gcode.str(), result);
config.gcode_flavor.value = gcfMarlinFirmware; }
// s_IsBBLPrinter selects the "; FEATURE: " role tags this G-code uses.
const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter; // Objects A and B on the first layer and A again on the second, with A's brim and support. The skirt and the
const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; }); // prime tower belong to neither.
GCodeProcessor::s_IsBBLPrinter = true; void process_two_objects(GCodeProcessorResult &result)
ScopedTemporaryFile temp(".gcode"); {
std::ofstream(temp.string()) << gcode.str(); std::ostringstream gcode;
GCodeProcessor processor; gcode << "M83\nG90\n"
processor.apply_config(config); << "; CHANGE_LAYER\n; LAYER_HEIGHT: 0.2\nG1 Z0.2 F12000\n"
processor.process_file(temp.string()); << "; FEATURE: Skirt\nG1 X0 Y100 E5 F3000\n"
result = std::move(processor.extract_result()); << "; FEATURE: Brim\nG1 X8 Y8 F12000\nG1 X12 Y8 E1 F3000\n"
<< "; FEATURE: Support\nG1 X10 Y20 F12000\nG1 X10 Y30 E1 F3000\n"
<< "; FEATURE: Outer wall\nG1 X10 Y10 F12000\nG1 X20 Y10 E1 F3000\n"
<< "; FEATURE: Outer wall\nG1 X50 Y50 F12000\nG1 X60 Y50 E2 F3000\n"
<< "; FEATURE: Prime tower\nG1 X80 Y80 F12000\nG1 X90 Y80 E1 F3000\n"
<< "; CHANGE_LAYER\n; LAYER_HEIGHT: 0.2\nG1 Z0.4 F12000\n"
<< "; FEATURE: Outer wall\nG1 X10 Y10 F12000\nG1 X20 Y10 E1 F3000\n";
process_gcode(gcode.str(), result);
}
// Bead centers of process_two_objects(), half the 0.2 mm layer below the nozzle.
const Vec3d a_brim(10., 8., 0.1), a_support(10., 25., 0.1), a_wall_0(15., 10., 0.1), a_wall_1(15., 10., 0.3), b_wall(55., 50., 0.1);
Vec3d center_of(const GCodeProcessorResult::ObjectMass::Sum &sum) { return sum.moment / sum.mass; }
// One filament, so each bead weighs as much as the E it was extruded with.
Vec3d weighted_center(std::initializer_list<std::pair<double, Vec3d>> beads)
{
double mass = 0.;
Vec3d moment = Vec3d::Zero();
for (const auto &[e, center] : beads) {
mass += e;
moment += e * center;
}
return moment / mass;
} }
bool is_block_move(const GCodeProcessorResult::MoveVertex &move) bool is_block_move(const GCodeProcessorResult::MoveVertex &move)
@@ -281,3 +324,181 @@ TEST_CASE("Rewritten G-code that cannot be re-read keeps the moves and hides the
REQUIRE(result.moves.size() == exported_moves.size()); REQUIRE(result.moves.size() == exported_moves.size());
CHECK(result.moves.back().gcode_id == exported_moves.back().gcode_id); CHECK(result.moves.back().gcode_id == exported_moves.back().gcode_id);
} }
TEST_CASE("The plate's center of mass takes every extrusion of G-code without a print behind it", "[GCodeProcessor]")
{
GCodeProcessorResult result;
process_two_objects(result);
CHECK(result.object_masses.empty());
CHECK(result.body_masses.empty());
const GCodeProcessorResult::ObjectMass &plate = result.plate_mass;
REQUIRE(plate.printed_up_to_layer.size() == 2);
CHECK_THAT((center_of(plate.printed_up_to_layer.front()) -
weighted_center({ { 1., a_brim }, { 1., a_support }, { 1., a_wall_0 }, { 2., b_wall } })).norm(),
Catch::Matchers::WithinAbs(0., 1e-5));
CHECK_THAT((center_of(plate.printed_up_to_layer.back()) -
weighted_center({ { 1., a_brim }, { 1., a_support }, { 1., a_wall_0 }, { 2., b_wall }, { 1., a_wall_1 } })).norm(),
Catch::Matchers::WithinAbs(0., 1e-5));
// Each bead weighs its volume at the default density and spreads along its move, (a^2 + ab + b^2) / 3 for one from
// a to b: the brim from x 8 to 12 at y 8, the support at x 10 from y 20 to 30, A's walls from x 10 to 20 at y 10 and
// B's from x 50 to 60 at y 50 with twice the filament, all at z 0.1 but A's second wall at 0.3.
const GCodeProcessorResult::ObjectMass::Sum total = plate.total();
CHECK_THAT(total.mass / total.volume, Catch::Matchers::WithinRel(double(DEFAULT_FILAMENT_DENSITY), 1e-6));
const Vec3d second = total.second / total.mass;
CHECK_THAT(second.x(), Catch::Matchers::WithinRel((304. / 3. + 100. + 2. * 700. / 3. + 2. * 9100. / 3.) / 6., 1e-6));
CHECK_THAT(second.y(), Catch::Matchers::WithinRel((64. + 1900. / 3. + 2. * 100. + 2. * 2500.) / 6., 1e-6));
CHECK_THAT(second.z(), Catch::Matchers::WithinRel((5. * 0.01 + 0.09) / 6., 1e-5));
// The beads' center lines, brim and support included, from the first layer's bottom to the second's top.
CHECK_THAT((plate.box.min - Vec3d(8., 8., 0.)).norm(), Catch::Matchers::WithinAbs(0., 1e-5));
CHECK_THAT((plate.box.max - Vec3d(60., 50., 0.4)).norm(), Catch::Matchers::WithinAbs(0., 1e-5));
}
TEST_CASE("Each sliced cube's center of mass is its center, and the brim lowers the plate's printed one", "[GCodeProcessor]")
{
const bool copies = GENERATE(false, true);
INFO((copies ? "two copies of one cube" : "two cubes"));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "outer_only" }, { "brim_width", 5 }, { "combine_brims", 0 } });
std::vector<TriangleMesh> cubes{ Test::cube(20) };
if (!copies)
cubes.emplace_back(Test::cube(20));
Print print;
Model model;
Test::init_print(std::move(cubes), print, model, config, nullptr, true, copies ? 2 : 1);
GCodeProcessorResult result;
Test::gcode(print, &result);
CHECK(result.body_masses.empty());
REQUIRE(result.object_masses.size() == 2);
for (const ModelObject *object : model.objects)
for (size_t instance = 0; instance < object->instances.size(); ++instance) {
const Vec3d center = object->instance_bounding_box(instance).center();
const auto mass = std::min_element(result.object_masses.begin(), result.object_masses.end(), [&center](const auto &l, const auto &r) {
return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm();
});
// Off the center only by the infill's alignment and the top and bottom shells.
const Vec3d part = center_of(mass->total());
CHECK_THAT(part.x(), Catch::Matchers::WithinAbs(center.x(), 0.5));
CHECK_THAT(part.y(), Catch::Matchers::WithinAbs(center.y(), 0.5));
CHECK_THAT(part.z(), Catch::Matchers::WithinAbs(center.z(), 1.));
// The outer walls' center lines run half a line inside the cube's sides, of copies touching each other too.
const BoundingBoxf3 box = object->instance_bounding_box(instance);
for (int axis = 0; axis < 3; ++axis) {
CHECK_THAT(mass->box.min[axis], Catch::Matchers::WithinAbs(box.min[axis], 0.3));
CHECK_THAT(mass->box.max[axis], Catch::Matchers::WithinAbs(box.max[axis], 0.3));
}
}
GCodeProcessorResult::ObjectMass::Sum objects;
for (const GCodeProcessorResult::ObjectMass &object : result.object_masses)
objects.add(object.total());
const GCodeProcessorResult::ObjectMass::Sum plate = result.plate_mass.total();
CHECK(plate.mass > objects.mass);
CHECK(center_of(plate).z() < center_of(objects).z());
}
TEST_CASE("Each cube's raft is its support, centered below it", "[GCodeProcessor]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "raft_layers", 3 } });
Print print;
Model model;
Test::init_print({ Test::cube(20), Test::cube(20) }, print, model, config);
GCodeProcessorResult result;
Test::gcode(print, &result);
REQUIRE(result.support_masses.size() == 2);
for (size_t i = 0; i < 2; ++i) {
const GCodeProcessorResult::ObjectMass::Sum support = result.support_masses[i].total();
const Vec3d object = center_of(result.object_masses[i].total());
REQUIRE(support.mass > 0.);
CHECK_THAT(center_of(support).x(), Catch::Matchers::WithinAbs(object.x(), 1.));
CHECK_THAT(center_of(support).y(), Catch::Matchers::WithinAbs(object.y(), 1.));
CHECK(center_of(support).z() < 1.);
}
}
TEST_CASE("A spiral vase cube counts all its extrusions, rising through each layer", "[GCodeProcessor]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "spiral_mode", 1 }, { "wall_loops", 1 },
{ "top_shell_layers", 0 }, { "sparse_infill_density", 0 } });
Print print;
Model model;
Test::init_print({ Test::cube(20) }, print, model, config);
GCodeProcessorResult result;
Test::gcode(print, &result);
REQUIRE(result.object_masses.size() == 1);
CHECK_THAT(result.object_masses.front().total().mass, Catch::Matchers::WithinRel(result.plate_mass.total().mass, 1e-6));
}
TEST_CASE("Each separate part of an assembly gets its center of mass, overlapping parts one", "[GCodeProcessor]")
{
const bool overlapping = GENERATE(false, true);
// Separated infills finds the bodies first, which the G-code export then takes.
const bool separated = GENERATE(false, true);
INFO((overlapping ? "overlapping parts" : "separate parts") << (separated ? ", separated infills" : ""));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "separated_infills", separated ? 1 : 0 } });
TriangleMesh first = make_cube(20, 20, 20);
TriangleMesh second = make_cube(20, 20, 20);
first.translate(50, 50, 0);
second.translate(overlapping ? 60 : 90, 50, 0);
Print print;
Model model;
Test::init_print({ first }, print, model, config, nullptr, false);
model.objects.front()->add_volume(std::move(second), ModelVolumeType::MODEL_PART, false);
print.apply(model, config);
GCodeProcessorResult result;
Test::gcode(print, &result);
REQUIRE(result.object_masses.size() == 1);
CHECK(result.object_masses.front().assembly);
// One body is the object itself.
if (overlapping) {
CHECK(result.body_masses.empty());
return;
}
REQUIRE(result.body_masses.size() == 2);
CHECK(print.objects().front()->separated_body_bboxes().size() == (separated ? 2 : 0));
const ModelObject &object = *model.objects.front();
for (const ModelVolume *volume : object.volumes) {
const Vec3d center = volume->mesh().transformed_bounding_box(object.instances.front()->get_matrix() * volume->get_matrix()).center();
const auto body = std::min_element(result.body_masses.begin(), result.body_masses.end(), [&center](const auto &l, const auto &r) {
return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm();
});
const Vec3d part = center_of(body->total());
CHECK_THAT(part.x(), Catch::Matchers::WithinAbs(center.x(), 0.5));
CHECK_THAT(part.y(), Catch::Matchers::WithinAbs(center.y(), 0.5));
CHECK_THAT(part.z(), Catch::Matchers::WithinAbs(center.z(), 1.));
}
}
TEST_CASE("Each extrusion weighs its filament's density", "[GCodeProcessor]")
{
// Two like cubes, the second's filament three times as dense.
DynamicPrintConfig config = Test::multifilament_config(2, { { "filament_density", "1,3" }, { "skirt_loops", 0 }, { "brim_type", "no_brim" } });
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{ { { "extruder", 1 } }, { { "extruder", 2 } } };
Print print;
Model model;
Test::init_print({ Test::cube(20), Test::cube(20) }, print, model, config, &overrides);
GCodeProcessorResult result;
Test::gcode(print, &result);
REQUIRE(result.object_masses.size() == 2);
std::vector<const GCodeProcessorResult::ObjectMass *> masses;
for (const ModelObject *object : model.objects) {
const Vec3d center = object->instance_bounding_box(0).center();
masses.emplace_back(&*std::min_element(result.object_masses.begin(), result.object_masses.end(), [&center](const auto &l, const auto &r) {
return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm();
}));
}
CHECK_THAT(masses[1]->total().mass / masses[0]->total().mass, Catch::Matchers::WithinRel(3., 0.02));
// The plate's center lies three quarters of the way to the dense cube.
const Vec3d plate = center_of(result.plate_mass.total());
const Vec3d light = center_of(masses[0]->total());
const Vec3d dense = center_of(masses[1]->total());
CHECK_THAT((plate - light).dot(dense - light) / (dense - light).squaredNorm(), Catch::Matchers::WithinAbs(0.75, 0.01));
}
+1
View File
@@ -65,6 +65,7 @@ add_executable(${_TEST_NAME}_tests
test_ordering_strategies.cpp test_ordering_strategies.cpp
# test_png_io.cpp # test_png_io.cpp
test_indexed_triangle_set.cpp test_indexed_triangle_set.cpp
test_connected_bodies.cpp
test_texture_displacement.cpp test_texture_displacement.cpp
test_instance_lock.cpp test_instance_lock.cpp
../libnest2d/printer_parts.cpp ../libnest2d/printer_parts.cpp
+154
View File
@@ -0,0 +1,154 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/ConnectedBodies.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/libslic3r.h"
#include <cstddef>
#include <utility>
#include <vector>
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
namespace {
ExPolygon rectangle(double x, double width) { return ExPolygon(Polygon::new_scale({ { x, 0. }, { x + width, 0. }, { x + width, 10. }, { x, 10. } })); }
} // namespace
TEST_CASE("Islands overlapping their neighbors' make one body", "[ConnectedBodies]")
{
const ExPolygons apart = { rectangle(0., 10.), rectangle(20., 10.) };
const ExPolygons bridge = { rectangle(0., 30.) };
const ExPolygons left = { rectangle(0., 10.) };
const ExPolygons right = { rectangle(20., 10.) };
size_t count = 0;
const std::vector<std::vector<size_t>> stacked = connected_bodies({ &apart, &apart }, count);
CHECK(count == 2);
CHECK(stacked[1][0] == stacked[0][0]);
CHECK(stacked[1][1] == stacked[0][1]);
connected_bodies({ &apart, &bridge, &apart }, count);
CHECK(count == 1);
// Neighbors that do not overlap stay apart even with one island a layer.
connected_bodies({ &left, &right }, count);
CHECK(count == 2);
}
TEST_CASE("The island locator tests the outlines only where boxes overlap", "[ConnectedBodies]")
{
const auto square = [](double from, double to) {
return Polygon::new_scale({ { from, from }, { to, from }, { to, to }, { from, to } });
};
Polygon hole = square(5., 25.);
hole.make_clockwise();
ExPolygon ring(square(0., 30.));
ring.holes.emplace_back(hole);
ExPolygon alone(square(40., 50.));
const ExPolygons islands = { ring, ExPolygon(square(10., 20.)), alone };
const IslandLocator locator(islands, scaled<coord_t>(1.));
const auto at = [](double x, double y) { return Point::new_scale(x, y); };
CHECK(locator.find(at(2., 2.)).first == 0);
CHECK(locator.find(at(15., 15.)).first == 1);
// In the ring's hole, outside the island within it, the nearest outline counts.
CHECK(locator.find(at(7., 15.)).first == 0);
CHECK(locator.find(at(9.5, 15.)).first == 1);
// An island no other box reaches takes the margin past its outline.
CHECK(locator.find(at(50.5, 45.)).first == 2);
// Unless strict, as for an island whose neighbor is another instance's: then it is only the nearest, 0.5 mm away.
const auto [nearest, distance] = locator.find(at(50.5, 45.), true);
CHECK(nearest == 2);
CHECK_THAT(distance, WithinRel(sqr(scaled<double>(0.5)), 1e-6));
CHECK_FALSE(locator.holds(2, at(50.5, 45.), true));
CHECK(locator.holds(2, at(50.5, 45.)));
CHECK_FALSE(locator.holds(1, at(7., 15.)));
CHECK(locator.find(at(35., 45.)).first == -1);
}
TEST_CASE("Separate solids are separate bodies", "[ConnectedBodies]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
const auto bodies = solid_bodies({ { &cube, Transform3d::Identity() }, { &cube, Geometry::translation_transform({ 20., 0., 0. }) } }, { 1., 1. }, {}, 100);
REQUIRE(bodies.size() == 2);
CHECK_THAT(bodies[0].mass, WithinRel(1000., 1e-4));
CHECK_THAT((bodies[0].center - Vec3d(5., 5., 5.)).norm(), WithinAbs(0., 1e-4));
CHECK_THAT(bodies[1].mass, WithinRel(1000., 1e-4));
CHECK_THAT((bodies[1].center - Vec3d(25., 5., 5.)).norm(), WithinAbs(0., 1e-4));
}
TEST_CASE("Overlapping solids are one body that counts the overlap once", "[ConnectedBodies]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
const auto bodies = solid_bodies({ { &cube, Transform3d::Identity() }, { &cube, Geometry::translation_transform({ 5., 0., 0. }) } }, { 1., 1. }, {}, 100);
// Their union is a 15 x 10 x 10 box, which spreads a^2 / 12 along each side a.
REQUIRE(bodies.size() == 1);
const SolidBody &body = bodies.front();
CHECK_THAT(body.mass, WithinRel(1500., 1e-4));
CHECK_THAT(body.volume, WithinRel(1500., 1e-4));
CHECK_THAT((body.center - Vec3d(7.5, 5., 5.)).norm(), WithinAbs(0., 1e-4));
const Matrix3d spread = Vec3d(225., 100., 100.).asDiagonal() * (1. / 12.);
CHECK_THAT((body.spread - spread).norm(), WithinAbs(0., 1e-4));
// Turned a quarter about z, the box spans what was its y in -x.
const BoundingBoxf3 box = body.bounding_box(Geometry::rotation_transform({ 0., 0., 0.5 * PI }));
CHECK_THAT((box.min - Vec3d(-10., 0., 0.)).norm(), WithinAbs(0., 1e-4));
CHECK_THAT((box.max - Vec3d(0., 15., 10.)).norm(), WithinAbs(0., 1e-4));
}
TEST_CASE("A negative solid is cut away from the body", "[ConnectedBodies]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
const indexed_triangle_set notch = its_make_cube(4., 4., 4.);
const auto bodies = solid_bodies({ { &cube, Transform3d::Identity() } }, { 1. }, { { &notch, Transform3d::Identity() } }, 100);
// A 10 mm cube centered at 5 less a 4 mm cube centered at 2, in each axis alike.
const double expected = (1000. * 5. - 64. * 2.) / (1000. - 64.);
REQUIRE(bodies.size() == 1);
CHECK_THAT(bodies[0].mass, WithinRel(1000. - 64., 1e-4));
CHECK_THAT((bodies[0].center - Vec3d(expected, expected, expected)).norm(), WithinAbs(0., 1e-4));
}
TEST_CASE("Each solid weighs its density, the later of two overlapping ones the overlap", "[ConnectedBodies]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
const MeshInPlace left{ &cube, Transform3d::Identity() };
const MeshInPlace right{ &cube, Geometry::translation_transform({ 5., 0., 0. }) };
// The right cube, three times as dense, prints the overlap from x 5 to 10.
auto bodies = solid_bodies({ left, right }, { 1., 3. }, {}, 100);
REQUIRE(bodies.size() == 1);
CHECK_THAT(bodies[0].mass, WithinRel(500. + 3. * 1000., 1e-4));
CHECK_THAT(bodies[0].volume, WithinRel(1500., 1e-4));
CHECK_THAT(bodies[0].center.x(), WithinAbs((500. * 2.5 + 3000. * 10.) / 3500., 1e-4));
// Listed the other way round, the left cube prints it.
bodies = solid_bodies({ right, left }, { 3., 1. }, {}, 100);
REQUIRE(bodies.size() == 1);
CHECK_THAT(bodies[0].mass, WithinRel(1000. + 3. * 500., 1e-4));
CHECK_THAT(bodies[0].center.x(), WithinAbs((1000. * 5. + 1500. * 12.5) / 2500., 1e-4));
}
TEST_CASE("Separate solids weigh their own densities", "[ConnectedBodies]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
const auto bodies = solid_bodies({ { &cube, Transform3d::Identity() }, { &cube, Geometry::translation_transform({ 20., 0., 0. }) } },
{ 1.24, 2. }, {}, 100);
REQUIRE(bodies.size() == 2);
CHECK_THAT(bodies[0].mass, WithinRel(1240., 1e-4));
CHECK_THAT(bodies[1].mass, WithinRel(2000., 1e-4));
}
+36
View File
@@ -255,3 +255,39 @@ TEST_CASE("blend_color_multi weights components", "[FilamentMixer]")
REQUIRE(std::abs(comp(mixed, i) - comp("#123456", i)) <= 8); REQUIRE(std::abs(comp(mixed, i) - comp("#123456", i)) <= 8);
} }
} }
TEST_CASE("format_mixed_ratios normalises weights to four decimals", "[FilamentMixer]")
{
REQUIRE(format_mixed_components({1, 3}) == "1,3");
REQUIRE(format_mixed_ratios({50, 50}) == "0.5000,0.5000");
REQUIRE(format_mixed_ratios({1, 2}) == "0.3333,0.6667");
REQUIRE(format_mixed_ratios({1, 1}) == format_mixed_ratios({50, 50}));
}
TEST_CASE("find_fixed_mixed_filament reuses only a fixed slot of the same blend", "[FilamentMixer]")
{
// Physical slots 0 and 1; slot 2 blends them 50:50 as a gradient, slot 3 at a fixed 50:50.
DynamicPrintConfig cfg;
cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, true, true}));
cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", "1,2", "1,2"}));
cfg.set_key_value("filament_mixed_sublayer_ratios",
new ConfigOptionStrings({"", "", format_mixed_ratios({50, 50}), format_mixed_ratios({50, 50})}));
cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, true, false}));
SECTION("The fixed slot is found, whatever scale the weights are given at") {
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == 3);
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {1, 1}) == 3);
}
SECTION("A gradient slot with the same components and ratios is not a match") {
cfg.option<ConfigOptionBools>("filament_is_mixed")->values[3] = false;
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == -1);
}
SECTION("A project without the gradient key still matches its fixed slots") {
cfg.erase("filament_mixed_gradient");
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == 2);
}
SECTION("Another ratio or another component order is a different blend") {
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {1, 2}) == -1);
REQUIRE(find_fixed_mixed_filament(cfg, {2, 1}, {50, 50}) == -1);
}
}
@@ -12,11 +12,16 @@
#include <string> #include <string>
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/Geometry.hpp"
#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMesh.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
using namespace Slic3r; using namespace Slic3r;
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
TEST_CASE("Split empty mesh", "[its_split][its]") { TEST_CASE("Split empty mesh", "[its_split][its]") {
@@ -317,3 +322,75 @@ TEST_CASE("Simplified cube should not be empty.", "[its]")
its_quadric_edge_collapse(its, wanted_count, &max_error); its_quadric_edge_collapse(its, wanted_count, &max_error);
CHECK(!its.indices.empty()); CHECK(!its.indices.empty());
} }
TEST_CASE("A box far from the origin has its center of mass at its center and spreads as a box", "[its]")
{
indexed_triangle_set box = its_make_cube(10., 20., 30.);
for (Vec3f &v : box.vertices)
v += Vec3f(1000.f, 2000.f, 300.f);
const MassProperties solid = its_mass_properties(box);
CHECK_THAT(solid.volume, WithinRel(10. * 20. * 30., 1e-6));
CHECK_THAT(solid.mass, WithinRel(solid.volume, 1e-12));
CHECK_THAT(solid.center.x(), WithinAbs(1005., 1e-6));
CHECK_THAT(solid.center.y(), WithinAbs(2010., 1e-6));
CHECK_THAT(solid.center.z(), WithinAbs(315., 1e-6));
// A box of side a spreads a^2 / 12 along it.
const Matrix3d spread = Vec3d(100., 400., 900.).asDiagonal() * (1. / 12.);
CHECK_THAT((solid.spread - spread).norm(), WithinAbs(0., 1e-6));
}
TEST_CASE("The center of mass of a cone lies a quarter of its height above the base", "[its]")
{
// Neither the surface centroid nor the vertex average lands there.
const double h = 40.;
const MassProperties solid = its_mass_properties(its_make_cone(10., h));
CHECK(solid.volume > 0.);
CHECK_THAT(solid.center.z(), WithinAbs(h / 4., 1e-4));
CHECK_THAT(solid.center.x(), WithinAbs(0., 1e-4));
CHECK_THAT(solid.center.y(), WithinAbs(0., 1e-4));
// 3 h^2 / 80 along the axis.
CHECK_THAT(solid.spread(2, 2), WithinRel(3. * h * h / 80., 1e-4));
}
TEST_CASE("A cavity moves the center of mass away from it", "[its]")
{
indexed_triangle_set solid = its_make_cube(20., 20., 20.);
indexed_triangle_set cavity = its_make_cube(10., 10., 8.);
for (Vec3f &v : cavity.vertices)
v += Vec3f(5.f, 5.f, 10.f);
its_flip_triangles(cavity);
its_merge(solid, cavity);
const MassProperties hollow = its_mass_properties(solid);
// A 20 mm cube centered at z 10 less a 10x10x8 mm cavity centered at z 14.
CHECK_THAT(hollow.volume, WithinRel(8000. - 800., 1e-6));
CHECK_THAT(hollow.center.x(), WithinAbs(10., 1e-6));
CHECK_THAT(hollow.center.y(), WithinAbs(10., 1e-6));
CHECK_THAT(hollow.center.z(), WithinAbs((8000. * 10. - 800. * 14.) / (8000. - 800.), 1e-6));
}
TEST_CASE("The mass properties follow an affine transformation of the mesh", "[its]")
{
indexed_triangle_set cone = its_make_cone(10., 40.);
const MassProperties solid = its_mass_properties(cone);
const Transform3d trafo = Geometry::translation_transform({ 50., -20., 7. }) * Geometry::rotation_transform({ 0.3, -0.5, 1.2 }) *
Geometry::scale_transform({ 2., 0.5, 1.5 });
for (Vec3f &v : cone.vertices)
v = (trafo * v.cast<double>()).cast<float>();
const MassProperties moved = its_mass_properties(cone);
const MassProperties expected = solid.transformed(trafo);
CHECK_THAT(moved.volume, WithinRel(expected.volume, 1e-5));
CHECK_THAT(moved.mass, WithinRel(expected.mass, 1e-5));
CHECK_THAT((moved.center - expected.center).norm(), WithinAbs(0., 1e-4));
CHECK_THAT((moved.spread - expected.spread).norm(), WithinAbs(0., 1e-3));
}
TEST_CASE("Flipped faces keep the mass properties", "[its]")
{
indexed_triangle_set cone = its_make_cone(10., 40.);
const MassProperties solid = its_mass_properties(cone);
its_flip_triangles(cone);
const MassProperties flipped = its_mass_properties(cone);
CHECK_THAT(flipped.volume, WithinRel(solid.volume, 1e-9));
CHECK_THAT((flipped.center - solid.center).norm(), WithinAbs(0., 1e-9));
CHECK_THAT((flipped.spread - solid.spread).norm(), WithinAbs(0., 1e-9));
}
@@ -5888,6 +5888,21 @@ TEST_CASE("A project saved with pressure advance per filament applies it to ever
check_double_vector(petg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.97 }); check_double_vector(petg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.97 });
} }
TEST_CASE("A multi-toolhead project saved without filament self indices loads every filament", "[Preset][Bundle]")
{
const std::vector<std::string> colors = { "#FF0000", "#000000", "#FFFFFF", "#FFFF00" };
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.opt<ConfigOptionStrings>("filament_colour")->values = colors;
config.opt<ConfigOptionFloats>("nozzle_diameter")->values = std::vector<double>(colors.size(), 0.4);
config.option<ConfigOptionBool>("single_extruder_multi_material")->value = false;
Preset::normalize(config);
PresetBundle bundle;
REQUIRE_NOTHROW(bundle.load_config_model("test.3mf", std::move(config)));
CHECK(bundle.filament_presets.size() == colors.size());
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values == colors);
}
TEST_CASE("A system preset resolves by name from the bundled profiles", "[Preset][Bundle]") TEST_CASE("A system preset resolves by name from the bundled profiles", "[Preset][Bundle]")
{ {
ScopedTemporaryDir temp_dir; ScopedTemporaryDir temp_dir;
@@ -68,6 +68,28 @@ static std::shared_ptr<std::vector<unsigned char>> make_flat_gray_png(uint8_t va
return std::make_shared<std::vector<unsigned char>>(std::move(bytes)); return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
} }
// The same round trip for a flat colour image, which decode_height_texture() reads through its colour path.
static std::shared_ptr<std::vector<unsigned char>> make_flat_rgb_png(uint8_t r, uint8_t g, uint8_t b, size_t w = 4, size_t h = 4)
{
std::vector<uint8_t> rgb;
for (size_t i = 0; i < w * h; ++i)
rgb.insert(rgb.end(), { r, g, b });
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
REQUIRE(Slic3r::png::write_rgb_to_file(tmp_path.string(), w, h, rgb));
std::vector<unsigned char> bytes;
{
std::ifstream ifs(tmp_path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
REQUIRE_FALSE(bytes.empty());
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
// A hard-edged black/white checkerboard, the worst case for a height map: every texel boundary is a // A hard-edged black/white checkerboard, the worst case for a height map: every texel boundary is a
// step, which is precisely the relief the post-process smoothing exists to round off. // step, which is precisely the relief the post-process smoothing exists to round off.
static std::shared_ptr<std::vector<unsigned char>> make_checkerboard_png(size_t w = 16, size_t h = 16) static std::shared_ptr<std::vector<unsigned char>> make_checkerboard_png(size_t w = 16, size_t h = 16)
@@ -2135,3 +2157,20 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T
CHECK(second <= single * 5 / 4); CHECK(second <= single * 5 / 4);
} }
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
{
TextureDisplacementLayer gray;
gray.image_data = make_flat_gray_png(128);
CHECK_FALSE(height_texture_has_color(gray));
TextureDisplacementLayer color;
color.image_data = make_flat_rgb_png(200, 40, 10);
// Before the image is decoded and after, and whatever the smoothing.
CHECK(height_texture_has_color(color));
CHECK(decode_height_texture(color).has_color());
CHECK(height_texture_has_color(color));
color.smoothing = 0.5f;
CHECK(height_texture_has_color(color));
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
}
+1
View File
@@ -36,6 +36,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_audit.cpp test_plugin_audit.cpp
test_plugin_json_depth.cpp test_plugin_json_depth.cpp
test_shortcuts.cpp test_shortcuts.cpp
test_texture_color_palette.cpp
test_file_url.cpp test_file_url.cpp
test_user_manager.cpp test_user_manager.cpp
../fff_print/test_helpers.cpp ../fff_print/test_helpers.cpp
@@ -0,0 +1,294 @@
// The texture displacement gizmo's palette helpers live in libslic3r_gui; this is the suite that links it.
// Same Windows include prologue as test_filament_bitmap_utils.cpp (wx pulls in <windows.h>; keep
// WIN32_LEAN_AND_MEAN / NOMINMAX ahead of the Catch2 headers).
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <ios>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/Color.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PNGReadWrite.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/Utils/ColorSpaceConvert.hpp"
#include "test_utils.hpp"
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
using Gizmo = Slic3r::GUI::GLGizmoTextureDisplacement;
using Entry = Gizmo::PaletteEntry;
using MixTarget = Gizmo::MixTarget;
namespace {
const ColorRGBA BLACK{ 0.f, 0.f, 0.f, 1.f };
const ColorRGBA WHITE{ 1.f, 1.f, 1.f, 1.f };
const ColorRGBA RED{ 1.f, 0.f, 0.f, 1.f };
const ColorRGBA BLUE{ 0.f, 0.f, 1.f, 1.f };
const ColorRGBA YELLOW{ 1.f, 1.f, 0.f, 1.f };
MixTarget target(const Vec3f &rgb, float weight)
{
MixTarget t;
RGB2Lab(rgb.x(), rgb.y(), rgb.z(), &t.lab.x(), &t.lab.y(), &t.lab.z());
t.weight = weight;
return t;
}
// The colour a mixed slot of these two filaments shows, as the sidebar computes it.
Vec3f slot_color(const ColorRGBA &a, const ColorRGBA &b, int a_percent)
{
ColorRGB c;
REQUIRE(decode_color(blend_color_multi({ encode_color(a), encode_color(b) }, { a_percent, 100 - a_percent }), c));
return Vec3f(c.r(), c.g(), c.b());
}
// A colour image layer, through Slic3r's own PNG writer so decode_height_texture() reads it the way it
// reads an imported texture.
TextureDisplacementLayer color_layer(int w, int h, const std::vector<uint8_t> &rgb)
{
ScopedTemporaryFile png(".png");
REQUIRE(png::write_rgb_to_file(png.string(), size_t(w), size_t(h), rgb));
std::ifstream in(png.string(), std::ios::binary);
std::vector<unsigned char> bytes{ std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>() };
REQUIRE_FALSE(bytes.empty());
TextureDisplacementLayer layer;
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::move(bytes));
layer.color_enabled = true;
return layer;
}
// A red/green ramp over a fixed blue: colours spread over many bins, so the image is not flat-colour.
TextureDisplacementLayer gradient_layer()
{
const int n = 64;
std::vector<uint8_t> rgb;
for (int y = 0; y < n; ++y)
for (int x = 0; x < n; ++x) {
rgb.push_back(uint8_t(x * 4));
rgb.push_back(uint8_t(y * 4));
rgb.push_back(128);
}
return color_layer(n, n, rgb);
}
// A 2048x1100 image, over mix_targets()' sampling budget, that is pure red wherever `red(x, y)` holds and
// elsewhere a gradient spread over far more than eight coarse bins, so the image is not flat-colour.
template<class RedFn> TextureDisplacementLayer striped_layer(RedFn red)
{
const int w = 2048, h = 1100;
std::vector<uint8_t> rgb;
rgb.reserve(size_t(w) * size_t(h) * 3);
for (int y = 0; y < h; ++y)
for (int x = 0; x < w; ++x) {
const bool is_red = red(x, y);
rgb.push_back(is_red ? 255 : uint8_t(x * 255 / w));
rgb.push_back(is_red ? 0 : uint8_t(y * 255 / h));
rgb.push_back(is_red ? 0 : 128);
}
return color_layer(w, h, rgb);
}
// How much of the targets' weight is pure red.
float red_weight(const std::vector<MixTarget> &targets)
{
Vec3f red;
RGB2Lab(1.f, 0.f, 0.f, &red.x(), &red.y(), &red.z());
float weight = 0.f;
for (const MixTarget &t : targets)
if ((t.lab - red).norm() < 3.f)
weight += t.weight;
return weight;
}
} // namespace
TEST_CASE("mix targets cover each colouring photo layer once and skip the rest", "[TextureColorPalette][TextureDisplacement]")
{
TextureDisplacementLayer photo = gradient_layer();
const std::vector<MixTarget> targets = Gizmo::mix_targets({ photo });
REQUIRE_FALSE(targets.empty());
float total = 0.f;
for (const MixTarget &t : targets)
total += t.weight;
REQUIRE_THAT(total, WithinAbs(1., 1e-4));
SECTION("A layer that does not colour gives no targets") {
photo.color_enabled = false;
REQUIRE(Gizmo::mix_targets({ photo }).empty());
}
SECTION("A flat-colour image prints in single filaments, so it gives no targets") {
const TextureDisplacementLayer flat = color_layer(8, 8, std::vector<uint8_t>(8 * 8 * 3, 200));
REQUIRE(Gizmo::mix_targets({ flat }).empty());
}
}
TEST_CASE("mix targets of a large image weigh each colour by its share, whatever its stripes", "[TextureColorPalette][TextureDisplacement]")
{
// Sampled rather than read in full, these must not line up with the samples: a fixed sampling step
// sees only one phase of a stripe pattern, and a generator whose offsets repeat sees only some.
SECTION("Red on every other column") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int) { return x % 2 == 0; }) })),
WithinAbs(1. / 2., 0.03));
}
SECTION("Red on every fourth column") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int) { return x % 4 == 0; }) })),
WithinAbs(1. / 4., 0.03));
}
SECTION("Red on diagonals") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int y) { return (x - y) % 3 == 0; }) })),
WithinAbs(1. / 3., 0.03));
}
}
TEST_CASE("the mix ranked first is the one the image needs most", "[TextureColorPalette][TextureDisplacement]")
{
// The whole image is exactly the colour of a 1:1 black/white slot.
const std::vector<MixTarget> targets = { target(slot_color(BLACK, WHITE, 50), 1.f) };
const std::vector<Entry> ranked = Gizmo::rank_mixes({ BLACK, WHITE }, targets, 1);
REQUIRE(ranked.size() == 1);
CHECK(ranked.front().a == 0);
CHECK(ranked.front().b == 1);
CHECK(ranked.front().num * 2 == ranked.front().den);
}
TEST_CASE("an image the filaments already match ranks no mixes", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<MixTarget> targets = { target(Vec3f(1.f, 0.f, 0.f), 0.5f), target(Vec3f(0.f, 0.f, 1.f), 0.5f) };
REQUIRE(Gizmo::rank_mixes({ RED, BLUE }, targets, 8).empty());
}
TEST_CASE("ranked mixes stay within the limit and show their slot's colour", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<ColorRGBA> filaments = { RED, BLUE, YELLOW };
const std::vector<MixTarget> targets = Gizmo::mix_targets({ gradient_layer() });
for (const int limit : { 1, 3 }) {
const std::vector<Entry> ranked = Gizmo::rank_mixes(filaments, targets, limit);
CHECK(int(ranked.size()) <= limit);
CHECK_FALSE(ranked.empty());
}
const std::vector<Entry> ranked = Gizmo::rank_mixes(filaments, targets, 6);
for (const Entry &e : ranked) {
REQUIRE(e.is_mix());
const Vec3f expected = slot_color(filaments[size_t(e.a)], filaments[size_t(e.b)], e.a_percent());
CHECK_THAT(e.rgb.x(), WithinAbs(expected.x(), 1e-6));
CHECK_THAT(e.rgb.y(), WithinAbs(expected.y(), 1e-6));
CHECK_THAT(e.rgb.z(), WithinAbs(expected.z(), 1e-6));
}
}
TEST_CASE("picked mixes never need more slots than the project has free", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<Entry> ranking = { { Vec3f::Zero(), 0, 1, 1, 2 }, { Vec3f::Zero(), 0, 1, 1, 3 }, { Vec3f::Zero(), 0, 1, 2, 3 } };
const auto reusable_second = [](const Entry &e) { return e.num == 1 && e.den == 3; };
SECTION("The count caps the pick") {
REQUIRE(Gizmo::pick_mixes(ranking, 2, 10, nullptr).size() == 2);
}
SECTION("With no free slot only a mix that already has one is kept") {
const std::vector<Entry> picked = Gizmo::pick_mixes(ranking, 3, 0, reusable_second);
REQUIRE(picked.size() == 1);
CHECK(picked.front().den == 3);
CHECK(picked.front().num == 1);
}
SECTION("A reusable mix costs no free slot") {
const std::vector<Entry> picked = Gizmo::pick_mixes(ranking, 3, 1, reusable_second);
REQUIRE(picked.size() == 2);
CHECK(picked[0].den == 2);
CHECK(picked[1].den == 3);
}
}
TEST_CASE("a baked mix paints the slot it was given, wherever that slot sits", "[TextureColorPalette][TextureDisplacement]")
{
// Two filaments, then two mixes of them. Palette index 2 is a mix, but its slot need not be
// filament 2: a project that already holds other mixed slots puts it further along.
const std::vector<Entry> palette = Gizmo::make_palette({ BLACK, WHITE }, { { Vec3f::Zero(), 0, 1, 1, 2 }, { Vec3f::Zero(), 0, 1, 1, 3 } });
REQUIRE(palette.size() == 4);
// Filament 0, the 1:1 mix twice, nothing: the 1:2 mix is never used.
const std::vector<uint8_t> triangle_color = { 1, 3, 3, 0 };
std::vector<Entry> asked;
const auto slot_seven = [&asked](const Entry &e) {
asked.push_back(e);
return 7;
};
const std::vector<int> filament = Gizmo::palette_filaments(palette, triangle_color, slot_seven);
REQUIRE(filament.size() == 4);
CHECK(filament[0] == 0);
CHECK(filament[1] == 1);
CHECK(filament[2] == 7);
CHECK(filament[3] == -1);
// Asking creates a slot, so an unused mix is never asked for.
REQUIRE(asked.size() == 1);
CHECK(asked.front().den == 2);
SECTION("A mix that gets no slot prints in its dominant component") {
const std::vector<int> fallback = Gizmo::palette_filaments(palette, { 3, 4 }, [](const Entry &) { return -1; });
CHECK(fallback[2] == 0); // 1:1 - the first component
CHECK(fallback[3] == 1); // 1 part black in 3 - white dominates
}
}
TEST_CASE("the model's colour paint is drawn by filament, and only where no layer paint covers it", "[TextureColorPalette][TextureDisplacement]")
{
// A strip of four triangles: filament 2 on the first and last, filament 5 on the second, and the third
// left to the volume's own filament.
indexed_triangle_set strip;
strip.vertices = { Vec3f(0, 0, 0), Vec3f(1, 0, 0), Vec3f(0, 1, 0), Vec3f(1, 1, 0), Vec3f(0, 2, 0), Vec3f(1, 2, 0) };
strip.indices = { stl_triangle_vertex_indices(0, 1, 2), stl_triangle_vertex_indices(1, 3, 2), stl_triangle_vertex_indices(2, 3, 4),
stl_triangle_vertex_indices(3, 5, 4) };
const TriangleMesh mesh(strip);
TriangleSelector paint(mesh);
paint.set_facet(0, EnforcerBlockerType(2));
paint.set_facet(1, EnforcerBlockerType(5));
paint.set_facet(3, EnforcerBlockerType(2));
const Gizmo::PaintedColors colors = Gizmo::painted_colors(mesh, paint.serialize());
// Grouped by filament; the triangle in the volume's own filament is never drawn.
REQUIRE(colors.facets.indices.size() == 3);
REQUIRE(colors.source.size() == 3);
REQUIRE(colors.state.size() == 3);
CHECK(colors.state == std::vector<int>{ 2, 2, 5 });
CHECK(colors.source == std::vector<int>{ 0, 3, 1 });
SECTION("A model triangle a layer's paint covers is left to the preview") {
std::vector<bool> excluded(mesh.its.indices.size(), false);
excluded[3] = true;
const std::vector<size_t> kept = colors.outside(excluded);
REQUIRE(kept.size() == 2);
CHECK(colors.source[kept[0]] == 0);
CHECK(colors.source[kept[1]] == 1);
}
SECTION("A mask shorter than the model leaves the rest drawn") {
CHECK(colors.outside({ true }).size() == 2);
}
}