mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-11 01:41:03 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24cefcba62 | ||
|
|
bffb5116a5 | ||
|
|
dca9b96ab2 | ||
|
|
83e0253dc8 | ||
|
|
3350d07269 | ||
|
|
d4986e44bd | ||
|
|
9f25c89994 | ||
|
|
d208e45736 | ||
|
|
4a20168742 | ||
|
|
1466c0e57f | ||
|
|
cd02116242 | ||
|
|
35bac4cb69 | ||
|
|
0f3e8fbf27 |
@@ -1,73 +1,182 @@
|
||||
name: Check Cache
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
os:
|
||||
required: true
|
||||
type: string
|
||||
arch:
|
||||
required: false
|
||||
type: string
|
||||
compiler:
|
||||
required: false
|
||||
type: string
|
||||
default: msvc
|
||||
build-deps-only:
|
||||
required: false
|
||||
type: boolean
|
||||
force-build:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
check_cache: # determines if there is a cache and outputs variables used in caching process
|
||||
name: Check Cache
|
||||
runs-on: ${{ inputs.os }}
|
||||
outputs:
|
||||
cache-key: ${{ steps.set_outputs.outputs.cache-key }}
|
||||
cache-path: ${{ steps.set_outputs.outputs.cache-path }}
|
||||
valid-cache: ${{ steps.cache_deps.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: 'false'
|
||||
|
||||
- name: set outputs
|
||||
id: set_outputs
|
||||
env:
|
||||
# Anything that changes how the tree is built belongs in the key, or a job
|
||||
# restores one it cannot use. Linux amd64 passes no arch deliberately, so
|
||||
# 'linux-clang' keeps the cache it already has.
|
||||
cache-os: ${{ runner.os == 'macOS' && format('{0}-{1}', inputs.os, inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
|
||||
# The Windows ARM64 deps build in build-arm64, all others under build;
|
||||
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
|
||||
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
|
||||
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
|
||||
run: |
|
||||
echo cache-key=${{ env.cache-os }}-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }} >> ${{ env.output-cmd }}
|
||||
echo cache-path=${{ github.workspace }}/deps/build${{ env.dep-folder-name }} >> ${{ env.output-cmd }}
|
||||
|
||||
- name: load cache
|
||||
id: cache_deps
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ steps.set_outputs.outputs.cache-path }}
|
||||
key: ${{ steps.set_outputs.outputs.cache-key }}
|
||||
lookup-only: true
|
||||
|
||||
build_deps: # call next step
|
||||
name: Build Deps
|
||||
needs: [check_cache]
|
||||
uses: ./.github/workflows/build_deps.yml
|
||||
with:
|
||||
cache-key: ${{ needs.check_cache.outputs.cache-key }}
|
||||
cache-path: ${{ needs.check_cache.outputs.cache-path }}
|
||||
valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' }}
|
||||
os: ${{ inputs.os }}
|
||||
arch: ${{ inputs.arch }}
|
||||
compiler: ${{ inputs.compiler }}
|
||||
build-deps-only: ${{ inputs.build-deps-only }}
|
||||
force-build: ${{ inputs.force-build }}
|
||||
secrets: inherit
|
||||
name: Check Cache
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
os:
|
||||
required: true
|
||||
type: string
|
||||
arch:
|
||||
required: false
|
||||
type: string
|
||||
compiler:
|
||||
required: false
|
||||
type: string
|
||||
default: msvc
|
||||
build-deps-only:
|
||||
required: false
|
||||
type: boolean
|
||||
force-build:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
# Read by scripts/ci_deps_cache.sh.
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
WORKFLOW_REF: ${{ github.workflow_ref }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
|
||||
jobs:
|
||||
check_cache: # determines if there is a cache and outputs variables used in caching process
|
||||
name: Check Cache
|
||||
# Hosted macOS runners are scarce, so their lookup runs on Linux.
|
||||
runs-on: ${{ startsWith(inputs.os, 'macos-') && 'ubuntu-24.04' || inputs.os }}
|
||||
outputs:
|
||||
cache-key: ${{ steps.set_outputs.outputs.cache-key }}
|
||||
cache-path: ${{ steps.set_outputs.outputs.cache-path }}
|
||||
valid-cache: ${{ steps.cache_deps.outputs.cache-hit }}
|
||||
macos-path: ${{ steps.macos_path.outputs.path }}
|
||||
key-inputs: ${{ steps.wait_check.outputs.key-inputs }}
|
||||
wait-for: ${{ steps.wait_check.outputs.wait-for }}
|
||||
wait-since: ${{ steps.wait_check.outputs.wait-since }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: 'false'
|
||||
# The wait check compares the merge commit with its first parent.
|
||||
fetch-depth: ${{ startsWith(inputs.os, 'macos-') && 2 || 1 }}
|
||||
# Fetches only deps/, which the key hashes, and the wait script.
|
||||
sparse-checkout: |
|
||||
/deps/
|
||||
/scripts/ci_deps_cache.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: set outputs
|
||||
id: set_outputs
|
||||
env:
|
||||
# Anything that changes how the tree is built belongs in the key, or a job
|
||||
# restores one it cannot use. Linux amd64 passes no arch deliberately, so
|
||||
# 'linux-clang' keeps the cache it already has.
|
||||
cache-os: ${{ (runner.os == 'macOS' || startsWith(inputs.os, 'macos-')) && format('{0}-{1}', inputs.os, inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
|
||||
# The Windows ARM64 deps build in build-arm64, all others under build;
|
||||
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
|
||||
dep-folder-name: ${{ (runner.os == 'macOS' || startsWith(inputs.os, 'macos-')) && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
|
||||
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
|
||||
run: |
|
||||
echo cache-key=${{ env.cache-os }}-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }} >> ${{ env.output-cmd }}
|
||||
# Relative to the workspace, which differs from the build's when this job runs on Linux.
|
||||
echo cache-path=deps/build${{ env.dep-folder-name }} >> ${{ env.output-cmd }}
|
||||
|
||||
# actions/cache matches the path too, so a Linux lookup for macOS uses the
|
||||
# workspace of a hosted macOS job.
|
||||
- name: macOS cache path
|
||||
id: macos_path
|
||||
if: ${{ startsWith(inputs.os, 'macos-') }}
|
||||
run: echo "path=/Users/runner/work/${GITHUB_REPOSITORY#*/}/${GITHUB_REPOSITORY#*/}/${{ steps.set_outputs.outputs.cache-path }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: load cache
|
||||
id: cache_deps
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ steps.macos_path.outputs.path || format('{0}/{1}', github.workspace, steps.set_outputs.outputs.cache-path) }}
|
||||
key: ${{ steps.set_outputs.outputs.cache-key }}
|
||||
lookup-only: true
|
||||
|
||||
# A pull request that misses the cache can wait for a base branch build of the same deps.
|
||||
- name: check for a base branch build to wait for
|
||||
id: wait_check
|
||||
if: ${{ startsWith(inputs.os, 'macos-') && github.event_name == 'pull_request' && steps.cache_deps.outputs.cache-hit != 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: bash scripts/ci_deps_cache.sh check
|
||||
|
||||
wait_for_deps:
|
||||
name: Wait for base branch deps
|
||||
needs: [check_cache]
|
||||
if: ${{ needs.check_cache.outputs.wait-for != '' }}
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
WAIT_MINUTES: 120
|
||||
# Must exceed WAIT_MINUTES.
|
||||
timeout-minutes: 130
|
||||
# Waiting runs queue here, and only the first one holds a runner.
|
||||
concurrency:
|
||||
group: deps-wait-${{ github.base_ref }}-${{ needs.check_cache.outputs.cache-key }}
|
||||
queue: max
|
||||
outputs:
|
||||
hit: ${{ steps.lookup.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: scripts/ci_deps_cache.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: wait for the deps cache
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
KEY: ${{ needs.check_cache.outputs.cache-key }}
|
||||
KEY_INPUTS: ${{ needs.check_cache.outputs.key-inputs }}
|
||||
WAIT_FOR: ${{ needs.check_cache.outputs.wait-for }}
|
||||
WAIT_SINCE: ${{ needs.check_cache.outputs.wait-since }}
|
||||
ARCH: ${{ inputs.arch }}
|
||||
run: bash scripts/ci_deps_cache.sh wait
|
||||
|
||||
- name: load cache
|
||||
id: lookup
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ needs.check_cache.outputs.macos-path }}
|
||||
key: ${{ needs.check_cache.outputs.cache-key }}
|
||||
lookup-only: true
|
||||
|
||||
# Hosted macOS runners are scarce, so a pull request's macOS build waits until
|
||||
# one is free that push and nightly builds do not need. See the script.
|
||||
macos_admission:
|
||||
# The macos-priority label, when the run starts, puts a pull request in its own
|
||||
# line, which the normal one yields to. The script matches both names.
|
||||
name: ${{ contains(github.event.pull_request.labels.*.name, 'macos-priority') && 'Wait for a macOS runner (priority)' || 'Wait for a macOS runner' }}
|
||||
needs: [check_cache, wait_for_deps]
|
||||
if: ${{ !cancelled() && needs.check_cache.result == 'success' && startsWith(inputs.os, 'macos-') && github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
WAIT_MINUTES: 240
|
||||
PRIORITY: ${{ contains(github.event.pull_request.labels.*.name, 'macos-priority') }}
|
||||
# Must exceed WAIT_MINUTES.
|
||||
timeout-minutes: 250
|
||||
# One line for every arch of every pull request, and only its first job polls.
|
||||
concurrency:
|
||||
group: ${{ contains(github.event.pull_request.labels.*.name, 'macos-priority') && 'macos-admission-priority' || 'macos-admission' }}
|
||||
queue: max
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: scripts/ci_macos_admission.py
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: wait for a macOS runner
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
MACOS_RUNNER_LIMIT: ${{ vars.MACOS_RUNNER_LIMIT }}
|
||||
# -u, or the log shows nothing until the wait ends.
|
||||
run: python3 -u scripts/ci_macos_admission.py
|
||||
|
||||
build_deps: # call next step
|
||||
name: Build Deps
|
||||
needs: [check_cache, wait_for_deps, macos_admission]
|
||||
# A failed wait counts as no cache, so the deps are built here. A failed
|
||||
# admission still builds rather than skip macOS.
|
||||
if: ${{ !cancelled() && needs.check_cache.result == 'success' }}
|
||||
uses: ./.github/workflows/build_deps.yml
|
||||
with:
|
||||
cache-key: ${{ needs.check_cache.outputs.cache-key }}
|
||||
cache-path: ${{ needs.check_cache.outputs.cache-path }}
|
||||
valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' || needs.wait_for_deps.outputs.hit == 'true' }}
|
||||
os: ${{ inputs.os }}
|
||||
arch: ${{ inputs.arch }}
|
||||
compiler: ${{ inputs.compiler }}
|
||||
build-deps-only: ${{ inputs.build-deps-only }}
|
||||
force-build: ${{ inputs.force-build }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: load cached deps
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ inputs.cache-path }}
|
||||
path: ${{ github.workspace }}/${{ inputs.cache-path }}
|
||||
key: ${{ inputs.cache-key }}
|
||||
|
||||
- uses: lukka/get-cmake@latest
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
choco install strawberryperl
|
||||
}
|
||||
# cache-path is the install directory inside the deps build directory.
|
||||
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
|
||||
$deps = (Split-Path "${{ github.workspace }}/${{ inputs.cache-path }}").Replace('\', '/')
|
||||
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
|
||||
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
|
||||
.\build_win.bat -d --arch ${{ inputs.arch }} --deps-dir $deps @flags
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
if: ${{ !(runner.os == 'macOS' && inputs.macos-combine-only) }}
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ inputs.cache-path }}
|
||||
path: ${{ github.workspace }}/${{ inputs.cache-path }}
|
||||
key: ${{ inputs.cache-key }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
@@ -460,7 +460,7 @@ jobs:
|
||||
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
|
||||
run: |
|
||||
# cache-path is the install directory inside the deps build directory.
|
||||
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
|
||||
$deps = (Split-Path "${{ github.workspace }}/${{ inputs.cache-path }}").Replace('\', '/')
|
||||
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
|
||||
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
|
||||
.\build_win.bat -s --tests -i --arch ${{ inputs.arch }} --build-dir $env:BUILD_DIR --deps-dir $deps @flags
|
||||
@@ -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
|
||||
- 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' }}
|
||||
uses: rickstaa/action-create-tag@v1
|
||||
with:
|
||||
tag: "nightly-builds"
|
||||
tag_exists_error: false
|
||||
force_push_tag: true
|
||||
message: "nightly-builds"
|
||||
run: |
|
||||
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
|
||||
git push -f origin refs/tags/nightly-builds
|
||||
|
||||
- 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' }}
|
||||
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
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
|
||||
with:
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
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
|
||||
run: |
|
||||
|
||||
@@ -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()
|
||||
Vendored
-3
@@ -379,10 +379,8 @@ include(Boost/Boost.cmake)
|
||||
|
||||
include(Cereal/Cereal.cmake)
|
||||
include(Qhull/Qhull.cmake)
|
||||
include(GLEW/GLEW.cmake)
|
||||
|
||||
include(GLFW/GLFW.cmake)
|
||||
include(OpenCSG/OpenCSG.cmake)
|
||||
set(SLVS_PKG "")
|
||||
if (SLIC3R_CAD)
|
||||
include(SLVS/SLVS.cmake)
|
||||
@@ -478,7 +476,6 @@ set(_dep_list
|
||||
dep_Draco
|
||||
dep_NLopt
|
||||
dep_OpenVDB
|
||||
dep_OpenCSG
|
||||
${SLVS_PKG}
|
||||
dep_OpenCV
|
||||
dep_Eigen
|
||||
|
||||
Vendored
-2
@@ -1,6 +1,4 @@
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
Vendored
-14
@@ -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 ()
|
||||
Vendored
-44
@@ -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}
|
||||
)
|
||||
Vendored
-73
@@ -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.
|
||||
Vendored
-251
@@ -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/
|
||||
|
||||
https://github.com/nigels-com/glew
|
||||
|
||||
[](https://travis-ci.org/nigels-com/glew)
|
||||
[](https://gitter.im/nigels-com/glew?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](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ärkkä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).
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
2.2.0
|
||||
Vendored
-3051
File diff suppressed because it is too large
Load Diff
Vendored
-26427
File diff suppressed because it is too large
Load Diff
Vendored
-1831
File diff suppressed because it is too large
Load Diff
Vendored
-1468
File diff suppressed because it is too large
Load Diff
Vendored
-31949
File diff suppressed because it is too large
Load Diff
Vendored
-101
@@ -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}
|
||||
)
|
||||
Vendored
-17
@@ -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 ()
|
||||
@@ -191,7 +191,7 @@ public:
|
||||
tail->next = std::move(p);
|
||||
tail = new_tail;
|
||||
}
|
||||
data_cond.notify_one();
|
||||
disrupt_wait_for_data();
|
||||
}
|
||||
|
||||
void wait_and_pop(T& value)
|
||||
|
||||
@@ -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.
|
||||
@@ -62,23 +62,6 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
|
||||
clip do not leave slivers.
|
||||
- Open polylines are clipped with the non-zero rule and keep their direction.
|
||||
|
||||
### Tiled booleans
|
||||
|
||||
The sweep slows down with the number of edges crossing a scan line, so a layer
|
||||
cut into thousands of pieces makes every whole-layer boolean expensive.
|
||||
`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of
|
||||
non-overlapping `ExPolygons`, group them into tiles with
|
||||
`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only
|
||||
the clip polygons near it, cut to the tile's box. Below 128 pieces there is a
|
||||
single tile, and they are the plain `diff_ex()` / `intersection_ex()`.
|
||||
|
||||
The result covers the same area as the plain call. Without the safety offset
|
||||
the rings are the same. With it, each tile unites only the clip polygons near
|
||||
it, so a clip edge that the whole-layer union splits where it crosses a distant
|
||||
clip polygon stays whole, and a crossing with the subject can round 1 unit
|
||||
differently. The tiles' results are concatenated in tile order, so the order of
|
||||
the output `ExPolygons` differs from the plain call.
|
||||
|
||||
### Offsets
|
||||
|
||||
- Before offsetting, input vertices closer than
|
||||
|
||||
@@ -18,8 +18,8 @@ each body on its own (see Octree infill).
|
||||
## Bodies
|
||||
|
||||
`PrintObject::prepare_infill()` groups the islands of every layer (`lslices`)
|
||||
into 3D connected bodies before bridges are detected, so bridge anchors and
|
||||
printed infill share one origin. Islands on adjacent layers belong to one body
|
||||
into 3D connected bodies with `connected_bodies()` before bridges are detected,
|
||||
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
|
||||
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
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
#add_subdirectory(openvdb)
|
||||
# add_subdirectory(meshboolean)
|
||||
add_subdirectory(its_neighbor_index)
|
||||
# add_subdirectory(opencsg)
|
||||
#add_subdirectory(aabb-evaluation)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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() {}
|
||||
+1
-5
@@ -23,15 +23,11 @@ RUN apt-get update && apt-get install -y \
|
||||
libcairo2-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libdbus-1-dev \
|
||||
libglew-dev \
|
||||
libglu1-mesa-dev \
|
||||
libglu1-mesa-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamerd-3-dev \
|
||||
libgstreamerd-3-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgstreamer-plugins-good1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libgtk-3-dev \
|
||||
libsecret-1-dev \
|
||||
libsoup2.4-dev \
|
||||
libssl3 \
|
||||
|
||||
@@ -31,8 +31,6 @@ RUN apt-get update && apt-get install -y \
|
||||
libcairo2-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libdbus-1-dev \
|
||||
libglew-dev \
|
||||
libglu1-mesa-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamerd-3-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
# Lets a pull request's macOS deps build wait for a push build of the base
|
||||
# branch that builds the same deps, from a Linux job. `check` runs after a cache
|
||||
# miss and names a queued or running base build with the same deps/ and .github/
|
||||
# to wait for, unless the pull request changes either. `wait` waits until that
|
||||
# build saves KEY. actions/cache then confirms the entry can be restored.
|
||||
#
|
||||
# Environment: GH_TOKEN, REPO, WORKFLOW_REF and BASE_REF, plus KEY_INPUTS,
|
||||
# WAIT_FOR, WAIT_SINCE, WAIT_MINUTES, KEY and ARCH for `wait`.
|
||||
# Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT.
|
||||
set -uo pipefail
|
||||
|
||||
workflow=${WORKFLOW_REF%%@*}
|
||||
workflow=${workflow##*/}
|
||||
|
||||
# Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml.
|
||||
check_job_name="build_macos_arch (${ARCH:-}) / Check Cache"
|
||||
deps_job_name="build_macos_arch (${ARCH:-}) / Build Deps / Build Deps"
|
||||
|
||||
out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; }
|
||||
|
||||
# The cache key depends on deps/ and on the workflows under .github/.
|
||||
local_key_inputs() { echo "$(git rev-parse "$1:.github") $(git rev-parse "$1:deps")"; }
|
||||
remote_key_inputs() {
|
||||
gh api "repos/$REPO/git/trees/$1" < /dev/null \
|
||||
--jq '[.tree[] | select(.path == ".github" or .path == "deps") | {key: .path, value: .sha}] | from_entries | "\(.[".github"]) \(.deps)"'
|
||||
}
|
||||
|
||||
# Prints the id of a push build of the base branch that has not finished, is not
|
||||
# one of the ids in $1, and has KEY_INPUTS.
|
||||
matching_base_build() {
|
||||
local status runs id sha
|
||||
runs=$(for status in in_progress queued pending waiting requested; do
|
||||
gh api -X GET "repos/$REPO/actions/workflows/$workflow/runs" \
|
||||
-f branch="$BASE_REF" -f event=push -f status="$status" -f per_page=10 < /dev/null \
|
||||
| jq -r '.workflow_runs[] | "\(.id) \(.head_sha)"'
|
||||
done)
|
||||
while read -r id sha; do
|
||||
[ -n "$id" ] || continue
|
||||
[[ " ${1:-} " == *" $id "* ]] && continue
|
||||
if [ "$(remote_key_inputs "$sha")" = "$KEY_INPUTS" ]; then
|
||||
echo "$id"
|
||||
return
|
||||
fi
|
||||
done <<< "$runs"
|
||||
}
|
||||
|
||||
check() {
|
||||
KEY_INPUTS=$(local_key_inputs HEAD)
|
||||
out "key-inputs=$KEY_INPUTS"
|
||||
if [ "$KEY_INPUTS" != "$(local_key_inputs HEAD^1)" ]; then
|
||||
echo "This pull request changes deps/ or .github/, so it builds its own deps."
|
||||
return
|
||||
fi
|
||||
local id
|
||||
id=$(matching_base_build)
|
||||
if [ -n "$id" ]; then
|
||||
echo "Build $id of $BASE_REF has the same deps/ and .github/. Waiting for its cache."
|
||||
out "wait-for=$id"
|
||||
out "wait-since=$(date +%s)"
|
||||
else
|
||||
echo "No build of $BASE_REF is building these deps."
|
||||
fi
|
||||
}
|
||||
|
||||
key_saved() {
|
||||
gh cache list -R "$REPO" --ref "refs/heads/$BASE_REF" --key "$KEY" --json key < /dev/null \
|
||||
| jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null
|
||||
}
|
||||
|
||||
wait_for() {
|
||||
# Counts the limit from the check, including time queued behind other waiting
|
||||
# pull requests.
|
||||
local id=$WAIT_FOR seen=$WAIT_FOR deadline=$((${WAIT_SINCE:-$(date +%s)} + WAIT_MINUTES * 60)) jobs job check warned=
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
if key_saved; then
|
||||
echo "Found $KEY."
|
||||
return
|
||||
fi
|
||||
jobs=$(gh api -X GET "repos/$REPO/actions/runs/$id/jobs" -f filter=latest -f per_page=100 < /dev/null)
|
||||
job=$(jq -r --arg name "$deps_job_name" 'first(.jobs[] | select(.name == $name) | "\(.status) \(.conclusion)") // ""' <<< "$jobs")
|
||||
case "$job" in
|
||||
"completed success" | "completed skipped")
|
||||
# The new entry can take a moment to be listed, and a skipped job means
|
||||
# the build found the cache, possibly under the default branch.
|
||||
for _ in 1 2 3; do
|
||||
key_saved && { echo "Found $KEY."; return; }
|
||||
sleep 20
|
||||
done
|
||||
echo "Build $id of $BASE_REF finished its deps without saving $KEY."
|
||||
return ;;
|
||||
"completed cancelled") ;;
|
||||
completed*)
|
||||
echo "The deps job of build $id of $BASE_REF did not succeed."
|
||||
return ;;
|
||||
# The deps job exists only once the build's cache check has succeeded.
|
||||
"")
|
||||
case "$(gh api "repos/$REPO/actions/runs/$id" --jq '"\(.status) \(.conclusion)"' < /dev/null)" in
|
||||
"completed cancelled") ;;
|
||||
completed*)
|
||||
check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs")
|
||||
if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then
|
||||
echo "::warning title=Deps wait::Build $id of $BASE_REF has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
|
||||
warned=1
|
||||
fi ;;
|
||||
*)
|
||||
sleep 120
|
||||
continue ;;
|
||||
esac ;;
|
||||
*)
|
||||
sleep 120
|
||||
continue ;;
|
||||
esac
|
||||
# Follows another unfinished build with the same key inputs, such as the
|
||||
# push that replaced a cancelled one.
|
||||
id=$(matching_base_build "$seen")
|
||||
[ -n "$id" ] || break
|
||||
seen="$seen $id"
|
||||
echo "Following build $id of $BASE_REF."
|
||||
done
|
||||
echo "No build of $BASE_REF saved $KEY."
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
check) check ;;
|
||||
wait) wait_for ;;
|
||||
*) echo "usage: $0 check|wait" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Holds a pull request's macOS build until a hosted macOS runner is free for it.
|
||||
|
||||
Runs from a Linux job in build_check_cache.yml, once per macOS arch, in a single
|
||||
repo-wide line (a concurrency group with queue: max), so only the job at the
|
||||
front of the line polls. It lets its arch in when the runners every active Build
|
||||
all run holds or still needs, plus one for this arch, fit in MACOS_RUNNER_LIMIT:
|
||||
|
||||
- A push, nightly or manual run holds RESERVE runners until its macOS work is
|
||||
done. The jobs API lists only the jobs a run has reached, so what it still
|
||||
needs cannot be counted and is reserved instead.
|
||||
- A pull request run holds one runner for each arch let in until that arch's
|
||||
app build is done, and none after. A run holding a runner while it waits for
|
||||
its other arch would deadlock: the other arch can sit in the line behind a
|
||||
pull request that waits for that runner.
|
||||
- A run holds at least the macOS jobs it has queued or running.
|
||||
|
||||
A pull request's universal build and tests skip the line and are not held for
|
||||
in advance. They take minutes, and holding two runners for them through the
|
||||
slower arch's build left runners idle while other builds waited.
|
||||
- In the minutes around the nightly's cron time, RESERVE runners are held for it
|
||||
until its run appears.
|
||||
|
||||
A pull request labelled macos-priority when its run starts waits in a separate
|
||||
line under the same rule, and the normal line counts every priority arch still
|
||||
waiting as holding a runner, so the next free runners go to it.
|
||||
|
||||
Any API error repeated FAILURES_BEFORE_ADMIT times, and the WAIT_MINUTES limit,
|
||||
let the arch in, so a fault here never blocks pull requests.
|
||||
|
||||
Environment: GH_TOKEN, REPO, WORKFLOW_REF, GITHUB_RUN_ID, and optionally PRIORITY,
|
||||
MACOS_RUNNER_LIMIT, WAIT_MINUTES, POLL_SECONDS and GITHUB_API_URL.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
# A push, nightly or manual run builds arm64 and x86_64 at the same time, then
|
||||
# the universal build and the macOS tests at the same time.
|
||||
RESERVE = 2
|
||||
|
||||
# Must match the job names in build_all.yml and build_check_cache.yml.
|
||||
ARCH_PREFIX = "build_macos_arch ("
|
||||
GATE = "Wait for a macOS runner"
|
||||
PRIORITY_GATE = GATE + " (priority)"
|
||||
FINAL_JOBS = {"Build macOS Universal", "macOS arm64"}
|
||||
# Must match the cron in build_all.yml.
|
||||
NIGHTLY_UTC = datetime.time(2, 15)
|
||||
NIGHTLY_REPO = "OrcaSlicer/OrcaSlicer"
|
||||
NIGHTLY_LEAD = datetime.timedelta(minutes=10)
|
||||
NIGHTLY_GRACE = datetime.timedelta(minutes=45)
|
||||
|
||||
ACTIVE = {"queued", "in_progress", "waiting", "pending", "requested"}
|
||||
# Runs created this recently are listed whatever their status.
|
||||
RECENT = datetime.timedelta(hours=24)
|
||||
FAILURES_BEFORE_ADMIT = 3
|
||||
|
||||
|
||||
def is_macos(job):
|
||||
return any(label.startswith("macos-") for label in job.get("labels") or [])
|
||||
|
||||
|
||||
def macos_done(jobs):
|
||||
"""True once the universal build and the macOS tests have finished or been skipped.
|
||||
Both start together when the arch builds finish."""
|
||||
# A skipped caller job is listed under its own name, a started one as "<name> / <job>".
|
||||
final = [job for job in jobs if job["name"].split(" / ")[0] in FINAL_JOBS]
|
||||
return bool(final) and all(job["status"] == "completed" for job in final)
|
||||
|
||||
|
||||
def gates(jobs, names=(GATE, PRIORITY_GATE)):
|
||||
return [job for job in jobs if job["name"].split(" / ")[-1] in names]
|
||||
|
||||
|
||||
def arch_of(job):
|
||||
name = job["name"]
|
||||
return name[len(ARCH_PREFIX):name.find(")")] if name.startswith(ARCH_PREFIX) else None
|
||||
|
||||
|
||||
def admitted_arches(jobs):
|
||||
return {arch_of(job) for job in gates(jobs) if job["conclusion"] == "success"}
|
||||
|
||||
|
||||
def arch_built(jobs, arch):
|
||||
"""True once the arch's app build finished, or one of its jobs failed or was cancelled."""
|
||||
own = [job for job in jobs if arch_of(job) == arch]
|
||||
return any(job["conclusion"] in ("failure", "cancelled") for job in own) or \
|
||||
any(" / Build OrcaSlicer" in job["name"] and job["status"] == "completed" for job in own)
|
||||
|
||||
|
||||
def priority_waiting(jobs):
|
||||
"""How many of the run's arches wait in the priority line."""
|
||||
return sum(1 for job in gates(jobs, (PRIORITY_GATE,)) if job["status"] != "completed")
|
||||
|
||||
|
||||
def run_demand(run, jobs, yield_to_priority=False):
|
||||
"""macOS runners a run holds or still needs. With yield_to_priority, each
|
||||
priority arch still waiting counts as holding one."""
|
||||
# A run with no jobs that is pending waits behind another run of its
|
||||
# concurrency group, which holds the runners for both.
|
||||
if not jobs and run["status"] in ("pending", "waiting"):
|
||||
return 0
|
||||
active = sum(1 for job in jobs if is_macos(job) and job["status"] in ACTIVE)
|
||||
if macos_done(jobs):
|
||||
return active
|
||||
if run["event"] != "pull_request":
|
||||
return max(active, RESERVE)
|
||||
held = sum(1 for arch in admitted_arches(jobs) if not arch_built(jobs, arch))
|
||||
if yield_to_priority:
|
||||
held += priority_waiting(jobs)
|
||||
return max(active, held)
|
||||
|
||||
|
||||
def nightly_window(now):
|
||||
"""The window around today's nightly cron time, as (start, end) in UTC."""
|
||||
cron = datetime.datetime.combine(now.date(), NIGHTLY_UTC, tzinfo=datetime.timezone.utc)
|
||||
return cron - NIGHTLY_LEAD, cron + NIGHTLY_GRACE
|
||||
|
||||
|
||||
def parse_time(value):
|
||||
return datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
class Api:
|
||||
def __init__(self, token, url="https://api.github.com"):
|
||||
self.token = token
|
||||
self.url = url.rstrip("/")
|
||||
|
||||
def get(self, path, **params):
|
||||
query = urllib.parse.urlencode(params)
|
||||
request = urllib.request.Request(f"{self.url}/{path}?{query}", headers={
|
||||
"Accept": "application/vnd.github+json",
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
})
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
return json.load(response)
|
||||
|
||||
|
||||
def list_runs(api, repo, workflow, **params):
|
||||
runs, page = [], 1
|
||||
while True:
|
||||
body = api.get(f"repos/{repo}/actions/workflows/{workflow}/runs",
|
||||
per_page=100, page=page, **params)
|
||||
runs += body["workflow_runs"]
|
||||
if len(runs) >= body["total_count"] or not body["workflow_runs"]:
|
||||
return runs
|
||||
page += 1
|
||||
|
||||
|
||||
def latest_run(api, repo, workflow, **params):
|
||||
runs = api.get(f"repos/{repo}/actions/workflows/{workflow}/runs",
|
||||
per_page=1, **params)["workflow_runs"]
|
||||
return runs[0] if runs else None
|
||||
|
||||
|
||||
def measure(api, repo, workflow, run_id, now, priority=False):
|
||||
"""Total macOS runners held or needed, and one line per run that holds any.
|
||||
The priority line does not count the priority runs waiting behind it."""
|
||||
total, lines = 0, []
|
||||
# A run is listed as queued whenever one of its jobs waits for a runner, and
|
||||
# as pending whenever one waits in a concurrency group, so runs in any of these
|
||||
# can hold runners. A run that moves between the lists between the calls is
|
||||
# in neither, as one is whenever the job ahead of this one leaves the line, so
|
||||
# runs created recently are listed by creation time as well, and this run is
|
||||
# read directly.
|
||||
since = (now - RECENT).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
runs = {run["id"]: run for status in ("in_progress", "queued", "pending", "waiting")
|
||||
for run in list_runs(api, repo, workflow, status=status)}
|
||||
runs.update((run["id"], run) for run in list_runs(api, repo, workflow, created=f">={since}")
|
||||
if run["status"] != "completed")
|
||||
runs[run_id] = api.get(f"repos/{repo}/actions/runs/{run_id}")
|
||||
for run in runs.values():
|
||||
jobs = api.get(f"repos/{repo}/actions/runs/{run['id']}/jobs",
|
||||
filter="latest", per_page=100)["jobs"]
|
||||
own = run["id"] == run_id
|
||||
demand = run_demand(run, jobs, yield_to_priority=not priority and not own)
|
||||
if demand:
|
||||
total += demand
|
||||
waiting = ", priority, waiting" if priority_waiting(jobs) and not own else ""
|
||||
lines.append(f" {demand} run {run['id']} ({run['event']}, {run['head_branch']}{waiting})")
|
||||
|
||||
# build_all.yml runs the nightly only in the main repository.
|
||||
start, end = nightly_window(now)
|
||||
if repo == NIGHTLY_REPO and start <= now < end:
|
||||
last = latest_run(api, repo, workflow, event="schedule")
|
||||
if not last or parse_time(last["created_at"]) < start:
|
||||
total += RESERVE
|
||||
lines.append(f" {RESERVE} the nightly, due at {NIGHTLY_UTC:%H:%M} UTC")
|
||||
return total, lines
|
||||
|
||||
|
||||
def wait(measure_now, limit, wait_minutes, poll_seconds,
|
||||
clock=time.monotonic, sleep=time.sleep, log=print):
|
||||
"""Polls until this arch fits. Returns the reason it was let in."""
|
||||
deadline = clock() + wait_minutes * 60
|
||||
failures = 0
|
||||
while True:
|
||||
try:
|
||||
total, lines = measure_now()
|
||||
except (urllib.error.URLError, OSError, ValueError, KeyError, TypeError) as error:
|
||||
failures += 1
|
||||
log(f"::warning title=macOS admission::Could not read the queue ({error}).")
|
||||
if failures >= FAILURES_BEFORE_ADMIT:
|
||||
return "the queue could not be read"
|
||||
else:
|
||||
failures = 0
|
||||
log(f"{total} of {limit} macOS runners held or needed:")
|
||||
for line in lines:
|
||||
log(line)
|
||||
if total + 1 <= limit:
|
||||
return "a runner is free"
|
||||
if clock() + poll_seconds >= deadline:
|
||||
return f"it waited {wait_minutes} minutes"
|
||||
sleep(poll_seconds)
|
||||
|
||||
|
||||
def main():
|
||||
repo = os.environ["REPO"]
|
||||
workflow = os.environ["WORKFLOW_REF"].split("@")[0].rsplit("/", 1)[-1]
|
||||
api = Api(os.environ["GH_TOKEN"], os.environ.get("GITHUB_API_URL", "https://api.github.com"))
|
||||
run_id = int(os.environ["GITHUB_RUN_ID"])
|
||||
limit = int(os.environ.get("MACOS_RUNNER_LIMIT") or 5)
|
||||
reason = wait(
|
||||
lambda: measure(api, repo, workflow, run_id, datetime.datetime.now(datetime.timezone.utc),
|
||||
priority=os.environ.get("PRIORITY") == "true"),
|
||||
limit,
|
||||
wait_minutes=int(os.environ.get("WAIT_MINUTES") or 240),
|
||||
poll_seconds=int(os.environ.get("POLL_SECONDS") or 180),
|
||||
)
|
||||
print(f"Letting this macOS build in: {reason}.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -55,21 +55,6 @@ modules:
|
||||
url: https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-1.22.8.tar.xz
|
||||
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
|
||||
buildsystem: cmake-ninja
|
||||
sources:
|
||||
@@ -217,12 +202,6 @@ modules:
|
||||
sha256: a133ddc3d3c66143eba9035621db8e0bcf34dba1ee9514a9e23e96afd39fd57a
|
||||
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)
|
||||
- type: file
|
||||
url: https://github.com/JacobStoren/SolveSpaceLib/archive/4d8704523e4bf212fadf5189f92484244f670fea.zip
|
||||
|
||||
@@ -11,7 +11,6 @@ export REQUIRED_DEV_PACKAGES=(
|
||||
file
|
||||
gettext
|
||||
git
|
||||
glew
|
||||
gst-plugins-good
|
||||
gstreamer
|
||||
gtk3
|
||||
|
||||
@@ -11,7 +11,6 @@ export REQUIRED_DEV_PACKAGES=(
|
||||
file
|
||||
gettext
|
||||
git
|
||||
glew
|
||||
gst-plugins-good
|
||||
gstreamer
|
||||
gtk3
|
||||
|
||||
@@ -6,10 +6,9 @@ export REQUIRED_BUNDLES=(
|
||||
c-basic
|
||||
dev-utils
|
||||
devpkg-curl
|
||||
devpkg-glew
|
||||
devpkg-glu
|
||||
devpkg-gstreamer
|
||||
devpkg-gtk3
|
||||
devpkg-libglvnd
|
||||
devpkg-libmspack
|
||||
devpkg-libsecret
|
||||
devpkg-openssl
|
||||
|
||||
@@ -14,7 +14,7 @@ REQUIRED_DEV_PACKAGES=(
|
||||
gstreamer1.0-gtk3
|
||||
libcurl4-openssl-dev
|
||||
libdbus-1-dev
|
||||
libglew-dev
|
||||
libgl-dev
|
||||
libgstreamerd-3-dev
|
||||
libgtk-3-dev
|
||||
libmspack-dev
|
||||
|
||||
@@ -22,7 +22,7 @@ REQUIRED_DEV_PACKAGES=(
|
||||
libspnav-devel
|
||||
libtool
|
||||
m4
|
||||
mesa-libGLU-devel
|
||||
mesa-libGL-devel
|
||||
ninja-build
|
||||
openssl-devel
|
||||
perl-FindBin
|
||||
|
||||
@@ -18,7 +18,6 @@ REQUIRED_DEV_PACKAGES=(
|
||||
dev-vcs/git
|
||||
gui-libs/eglexternalplatform
|
||||
kde-frameworks/extra-cmake-modules
|
||||
media-libs/glew
|
||||
media-libs/gst-plugins-base:1.0
|
||||
media-libs/gstreamer:1.0
|
||||
media-plugins/gst-plugins-gtk:1.0
|
||||
@@ -31,6 +30,7 @@ REQUIRED_DEV_PACKAGES=(
|
||||
sys-devel/gettext
|
||||
sys-devel/m4
|
||||
virtual/libudev
|
||||
virtual/opengl
|
||||
x11-libs/gtk+:3
|
||||
dev-util/pkgconf
|
||||
dev-lang/yasm
|
||||
|
||||
@@ -21,7 +21,7 @@ REQUIRED_DEV_PACKAGES=(
|
||||
libspnav-devel
|
||||
libtool
|
||||
m4
|
||||
glu-devel
|
||||
Mesa-libGL-devel
|
||||
ninja-build
|
||||
openssl-devel
|
||||
perl-FindBin-Real
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for scripts/ci_macos_admission.py (stdlib unittest, no external deps).
|
||||
|
||||
Run from the repo root: python -m unittest discover -s scripts/tests -v
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import urllib.error
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
import ci_macos_admission as admission # noqa: E402
|
||||
|
||||
UTC = datetime.timezone.utc
|
||||
NOON = datetime.datetime(2026, 10, 10, 12, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def job(name, status="completed", conclusion="success", labels=("macos-15",)):
|
||||
return {"name": name, "status": status, "conclusion": conclusion, "labels": list(labels)}
|
||||
|
||||
|
||||
def gate(arch, status="completed", conclusion="success", priority=False):
|
||||
suffix = " (priority)" if priority else ""
|
||||
return job(f"build_macos_arch ({arch}) / Wait for a macOS runner{suffix}", status, conclusion,
|
||||
labels=("ubuntu-24.04",))
|
||||
|
||||
|
||||
def build(arch, status="in_progress"):
|
||||
return job(f"build_macos_arch ({arch}) / Build Deps / Build OrcaSlicer / Build OrcaSlicer",
|
||||
status, None if status != "completed" else "success")
|
||||
|
||||
|
||||
CHECK_CACHE = job("build_macos_arch (arm64) / Check Cache", labels=("ubuntu-24.04",))
|
||||
UNIVERSAL_DONE = job("Build macOS Universal / Build OrcaSlicer")
|
||||
TESTS_DONE = job("macOS arm64 / Unit Tests")
|
||||
PUSH = {"id": 1, "event": "push", "head_branch": "main", "status": "in_progress"}
|
||||
PR = {"id": 2, "event": "pull_request", "head_branch": "topic", "status": "in_progress"}
|
||||
|
||||
|
||||
class MacosDoneTest(unittest.TestCase):
|
||||
def test_not_done_before_the_final_jobs_are_listed(self):
|
||||
self.assertFalse(admission.macos_done([CHECK_CACHE, build("arm64", "completed")]))
|
||||
|
||||
def test_not_done_while_a_final_job_runs(self):
|
||||
running = job("macOS arm64 / Unit Tests", "in_progress", None)
|
||||
self.assertFalse(admission.macos_done([UNIVERSAL_DONE, running]))
|
||||
|
||||
def test_done_when_the_final_jobs_finished(self):
|
||||
self.assertTrue(admission.macos_done([build("arm64", "completed"), UNIVERSAL_DONE, TESTS_DONE]))
|
||||
|
||||
def test_done_when_the_final_jobs_were_skipped(self):
|
||||
# A skipped caller job is listed under its own name, without labels.
|
||||
skipped = [job("Build macOS Universal", conclusion="skipped", labels=()),
|
||||
job("macOS arm64", conclusion="skipped", labels=())]
|
||||
self.assertTrue(admission.macos_done(skipped))
|
||||
|
||||
|
||||
class RunDemandTest(unittest.TestCase):
|
||||
def test_push_reserves_before_its_macos_jobs_are_listed(self):
|
||||
self.assertEqual(admission.run_demand(PUSH, []), admission.RESERVE)
|
||||
self.assertEqual(admission.run_demand(PUSH, [CHECK_CACHE]), admission.RESERVE)
|
||||
|
||||
def test_push_keeps_its_reserve_between_stages(self):
|
||||
jobs = [build("arm64", "completed"), build("x86_64", "completed")]
|
||||
self.assertEqual(admission.run_demand(PUSH, jobs), admission.RESERVE)
|
||||
|
||||
def test_push_releases_when_macos_is_done(self):
|
||||
jobs = [build("arm64", "completed"), UNIVERSAL_DONE, TESTS_DONE]
|
||||
self.assertEqual(admission.run_demand(PUSH, jobs), 0)
|
||||
|
||||
def test_queued_jobs_count_as_busy(self):
|
||||
jobs = [build("arm64", "queued")]
|
||||
self.assertEqual(admission.run_demand(PR, jobs), 1)
|
||||
|
||||
def test_pull_request_holds_nothing_before_it_is_let_in(self):
|
||||
self.assertEqual(admission.run_demand(PR, [CHECK_CACHE, gate("arm64", "in_progress", None)]), 0)
|
||||
|
||||
def test_one_arch_let_in_holds_a_runner_until_it_is_built(self):
|
||||
# Including before its first macOS job is listed, and between its stages.
|
||||
self.assertEqual(admission.run_demand(PR, [gate("arm64")]), 1)
|
||||
self.assertEqual(admission.run_demand(PR, [gate("arm64"), build("arm64")]), 1)
|
||||
|
||||
def test_one_arch_let_in_holds_nothing_once_it_is_built(self):
|
||||
# Its other arch may be in the line behind a pull request waiting for this
|
||||
# runner, so holding it would deadlock.
|
||||
jobs = [gate("arm64"), build("arm64", "completed"), gate("x86_64", "pending", None)]
|
||||
self.assertEqual(admission.run_demand(PR, jobs), 0)
|
||||
|
||||
def test_one_arch_let_in_holds_nothing_once_it_failed(self):
|
||||
failed = job("build_macos_arch (arm64) / Build Deps / Build Deps", conclusion="failure")
|
||||
self.assertEqual(admission.run_demand(PR, [gate("arm64"), failed]), 0)
|
||||
|
||||
def test_both_arches_let_in_hold_only_what_still_builds(self):
|
||||
# The universal build and tests are not held for in advance.
|
||||
jobs = [gate("arm64"), gate("x86_64"), build("arm64", "completed"), build("x86_64")]
|
||||
self.assertEqual(admission.run_demand(PR, jobs), 1)
|
||||
|
||||
def test_universal_build_and_tests_count_once_queued(self):
|
||||
built = [gate("arm64"), gate("x86_64"), build("arm64", "completed"), build("x86_64", "completed")]
|
||||
self.assertEqual(admission.run_demand(PR, built), 0)
|
||||
queued = [job("Build macOS Universal / Build OrcaSlicer", "queued", None),
|
||||
job("macOS arm64 / Unit Tests", "in_progress", None)]
|
||||
self.assertEqual(admission.run_demand(PR, built + queued), 2)
|
||||
|
||||
def test_pull_request_releases_when_macos_is_done(self):
|
||||
jobs = [gate("arm64"), gate("x86_64"), UNIVERSAL_DONE, TESTS_DONE]
|
||||
self.assertEqual(admission.run_demand(PR, jobs), 0)
|
||||
|
||||
def test_pull_request_without_the_line_holds_what_it_runs(self):
|
||||
# A run started from a workflow without the line.
|
||||
self.assertEqual(admission.run_demand(PR, [build("arm64"), build("x86_64", "queued")]), 2)
|
||||
|
||||
def test_finished_and_non_macos_jobs_are_not_counted(self):
|
||||
jobs = [build("arm64", "completed"), CHECK_CACHE,
|
||||
job("build_linux (ubuntu-24.04) / Check Cache", "in_progress", None, ("ubuntu-24.04",))]
|
||||
self.assertEqual(admission.run_demand(PR, jobs), 0)
|
||||
|
||||
|
||||
class PriorityTest(unittest.TestCase):
|
||||
WAITING = [gate("arm64", "in_progress", None, priority=True), gate("x86_64", "pending", None, priority=True)]
|
||||
|
||||
def test_normal_line_yields_to_a_waiting_priority_run(self):
|
||||
self.assertEqual(admission.run_demand(PR, self.WAITING, yield_to_priority=True), admission.RESERVE)
|
||||
|
||||
def test_priority_line_does_not_count_priority_runs_behind_it(self):
|
||||
self.assertEqual(admission.run_demand(PR, self.WAITING), 0)
|
||||
|
||||
def test_a_waiting_normal_run_is_not_yielded_to(self):
|
||||
waiting = [gate("arm64", "in_progress", None), gate("x86_64", "pending", None)]
|
||||
self.assertEqual(admission.run_demand(PR, waiting, yield_to_priority=True), 0)
|
||||
|
||||
def test_a_priority_run_half_let_in_still_counts_its_waiting_arch(self):
|
||||
jobs = [gate("arm64", priority=True), gate("x86_64", "pending", None, priority=True)]
|
||||
self.assertEqual(admission.run_demand(PR, jobs), 1)
|
||||
self.assertEqual(admission.run_demand(PR, jobs, yield_to_priority=True), 2)
|
||||
|
||||
def test_skipped_priority_jobs_of_other_platforms_are_not_waiting(self):
|
||||
jobs = [job("build_linux (ubuntu-24.04) / Wait for a macOS runner (priority)",
|
||||
conclusion="skipped", labels=("ubuntu-24.04",))]
|
||||
self.assertEqual(admission.run_demand(PR, jobs, yield_to_priority=True), 0)
|
||||
|
||||
def test_measure_from_each_line(self):
|
||||
api = FakeApi({("runs", "pending"): [dict(PR, status="pending")], jobs_path(2): self.WAITING})
|
||||
total, lines = admission.measure(api, "o/r", "build_all.yml", 99, NOON)
|
||||
self.assertEqual(total, admission.RESERVE)
|
||||
self.assertIn("priority, waiting", lines[0])
|
||||
total, _ = admission.measure(api, "o/r", "build_all.yml", 99, NOON, priority=True)
|
||||
self.assertEqual(total, 0)
|
||||
|
||||
def test_own_waiting_run_is_not_counted(self):
|
||||
api = FakeApi({("runs", "pending"): [dict(PR, status="pending")], jobs_path(2): self.WAITING})
|
||||
total, _ = admission.measure(api, "o/r", "build_all.yml", 2, NOON)
|
||||
self.assertEqual(total, 0)
|
||||
|
||||
|
||||
class FakeApi:
|
||||
"""Answers GET requests from a dict of path -> list of pages (or one body)."""
|
||||
|
||||
def __init__(self, responses):
|
||||
self.responses = responses
|
||||
self.calls = []
|
||||
|
||||
def get(self, path, **params):
|
||||
self.calls.append((path, params))
|
||||
if path.endswith("/runs") and ("status" in params or "created" in params):
|
||||
runs = self.responses.get(("runs", params.get("status", "recent")), [])
|
||||
start = (params["page"] - 1) * params["per_page"]
|
||||
return {"total_count": len(runs), "workflow_runs": runs[start:start + params["per_page"]]}
|
||||
if path.endswith("/runs"):
|
||||
return {"workflow_runs": self.responses.get(("runs", params.get("event")), [])[:1]}
|
||||
if path.endswith("/jobs"):
|
||||
return {"jobs": self.responses.get(path, [])}
|
||||
# A run read by its id, by default a pull request with no jobs listed.
|
||||
return self.responses.get(path, dict(PR, id=int(path.rsplit("/", 1)[-1])))
|
||||
|
||||
|
||||
def jobs_path(run_id):
|
||||
return f"repos/o/r/actions/runs/{run_id}/jobs"
|
||||
|
||||
|
||||
class MeasureTest(unittest.TestCase):
|
||||
def test_sums_in_progress_and_queued_runs(self):
|
||||
queued_pr = dict(PR, id=3, status="queued")
|
||||
api = FakeApi({
|
||||
("runs", "in_progress"): [PUSH, PR],
|
||||
("runs", "queued"): [queued_pr],
|
||||
jobs_path(1): [CHECK_CACHE],
|
||||
jobs_path(2): [gate("arm64"), build("arm64")],
|
||||
jobs_path(3): [gate("arm64"), gate("x86_64"), build("arm64", "queued")],
|
||||
})
|
||||
total, lines = admission.measure(api, "o/r", "build_all.yml", 99, NOON)
|
||||
self.assertEqual(total, admission.RESERVE + 1 + admission.RESERVE)
|
||||
self.assertEqual(len(lines), 3)
|
||||
|
||||
def test_counts_a_pending_run_that_holds_runners(self):
|
||||
# One of its jobs waits in a concurrency group while its macOS builds run.
|
||||
pending_pr = dict(PR, status="pending")
|
||||
api = FakeApi({("runs", "pending"): [pending_pr], jobs_path(2): [gate("arm64"), build("arm64")]})
|
||||
total, _ = admission.measure(api, "o/r", "build_all.yml", 99, NOON)
|
||||
self.assertEqual(total, 1)
|
||||
|
||||
def test_a_push_waiting_behind_another_holds_nothing(self):
|
||||
api = FakeApi({("runs", "pending"): [dict(PUSH, status="pending")], jobs_path(1): []})
|
||||
total, _ = admission.measure(api, "o/r", "build_all.yml", 99, NOON)
|
||||
self.assertEqual(total, 0)
|
||||
|
||||
def test_reads_every_page_of_runs(self):
|
||||
runs = [dict(PR, id=i) for i in range(150)]
|
||||
responses = {("runs", "in_progress"): runs}
|
||||
responses.update({jobs_path(i): [gate("arm64")] for i in range(150)})
|
||||
total, _ = admission.measure(FakeApi(responses), "o/r", "build_all.yml", 999, NOON)
|
||||
self.assertEqual(total, 150)
|
||||
|
||||
def test_reserves_for_the_nightly_until_its_run_appears(self):
|
||||
due = datetime.datetime(2026, 10, 10, 2, 10, tzinfo=UTC)
|
||||
yesterday = {"created_at": "2026-10-09T02:20:00Z"}
|
||||
total, lines = admission.measure(
|
||||
FakeApi({("runs", "schedule"): [yesterday]}), "OrcaSlicer/OrcaSlicer", "build_all.yml", 99, due)
|
||||
self.assertEqual(total, admission.RESERVE)
|
||||
self.assertIn("nightly", lines[0])
|
||||
|
||||
def test_no_nightly_reserve_once_its_run_exists(self):
|
||||
due = datetime.datetime(2026, 10, 10, 2, 30, tzinfo=UTC)
|
||||
today = {"created_at": "2026-10-10T02:21:00Z"}
|
||||
total, _ = admission.measure(
|
||||
FakeApi({("runs", "schedule"): [today]}), "OrcaSlicer/OrcaSlicer", "build_all.yml", 99, due)
|
||||
self.assertEqual(total, 0)
|
||||
|
||||
def test_no_nightly_reserve_in_a_fork(self):
|
||||
due = datetime.datetime(2026, 10, 10, 2, 10, tzinfo=UTC)
|
||||
total, _ = admission.measure(FakeApi({}), "fork/OrcaSlicer", "build_all.yml", 99, due)
|
||||
self.assertEqual(total, 0)
|
||||
|
||||
def test_counts_a_run_in_both_lists_once(self):
|
||||
api = FakeApi({
|
||||
("runs", "in_progress"): [PUSH],
|
||||
("runs", "queued"): [dict(PUSH, status="queued")],
|
||||
jobs_path(1): [],
|
||||
})
|
||||
total, _ = admission.measure(api, "o/r", "build_all.yml", 99, NOON)
|
||||
self.assertEqual(total, admission.RESERVE)
|
||||
|
||||
def test_counts_its_own_run(self):
|
||||
jobs = [gate("arm64"), build("arm64"), gate("x86_64", "in_progress", None)]
|
||||
api = FakeApi({("runs", "in_progress"): [PR], jobs_path(2): jobs})
|
||||
self.assertEqual(admission.measure(api, "o/r", "build_all.yml", 2, NOON)[0], 1)
|
||||
|
||||
def test_a_run_whose_other_arch_waits_behind_this_one_does_not_block_it(self):
|
||||
# The line is A-arm64, B-arm64, A-x86_64. A's arm64 is built and its
|
||||
# x86_64 is pending behind B, so A must not hold a runner B waits for.
|
||||
other = dict(PR, id=1, status="pending")
|
||||
api = FakeApi({
|
||||
("runs", "pending"): [other, dict(PR, status="pending")],
|
||||
jobs_path(1): [gate("arm64"), build("arm64", "completed"), gate("x86_64", "pending", None)],
|
||||
jobs_path(2): [gate("arm64", "in_progress", None)],
|
||||
})
|
||||
total, _ = admission.measure(api, "o/r", "build_all.yml", 2, NOON)
|
||||
self.assertEqual(total, 0)
|
||||
|
||||
def test_counts_its_own_run_when_no_status_list_has_it(self):
|
||||
# The run moves from pending to in_progress as its last gate leaves the
|
||||
# line, and is in neither list while that gate checks.
|
||||
jobs = [gate("x86_64"), build("x86_64", "queued"), gate("arm64", "in_progress", None)]
|
||||
api = FakeApi({jobs_path(2): jobs})
|
||||
self.assertEqual(admission.measure(api, "o/r", "build_all.yml", 2, NOON)[0], 1)
|
||||
|
||||
def test_counts_a_recent_run_that_no_status_list_has(self):
|
||||
api = FakeApi({("runs", "recent"): [PR], jobs_path(2): [gate("arm64"), build("arm64")]})
|
||||
self.assertEqual(admission.measure(api, "o/r", "build_all.yml", 99, NOON)[0], 1)
|
||||
|
||||
def test_lists_recent_runs_from_a_day_before(self):
|
||||
api = FakeApi({})
|
||||
admission.measure(api, "o/r", "build_all.yml", 99, NOON)
|
||||
self.assertIn(">=2026-10-09T12:00:00Z", [params.get("created") for _, params in api.calls])
|
||||
|
||||
def test_finished_recent_runs_are_not_read(self):
|
||||
done = dict(PUSH, status="completed")
|
||||
api = FakeApi({("runs", "recent"): [done]})
|
||||
total, _ = admission.measure(api, "o/r", "build_all.yml", 99, NOON)
|
||||
self.assertEqual(total, 0)
|
||||
self.assertNotIn(jobs_path(1), [path for path, _ in api.calls])
|
||||
|
||||
def test_no_nightly_lookup_outside_its_window(self):
|
||||
api = FakeApi({})
|
||||
admission.measure(api, "o/r", "build_all.yml", 99, NOON)
|
||||
self.assertFalse(any("event" in params for _, params in api.calls))
|
||||
|
||||
|
||||
class WaitTest(unittest.TestCase):
|
||||
def run_wait(self, results, limit=5, wait_minutes=60, poll_seconds=120):
|
||||
results = iter(results)
|
||||
now = [0.0]
|
||||
|
||||
def measure_now():
|
||||
result = next(results)
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
return result, []
|
||||
|
||||
def sleep(seconds):
|
||||
now[0] += seconds
|
||||
|
||||
reason = admission.wait(measure_now, limit, wait_minutes, poll_seconds,
|
||||
clock=lambda: now[0], sleep=sleep, log=lambda _: None)
|
||||
return reason, now[0]
|
||||
|
||||
def test_lets_in_when_a_runner_fits(self):
|
||||
self.assertEqual(self.run_wait([4]), ("a runner is free", 0))
|
||||
|
||||
def test_waits_while_full(self):
|
||||
self.assertEqual(self.run_wait([5, 6, 4]), ("a runner is free", 240))
|
||||
|
||||
|
||||
def test_lets_in_after_repeated_api_errors(self):
|
||||
error = urllib.error.URLError("rate limited")
|
||||
reason, _ = self.run_wait([error, error, error])
|
||||
self.assertEqual(reason, "the queue could not be read")
|
||||
|
||||
def test_a_good_read_resets_the_error_count(self):
|
||||
error = urllib.error.URLError("rate limited")
|
||||
reason, _ = self.run_wait([error, error, 5, error, error, 3])
|
||||
self.assertEqual(reason, "a runner is free")
|
||||
|
||||
def test_lets_in_at_the_time_limit(self):
|
||||
reason, elapsed = self.run_wait([5] * 100, wait_minutes=10, poll_seconds=120)
|
||||
self.assertEqual(reason, "it waited 10 minutes")
|
||||
self.assertLess(elapsed, 600)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -189,7 +189,6 @@ else ()
|
||||
target_link_libraries(OrcaSlicer ${CMAKE_DL_LIBS} -lstdc++ Threads::Threads pangoft2-1.0)
|
||||
endif ()
|
||||
|
||||
# Add the Slic3r GUI library, libcurl, OpenGL and GLU libraries.
|
||||
if (SLIC3R_GUI)
|
||||
# target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES})
|
||||
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
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -411,6 +411,9 @@ void AppConfig::set_defaults()
|
||||
if (get("show_overhang").empty())
|
||||
set_bool("show_overhang", false);
|
||||
|
||||
if (get("show_center_of_mass").empty())
|
||||
set_bool("show_center_of_mass", false);
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
//#ifdef SUPPORT_3D_CONNEXION
|
||||
|
||||
@@ -112,6 +112,8 @@ set(lisbslic3r_sources
|
||||
CommonDefs.hpp
|
||||
Config.cpp
|
||||
Config.hpp
|
||||
ConnectedBodies.cpp
|
||||
ConnectedBodies.hpp
|
||||
ContourZ.cpp
|
||||
CustomGCode.cpp
|
||||
CustomGCode.hpp
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Slic3r { namespace csg {
|
||||
// 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
|
||||
// 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...
|
||||
//
|
||||
// A few simple templated interface functions are provided here and a default
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#include <numeric>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
@@ -802,72 +800,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
|
||||
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); }
|
||||
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset)
|
||||
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); }
|
||||
|
||||
namespace ClipperUtils {
|
||||
std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile)
|
||||
{
|
||||
BoundingBox extent;
|
||||
std::vector<BoundingBox> bboxes;
|
||||
bboxes.reserve(expolygons.size());
|
||||
for (const ExPolygon &expoly : expolygons) {
|
||||
bboxes.emplace_back(get_extents(expoly));
|
||||
extent.merge(bboxes.back());
|
||||
}
|
||||
if (! extent.defined)
|
||||
return {};
|
||||
const int tiles = std::clamp(int(std::sqrt(double(expolygons.size()) / double(std::max<size_t>(per_tile, 1)))), 1, 32);
|
||||
const Point size = extent.size();
|
||||
const coord_t tile_w = std::max<coord_t>(1, size.x() / tiles + 1), tile_h = std::max<coord_t>(1, size.y() / tiles + 1);
|
||||
std::vector<ExPolygonsTile> out(size_t(tiles * tiles));
|
||||
for (size_t i = 0; i < expolygons.size(); ++ i) {
|
||||
const Point c = bboxes[i].center();
|
||||
ExPolygonsTile &tile = out[size_t(std::clamp(int((c.y() - extent.min.y()) / tile_h), 0, tiles - 1) * tiles +
|
||||
std::clamp(int((c.x() - extent.min.x()) / tile_w), 0, tiles - 1))];
|
||||
tile.members.emplace_back(i);
|
||||
tile.bbox.merge(bboxes[i]);
|
||||
}
|
||||
out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end());
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
|
||||
{
|
||||
// A few dozen subject ExPolygons to a tile, each tile one Clipper call with the clip cut to the tile's box.
|
||||
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(subject, 32);
|
||||
// One tile is the plain call: cutting the clip would only cost time.
|
||||
if (tiles.size() <= 1)
|
||||
return _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset);
|
||||
std::vector<BoundingBox> clip_bboxes;
|
||||
clip_bboxes.reserve(clip.size());
|
||||
for (const Polygon &polygon : clip)
|
||||
clip_bboxes.emplace_back(get_extents(polygon));
|
||||
|
||||
std::vector<Slic3r::ExPolygons> out_tiles(tiles.size());
|
||||
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
|
||||
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
|
||||
Slic3r::ExPolygons local_subject;
|
||||
local_subject.reserve(tile.members.size());
|
||||
for (size_t i : tile.members)
|
||||
local_subject.emplace_back(subject[i]);
|
||||
// Grown so that the cut edges of the clip stay clear of the subject, also after the safety offset.
|
||||
const BoundingBox bbox = tile.bbox.inflated(SCALED_EPSILON);
|
||||
Polygons local_clip;
|
||||
for (size_t i = 0; i < clip.size(); ++i)
|
||||
if (clip_bboxes[i].overlap(bbox))
|
||||
if (Polygon clipped = ClipperUtils::clip_clipper_polygon_with_subject_bbox(clip[i], bbox); ! clipped.empty())
|
||||
local_clip.emplace_back(std::move(clipped));
|
||||
out_tiles[tile_idx] = _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(local_subject), ClipperUtils::PolygonsProvider(local_clip), do_safety_offset);
|
||||
});
|
||||
Slic3r::ExPolygons out;
|
||||
for (Slic3r::ExPolygons &out_tile : out_tiles)
|
||||
append(out, std::move(out_tile));
|
||||
return out;
|
||||
}
|
||||
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
|
||||
{ return clipper_ex_by_piece(ctDifference, subject, clip, do_safety_offset); }
|
||||
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
|
||||
{ return clipper_ex_by_piece(ctIntersection, subject, clip, do_safety_offset); }
|
||||
// May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative).
|
||||
Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type)
|
||||
{ return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); }
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "Polyline.hpp"
|
||||
#include "Line.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Surface.hpp"
|
||||
@@ -333,15 +332,6 @@ namespace ClipperUtils {
|
||||
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
|
||||
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
|
||||
|
||||
// Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run Clipper on a
|
||||
// layer of many pieces tile by tile. Returns the non-empty tiles, each with the indices of its ExPolygons and their box.
|
||||
struct ExPolygonsTile
|
||||
{
|
||||
BoundingBox bbox;
|
||||
std::vector<size_t> members;
|
||||
};
|
||||
[[nodiscard]] std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile);
|
||||
|
||||
}
|
||||
|
||||
// offset Polygons
|
||||
@@ -537,11 +527,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
|
||||
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
// diff_ex() / intersection_ex() of the subject split into tiles, each against only the part of the clip near it, the tiles in
|
||||
// parallel. The same area as the operation on the whole subject when its ExPolygons do not overlap, and much faster for a
|
||||
// subject of thousands of pieces spread over a layer: Clipper slows down with the number of edges crossing a scan line.
|
||||
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip);
|
||||
Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip);
|
||||
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip);
|
||||
|
||||
@@ -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, ¶ms](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 ®ion, 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 ®ion : 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
|
||||
@@ -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
|
||||
+12
-31
@@ -22,8 +22,6 @@
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../Surface.hpp"
|
||||
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include "AABBTreeLines.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
@@ -677,28 +675,24 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
|
||||
if (!line_based_pattern) {
|
||||
const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing);
|
||||
|
||||
// Each expolygon is split on its own, so they run in parallel and are collected in their original order.
|
||||
std::vector<std::pair<ExPolygons, ExPolygons>> split_parts(fill.expolygons.size()); // normal, narrow
|
||||
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t idx) {
|
||||
const ExPolygon &expolygon = fill.expolygons[idx];
|
||||
for (const ExPolygon &expolygon : fill.expolygons) {
|
||||
Polygons filled_area = to_polygons(expolygon);
|
||||
|
||||
// "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon.
|
||||
Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing));
|
||||
|
||||
if (inner_area.empty()) {
|
||||
split_parts[idx].second.emplace_back(expolygon);
|
||||
return;
|
||||
narrow_infill.emplace_back(expolygon);
|
||||
continue;
|
||||
}
|
||||
|
||||
ExPolygons inner_ex = union_ex(inner_area);
|
||||
ExPolygons expolys{expolygon};
|
||||
split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area
|
||||
split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area
|
||||
});
|
||||
for (auto &[normal_ex, narrow_ex] : split_parts) {
|
||||
append(normal_infill, std::move(normal_ex));
|
||||
append(narrow_infill, std::move(narrow_ex));
|
||||
ExPolygons narrow_ex = diff_ex(expolys, inner_ex);
|
||||
ExPolygons normal_ex = intersection_ex(expolys, inner_ex);
|
||||
|
||||
append(normal_infill, normal_ex); // normal infill area
|
||||
append(narrow_infill, narrow_ex); // narrow infill area
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -720,10 +714,7 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
|
||||
}
|
||||
const double aligning_angle = -base_angle + PI;
|
||||
|
||||
// Each expolygon is reconstructed on its own, so they run in parallel and are collected in their original order.
|
||||
std::vector<Polygons> split_reconstructed(fill.expolygons.size());
|
||||
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t expolygon_idx) {
|
||||
const ExPolygon &expolygon = fill.expolygons[expolygon_idx];
|
||||
for (const ExPolygon &expolygon : fill.expolygons) {
|
||||
Polygons filled_area = to_polygons(expolygon);
|
||||
polygons_rotate(filled_area, aligning_angle);
|
||||
BoundingBox bb = get_extents(filled_area);
|
||||
@@ -854,10 +845,8 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
|
||||
}
|
||||
}
|
||||
|
||||
split_reconstructed[expolygon_idx] = std::move(reconstructed_area);
|
||||
});
|
||||
for (Polygons &reconstructed_area : split_reconstructed)
|
||||
polygons_append(normal_fill_areas, std::move(reconstructed_area));
|
||||
polygons_append(normal_fill_areas, reconstructed_area);
|
||||
}
|
||||
|
||||
polygons_rotate(normal_fill_areas, -aligning_angle);
|
||||
|
||||
@@ -1484,15 +1473,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
|
||||
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
|
||||
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
|
||||
|
||||
// Only the part of the layer-wide no-overlap area under this expolygon matters, so clip it to the
|
||||
// expolygon's box first (padded past the safety offset, which grows the clip side). The result is
|
||||
// identical; the cost is not: a layer split into many small fills, e.g. by colour painting,
|
||||
// otherwise intersects every one of them with the whole layer.
|
||||
BoundingBox no_overlap_bbox = get_extents(expoly);
|
||||
no_overlap_bbox.offset(SCALED_EPSILON);
|
||||
f->no_overlap_expolygons = intersection_ex(
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(surface_fill.no_overlap_expolygons, no_overlap_bbox),
|
||||
ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
|
||||
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
|
||||
if (params.symmetric_infill_y_axis) {
|
||||
params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
|
||||
expoly.symmetric_y(params.symmetric_y_axis);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "Polygon.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "PrintBase.hpp"
|
||||
#include "ConnectedBodies.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "enum_bitmask.hpp"
|
||||
#include "libslic3r.h"
|
||||
@@ -2590,6 +2591,156 @@ WipeTowerType GCode::wipe_tower_type()
|
||||
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)
|
||||
{
|
||||
PROFILE_CLEAR();
|
||||
@@ -3112,6 +3263,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
// modifies m_silent_time_estimator_enabled
|
||||
DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled,
|
||||
print.get_layered_nozzle_group_result());
|
||||
set_mass_locator(m_processor, print);
|
||||
const bool is_bbl_printers = print.is_BBL_printer();
|
||||
const bool skip_config_block = print.config().gcode_skip_config_block;
|
||||
const WipeTowerType wipe_tower_type = print.wipe_tower_type();
|
||||
|
||||
@@ -89,7 +89,6 @@ static const float DEFAULT_TRAVEL_ACCELERATION = 1250.0f;
|
||||
static const size_t MIN_EXTRUDERS_COUNT = 5;
|
||||
static const float DEFAULT_FILAMENT_DIAMETER = 1.75f;
|
||||
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 int DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE = 0;
|
||||
static const Slic3r::Vec3f DEFAULT_EXTRUDER_OFFSET = Slic3r::Vec3f::Zero();
|
||||
@@ -2604,6 +2603,10 @@ void GCodeProcessorResult::reset() {
|
||||
lock();
|
||||
|
||||
moves.clear();
|
||||
plate_mass = {};
|
||||
object_masses.clear();
|
||||
body_masses.clear();
|
||||
support_masses.clear();
|
||||
lines_ends.clear();
|
||||
printable_area = Pointfs();
|
||||
//BBS: add bed exclude area
|
||||
@@ -3702,6 +3705,7 @@ void GCodeProcessor::reset()
|
||||
m_g1_line_id = 0;
|
||||
m_layer_id = 0;
|
||||
m_cp_color.reset();
|
||||
m_mass_locator = nullptr;
|
||||
|
||||
m_producer = EProducer::Unknown;
|
||||
|
||||
@@ -3841,6 +3845,7 @@ void GCodeProcessor::process_buffer(const std::string &buffer)
|
||||
void GCodeProcessor::finalize(bool post_process)
|
||||
{
|
||||
m_result.z_offset = m_z_offset;
|
||||
finalize_object_masses();
|
||||
|
||||
// update width/height of wipe 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);
|
||||
}
|
||||
|
||||
if (type == EMoveType::Extrude)
|
||||
add_object_mass(filament_id, area_filament_cross_section * delta_pos[E]);
|
||||
|
||||
// store move
|
||||
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)
|
||||
{
|
||||
m_used_filaments.process_role_cache(this);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "libslic3r/CommonDefs.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/ArcFitter.hpp"
|
||||
@@ -35,6 +36,9 @@ namespace Slic3r {
|
||||
|
||||
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
|
||||
#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"
|
||||
@@ -270,9 +274,44 @@ class Print;
|
||||
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;
|
||||
unsigned int id;
|
||||
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.
|
||||
std::vector<size_t> lines_ends;
|
||||
Pointfs printable_area;
|
||||
@@ -360,6 +399,10 @@ class Print;
|
||||
filename = std::forward<Other>(other).filename;
|
||||
id = std::forward<Other>(other).id;
|
||||
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;
|
||||
printable_area = std::forward<Other>(other).printable_area;
|
||||
bed_exclude_area = std::forward<Other>(other).bed_exclude_area;
|
||||
@@ -1099,6 +1142,15 @@ class Print;
|
||||
};
|
||||
#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:
|
||||
CommandProcessor m_command_processor;
|
||||
GCodeReader m_parser;
|
||||
@@ -1126,6 +1178,7 @@ class Print;
|
||||
bool m_skippable{false};
|
||||
SkipType m_skippable_type{SkipType::stNone};
|
||||
int m_object_label_id{-1};
|
||||
MassLocator m_mass_locator;
|
||||
float m_print_z{0.0f};
|
||||
std::vector<float> m_remaining_volume;
|
||||
ExtruderTemps m_filament_nozzle_temp;
|
||||
@@ -1280,6 +1333,13 @@ class Print;
|
||||
const std::vector<std::set<int>>& unprintable_filament_types );
|
||||
void apply_config(const PrintConfig& config);
|
||||
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
|
||||
// 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) {
|
||||
@@ -1534,6 +1594,8 @@ class Print;
|
||||
|
||||
//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 add_object_mass(int filament_id, float volume);
|
||||
void finalize_object_masses();
|
||||
|
||||
void set_extrusion_role(ExtrusionRole role);
|
||||
// Resolve the SKIPPABLE_TYPE payload to a SkipType.
|
||||
|
||||
@@ -140,87 +140,15 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
|
||||
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
|
||||
};
|
||||
|
||||
// For many islands, the same scan with the edges binned in a uniform grid over their boxes, so each edge is only tested against the edges sharing a
|
||||
// cell with it - two edges whose boxes overlap always do. It returns the same crossing as the all-pairs scan
|
||||
// (smallest i, then smallest j), so the result is unchanged. The all-pairs scan is quadratic in the edge count and
|
||||
// runs again after every reversal; rebuilding the grid costs more than it saves below the threshold.
|
||||
constexpr size_t grid_min_size = 500;
|
||||
BoundingBox extent;
|
||||
for (size_t idx : path)
|
||||
extent.merge(centers[idx]);
|
||||
const int grid_n = std::clamp(int(std::sqrt(double(pn))), 1, 256);
|
||||
const coord_t cell_w = std::max<coord_t>(1, (extent.max.x() - extent.min.x()) / grid_n + 1);
|
||||
const coord_t cell_h = std::max<coord_t>(1, (extent.max.y() - extent.min.y()) / grid_n + 1);
|
||||
const auto for_cells = [&](const Point& a, const Point& b, auto&& fn) {
|
||||
const int x0 = int((std::min(a.x(), b.x()) - extent.min.x()) / cell_w), x1 = int((std::max(a.x(), b.x()) - extent.min.x()) / cell_w);
|
||||
const int y0 = int((std::min(a.y(), b.y()) - extent.min.y()) / cell_h), y1 = int((std::max(a.y(), b.y()) - extent.min.y()) / cell_h);
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
fn(y * grid_n + x);
|
||||
};
|
||||
std::vector<std::vector<size_t>> edge_cells(size_t(grid_n) * grid_n);
|
||||
|
||||
auto find_crossing_grid = [&]() -> std::pair<size_t, size_t> {
|
||||
for (std::vector<size_t>& cell : edge_cells)
|
||||
cell.clear();
|
||||
for (size_t j = 0; j < n_edges; ++j)
|
||||
for_cells(centers[path[j]], centers[path[(j + 1) % pn]], [&](int cell) { edge_cells[cell].emplace_back(j); });
|
||||
|
||||
for (size_t i = 0; i < n_edges; ++i) {
|
||||
const Point& ai = centers[path[i]];
|
||||
const Point& bi = centers[path[(i + 1) % pn]];
|
||||
|
||||
size_t first_j = std::numeric_limits<size_t>::max();
|
||||
for_cells(ai, bi, [&](int cell) {
|
||||
for (size_t j : edge_cells[cell]) {
|
||||
if (j < i + 2 || j >= first_j) continue;
|
||||
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
|
||||
if (i == 0 && j == pn - 1) continue;
|
||||
|
||||
const Point& aj = centers[path[j]];
|
||||
const Point& bj = centers[path[(j + 1) % pn]];
|
||||
|
||||
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
|
||||
if (Geometry::segments_intersect(ai, bi, aj, bj))
|
||||
first_j = j;
|
||||
}
|
||||
});
|
||||
if (first_j != std::numeric_limits<size_t>::max())
|
||||
return {i, first_j};
|
||||
}
|
||||
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
|
||||
};
|
||||
|
||||
// Process crossings one at a time: find first, reverse it, restart scan.
|
||||
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
|
||||
const int max_iters = static_cast<int>(pn * pn);
|
||||
int max_iters = static_cast<int>(pn * pn);
|
||||
bool improved = false;
|
||||
// Reversing between two segments that only touch or overlap along a line need not remove the intersection, so on
|
||||
// islands laid out on a regular grid (a tiled texture, an array of parts) the loop can cycle through the same
|
||||
// orderings until the pn * pn cap. Once an ordering repeats the rest of the loop is periodic, so only the steps
|
||||
// to the ordering the capped loop would have stopped on are taken.
|
||||
std::unordered_map<uint64_t, int> seen_paths; // path hash -> reversals done when it was reached
|
||||
const auto path_hash = [&path]() {
|
||||
uint64_t h = 1469598103934665603ull; // FNV-1a
|
||||
for (size_t idx : path)
|
||||
h = (h ^ uint64_t(idx)) * 1099511628211ull;
|
||||
return h;
|
||||
};
|
||||
const auto reverse_first_crossing = [&]() {
|
||||
auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing();
|
||||
if (ci == std::numeric_limits<size_t>::max())
|
||||
return false;
|
||||
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
|
||||
return true;
|
||||
};
|
||||
seen_paths.emplace(path_hash(), 0);
|
||||
for (int iter = 1; iter <= max_iters && reverse_first_crossing(); ++iter) {
|
||||
while (max_iters-- > 0) {
|
||||
auto [ci, cj] = find_crossing();
|
||||
if (ci == std::numeric_limits<size_t>::max()) break;
|
||||
improved = true;
|
||||
if (auto [it, inserted] = seen_paths.emplace(path_hash(), iter); !inserted) {
|
||||
for (int steps = (max_iters - iter) % (iter - it->second); steps > 0; --steps)
|
||||
reverse_first_crossing();
|
||||
break;
|
||||
}
|
||||
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
|
||||
}
|
||||
return improved;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -1211,21 +1210,21 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|
||||
const size_t layer_idx, const float max_distance,
|
||||
const SeamPlacerImpl::SeamComparator &comparator) const {
|
||||
using namespace SeamPlacerImpl;
|
||||
// Find the best nearby point and the nearest one. A layer of a fine relief has tens of thousands of candidates within
|
||||
// the radius, so they are looked at as the search finds them rather than collected into a vector first.
|
||||
constexpr size_t none = std::numeric_limits<size_t>::max();
|
||||
size_t best_nearby_point_index = none;
|
||||
size_t nearest_point_index = none;
|
||||
visit_nearby_points(*layers[layer_idx].points_tree, projected_position, max_distance,
|
||||
[&layers, &comparator, &projected_position, layer_idx, &best_nearby_point_index, &nearest_point_index]
|
||||
(size_t nearby_point_index) {
|
||||
if (best_nearby_point_index == none) {
|
||||
// The first point found starts both, as the first of the collected ones did.
|
||||
best_nearby_point_index = nearest_point_index = nearby_point_index;
|
||||
}
|
||||
std::vector<size_t> nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position,
|
||||
max_distance);
|
||||
|
||||
if (nearby_points_indices.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t best_nearby_point_index = nearby_points_indices[0];
|
||||
size_t nearest_point_index = nearby_points_indices[0];
|
||||
|
||||
// Now find best nearby point, nearest point, and corresponding indices
|
||||
for (const size_t &nearby_point_index : nearby_points_indices) {
|
||||
const SeamCandidate &point = layers[layer_idx].points[nearby_point_index];
|
||||
if (point.perimeter.finalized) {
|
||||
return; // skip over finalized perimeters, try to find some that is not finalized
|
||||
continue; // skip over finalized perimeters, try to find some that is not finalized
|
||||
}
|
||||
if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index],
|
||||
projected_position.head<2>())
|
||||
@@ -1237,10 +1236,6 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|
||||
|| layers[layer_idx].points[nearest_point_index].perimeter.finalized) {
|
||||
nearest_point_index = nearby_point_index;
|
||||
}
|
||||
});
|
||||
|
||||
if (best_nearby_point_index == none) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index];
|
||||
|
||||
@@ -318,36 +318,6 @@ std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const P
|
||||
return visitor.result;
|
||||
}
|
||||
|
||||
// Visits the points within max_distance of center, in the order find_nearby_points() would collect them, and hands
|
||||
// each of them to `visitor_fn` instead of returning them all: a search over a dense set spends more on collecting the
|
||||
// points into a vector than on the search itself, and its caller usually keeps only a few of them.
|
||||
template<typename KDTreeIndirectType, typename PointType, typename VisitorFn>
|
||||
void visit_nearby_points(const KDTreeIndirectType &kdtree, const PointType ¢er,
|
||||
const typename KDTreeIndirectType::CoordType &max_distance, VisitorFn visitor_fn)
|
||||
{
|
||||
using CoordType = typename KDTreeIndirectType::CoordType;
|
||||
|
||||
struct Visitor {
|
||||
const KDTreeIndirectType &kdtree;
|
||||
const PointType center;
|
||||
const CoordType max_distance_squared;
|
||||
VisitorFn visitor_fn;
|
||||
|
||||
unsigned int operator()(size_t idx, size_t dimension) {
|
||||
auto dist = CoordType(0);
|
||||
for (size_t i = 0; i < KDTreeIndirectType::NumDimensions; ++i) {
|
||||
CoordType d = center[i] - kdtree.coordinate(idx, i);
|
||||
dist += d * d;
|
||||
}
|
||||
if (dist < max_distance_squared)
|
||||
visitor_fn(idx);
|
||||
return kdtree.descent_mask(center[dimension], max_distance_squared, idx, dimension);
|
||||
}
|
||||
} visitor { kdtree, center, max_distance * max_distance, visitor_fn };
|
||||
|
||||
kdtree.visit(visitor);
|
||||
}
|
||||
|
||||
template<typename KDTreeIndirectType, typename PointType>
|
||||
std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType ¢er,
|
||||
const typename KDTreeIndirectType::CoordType& max_distance)
|
||||
|
||||
@@ -99,11 +99,10 @@ void LayerRegion::slices_to_fill_surfaces_clipped()
|
||||
by_surface[size_t(surface.surface_type)].emplace_back(&surface);
|
||||
// Trim surfaces by the fill_boundaries.
|
||||
this->fill_surfaces.surfaces.clear();
|
||||
const Polygons fill_boundaries = to_polygons(this->fill_expolygons);
|
||||
for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) {
|
||||
const SurfacesPtr &this_surfaces = by_surface[surface_type];
|
||||
if (! this_surfaces.empty())
|
||||
this->fill_surfaces.append(intersection_ex_by_piece(to_expolygons(this_surfaces), fill_boundaries), SurfaceType(surface_type));
|
||||
this->fill_surfaces.append(intersection_ex(this_surfaces, this->fill_expolygons), SurfaceType(surface_type));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "Surface.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <numeric>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <list>
|
||||
@@ -1363,15 +1362,10 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
}
|
||||
#endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM
|
||||
|
||||
// When the upper surface of an object is occluded, it should no longer be considered the upper surface.
|
||||
// Every (colour, layer) pair is trimmed on its own, so they all run at once: the painted faces of a finely
|
||||
// textured part project hundreds of thousands of triangles onto one layer, which used to be trimmed serially.
|
||||
// When the upper surface of an object is occluded, it should no longer be considered the upper surface
|
||||
{
|
||||
const size_t occluded_pairs = num_facets_states * layers.size();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, occluded_pairs), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t pair_idx = range.begin(); pair_idx < range.end(); ++pair_idx) {
|
||||
const size_t extruder_idx = pair_idx / layers.size();
|
||||
const size_t layer_idx = pair_idx % layers.size();
|
||||
for (size_t extruder_idx = 0; extruder_idx < num_facets_states; ++extruder_idx) {
|
||||
for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) {
|
||||
if (!top_raw[extruder_idx].empty() && !top_raw[extruder_idx][layer_idx].empty() && layer_idx + 1 < layers.size()) {
|
||||
top_raw[extruder_idx][layer_idx] = diff(top_raw[extruder_idx][layer_idx], input_expolygons[layer_idx + 1]);
|
||||
}
|
||||
@@ -1379,7 +1373,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
bottom_raw[extruder_idx][layer_idx] = diff(bottom_raw[extruder_idx][layer_idx], input_expolygons[layer_idx - 1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<ExPolygons>> triangles_by_color_bottom(num_facets_states);
|
||||
@@ -1437,58 +1431,11 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
return out;
|
||||
};
|
||||
|
||||
// Projects a painted top or bottom face `ex` of layer `layer_idx` onto the shell layers below or above it (in
|
||||
// `shell_layers`, nearest first), one more perimeter in on each, stopping at the first layer where nothing is left.
|
||||
// Only the slices within the deepest offset of `ex` (three times that with the miter joins) decide the result, so the
|
||||
// work is done per tile of `ex`'s ExPolygons on the slices cut to the tile's box grown by that much: the same result, but
|
||||
// each Clipper call stays the size of a tile rather than of a layer cut through a fine relief, and the tiles run in
|
||||
// parallel.
|
||||
const auto project_to_shells = [&input_expolygons](const ExPolygons &ex, size_t layer_idx, const std::vector<size_t> &shell_layers,
|
||||
const LayerColorStat &stat, ShellProjections &dst) {
|
||||
std::vector<float> offsets(shell_layers.size());
|
||||
float offset = 0.f;
|
||||
for (size_t i = 0; i < shell_layers.size(); ++i) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
offsets[i] = offset;
|
||||
}
|
||||
if (offsets.empty())
|
||||
return;
|
||||
const coord_t reach = coord_t(std::ceil(DefaultMiterLimit * std::abs(offsets.back()))) + 10 * SCALED_EPSILON;
|
||||
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(ex, 16);
|
||||
// [shell layer][tile]
|
||||
std::vector<std::vector<ExPolygons>> shells(shell_layers.size(), std::vector<ExPolygons>(tiles.size()));
|
||||
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
|
||||
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
|
||||
const BoundingBox bbox = tile.bbox.inflated(reach);
|
||||
ExPolygons tile_ex;
|
||||
tile_ex.reserve(tile.members.size());
|
||||
for (size_t i : tile.members)
|
||||
tile_ex.emplace_back(ex[i]);
|
||||
Polygons layer_slices_trimmed = ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[layer_idx], bbox);
|
||||
for (size_t i = 0; i < shell_layers.size() && ! layer_slices_trimmed.empty(); ++i) {
|
||||
const ExPolygons trimmed = intersection_ex(layer_slices_trimmed, ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[shell_layers[i]], bbox));
|
||||
shells[i][tile_idx] = opening_ex(intersection_ex(tile_ex, offset_ex(trimmed, offsets[i])), stat.small_region_threshold);
|
||||
layer_slices_trimmed = to_polygons(trimmed);
|
||||
}
|
||||
});
|
||||
for (size_t i = 0; i < shell_layers.size(); ++i) {
|
||||
ExPolygons shell;
|
||||
for (ExPolygons &tile_shell : shells[i])
|
||||
append(shell, std::move(tile_shell));
|
||||
if (shell.empty())
|
||||
break;
|
||||
dst.emplace_back(shell_layers[i], std::move(shell));
|
||||
}
|
||||
};
|
||||
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
|
||||
&throw_on_cancel_callback, &bottom_raw, &triangles_by_color_bottom, &project_to_shells,
|
||||
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
|
||||
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
|
||||
// Each colour writes only its own vectors, so the colours run in parallel: a painted top or bottom face
|
||||
// projects onto a single layer, which otherwise did all of its colours on one thread.
|
||||
tbb::parallel_for(size_t(0), size_t(num_facets_states), [&](size_t color_idx) {
|
||||
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
|
||||
throw_on_cancel_callback();
|
||||
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
|
||||
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
|
||||
@@ -1497,10 +1444,18 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
top_ex = opening_ex(top_ex, stat.small_region_threshold);
|
||||
if (! top_ex.empty()) {
|
||||
append(triangles_by_color_top[color_idx][layer_idx], top_ex);
|
||||
std::vector<size_t> shell_layers;
|
||||
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx)
|
||||
shell_layers.emplace_back(size_t(last_idx));
|
||||
project_to_shells(top_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_top[color_idx][layer_idx]);
|
||||
float offset = 0.f;
|
||||
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
|
||||
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
//offset -= stat.extrusion_width ;
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
|
||||
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
|
||||
if (last.empty())
|
||||
break;
|
||||
shell_triangles_by_color_top[color_idx][layer_idx].emplace_back(size_t(last_idx), std::move(last));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
|
||||
@@ -1509,13 +1464,21 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
|
||||
if (! bottom_ex.empty()) {
|
||||
append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex);
|
||||
std::vector<size_t> shell_layers;
|
||||
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx)
|
||||
shell_layers.emplace_back(last_idx);
|
||||
project_to_shells(bottom_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_bottom[color_idx][layer_idx]);
|
||||
float offset = 0.f;
|
||||
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
|
||||
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
//offset -= stat.extrusion_width;
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
|
||||
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
|
||||
if (last.empty())
|
||||
break;
|
||||
shell_triangles_by_color_bottom[color_idx][layer_idx].emplace_back(last_idx, std::move(last));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1536,23 +1499,20 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
&shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
|
||||
throw_on_cancel_callback();
|
||||
// The per-colour unions below are independent of each other, so they run in parallel (a painted top or
|
||||
// bottom face puts all of its colours on one layer); whatever combines the colours stays in colour order.
|
||||
const auto merge_colour_union = [&](size_t color_idx) {
|
||||
ExPolygons painted_exploys;
|
||||
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
|
||||
auto &self = triangles_by_color_merged[color_idx][layer_idx];
|
||||
append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx]));
|
||||
append(self, std::move(triangles_by_color_top[color_idx][layer_idx]));
|
||||
self = union_ex(self);
|
||||
};
|
||||
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), merge_colour_union);
|
||||
|
||||
ExPolygons painted_exploys;
|
||||
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx)
|
||||
append(painted_exploys, triangles_by_color_merged[color_idx][layer_idx]);
|
||||
append(painted_exploys, self);
|
||||
}
|
||||
|
||||
painted_exploys = union_ex(painted_exploys);
|
||||
|
||||
//BBS: merge the top and bottom shell layers
|
||||
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), [&](size_t color_idx) {
|
||||
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
|
||||
auto &self = triangles_by_color_merged[color_idx][layer_idx];
|
||||
|
||||
auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys);
|
||||
@@ -1561,7 +1521,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
append(self, top_area);
|
||||
append(self, bottom_area);
|
||||
self = union_ex(self);
|
||||
});
|
||||
}
|
||||
// Trim one region by the other if some of the regions overlap.
|
||||
ExPolygons painted_regions;
|
||||
for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) {
|
||||
@@ -1928,69 +1888,7 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Finds the islands (layer ExPolygons) a region piece overlaps. A top or bottom region is projected from the neighbouring
|
||||
// layers and may reach past the island it belongs to, or over several islands.
|
||||
class IslandLocator
|
||||
{
|
||||
public:
|
||||
explicit IslandLocator(const ExPolygons &islands) : m_islands(islands)
|
||||
{
|
||||
m_bboxes.reserve(islands.size());
|
||||
for (const ExPolygon &island : islands) {
|
||||
m_bboxes.emplace_back(get_extents(island));
|
||||
m_extent.merge(m_bboxes.back());
|
||||
}
|
||||
if (!m_extent.defined)
|
||||
return;
|
||||
const Point size = m_extent.size();
|
||||
m_cell_w = std::max<coord_t>(1, size.x() / GRID + 1);
|
||||
m_cell_h = std::max<coord_t>(1, size.y() / GRID + 1);
|
||||
m_grid.assign(GRID * GRID, {});
|
||||
for (size_t i = 0; i < m_bboxes.size(); ++i)
|
||||
for_cells(m_bboxes[i], [&](int cell) { m_grid[cell].emplace_back(i); });
|
||||
}
|
||||
|
||||
void find(const ExPolygon &piece, std::vector<size_t> &out) const
|
||||
{
|
||||
out.clear();
|
||||
const BoundingBox bbox = get_extents(piece);
|
||||
if (!m_extent.defined || !m_extent.overlap(bbox))
|
||||
return;
|
||||
for_cells(bbox, [&](int cell) {
|
||||
for (size_t i : m_grid[cell])
|
||||
if (m_bboxes[i].overlap(bbox))
|
||||
out.emplace_back(i);
|
||||
});
|
||||
sort_remove_duplicates(out);
|
||||
if (out.size() > 1)
|
||||
out.erase(std::remove_if(out.begin(), out.end(), [&](size_t i) {
|
||||
const BoundingBox common(m_bboxes[i].min.cwiseMax(bbox.min), m_bboxes[i].max.cwiseMin(bbox.max));
|
||||
return intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(piece, common.inflated(SCALED_EPSILON)),
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(m_islands[i], common.inflated(SCALED_EPSILON))).empty();
|
||||
}), out.end());
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int GRID = 64;
|
||||
template<typename Fn> void for_cells(const BoundingBox &bb, Fn &&fn) const
|
||||
{
|
||||
const int x0 = std::clamp(int((bb.min.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1), x1 = std::clamp(int((bb.max.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1);
|
||||
const int y0 = std::clamp(int((bb.min.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1), y1 = std::clamp(int((bb.max.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1);
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
fn(y * GRID + x);
|
||||
}
|
||||
|
||||
const ExPolygons &m_islands;
|
||||
std::vector<BoundingBox> m_bboxes;
|
||||
BoundingBox m_extent;
|
||||
coord_t m_cell_w = 1, m_cell_h = 1;
|
||||
std::vector<std::vector<size_t>> m_grid;
|
||||
};
|
||||
|
||||
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<ExPolygons> &input_expolygons,
|
||||
const std::vector<std::vector<ExPolygons>> &segmented_regions,
|
||||
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<std::vector<ExPolygons>> &segmented_regions,
|
||||
std::vector<std::vector<ExPolygons>> &&top_and_bottom_layers,
|
||||
const size_t num_facets_states,
|
||||
const std::function<void()> &throw_on_cancel_callback)
|
||||
@@ -2001,91 +1899,33 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
|
||||
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
|
||||
// Every region of a layer is merged together with the regions of the islands it overlaps, and the islands are further
|
||||
// apart than the dimple removal below reaches, so this gives the same result as merging the layer at once. On a layer
|
||||
// cut through a fine relief every region shares thousands of hole contours with every other, and Clipper, splitting
|
||||
// and re-linking one huge polygon over and over, took anything up to half an hour for a layer; per island each operation
|
||||
// stays the size of the island, and the islands run in parallel.
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&](const tbb::blocked_range<size_t> &range) {
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
|
||||
assert(segmented_regions[layer_idx].size() == num_facets_states);
|
||||
throw_on_cancel_callback();
|
||||
// Group the islands joined by a region overlapping several of them; the last group takes the regions lying
|
||||
// outside every island.
|
||||
const ExPolygons &islands = input_expolygons[layer_idx];
|
||||
const IslandLocator locator(islands);
|
||||
std::vector<size_t> parent(islands.size() + 1);
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
const auto root = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
// Islands of every piece: side regions of colours 1.., then top/bottom regions of colours 0..
|
||||
std::vector<const ExPolygon *> pieces;
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
|
||||
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
|
||||
pieces.emplace_back(&piece);
|
||||
if (!top_and_bottom_layers.empty())
|
||||
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
|
||||
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
|
||||
pieces.emplace_back(&piece);
|
||||
std::vector<std::vector<size_t>> overlapped(pieces.size());
|
||||
tbb::parallel_for(size_t(0), pieces.size(), [&](size_t i) { locator.find(*pieces[i], overlapped[i]); });
|
||||
std::vector<size_t> piece_island(pieces.size());
|
||||
for (size_t i = 0; i < pieces.size(); ++i) {
|
||||
piece_island[i] = overlapped[i].empty() ? islands.size() : overlapped[i].front();
|
||||
for (size_t island : overlapped[i])
|
||||
parent[root(island)] = root(piece_island[i]);
|
||||
}
|
||||
std::vector<size_t> bucket_of(parent.size(), size_t(-1));
|
||||
size_t num_buckets = 0;
|
||||
for (size_t i = 0; i < parent.size(); ++i)
|
||||
if (size_t &b = bucket_of[root(i)]; b == size_t(-1))
|
||||
b = num_buckets++;
|
||||
|
||||
// [bucket][colour]
|
||||
std::vector<std::vector<ExPolygons>> sides(num_buckets, std::vector<ExPolygons>(num_facets_states));
|
||||
std::vector<std::vector<ExPolygons>> tops(num_buckets, std::vector<ExPolygons>(num_facets_states));
|
||||
size_t piece_idx = 0;
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
|
||||
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
|
||||
sides[bucket_of[root(piece_island[piece_idx++])]][extruder_id].emplace_back(piece);
|
||||
if (!top_and_bottom_layers.empty())
|
||||
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
|
||||
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
|
||||
tops[bucket_of[root(piece_island[piece_idx++])]][color_idx].emplace_back(piece);
|
||||
|
||||
// Side regions minus the top/bottom regions of every colour.
|
||||
std::vector<std::vector<ExPolygons>> merged(num_buckets, std::vector<ExPolygons>(num_facets_states));
|
||||
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
|
||||
Polygons tops_all;
|
||||
for (const ExPolygons &t : tops[bucket])
|
||||
polygons_append(tops_all, t);
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
|
||||
if (!sides[bucket][extruder_id].empty())
|
||||
merged[bucket][extruder_id] = tops_all.empty() ? std::move(sides[bucket][extruder_id]) :
|
||||
diff_ex_by_piece(sides[bucket][extruder_id], tops_all);
|
||||
});
|
||||
|
||||
// Then this colour's top/bottom regions, with the dimples removed (#7235) when the layer has side regions left.
|
||||
// Zero is skipped because it is the default color of the volume
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
|
||||
if (top_and_bottom_layers.empty() || top_and_bottom_layers[extruder_id][layer_idx].empty()) {
|
||||
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
|
||||
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
|
||||
continue;
|
||||
throw_on_cancel_callback();
|
||||
if (!segmented_regions[layer_idx][extruder_id].empty()) {
|
||||
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id];
|
||||
if (!top_and_bottom_layers.empty()) {
|
||||
for (const std::vector<ExPolygons> &top_and_bottom_by_extruder : top_and_bottom_layers) {
|
||||
if (!top_and_bottom_by_extruder[layer_idx].empty() && !segmented_regions_trimmed.empty()) {
|
||||
segmented_regions_trimmed = diff_ex(segmented_regions_trimmed, top_and_bottom_by_extruder[layer_idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed);
|
||||
}
|
||||
|
||||
if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) {
|
||||
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty();
|
||||
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]);
|
||||
|
||||
// Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
|
||||
if (!was_top_and_bottom_empty)
|
||||
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
|
||||
}
|
||||
bool was_top_and_bottom_empty = true;
|
||||
for (size_t bucket = 0; bucket < num_buckets && was_top_and_bottom_empty; ++bucket)
|
||||
was_top_and_bottom_empty = merged[bucket][extruder_id].empty();
|
||||
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
|
||||
ExPolygons ®ion = merged[bucket][extruder_id];
|
||||
append(region, tops[bucket][extruder_id]);
|
||||
if (!was_top_and_bottom_empty && !region.empty())
|
||||
region = offset2_ex(union_ex(region), float(SCALED_EPSILON), -float(SCALED_EPSILON));
|
||||
});
|
||||
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
|
||||
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
|
||||
}
|
||||
}
|
||||
}); // end of parallel_for
|
||||
@@ -2373,56 +2213,16 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
|
||||
|
||||
assert(!color_poly.empty());
|
||||
assert(!color_poly.front().empty());
|
||||
// Each island (an ExPolygon with its holes) is segmented on its own. Any point of an island is closer to
|
||||
// that island's contours than to any other island's - the way out crosses its own boundary first - so its
|
||||
// Voronoi cells, and with them its colour regions, depend on nothing else. A layer cut through a fine relief
|
||||
// has thousands of islands, and one Voronoi diagram over all of them degenerated into overlapping regions
|
||||
// that every boolean afterwards had to untangle. Per island the diagrams stay small and the islands run in
|
||||
// parallel; an island in a single colour needs no diagram at all.
|
||||
const ExPolygons &islands = input_expolygons[layer_idx];
|
||||
std::vector<std::pair<size_t, size_t>> island_contours(islands.size()); // [first, last) into color_poly
|
||||
{
|
||||
// The same order EdgeGrid::Grid::create() lists the contours in, and so colorize_contours().
|
||||
size_t idx = 0;
|
||||
for (size_t island_idx = 0; island_idx < islands.size(); ++island_idx) {
|
||||
const size_t first = idx;
|
||||
if (!islands[island_idx].contour.empty())
|
||||
++idx;
|
||||
for (const Polygon &hole : islands[island_idx].holes)
|
||||
if (!hole.empty())
|
||||
++idx;
|
||||
island_contours[island_idx] = {first, idx};
|
||||
}
|
||||
assert(idx == color_poly.size());
|
||||
if (has_layer_only_one_color(color_poly)) {
|
||||
// If the whole layer is painted using the same color, it is not needed to construct a Voronoi diagram for the segmentation of this layer.
|
||||
segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx];
|
||||
} else {
|
||||
MMU_Graph graph = build_graph(layer_idx, color_poly);
|
||||
remove_multiple_edges_in_vertices(graph, color_poly);
|
||||
graph.remove_nodes_with_one_arc();
|
||||
segmented_regions[layer_idx] = extract_colored_segments(graph, num_facets_states);
|
||||
//segmented_regions[layer_idx] = extract_colored_segments(color_poly, num_extruders, layer_idx);
|
||||
}
|
||||
std::vector<std::vector<ExPolygons>> island_regions(islands.size());
|
||||
tbb::parallel_for(size_t(0), islands.size(), [&](size_t island_idx) {
|
||||
const auto [first, last] = island_contours[island_idx];
|
||||
if (first == last)
|
||||
return;
|
||||
const std::vector<ColoredLines> island_poly(color_poly.begin() + first, color_poly.begin() + last);
|
||||
std::vector<ExPolygons> ®ions = island_regions[island_idx];
|
||||
if (has_layer_only_one_color(island_poly)) {
|
||||
regions.assign(num_facets_states, ExPolygons());
|
||||
regions[size_t(island_poly.front().front().color)].emplace_back(islands[island_idx]);
|
||||
} else {
|
||||
MMU_Graph graph = build_graph(layer_idx, island_poly);
|
||||
remove_multiple_edges_in_vertices(graph, island_poly);
|
||||
graph.remove_nodes_with_one_arc();
|
||||
regions = extract_colored_segments(graph, num_facets_states);
|
||||
// The faces of one colour tile it without overlapping; merged here, where an island is small,
|
||||
// every later boolean gets a few regions instead of thousands of faces sharing their edges. An
|
||||
// island with many holes keeps its faces: merged, each colour would be one region with thousands
|
||||
// of holes, and subtracting from that is far slower than from the faces one at a time.
|
||||
if (island_poly.size() <= 64)
|
||||
for (ExPolygons &faces : regions)
|
||||
if (faces.size() > 1)
|
||||
faces = union_ex(faces);
|
||||
}
|
||||
});
|
||||
for (std::vector<ExPolygons> ®ions : island_regions)
|
||||
for (size_t color_idx = 0; color_idx < regions.size(); ++color_idx)
|
||||
append(segmented_regions[layer_idx][color_idx], std::move(regions[color_idx]));
|
||||
|
||||
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
|
||||
export_regions_to_svg(debug_out_path("3-mm-regions-sides-%d-%d.svg", layer_idx, iRun), segmented_regions[layer_idx], input_expolygons[layer_idx]);
|
||||
@@ -2445,7 +2245,7 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
|
||||
throw_on_cancel_callback();
|
||||
}
|
||||
|
||||
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(input_expolygons, segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
|
||||
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
|
||||
throw_on_cancel_callback();
|
||||
|
||||
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
|
||||
|
||||
@@ -23,15 +23,11 @@ public:
|
||||
|
||||
MultiPoint() {}
|
||||
MultiPoint(const MultiPoint &other) : points(other.points) {}
|
||||
MultiPoint(MultiPoint &&other) noexcept : points(std::move(other.points)) {}
|
||||
MultiPoint(MultiPoint &&other) : points(std::move(other.points)) {}
|
||||
MultiPoint(std::initializer_list<Point> list) : points(list) {}
|
||||
explicit MultiPoint(const Points &_points) : points(_points) {}
|
||||
// Without it, the derived classes' move constructors passing std::move(points) here copied them, which
|
||||
// also means a moved-from Polygon or Polyline is now really empty where it used to silently keep its
|
||||
// points: a use-after-move anywhere in the tree that happened to work before now sees nothing.
|
||||
explicit MultiPoint(Points &&_points) noexcept : points(std::move(_points)) {}
|
||||
MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; }
|
||||
MultiPoint& operator=(MultiPoint &&other) noexcept { points = std::move(other.points); return *this; }
|
||||
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; }
|
||||
virtual ~MultiPoint() = default;
|
||||
void scale(double factor);
|
||||
void scale(double factor_x, double factor_y);
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
#include <tuple>
|
||||
#include <unordered_set>
|
||||
#include <thread>
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <vector>
|
||||
#include "libslic3r.h"
|
||||
#include <utility>
|
||||
@@ -2554,490 +2552,467 @@ void PerimeterGenerator::process_arachne()
|
||||
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
|
||||
// we need to process each island separately because we might have different
|
||||
// extra perimeters for each one
|
||||
// Each island is independent up to its outputs, so they are generated in parallel - a layer split into thousands
|
||||
// of islands (e.g. by colour painting) otherwise ran on one thread - and the outputs are then committed in the
|
||||
// original island order, which is what the extra overhang perimeters (applied to the last island's loops and to
|
||||
// all fill surfaces so far) depend on.
|
||||
struct ArachneSurfaceResult
|
||||
{
|
||||
ExtrusionEntityCollection loops;
|
||||
bool has_loops = false;
|
||||
ExPolygons infill;
|
||||
ExPolygons no_overlap;
|
||||
};
|
||||
std::vector<ArachneSurfaceResult> results(all_surfaces.size());
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, all_surfaces.size()), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) {
|
||||
const Surface &surface = all_surfaces[surface_idx];
|
||||
ArachneSurfaceResult &result = results[surface_idx];
|
||||
coord_t bead_width_0 = ext_perimeter_spacing;
|
||||
// detect how many perimeters must be generated for this island
|
||||
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
|
||||
int sparse_infill_density = this->config->sparse_infill_density.value;
|
||||
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
|
||||
loop_number++;
|
||||
for (const Surface& surface : all_surfaces) {
|
||||
coord_t bead_width_0 = ext_perimeter_spacing;
|
||||
// detect how many perimeters must be generated for this island
|
||||
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
|
||||
int sparse_infill_density = this->config->sparse_infill_density.value;
|
||||
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
|
||||
loop_number++;
|
||||
|
||||
// Set the bottommost layer to be one wall
|
||||
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
|
||||
if (is_bottom_layer && only_one_wall_first_layer)
|
||||
loop_number = 0;
|
||||
// Set the bottommost layer to be one wall
|
||||
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
|
||||
if (is_bottom_layer && only_one_wall_first_layer)
|
||||
loop_number = 0;
|
||||
|
||||
// Orca: set the topmost layer to be one wall according to the config
|
||||
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
|
||||
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
|
||||
loop_number = 0;
|
||||
// Orca: set the topmost layer to be one wall according to the config
|
||||
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
|
||||
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
|
||||
loop_number = 0;
|
||||
|
||||
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
|
||||
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
|
||||
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
|
||||
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
|
||||
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
|
||||
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
|
||||
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
|
||||
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
|
||||
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
|
||||
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
|
||||
|
||||
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
|
||||
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
|
||||
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
|
||||
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
|
||||
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
|
||||
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
|
||||
|
||||
coord_t wall_0_inset = 0;
|
||||
if (apply_precise_outer_wall)
|
||||
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
|
||||
coord_t wall_0_inset = 0;
|
||||
if (apply_precise_outer_wall)
|
||||
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
|
||||
|
||||
//PS: One wall top surface for Arachne
|
||||
ExPolygons top_expolygons;
|
||||
// Calculate how many inner loops remain when TopSurfaces is selected.
|
||||
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
|
||||
//PS: One wall top surface for Arachne
|
||||
ExPolygons top_expolygons;
|
||||
// Calculate how many inner loops remain when TopSurfaces is selected.
|
||||
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
|
||||
|
||||
// Set one perimeter when TopSurfaces is selected.
|
||||
if (only_one_wall_top && loop_number > 0)
|
||||
loop_number = 0;
|
||||
// Set one perimeter when TopSurfaces is selected.
|
||||
if (only_one_wall_top && loop_number > 0)
|
||||
loop_number = 0;
|
||||
|
||||
Arachne::WallToolPathsParams input_params_tmp = input_params;
|
||||
Arachne::WallToolPathsParams input_params_tmp = input_params;
|
||||
|
||||
Polygons last_p = to_polygons(last);
|
||||
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
|
||||
wall_0_inset, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
|
||||
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
|
||||
Polygons last_p = to_polygons(last);
|
||||
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
|
||||
wall_0_inset, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
|
||||
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
|
||||
|
||||
// Check if there are some remaining perimeters to generate (the number of perimeters
|
||||
// is greater than one together with enabled the single perimeter on top surface feature).
|
||||
if (inner_loop_number >= 0) {
|
||||
assert(upper_slices != nullptr);
|
||||
// Check if there are some remaining perimeters to generate (the number of perimeters
|
||||
// is greater than one together with enabled the single perimeter on top surface feature).
|
||||
if (inner_loop_number >= 0) {
|
||||
assert(upper_slices != nullptr);
|
||||
|
||||
coord_t perimeter_width = this->perimeter_flow.scaled_width();
|
||||
coord_t perimeter_width = this->perimeter_flow.scaled_width();
|
||||
|
||||
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
|
||||
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
|
||||
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
|
||||
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
|
||||
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
|
||||
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
|
||||
|
||||
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
|
||||
// contour uncovered, before bridges and too thin areas are filtered out.
|
||||
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
|
||||
// Contour bounding box.
|
||||
BoundingBox contour_bbox = get_extents(contour);
|
||||
contour_bbox.offset(SCALED_EPSILON);
|
||||
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
|
||||
// contour uncovered, before bridges and too thin areas are filtered out.
|
||||
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
|
||||
// Contour bounding box.
|
||||
BoundingBox contour_bbox = get_extents(contour);
|
||||
contour_bbox.offset(SCALED_EPSILON);
|
||||
|
||||
Polygons upper_slices_clipped;
|
||||
if (object_config->interface_shells) {
|
||||
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
|
||||
} else
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
|
||||
Polygons upper_slices_clipped;
|
||||
if (object_config->interface_shells) {
|
||||
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
|
||||
} else
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
|
||||
|
||||
ExPolygons top = diff_ex(contour, upper_slices_clipped);
|
||||
uncovered = !top.empty();
|
||||
if (top.empty())
|
||||
return top;
|
||||
ExPolygons top = diff_ex(contour, upper_slices_clipped);
|
||||
uncovered = !top.empty();
|
||||
if (top.empty())
|
||||
return top;
|
||||
|
||||
if (lower_slices != nullptr) {
|
||||
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
|
||||
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
|
||||
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
|
||||
if (lower_slices != nullptr) {
|
||||
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
|
||||
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
|
||||
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
|
||||
|
||||
// Remove bridges from top surface polygons.
|
||||
top = diff_ex(top, current_slices_bridges);
|
||||
}
|
||||
|
||||
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
|
||||
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
|
||||
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
|
||||
// due to thin lines being generated
|
||||
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
|
||||
|
||||
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
|
||||
return intersection_ex(top, contour);
|
||||
};
|
||||
|
||||
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
|
||||
std::vector<Arachne::VariableWidthLines> full_perimeters;
|
||||
Polygons full_inner_contour;
|
||||
bool full_perimeters_generated = false;
|
||||
auto generate_full_perimeters = [&]() {
|
||||
if (full_perimeters_generated)
|
||||
return;
|
||||
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
|
||||
full_perimeters = full_tool_paths.getToolPaths();
|
||||
full_inner_contour = full_tool_paths.getInnerContour();
|
||||
full_perimeters_generated = true;
|
||||
};
|
||||
|
||||
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
|
||||
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
|
||||
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
|
||||
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
|
||||
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
|
||||
// the width added, and only beads widened by more than twice the tolerance are looked for.
|
||||
const coord_t outer_wall_tolerance = bead_width_0 / 10;
|
||||
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
|
||||
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
|
||||
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
|
||||
bool nominal_uncovered = false;
|
||||
// Grown by an outer wall width to take in the outer walls bordering the top surface.
|
||||
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
|
||||
if (nominal_uncovered) {
|
||||
generate_full_perimeters();
|
||||
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
|
||||
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
|
||||
perimeters = { full_perimeters.front() };
|
||||
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
|
||||
}
|
||||
}
|
||||
// Remove bridges from top surface polygons.
|
||||
top = diff_ex(top, current_slices_bridges);
|
||||
}
|
||||
|
||||
bool uncovered = false;
|
||||
top_expolygons = get_top_expolygons(infill_contour, uncovered);
|
||||
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
|
||||
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
|
||||
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
|
||||
// due to thin lines being generated
|
||||
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
|
||||
|
||||
if (uncovered) {
|
||||
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
|
||||
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
|
||||
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
|
||||
// top-surface islands with inner walls that don't exist when the feature is disabled.
|
||||
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
|
||||
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
|
||||
: diff_ex(infill_contour, top_expolygons),
|
||||
wall_0_inset));
|
||||
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
|
||||
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
|
||||
return intersection_ex(top, contour);
|
||||
};
|
||||
|
||||
if (clip_walls_over_top) {
|
||||
Polygons kept_over_top;
|
||||
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
|
||||
// Route the top fill around the walls kept despite grazing the top.
|
||||
if (! kept_over_top.empty())
|
||||
top_expolygons = diff_ex(top_expolygons, kept_over_top);
|
||||
}
|
||||
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
|
||||
std::vector<Arachne::VariableWidthLines> full_perimeters;
|
||||
Polygons full_inner_contour;
|
||||
bool full_perimeters_generated = false;
|
||||
auto generate_full_perimeters = [&]() {
|
||||
if (full_perimeters_generated)
|
||||
return;
|
||||
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
|
||||
full_perimeters = full_tool_paths.getToolPaths();
|
||||
full_inner_contour = full_tool_paths.getInnerContour();
|
||||
full_perimeters_generated = true;
|
||||
};
|
||||
|
||||
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
|
||||
if (!perimeters.empty())
|
||||
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
|
||||
for (Arachne::ExtrusionLine &el : inner_perimeter)
|
||||
++el.inset_idx;
|
||||
|
||||
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
|
||||
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
|
||||
} else {
|
||||
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
|
||||
// feature is disabled.
|
||||
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
|
||||
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
|
||||
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
|
||||
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
|
||||
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
|
||||
// the width added, and only beads widened by more than twice the tolerance are looked for.
|
||||
const coord_t outer_wall_tolerance = bead_width_0 / 10;
|
||||
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
|
||||
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
|
||||
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
|
||||
bool nominal_uncovered = false;
|
||||
// Grown by an outer wall width to take in the outer walls bordering the top surface.
|
||||
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
|
||||
if (nominal_uncovered) {
|
||||
generate_full_perimeters();
|
||||
perimeters = std::move(full_perimeters);
|
||||
infill_contour = union_ex(full_inner_contour);
|
||||
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
|
||||
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
|
||||
perimeters = { full_perimeters.front() };
|
||||
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
|
||||
}
|
||||
}
|
||||
}
|
||||
//PS
|
||||
|
||||
loop_number = int(perimeters.size()) - 1;
|
||||
bool uncovered = false;
|
||||
top_expolygons = get_top_expolygons(infill_contour, uncovered);
|
||||
|
||||
#ifdef ARACHNE_DEBUG
|
||||
{
|
||||
static int iRun = 0;
|
||||
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
|
||||
if (uncovered) {
|
||||
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
|
||||
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
|
||||
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
|
||||
// top-surface islands with inner walls that don't exist when the feature is disabled.
|
||||
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
|
||||
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
|
||||
: diff_ex(infill_contour, top_expolygons),
|
||||
wall_0_inset));
|
||||
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
|
||||
|
||||
if (clip_walls_over_top) {
|
||||
Polygons kept_over_top;
|
||||
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
|
||||
// Route the top fill around the walls kept despite grazing the top.
|
||||
if (! kept_over_top.empty())
|
||||
top_expolygons = diff_ex(top_expolygons, kept_over_top);
|
||||
}
|
||||
|
||||
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
|
||||
if (!perimeters.empty())
|
||||
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
|
||||
for (Arachne::ExtrusionLine &el : inner_perimeter)
|
||||
++el.inset_idx;
|
||||
|
||||
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
|
||||
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
|
||||
} else {
|
||||
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
|
||||
// feature is disabled.
|
||||
generate_full_perimeters();
|
||||
perimeters = std::move(full_perimeters);
|
||||
infill_contour = union_ex(full_inner_contour);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
//PS
|
||||
|
||||
// All closed ExtrusionLine should have the same the first and the last point.
|
||||
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
|
||||
// equal the first and the last point.
|
||||
assert([&perimeters = std::as_const(perimeters)]() -> bool {
|
||||
for (const Arachne::VariableWidthLines& perimeter : perimeters)
|
||||
for (const Arachne::ExtrusionLine& el : perimeter)
|
||||
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
|
||||
return false;
|
||||
return true;
|
||||
}());
|
||||
loop_number = int(perimeters.size()) - 1;
|
||||
|
||||
int start_perimeter = int(perimeters.size()) - 1;
|
||||
int end_perimeter = -1;
|
||||
int direction = -1;
|
||||
#ifdef ARACHNE_DEBUG
|
||||
{
|
||||
static int iRun = 0;
|
||||
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
|
||||
}
|
||||
#endif
|
||||
|
||||
bool is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner ||
|
||||
this->config->wall_sequence == WallSequence::InnerOuterInner;
|
||||
// All closed ExtrusionLine should have the same the first and the last point.
|
||||
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
|
||||
// equal the first and the last point.
|
||||
assert([&perimeters = std::as_const(perimeters)]() -> bool {
|
||||
for (const Arachne::VariableWidthLines& perimeter : perimeters)
|
||||
for (const Arachne::ExtrusionLine& el : perimeter)
|
||||
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
|
||||
return false;
|
||||
return true;
|
||||
}());
|
||||
|
||||
int start_perimeter = int(perimeters.size()) - 1;
|
||||
int end_perimeter = -1;
|
||||
int direction = -1;
|
||||
|
||||
bool is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner ||
|
||||
this->config->wall_sequence == WallSequence::InnerOuterInner;
|
||||
|
||||
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
|
||||
is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner;
|
||||
}
|
||||
if (is_outer_wall_first) {
|
||||
start_perimeter = 0;
|
||||
end_perimeter = int(perimeters.size());
|
||||
direction = 1;
|
||||
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
|
||||
is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner;
|
||||
}
|
||||
if (is_outer_wall_first) {
|
||||
start_perimeter = 0;
|
||||
end_perimeter = int(perimeters.size());
|
||||
direction = 1;
|
||||
}
|
||||
|
||||
std::vector<Arachne::ExtrusionLine*> all_extrusions;
|
||||
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
|
||||
if (perimeters[perimeter_idx].empty())
|
||||
continue;
|
||||
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
|
||||
all_extrusions.emplace_back(&wall);
|
||||
}
|
||||
|
||||
// Find topological order with constraints from extrusions_constrains.
|
||||
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
|
||||
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
|
||||
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
|
||||
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
|
||||
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
|
||||
|
||||
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
|
||||
for (auto [before, after] : extrusions_constrains) {
|
||||
auto after_it = map_extrusion_to_idx.find(after);
|
||||
++blocked[after_it->second];
|
||||
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
|
||||
}
|
||||
|
||||
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
|
||||
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
|
||||
ordered_extrusions.reserve(all_extrusions.size());
|
||||
|
||||
while (ordered_extrusions.size() < all_extrusions.size()) {
|
||||
size_t best_candidate = 0;
|
||||
double best_distance_sqr = std::numeric_limits<double>::max();
|
||||
bool is_best_closed = false;
|
||||
|
||||
std::vector<size_t> available_candidates;
|
||||
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
|
||||
if (processed[candidate] || blocked[candidate])
|
||||
continue; // Not a valid candidate.
|
||||
available_candidates.push_back(candidate);
|
||||
}
|
||||
|
||||
std::vector<Arachne::ExtrusionLine*> all_extrusions;
|
||||
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
|
||||
if (perimeters[perimeter_idx].empty())
|
||||
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
|
||||
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
|
||||
});
|
||||
|
||||
for (const size_t candidate_path_idx : available_candidates) {
|
||||
auto& path = all_extrusions[candidate_path_idx];
|
||||
|
||||
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
|
||||
if (best_distance_sqr == std::numeric_limits<double>::max()) {
|
||||
best_candidate = candidate_path_idx;
|
||||
is_best_closed = path->is_closed;
|
||||
}
|
||||
continue;
|
||||
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
|
||||
all_extrusions.emplace_back(&wall);
|
||||
}
|
||||
|
||||
// Find topological order with constraints from extrusions_constrains.
|
||||
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
|
||||
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
|
||||
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
|
||||
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
|
||||
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
|
||||
|
||||
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
|
||||
for (auto [before, after] : extrusions_constrains) {
|
||||
auto after_it = map_extrusion_to_idx.find(after);
|
||||
++blocked[after_it->second];
|
||||
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
|
||||
}
|
||||
|
||||
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
|
||||
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
|
||||
ordered_extrusions.reserve(all_extrusions.size());
|
||||
|
||||
while (ordered_extrusions.size() < all_extrusions.size()) {
|
||||
size_t best_candidate = 0;
|
||||
double best_distance_sqr = std::numeric_limits<double>::max();
|
||||
bool is_best_closed = false;
|
||||
|
||||
std::vector<size_t> available_candidates;
|
||||
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
|
||||
if (processed[candidate] || blocked[candidate])
|
||||
continue; // Not a valid candidate.
|
||||
available_candidates.push_back(candidate);
|
||||
}
|
||||
|
||||
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
|
||||
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
|
||||
});
|
||||
|
||||
for (const size_t candidate_path_idx : available_candidates) {
|
||||
auto& path = all_extrusions[candidate_path_idx];
|
||||
|
||||
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
|
||||
if (best_distance_sqr == std::numeric_limits<double>::max()) {
|
||||
best_candidate = candidate_path_idx;
|
||||
is_best_closed = path->is_closed;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const Point candidate_position = path->junctions.front().p;
|
||||
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
|
||||
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
|
||||
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
|
||||
best_candidate = candidate_path_idx;
|
||||
best_distance_sqr = distance_sqr;
|
||||
is_best_closed = path->is_closed;
|
||||
}
|
||||
const Point candidate_position = path->junctions.front().p;
|
||||
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
|
||||
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
|
||||
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
|
||||
best_candidate = candidate_path_idx;
|
||||
best_distance_sqr = distance_sqr;
|
||||
is_best_closed = path->is_closed;
|
||||
}
|
||||
}
|
||||
|
||||
auto& best_path = all_extrusions[best_candidate];
|
||||
ordered_extrusions.push_back({ best_path, best_path->is_contour() });
|
||||
processed[best_candidate] = true;
|
||||
for (size_t unlocked_idx : blocking[best_candidate])
|
||||
blocked[unlocked_idx]--;
|
||||
|
||||
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
|
||||
if (best_path->is_closed)
|
||||
current_position = best_path->junctions[0].p; //We end where we started.
|
||||
else
|
||||
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
|
||||
}
|
||||
}
|
||||
|
||||
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
|
||||
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
|
||||
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
|
||||
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
|
||||
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
|
||||
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
|
||||
|
||||
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
|
||||
// for OI mode that is used the basis for IOI
|
||||
bringContoursToFront(ordered_extrusions);
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
|
||||
|
||||
// Debug statement to print spacing values:
|
||||
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
|
||||
auto& best_path = all_extrusions[best_candidate];
|
||||
ordered_extrusions.push_back({ best_path, best_path->is_contour() });
|
||||
processed[best_candidate] = true;
|
||||
for (size_t unlocked_idx : blocking[best_candidate])
|
||||
blocked[unlocked_idx]--;
|
||||
|
||||
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
|
||||
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
|
||||
coord_t threshold_external = (apply_precise_outer_wall)
|
||||
// Precise outer wall: use the full external spacing
|
||||
? ( this->ext_perimeter_flow.scaled_spacing()
|
||||
+ this->perimeter_flow.scaled_spacing()/2.0 )
|
||||
// Normal: half ext spacing plus half int spacing
|
||||
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
|
||||
+ this->perimeter_flow.scaled_spacing()/2.0 );
|
||||
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
|
||||
if (best_path->is_closed)
|
||||
current_position = best_path->junctions[0].p; //We end where we started.
|
||||
else
|
||||
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
|
||||
}
|
||||
}
|
||||
|
||||
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
|
||||
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
|
||||
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
|
||||
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
|
||||
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
|
||||
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
|
||||
|
||||
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
|
||||
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
|
||||
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
|
||||
// for OI mode that is used the basis for IOI
|
||||
bringContoursToFront(ordered_extrusions);
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
|
||||
|
||||
// Re-order extrusions based on distance
|
||||
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
|
||||
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
|
||||
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
|
||||
// Debug statement to print spacing values:
|
||||
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
|
||||
|
||||
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
|
||||
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
|
||||
coord_t threshold_external = (apply_precise_outer_wall)
|
||||
// Precise outer wall ⇒ use “full external spacing”
|
||||
? ( this->ext_perimeter_flow.scaled_spacing()
|
||||
+ this->perimeter_flow.scaled_spacing()/2.0 )
|
||||
// Normal ⇒ half ext spacing + half int spacing
|
||||
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
|
||||
+ this->perimeter_flow.scaled_spacing()/2.0 );
|
||||
|
||||
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
|
||||
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
|
||||
// We then advance the position index to move to the second island and continue until there are no more
|
||||
// perimeters left.
|
||||
while (position < reordered_extrusions.size()) {
|
||||
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
|
||||
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
|
||||
// run through the walls to get the index values that need re-ordering until the first one for each
|
||||
// is found. Start at "position" index to enable the for loop to iterate for multiple external
|
||||
// perimeters in a single island
|
||||
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
|
||||
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
|
||||
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
|
||||
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
|
||||
case 0: // external perimeter
|
||||
if (outer == -1)
|
||||
outer = arr_i;
|
||||
break;
|
||||
case 1: // first internal wall
|
||||
if (first_internal==-1 && arr_i>outer && outer!=-1){
|
||||
first_internal = arr_i;
|
||||
}
|
||||
break;
|
||||
case 2: // second internal wall
|
||||
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
|
||||
second_internal = arr_i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
|
||||
// This means we entered a new island.
|
||||
arr_i=arr_i-1; //step back one perimeter
|
||||
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
|
||||
break; // exit the for loop
|
||||
}
|
||||
}
|
||||
|
||||
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
|
||||
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
|
||||
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
|
||||
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
|
||||
|
||||
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
|
||||
if(arr_j >= second_internal){
|
||||
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
|
||||
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
|
||||
current_perimeter++;
|
||||
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
|
||||
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
|
||||
|
||||
// Re-order extrusions based on distance
|
||||
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
|
||||
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
|
||||
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
|
||||
|
||||
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
|
||||
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
|
||||
// We then advance the position index to move to the second island and continue until there are no more
|
||||
// perimeters left.
|
||||
while (position < reordered_extrusions.size()) {
|
||||
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
|
||||
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
|
||||
// run through the walls to get the index values that need re-ordering until the first one for each
|
||||
// is found. Start at "position" index to enable the for loop to iterate for multiple external
|
||||
// perimeters in a single island
|
||||
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
|
||||
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
|
||||
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
|
||||
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
|
||||
case 0: // external perimeter
|
||||
if (outer == -1)
|
||||
outer = arr_i;
|
||||
break;
|
||||
case 1: // first internal wall
|
||||
if (first_internal==-1 && arr_i>outer && outer!=-1){
|
||||
first_internal = arr_i;
|
||||
}
|
||||
}
|
||||
|
||||
for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order
|
||||
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1);
|
||||
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j];
|
||||
}
|
||||
|
||||
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array
|
||||
ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position];
|
||||
break;
|
||||
case 2: // second internal wall
|
||||
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
|
||||
second_internal = arr_i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
|
||||
// This means we entered a new island.
|
||||
arr_i=arr_i-1; //step back one perimeter
|
||||
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
|
||||
break; // exit the for loop
|
||||
}
|
||||
// go to the next perimeter from the current position to continue scanning for external walls in the same island
|
||||
position = arr_i + 1;
|
||||
}
|
||||
|
||||
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
|
||||
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
|
||||
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
|
||||
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
|
||||
|
||||
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
|
||||
if(arr_j >= second_internal){
|
||||
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
|
||||
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
|
||||
current_perimeter++;
|
||||
}
|
||||
}
|
||||
|
||||
for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order
|
||||
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1);
|
||||
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j];
|
||||
}
|
||||
|
||||
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array
|
||||
ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position];
|
||||
}
|
||||
// go to the next perimeter from the current position to continue scanning for external walls in the same island
|
||||
position = arr_i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool steep_overhang_contour = false;
|
||||
bool steep_overhang_hole = false;
|
||||
if (!config->overhang_reverse) {
|
||||
// Skip steep overhang detection no reverse is specified
|
||||
steep_overhang_contour = true;
|
||||
steep_overhang_hole = true;
|
||||
}
|
||||
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
|
||||
if (config->overhang_reverse) {
|
||||
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
|
||||
this->config->overhang_reverse_internal_only);
|
||||
}
|
||||
defer_unsupported_loops(*this, extrusion_coll);
|
||||
result.loops = std::move(extrusion_coll);
|
||||
result.has_loops = true;
|
||||
bool steep_overhang_contour = false;
|
||||
bool steep_overhang_hole = false;
|
||||
if (!config->overhang_reverse) {
|
||||
// Skip steep overhang detection no reverse is specified
|
||||
steep_overhang_contour = true;
|
||||
steep_overhang_hole = true;
|
||||
}
|
||||
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
|
||||
if (config->overhang_reverse) {
|
||||
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
|
||||
this->config->overhang_reverse_internal_only);
|
||||
}
|
||||
defer_unsupported_loops(*this, extrusion_coll);
|
||||
this->loops->append(extrusion_coll);
|
||||
}
|
||||
|
||||
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
|
||||
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
|
||||
|
||||
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
|
||||
infill_contour.clear(); // Infill region is too small, so let's filter it out.
|
||||
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
|
||||
infill_contour.clear(); // Infill region is too small, so let's filter it out.
|
||||
|
||||
// create one more offset to be used as boundary for fill
|
||||
// we offset by half the perimeter spacing (to get to the actual infill boundary)
|
||||
// and then we offset back and forth by half the infill spacing to only consider the
|
||||
// non-collapsing regions
|
||||
coord_t inset =
|
||||
(loop_number < 0) ? 0 :
|
||||
(loop_number == 0) ?
|
||||
// one loop
|
||||
ext_perimeter_spacing :
|
||||
// two or more loops?
|
||||
perimeter_spacing;
|
||||
coord_t top_inset = inset;
|
||||
// create one more offset to be used as boundary for fill
|
||||
// we offset by half the perimeter spacing (to get to the actual infill boundary)
|
||||
// and then we offset back and forth by half the infill spacing to only consider the
|
||||
// non-collapsing regions
|
||||
coord_t inset =
|
||||
(loop_number < 0) ? 0 :
|
||||
(loop_number == 0) ?
|
||||
// one loop
|
||||
ext_perimeter_spacing :
|
||||
// two or more loops?
|
||||
perimeter_spacing;
|
||||
coord_t top_inset = inset;
|
||||
|
||||
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
if(is_topmost_layer || is_bottom_layer)
|
||||
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
else
|
||||
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
if(is_topmost_layer || is_bottom_layer)
|
||||
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
else
|
||||
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
|
||||
// simplify infill contours according to resolution
|
||||
Polygons pp;
|
||||
for (ExPolygon& ex : infill_contour)
|
||||
ex.simplify_p(m_scaled_resolution, &pp);
|
||||
ExPolygons not_filled_exp = union_ex(pp);
|
||||
// collapse too narrow infill areas
|
||||
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
|
||||
// simplify infill contours according to resolution
|
||||
Polygons pp;
|
||||
for (ExPolygon& ex : infill_contour)
|
||||
ex.simplify_p(m_scaled_resolution, &pp);
|
||||
ExPolygons not_filled_exp = union_ex(pp);
|
||||
// collapse too narrow infill areas
|
||||
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
|
||||
|
||||
ExPolygons infill_exp = offset2_ex(
|
||||
ExPolygons infill_exp = offset2_ex(
|
||||
not_filled_exp,
|
||||
float(-min_perimeter_infill_spacing / 2.),
|
||||
float(inset + min_perimeter_infill_spacing / 2.));
|
||||
// append infill areas to fill_surfaces
|
||||
if (!top_expolygons.empty()) {
|
||||
infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset)));
|
||||
}
|
||||
this->fill_surfaces->append(infill_exp, stInternal);
|
||||
|
||||
apply_extra_perimeters(infill_exp);
|
||||
|
||||
// BBS: get the no-overlap infill expolygons
|
||||
{
|
||||
ExPolygons polyWithoutOverlap;
|
||||
polyWithoutOverlap = offset2_ex(
|
||||
not_filled_exp,
|
||||
float(-min_perimeter_infill_spacing / 2.),
|
||||
float(inset + min_perimeter_infill_spacing / 2.));
|
||||
// append infill areas to fill_surfaces
|
||||
if (!top_expolygons.empty()) {
|
||||
infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset)));
|
||||
}
|
||||
result.infill = std::move(infill_exp);
|
||||
|
||||
// BBS: get the no-overlap infill expolygons
|
||||
{
|
||||
ExPolygons polyWithoutOverlap;
|
||||
polyWithoutOverlap = offset2_ex(
|
||||
not_filled_exp,
|
||||
float(-min_perimeter_infill_spacing / 2.),
|
||||
float(+min_perimeter_infill_spacing / 2.));
|
||||
if (!top_expolygons.empty())
|
||||
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
|
||||
result.no_overlap = std::move(polyWithoutOverlap);
|
||||
}
|
||||
}
|
||||
});
|
||||
for (ArachneSurfaceResult &result : results) {
|
||||
if (result.has_loops)
|
||||
// Moved, not copied: append(const ExtrusionEntity &) clones the whole wall tree of the island.
|
||||
this->loops->append(std::move(result.loops));
|
||||
this->fill_surfaces->append(result.infill, stInternal);
|
||||
apply_extra_perimeters(result.infill);
|
||||
append(*this->fill_no_overlap, std::move(result.no_overlap));
|
||||
float(+min_perimeter_infill_spacing / 2.));
|
||||
if (!top_expolygons.empty())
|
||||
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
|
||||
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
explicit Polygon(const Points &points) : MultiPoint(points) {}
|
||||
Polygon(std::initializer_list<Point> points) : MultiPoint(points) {}
|
||||
Polygon(const Polygon &other) : MultiPoint(other.points) {}
|
||||
Polygon(Polygon &&other) noexcept : MultiPoint(std::move(other.points)) {}
|
||||
Polygon(Polygon &&other) : MultiPoint(std::move(other.points)) {}
|
||||
static Polygon new_scale(const std::vector<Vec2d> &points) {
|
||||
Polygon pgn;
|
||||
pgn.points.reserve(points.size());
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
return pgn;
|
||||
}
|
||||
Polygon& operator=(const Polygon &other) { points = other.points; return *this; }
|
||||
Polygon& operator=(Polygon &&other) noexcept { points = std::move(other.points); return *this; }
|
||||
Polygon& operator=(Polygon &&other) { points = std::move(other.points); return *this; }
|
||||
|
||||
Point& operator[](Points::size_type idx) { return this->points[idx]; }
|
||||
const Point& operator[](Points::size_type idx) const { return this->points[idx]; }
|
||||
|
||||
@@ -26,7 +26,7 @@ class Polyline : public MultiPoint {
|
||||
public:
|
||||
Polyline() {};
|
||||
Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {}
|
||||
Polyline(Polyline &&other) noexcept : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
|
||||
Polyline(Polyline &&other) : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
|
||||
Polyline(std::initializer_list<Point> list) : MultiPoint(list) {
|
||||
fitting_result.clear();
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public:
|
||||
fitting_result = other.fitting_result;
|
||||
return *this;
|
||||
}
|
||||
Polyline& operator=(Polyline&& other) noexcept {
|
||||
Polyline& operator=(Polyline&& other) {
|
||||
points = std::move(other.points);
|
||||
fitting_result = std::move(other.fitting_result);
|
||||
return *this;
|
||||
|
||||
@@ -5381,13 +5381,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
bool process_multi_extruder = false;
|
||||
std::vector<int> filament_variant_index;
|
||||
size_t extruder_variant_count;
|
||||
if (!config.option<ConfigOptionInts>("filament_self_index")) {
|
||||
std::vector<int>& filament_self_indice = config.option<ConfigOptionInts>("filament_self_index", true)->values;
|
||||
// A config loaded over the full defaults has a one-entry index even when the file has none.
|
||||
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);
|
||||
for (int index = 0; index < num_filaments; index++)
|
||||
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
|
||||
// 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");
|
||||
|
||||
+197
-414
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "ConnectedBodies.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Layer.hpp"
|
||||
@@ -48,7 +49,6 @@
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <float.h>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <ios>
|
||||
#include <iomanip>
|
||||
@@ -75,7 +75,6 @@
|
||||
#include <Eigen/Core>
|
||||
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/parallel_invoke.h>
|
||||
#include <tbb/spin_mutex.h>
|
||||
#include <tbb/concurrent_unordered_set.h>
|
||||
|
||||
@@ -749,69 +748,19 @@ void PrintObject::prepare_infill()
|
||||
for (Layer *layer : m_layers)
|
||||
layer->lslices_separated_component_ids.clear();
|
||||
if (needs_separated_components) {
|
||||
const size_t nl = m_layers.size();
|
||||
std::vector<size_t> offset(nl + 1, 0); // Orca: flat index of the first island of each layer
|
||||
for (size_t i = 0; i < nl; ++ i)
|
||||
offset[i + 1] = offset[i] + m_layers[i]->lslices.size();
|
||||
const size_t nreg = offset[nl];
|
||||
// Orca: Union-find over every (layer, island).
|
||||
std::vector<size_t> parent(nreg);
|
||||
for (size_t i = 0; i < nreg; ++ i) parent[i] = i;
|
||||
auto find = [&parent](size_t x) {
|
||||
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) {
|
||||
std::vector<const ExPolygons *> islands;
|
||||
islands.reserve(m_layers.size());
|
||||
for (const Layer *layer : m_layers)
|
||||
islands.emplace_back(&layer->lslices);
|
||||
size_t bodies = 0;
|
||||
std::vector<std::vector<size_t>> ids = connected_bodies(islands, bodies, [this]() { m_print->throw_if_canceled(); });
|
||||
// Orca: Merge the bounding boxes of the islands of each body.
|
||||
m_separated_body_bboxes.assign(bodies, BoundingBox());
|
||||
for (size_t i = 0; i < m_layers.size(); ++ 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) {
|
||||
size_t &body = body_of_root[find(offset[i] + a)];
|
||||
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;
|
||||
}
|
||||
for (size_t a = 0; a < layer->lslices.size(); ++ a)
|
||||
m_separated_body_bboxes[ids[i][a]].merge(layer->lslices_bboxes[a]);
|
||||
layer->lslices_separated_component_ids = std::move(ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1980,9 +1929,7 @@ void PrintObject::detect_surfaces_type()
|
||||
bool interface_shells = ! spiral_mode && m_config.interface_shells.value;
|
||||
size_t num_layers = spiral_mode ? std::min(size_t(this->printing_region(0).config().bottom_shell_layers), m_layers.size()) : m_layers.size();
|
||||
|
||||
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
|
||||
// others, so the regions run next to each other instead of one after another, each still over all layers.
|
||||
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start";
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
for (Layer *layer : m_layers)
|
||||
@@ -2040,7 +1987,7 @@ void PrintObject::detect_surfaces_type()
|
||||
if (upper_layer) {
|
||||
ExPolygons upper_slices = interface_shells ?
|
||||
diff_ex(layerm_slices_surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) :
|
||||
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(upper_layer->lslices), ApplySafetyOffset::Yes);
|
||||
diff_ex(layerm_slices_surfaces, upper_layer->lslices, ApplySafetyOffset::Yes);
|
||||
surfaces_append(top, opening_ex(upper_slices, offset), stTop);
|
||||
} else {
|
||||
// if no upper layer, all surfaces of this one are solid
|
||||
@@ -2066,7 +2013,7 @@ void PrintObject::detect_surfaces_type()
|
||||
surfaces_append(
|
||||
bottom,
|
||||
opening_ex(
|
||||
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(lower_layer->lslices), ApplySafetyOffset::Yes),
|
||||
diff_ex(layerm_slices_surfaces, lower_layer->lslices, ApplySafetyOffset::Yes),
|
||||
offset),
|
||||
surface_type_bottom_other);
|
||||
// if user requested internal shells, we need to identify surfaces
|
||||
@@ -2097,44 +2044,34 @@ void PrintObject::detect_surfaces_type()
|
||||
// and top surfaces; let's do an intersection to discover them and consider them
|
||||
// as bottom surfaces (to allow for bridge detection)
|
||||
if (! top.empty() && ! bottom.empty()) {
|
||||
const auto cracks = intersection_ex_by_piece(to_expolygons(top), to_polygons(bottom));
|
||||
const auto cracks = intersection_ex(top, bottom);
|
||||
if (!cracks.empty()) {
|
||||
if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom
|
||||
const float small_crack_threshold = -layerm->flow(frExternalPerimeter).scaled_width() * 1.5;
|
||||
|
||||
// Only the bottom surfaces near a crack can take part: one that contains it must contain its box,
|
||||
// and one whose box misses the grown crack is left unchanged by removing it. A layer cut through
|
||||
// a fine relief has thousands of both, which made this loop quadratic.
|
||||
for (const auto& crack : cracks) {
|
||||
if (offset_ex(crack, small_crack_threshold).empty()) {
|
||||
// For small cracks, if it's part of a large bottom surface, then it should be added to bottom as well
|
||||
const BoundingBox crack_bbox = get_extents(crack);
|
||||
if (std::any_of(bottom.begin(), bottom.end(), [&crack, &crack_bbox, small_crack_threshold](const Surface& s) {
|
||||
if (std::any_of(bottom.begin(), bottom.end(), [&crack, small_crack_threshold](const Surface& s) {
|
||||
const auto& se = s.expolygon;
|
||||
return get_extents(se).inflated(SCALED_EPSILON).contains(crack_bbox)
|
||||
&& diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
|
||||
return diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
|
||||
&& se.area() > crack.area() * 2
|
||||
&& !offset_ex(diff_ex(se, crack), small_crack_threshold).empty();
|
||||
})) continue;
|
||||
|
||||
// Crack too small, leave it as part of the top surface, remove it from bottom surfaces
|
||||
const ExPolygons grown_crack = offset_ex(crack, -small_crack_threshold);
|
||||
const BoundingBox grown_bbox = get_extents(grown_crack);
|
||||
Surfaces bot_tmp;
|
||||
for (auto& b : bottom) {
|
||||
if (get_extents(b.expolygon).overlap(grown_bbox))
|
||||
surfaces_append(bot_tmp, diff_ex(b.expolygon, grown_crack), b.surface_type);
|
||||
else
|
||||
bot_tmp.emplace_back(std::move(b));
|
||||
surfaces_append(bot_tmp, diff_ex(b.expolygon, offset_ex(crack, -small_crack_threshold)), b.surface_type);
|
||||
}
|
||||
bottom = std::move(bot_tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExPolygons top_expolygons = to_expolygons(std::move(top));
|
||||
Polygons top_polygons = to_polygons(std::move(top));
|
||||
top.clear();
|
||||
surfaces_append(top, diff_ex_by_piece(top_expolygons, to_polygons(bottom)), stTop);
|
||||
surfaces_append(top, diff_ex(top_polygons, bottom), stTop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2225,7 +2162,7 @@ void PrintObject::detect_surfaces_type()
|
||||
{
|
||||
Polygons topbottom = to_polygons(top);
|
||||
polygons_append(topbottom, to_polygons(bottom));
|
||||
surfaces_append(surfaces_out, diff_ex_by_piece(surfaces_prev_expolys, topbottom), stInternal);
|
||||
surfaces_append(surfaces_out, diff_ex(surfaces_prev_expolys, topbottom), stInternal);
|
||||
}
|
||||
|
||||
surfaces_append(surfaces_out, std::move(top));
|
||||
@@ -2402,31 +2339,29 @@ void PrintObject::detect_surfaces_type()
|
||||
}
|
||||
}
|
||||
);
|
||||
// ==============================================================================================================
|
||||
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
|
||||
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
|
||||
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
|
||||
// ==============================================================================================================
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
|
||||
tbb::parallel_for( tbb::blocked_range<size_t>(0, m_layers.size()), [this, region_id](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
|
||||
Surfaces &surfs = m_layers[idx_layer]->m_regions[region_id]->slices.surfaces;
|
||||
for (Surface &s : surfs) {
|
||||
if (s.surface_type == stInternalAfterExternalBridge) {
|
||||
s.surface_type = stBottomBridge;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// ==============================================================================================================
|
||||
// === ORCA: End of second external bridge layer changes =======================================================
|
||||
// ==============================================================================================================
|
||||
|
||||
}); // for each this->print->region_count
|
||||
|
||||
// ==============================================================================================================
|
||||
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
|
||||
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
|
||||
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
|
||||
// ==============================================================================================================
|
||||
// Once all the regions have their second bridge layer, and before their slices are trimmed into fill surfaces below.
|
||||
if ((this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)) {
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_layers.size()), [this](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer)
|
||||
for (LayerRegion *layerm : m_layers[idx_layer]->regions())
|
||||
for (Surface &s : layerm->slices.surfaces)
|
||||
if (s.surface_type == stInternalAfterExternalBridge)
|
||||
s.surface_type = stBottomBridge;
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
}
|
||||
|
||||
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - start";
|
||||
// Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces.
|
||||
tbb::parallel_for(
|
||||
@@ -2443,7 +2378,7 @@ void PrintObject::detect_surfaces_type()
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end";
|
||||
});
|
||||
} // for each this->print->region_count
|
||||
|
||||
// Mark the object to have the region slices classified (typed, which also means they are split based on whether they are supported, bridging, top layers etc.)
|
||||
m_typed_slices = true;
|
||||
@@ -2510,10 +2445,8 @@ void PrintObject::process_external_surfaces()
|
||||
BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end";
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - start";
|
||||
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
|
||||
// others, so the regions run next to each other instead of one after another, each still over all layers.
|
||||
tbb::parallel_for(size_t(0), this->num_printing_regions(), [this, &surfaces_covered](size_t region_id) {
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - start";
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, m_layers.size()),
|
||||
[this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) {
|
||||
@@ -2528,9 +2461,9 @@ void PrintObject::process_external_surfaces()
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - end";
|
||||
m_print->throw_if_canceled();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - end";
|
||||
}
|
||||
}
|
||||
|
||||
void PrintObject::discover_vertical_shells()
|
||||
@@ -2569,10 +2502,10 @@ void PrintObject::discover_vertical_shells()
|
||||
// The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit.
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom";
|
||||
// One layer per task: on a layer cut through a fine relief the unions below take far longer than elsewhere, and a
|
||||
// few such layers next to each other must not end up in one task.
|
||||
//FIXME Improve the heuristics for a grain size.
|
||||
size_t grain_size = std::max(num_layers / 16, size_t(1));
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, num_layers, 1),
|
||||
tbb::blocked_range<size_t>(0, num_layers, grain_size),
|
||||
[this, &cache_top_botom_regions](const tbb::blocked_range<size_t>& range) {
|
||||
const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge };
|
||||
const size_t num_regions = this->num_printing_regions();
|
||||
@@ -2580,198 +2513,67 @@ void PrintObject::discover_vertical_shells()
|
||||
m_print->throw_if_canceled();
|
||||
const Layer &layer = *m_layers[idx_layer];
|
||||
DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer];
|
||||
const auto top_bottom_expansion = [&layer](size_t region_id) {
|
||||
return float(layer.m_regions[region_id]->flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
|
||||
};
|
||||
// Simulate single set of perimeters over all merged regions.
|
||||
float perimeter_offset = 0.f;
|
||||
float perimeter_min_spacing = FLT_MAX;
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
static size_t debug_idx = 0;
|
||||
++ debug_idx;
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
// The top surfaces, the bottom surfaces and the holes are independent of each other.
|
||||
tbb::parallel_invoke(
|
||||
[&]() {
|
||||
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
|
||||
append(cache.top_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_type(stTop), top_bottom_expansion(region_id)));
|
||||
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
|
||||
// Save some computing time by reducing the number of polygons.
|
||||
cache.top_surfaces = union_(cache.top_surfaces);
|
||||
},
|
||||
[&]() {
|
||||
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
|
||||
append(cache.bottom_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_types(surfaces_bottom), top_bottom_expansion(region_id)));
|
||||
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
|
||||
cache.bottom_surfaces = union_(cache.bottom_surfaces);
|
||||
},
|
||||
[&]() {
|
||||
// Simulate single set of perimeters over all merged regions.
|
||||
float perimeter_offset = 0.f;
|
||||
float perimeter_min_spacing = FLT_MAX;
|
||||
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
|
||||
const LayerRegion &layerm = *layer.m_regions[region_id];
|
||||
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
|
||||
// First find the maxium number of perimeters per region slice.
|
||||
unsigned int perimeters = 0;
|
||||
for (const Surface &s : layerm.slices.surfaces)
|
||||
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
|
||||
perimeters += layerm.region().config().wall_loops.value;
|
||||
// Then calculate the infill offset.
|
||||
if (perimeters > 0) {
|
||||
Flow extflow = layerm.flow(frExternalPerimeter);
|
||||
Flow flow = layerm.flow(frPerimeter);
|
||||
perimeter_offset = std::max(perimeter_offset,
|
||||
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
|
||||
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
|
||||
}
|
||||
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
|
||||
}
|
||||
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
|
||||
if (perimeter_offset > 0.) {
|
||||
// The layer.lslices are forced to merge by expanding them first.
|
||||
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
|
||||
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
|
||||
LayerRegion &layerm = *layer.m_regions[region_id];
|
||||
float top_bottom_expansion = float(layerm.flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
|
||||
// Top surfaces.
|
||||
append(cache.top_surfaces, offset(layerm.slices.filter_by_type(stTop), top_bottom_expansion));
|
||||
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
|
||||
// Bottom surfaces.
|
||||
append(cache.bottom_surfaces, offset(layerm.slices.filter_by_types(surfaces_bottom), top_bottom_expansion));
|
||||
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
|
||||
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
|
||||
// First find the maxium number of perimeters per region slice.
|
||||
unsigned int perimeters = 0;
|
||||
for (Surface &s : layerm.slices.surfaces)
|
||||
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
|
||||
perimeters += layerm.region().config().wall_loops.value;
|
||||
// Then calculate the infill offset.
|
||||
if (perimeters > 0) {
|
||||
Flow extflow = layerm.flow(frExternalPerimeter);
|
||||
Flow flow = layerm.flow(frPerimeter);
|
||||
perimeter_offset = std::max(perimeter_offset,
|
||||
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
|
||||
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
|
||||
}
|
||||
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
|
||||
}
|
||||
// Save some computing time by reducing the number of polygons.
|
||||
cache.top_surfaces = union_(cache.top_surfaces);
|
||||
cache.bottom_surfaces = union_(cache.bottom_surfaces);
|
||||
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
|
||||
if (perimeter_offset > 0.) {
|
||||
// The layer.lslices are forced to merge by expanding them first.
|
||||
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
|
||||
svg.draw(layer.lslices, "blue");
|
||||
svg.draw(union_ex(cache.holes), "red");
|
||||
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
|
||||
svg.Close();
|
||||
}
|
||||
{
|
||||
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
|
||||
svg.draw(layer.lslices, "blue");
|
||||
svg.draw(union_ex(cache.holes), "red");
|
||||
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
|
||||
svg.Close();
|
||||
}
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
}
|
||||
cache.holes = union_(cache.holes);
|
||||
});
|
||||
}
|
||||
cache.holes = union_(cache.holes);
|
||||
}
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom";
|
||||
}
|
||||
|
||||
// With one top/bottom cache for all regions, the shell and hole accumulation in the loop below depends on nothing
|
||||
// region-specific but the shell settings and the external perimeter spacing, so a region sharing them with an earlier
|
||||
// one reuses its result instead of repeating it: that accumulation is a union over several layers of top/bottom
|
||||
// surfaces, and a multi-material print has a region per filament.
|
||||
using AccumulationKey = std::array<double, 5>;
|
||||
struct ShellAccumulation
|
||||
{
|
||||
AccumulationKey key;
|
||||
Polygons shell;
|
||||
Polygons holes;
|
||||
};
|
||||
const auto accumulation_key = [](const PrintRegionConfig ®ion_config, const LayerRegion *layerm) {
|
||||
return AccumulationKey{ double(region_config.top_shell_layers.value), region_config.top_shell_thickness.value,
|
||||
double(region_config.bottom_shell_layers.value), region_config.bottom_shell_thickness.value,
|
||||
double(layerm->flow(frExternalPerimeter).scaled_spacing()) };
|
||||
};
|
||||
const auto accumulate_shell = [this, &cache_top_botom_regions](size_t idx_layer, const PrintRegionConfig ®ion_config,
|
||||
const LayerRegion *layerm, Polygons &shell, Polygons &holes) {
|
||||
const Layer *layer = m_layers[idx_layer];
|
||||
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
|
||||
auto combine_holes = [&holes](const Polygons &holes2) {
|
||||
if (holes.empty() || holes2.empty())
|
||||
holes.clear();
|
||||
else
|
||||
holes = intersection(holes, holes2);
|
||||
};
|
||||
auto combine_shells = [&shell](const Polygons &shells2) {
|
||||
if (shell.empty())
|
||||
shell = std::move(shells2);
|
||||
else if (! shells2.empty()) {
|
||||
polygons_append(shell, shells2);
|
||||
// Running the union_ using the Clipper library piece by piece is cheaper
|
||||
// than running the union_ all at once.
|
||||
shell = union_(shell);
|
||||
}
|
||||
};
|
||||
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
|
||||
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
|
||||
// Gather top regions projected to this layer.
|
||||
coordf_t print_z = layer->print_z;
|
||||
int i = int(idx_layer) + 1;
|
||||
int itop = int(idx_layer) + n_top_layers;
|
||||
bool at_least_one_top_projected = false;
|
||||
for (; i < int(cache_top_botom_regions.size()) &&
|
||||
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
|
||||
++ i) {
|
||||
at_least_one_top_projected = true;
|
||||
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
|
||||
combine_holes(cache.holes);
|
||||
combine_shells(cache.top_surfaces);
|
||||
}
|
||||
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
|
||||
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
|
||||
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
|
||||
// perimeter width of area
|
||||
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
|
||||
layerm->flow(frExternalPerimeter).scaled_spacing()),
|
||||
to_polygons(m_layers[i]->lslices));
|
||||
combine_shells(anchor_area);
|
||||
}
|
||||
|
||||
if (one_more_layer_below_top_bottom_surfaces)
|
||||
if (i < int(cache_top_botom_regions.size()) &&
|
||||
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
|
||||
combine_holes(cache_top_botom_regions[i].holes);
|
||||
}
|
||||
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
|
||||
// Gather bottom regions projected to this layer.
|
||||
coordf_t bottom_z = layer->bottom_z();
|
||||
int i = int(idx_layer) - 1;
|
||||
int ibottom = int(idx_layer) - n_bottom_layers;
|
||||
bool at_least_one_bottom_projected = false;
|
||||
for (; i >= 0 &&
|
||||
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
|
||||
-- i) {
|
||||
at_least_one_bottom_projected = true;
|
||||
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
|
||||
combine_holes(cache.holes);
|
||||
combine_shells(cache.bottom_surfaces);
|
||||
}
|
||||
|
||||
if (!at_least_one_bottom_projected && i >= 0) {
|
||||
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
|
||||
layerm->flow(frExternalPerimeter).scaled_spacing()),
|
||||
to_polygons(m_layers[i]->lslices));
|
||||
combine_shells(anchor_area);
|
||||
}
|
||||
|
||||
if (one_more_layer_below_top_bottom_surfaces)
|
||||
if (i >= 0 &&
|
||||
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
|
||||
combine_holes(cache_top_botom_regions[i].holes);
|
||||
}
|
||||
};
|
||||
std::vector<std::vector<ShellAccumulation>> shell_accumulations(top_bottom_surfaces_all_regions ? num_layers : 0);
|
||||
if (! shell_accumulations.empty()) {
|
||||
// Every (layer, key) pair is accumulated once, before the regions, so that nothing in the loop below is shared
|
||||
// between them and they can run next to each other.
|
||||
std::vector<std::array<size_t, 3>> todo; // layer, its slot, a region holding the key
|
||||
for (size_t idx_layer = 0; idx_layer < num_layers; ++ idx_layer) {
|
||||
std::vector<ShellAccumulation> &accumulations = shell_accumulations[idx_layer];
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
|
||||
if (this->printing_region(region_id).config().ensure_vertical_shell_thickness.value != evstAll)
|
||||
continue;
|
||||
const LayerRegion *layerm = m_layers[idx_layer]->m_regions[region_id];
|
||||
const AccumulationKey key = accumulation_key(layerm->region().config(), layerm);
|
||||
if (std::none_of(accumulations.begin(), accumulations.end(), [&key](const ShellAccumulation &a) { return a.key == key; })) {
|
||||
todo.push_back({ idx_layer, accumulations.size(), region_id });
|
||||
accumulations.push_back({ key, {}, {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
tbb::parallel_for(size_t(0), todo.size(), [this, &todo, &shell_accumulations, &accumulate_shell](size_t i) {
|
||||
m_print->throw_if_canceled();
|
||||
const LayerRegion *layerm = m_layers[todo[i][0]]->m_regions[todo[i][2]];
|
||||
ShellAccumulation &out = shell_accumulations[todo[i][0]][todo[i][1]];
|
||||
accumulate_shell(todo[i][0], layerm->region().config(), layerm, out.shell, out.holes);
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
}
|
||||
|
||||
const auto process_region = [&](size_t region_id) {
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
|
||||
const PrintRegion ®ion = this->printing_region(region_id);
|
||||
if (region.config().ensure_vertical_shell_thickness.value != evstAll )
|
||||
// This region will be handled by discover_horizontal_shells().
|
||||
return;
|
||||
continue;
|
||||
|
||||
//FIXME Improve the heuristics for a grain size.
|
||||
size_t grain_size = std::max(num_layers / 16, size_t(1));
|
||||
@@ -2811,7 +2613,7 @@ void PrintObject::discover_vertical_shells()
|
||||
grain_size = 1;
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, num_layers, grain_size),
|
||||
[this, region_id, &shell_accumulations, &accumulation_key, &accumulate_shell]
|
||||
[this, region_id, &cache_top_botom_regions]
|
||||
(const tbb::blocked_range<size_t>& range) {
|
||||
// printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end());
|
||||
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
|
||||
@@ -2861,19 +2663,80 @@ void PrintObject::discover_vertical_shells()
|
||||
}
|
||||
}
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
const AccumulationKey key = accumulation_key(region_config, layerm);
|
||||
const ShellAccumulation *reused = shell_accumulations.empty() ? nullptr :
|
||||
[&]() -> const ShellAccumulation * {
|
||||
for (const ShellAccumulation &a : shell_accumulations[idx_layer])
|
||||
if (a.key == key)
|
||||
return &a;
|
||||
return nullptr;
|
||||
}();
|
||||
if (reused != nullptr) {
|
||||
shell = reused->shell;
|
||||
holes = reused->holes;
|
||||
} else
|
||||
accumulate_shell(idx_layer, region_config, layerm, shell, holes);
|
||||
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
|
||||
auto combine_holes = [&holes](const Polygons &holes2) {
|
||||
if (holes.empty() || holes2.empty())
|
||||
holes.clear();
|
||||
else
|
||||
holes = intersection(holes, holes2);
|
||||
};
|
||||
auto combine_shells = [&shell](const Polygons &shells2) {
|
||||
if (shell.empty())
|
||||
shell = std::move(shells2);
|
||||
else if (! shells2.empty()) {
|
||||
polygons_append(shell, shells2);
|
||||
// Running the union_ using the Clipper library piece by piece is cheaper
|
||||
// than running the union_ all at once.
|
||||
shell = union_(shell);
|
||||
}
|
||||
};
|
||||
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
|
||||
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
|
||||
// Gather top regions projected to this layer.
|
||||
coordf_t print_z = layer->print_z;
|
||||
int i = int(idx_layer) + 1;
|
||||
int itop = int(idx_layer) + n_top_layers;
|
||||
bool at_least_one_top_projected = false;
|
||||
for (; i < int(cache_top_botom_regions.size()) &&
|
||||
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
|
||||
++ i) {
|
||||
at_least_one_top_projected = true;
|
||||
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
|
||||
combine_holes(cache.holes);
|
||||
combine_shells(cache.top_surfaces);
|
||||
}
|
||||
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
|
||||
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
|
||||
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
|
||||
// perimeter width of area
|
||||
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
|
||||
layerm->flow(frExternalPerimeter).scaled_spacing()),
|
||||
to_polygons(m_layers[i]->lslices));
|
||||
combine_shells(anchor_area);
|
||||
}
|
||||
|
||||
if (one_more_layer_below_top_bottom_surfaces)
|
||||
if (i < int(cache_top_botom_regions.size()) &&
|
||||
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
|
||||
combine_holes(cache_top_botom_regions[i].holes);
|
||||
}
|
||||
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
|
||||
// Gather bottom regions projected to this layer.
|
||||
coordf_t bottom_z = layer->bottom_z();
|
||||
int i = int(idx_layer) - 1;
|
||||
int ibottom = int(idx_layer) - n_bottom_layers;
|
||||
bool at_least_one_bottom_projected = false;
|
||||
for (; i >= 0 &&
|
||||
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
|
||||
-- i) {
|
||||
at_least_one_bottom_projected = true;
|
||||
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
|
||||
combine_holes(cache.holes);
|
||||
combine_shells(cache.bottom_surfaces);
|
||||
}
|
||||
|
||||
if (!at_least_one_bottom_projected && i >= 0) {
|
||||
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
|
||||
layerm->flow(frExternalPerimeter).scaled_spacing()),
|
||||
to_polygons(m_layers[i]->lslices));
|
||||
combine_shells(anchor_area);
|
||||
}
|
||||
|
||||
if (one_more_layer_below_top_bottom_surfaces)
|
||||
if (i >= 0 &&
|
||||
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
|
||||
combine_holes(cache_top_botom_regions[i].holes);
|
||||
}
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
|
||||
@@ -2967,8 +2830,11 @@ void PrintObject::discover_vertical_shells()
|
||||
Polygons object_volume;
|
||||
Polygons internal_volume;
|
||||
{
|
||||
if (idx_layer > 0 && idx_layer + 1 < m_layers.size())
|
||||
object_volume = to_polygons(intersection_ex_by_piece(m_layers[idx_layer - 1]->lslices, to_polygons(m_layers[idx_layer + 1]->lslices)));
|
||||
Polygons shrinked_bottom_slice = idx_layer > 0 ? to_polygons(m_layers[idx_layer - 1]->lslices) : Polygons{};
|
||||
Polygons shrinked_upper_slice = (idx_layer + 1) < m_layers.size() ?
|
||||
to_polygons(m_layers[idx_layer + 1]->lslices) :
|
||||
Polygons{};
|
||||
object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice);
|
||||
internal_volume = closing(polygonsInternal, SCALED_EPSILON);
|
||||
}
|
||||
|
||||
@@ -2979,34 +2845,15 @@ void PrintObject::discover_vertical_shells()
|
||||
// the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy
|
||||
// 2. the area does not fully cover an internal polygon
|
||||
// This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small
|
||||
// Both tests below compare a small piece against the whole layer. Done literally, that is
|
||||
// quadratic in the number of pieces, which is what a layer split up by colour painting has,
|
||||
// so each is restricted to the part of the layer near the piece with an identical result:
|
||||
// object_volume is clipped to the piece's box, and only the internal polygons whose box meets
|
||||
// the expanded piece take part in the count, since the others pass through the difference
|
||||
// unchanged and add the same number to both sides of it.
|
||||
std::vector<BoundingBox> internal_bboxes;
|
||||
internal_bboxes.reserve(internal_volume.size());
|
||||
for (const Polygon &poly : internal_volume)
|
||||
internal_bboxes.emplace_back(get_extents(poly));
|
||||
regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(),
|
||||
[&internal_volume, &internal_bboxes, &min_perimeter_infill_spacing,
|
||||
[&internal_volume, &min_perimeter_infill_spacing,
|
||||
&object_volume](const ExPolygon &p) {
|
||||
const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
|
||||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
|
||||
diff(to_polygons(p),
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
|
||||
object_volume, get_extents(p).inflated(SCALED_EPSILON)))
|
||||
.empty());
|
||||
if (!small)
|
||||
return false;
|
||||
const Polygons expanded = expand(to_polygons(p), min_perimeter_infill_spacing);
|
||||
const BoundingBox bbox = get_extents(expanded);
|
||||
Polygons nearby;
|
||||
for (size_t i = 0; i < internal_volume.size(); ++i)
|
||||
if (internal_bboxes[i].overlap(bbox))
|
||||
nearby.emplace_back(internal_volume[i]);
|
||||
return diff(nearby, expanded).size() >= nearby.size();
|
||||
return (p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
|
||||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
|
||||
diff(to_polygons(p), object_volume).empty())) &&
|
||||
diff(internal_volume,
|
||||
expand(to_polygons(p), min_perimeter_infill_spacing))
|
||||
.size() >= internal_volume.size();
|
||||
}),
|
||||
regularized_shell.end());
|
||||
}
|
||||
@@ -3028,9 +2875,8 @@ void PrintObject::discover_vertical_shells()
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
|
||||
// Trim the internal & internalvoid by the shell.
|
||||
const Polygons regularized_shell_polygons = to_polygons(regularized_shell);
|
||||
Slic3r::ExPolygons new_internal = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternal)), regularized_shell_polygons);
|
||||
Slic3r::ExPolygons new_internal_void = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternalVoid)), regularized_shell_polygons);
|
||||
Slic3r::ExPolygons new_internal = diff_ex(layerm->fill_surfaces.filter_by_type(stInternal), regularized_shell);
|
||||
Slic3r::ExPolygons new_internal_void = diff_ex(layerm->fill_surfaces.filter_by_type(stInternalVoid), regularized_shell);
|
||||
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
@@ -3057,15 +2903,7 @@ void PrintObject::discover_vertical_shells()
|
||||
layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final");
|
||||
}
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
}; // for each region
|
||||
if (top_bottom_surfaces_all_regions)
|
||||
// Nothing is shared between the regions, and a layer cut through a fine relief takes far longer than the others,
|
||||
// so they run next to each other instead of one after another.
|
||||
tbb::parallel_for(size_t(0), this->num_printing_regions(), process_region);
|
||||
else
|
||||
// Here every region fills the one top/bottom cache with its own surfaces first.
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id)
|
||||
process_region(region_id);
|
||||
} // for each region
|
||||
} // void PrintObject::discover_vertical_shells()
|
||||
|
||||
// #define DEBUG_BRIDGE_OVER_INFILL
|
||||
@@ -3586,16 +3424,6 @@ void PrintObject::bridge_over_infill()
|
||||
vertical_lines[i].b = Point{x, y_max};
|
||||
}
|
||||
|
||||
// The vertical lines only span the bridged area's x range, so anchors entirely outside it can never be
|
||||
// hit. Leaving them out gives the same intersections without building a tree over the whole layer's
|
||||
// boundary for every bridge.
|
||||
const coord_t scan_x_min = bb_x.min.x();
|
||||
const coord_t scan_x_max = bb_x.min.x() + coord_t(n_vlines) * scan_spacing;
|
||||
anchors.erase(std::remove_if(anchors.begin(), anchors.end(),
|
||||
[scan_x_min, scan_x_max](const Line &l) {
|
||||
return std::max(l.a.x(), l.b.x()) < scan_x_min || std::min(l.a.x(), l.b.x()) > scan_x_max;
|
||||
}),
|
||||
anchors.end());
|
||||
auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer<Line>{std::move(anchors)};
|
||||
auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)};
|
||||
|
||||
@@ -3840,58 +3668,26 @@ void PrintObject::bridge_over_infill()
|
||||
|
||||
std::vector<CandidateSurface> expanded_surfaces;
|
||||
expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
|
||||
// The expanded fill boundary depends only on the bridging flow, and total_fill_area is not
|
||||
// modified below, so build it once per spacing rather than once per candidate. A layer split
|
||||
// into many candidates (e.g. by colour painting) otherwise repeats a layer-wide offset for each.
|
||||
std::map<coord_t, Polylines> boundary_by_spacing;
|
||||
// expansion_area is a clean, non-overlapping set, so cutting a bridge out of it only changes the
|
||||
// polygons near that bridge. The rest are passed through untouched instead of being fed to Clipper
|
||||
// with the whole layer again for every candidate.
|
||||
// Not `near`/`far`: the Windows headers still define those as macros, and they expand to
|
||||
// nothing, which turns the declaration below into an empty one.
|
||||
const auto split_near = [](const Polygons &polys, const BoundingBox &bbox, Polygons &rest) {
|
||||
Polygons nearby;
|
||||
for (const Polygon &p : polys)
|
||||
(get_extents(p).overlap(bbox) ? nearby : rest).emplace_back(p);
|
||||
return nearby;
|
||||
};
|
||||
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
|
||||
region_config.sparse_infill_pattern == ipOctagramSpiral;
|
||||
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
|
||||
const Polygons expanded_polys = expand(candidate.new_polys, flow.scaled_spacing());
|
||||
// deep_infill_area and internal_unsupported_area cover the whole layer; only their part under
|
||||
// this candidate can change the results, so they are clipped to its box first.
|
||||
ExPolygons bridge_components;
|
||||
if (!expanded_polys.empty())
|
||||
bridge_components = intersection_ex(expanded_polys,
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
|
||||
deep_infill_area, get_extents(expanded_polys).inflated(SCALED_EPSILON)));
|
||||
ExPolygons bridge_components = intersection_ex(expand(candidate.new_polys, flow.scaled_spacing()), deep_infill_area);
|
||||
// Orca: Filter whole bridge areas so their holes remain holes.
|
||||
bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(),
|
||||
[&internal_unsupported_area](const ExPolygon &component) {
|
||||
return intersection_ex(component, ClipperUtils::clip_clipper_polygons_with_subject_bbox(
|
||||
internal_unsupported_area,
|
||||
get_extents(component).inflated(SCALED_EPSILON)))
|
||||
.empty();
|
||||
return intersection_ex(component, internal_unsupported_area).empty();
|
||||
}),
|
||||
bridge_components.end());
|
||||
Polygons area_to_be_bridge = to_polygons(std::move(bridge_components));
|
||||
|
||||
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
|
||||
|
||||
if (area_to_be_bridge.empty())
|
||||
continue;
|
||||
|
||||
// Not split like the cut of expansion_area below: the whole limiting area is grown by 30% of the spacing,
|
||||
// which merges neighbouring polygons, and any of its boundary can anchor the bridge.
|
||||
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
|
||||
|
||||
auto boundary_it = boundary_by_spacing.find(flow.scaled_spacing());
|
||||
if (boundary_it == boundary_by_spacing.end())
|
||||
boundary_it = boundary_by_spacing
|
||||
.emplace(flow.scaled_spacing(), to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing())))
|
||||
.first;
|
||||
Polylines boundary_plines = boundary_it->second;
|
||||
Polylines boundary_plines = to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing()));
|
||||
{
|
||||
Polylines limiting_plines = to_polylines(expand(limiting_area, 0.3f * flow.scaled_spacing()));
|
||||
boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end());
|
||||
@@ -3967,12 +3763,9 @@ void PrintObject::bridge_over_infill()
|
||||
// Check collision with other expanded surfaces
|
||||
{
|
||||
bool reconstruct = false;
|
||||
Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing());
|
||||
const BoundingBox tmp_expanded_bbox = get_extents(tmp_expanded_area);
|
||||
Polygons tmp_expanded_area = expand(bridging_area, 3.0f * flow.scaled_spacing());
|
||||
for (const CandidateSurface &s : expanded_surfaces) {
|
||||
// Surfaces whose boxes miss each other cannot intersect, which is most pairs on a busy layer.
|
||||
if (get_extents(s.new_polys).overlap(tmp_expanded_bbox) &&
|
||||
!intersection(s.new_polys, tmp_expanded_area).empty()) {
|
||||
if (!intersection(s.new_polys, tmp_expanded_area).empty()) {
|
||||
bridging_angle = s.bridge_angle;
|
||||
reconstruct = true;
|
||||
break;
|
||||
@@ -3996,20 +3789,10 @@ void PrintObject::bridge_over_infill()
|
||||
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
|
||||
bridging_angle, scan_spacing, true));
|
||||
}
|
||||
// Each of these meets one bridge with the whole layer, so the layer side is first cut down to the
|
||||
// bridge's box (and expansion_area split as above); the result is the same.
|
||||
if (!bridging_area.empty()) {
|
||||
const BoundingBox bridging_bbox = get_extents(bridging_area).inflated(SCALED_EPSILON);
|
||||
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(limiting_area, bridging_bbox));
|
||||
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_fill_area, bridging_bbox));
|
||||
bridging_area = diff(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_top_area, bridging_bbox));
|
||||
}
|
||||
if (!bridging_area.empty()) {
|
||||
Polygons kept;
|
||||
const Polygons cut = split_near(expansion_area, get_extents(bridging_area).inflated(SCALED_EPSILON), kept);
|
||||
append(kept, diff(cut, bridging_area));
|
||||
expansion_area = std::move(kept);
|
||||
}
|
||||
bridging_area = intersection(bridging_area, limiting_area);
|
||||
bridging_area = intersection(bridging_area, total_fill_area);
|
||||
bridging_area = diff(bridging_area, total_top_area);
|
||||
expansion_area = diff(expansion_area, bridging_area);
|
||||
|
||||
#ifdef DEBUG_BRIDGE_OVER_INFILL
|
||||
debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r),
|
||||
|
||||
@@ -1004,9 +1004,9 @@ public:
|
||||
::fread(&y, sizeof(coord_t), 1, file);
|
||||
poly.points.emplace_back(Point(x * scale, y * scale));
|
||||
}
|
||||
printf("Polygon %d, area: %lf\n", i, area(poly.points));
|
||||
if (which == -1 || which == i)
|
||||
m_support_polygons_deserialized.emplace_back(std::move(poly));
|
||||
printf("Polygon %d, area: %lf\n", i, area(poly.points));
|
||||
}
|
||||
::fread(&n_polygons, 4, 1, file);
|
||||
m_trimming_polygons_deserialized.reserve(n_polygons);
|
||||
|
||||
@@ -996,46 +996,16 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
|
||||
if (is_auto(stype) && config_detect_sharp_tails)
|
||||
{
|
||||
// BBS detect sharp tail
|
||||
// On a belt, "below" has to include the belt itself and the
|
||||
// shear-advanced lower layer, or every belt-contact island reads as
|
||||
// a sharp tail -- which is what the empty-predecessor skip above was
|
||||
// really masking. effective_lower is exactly that notion of below.
|
||||
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
|
||||
// Each island is tested only against the lower islands whose box meets its own; overlaps() tries
|
||||
// every pair, which is quadratic in the island counts of the two layers.
|
||||
std::vector<BoundingBox> lower_bboxes;
|
||||
lower_bboxes.reserve(tail_lower.size());
|
||||
for (const ExPolygon &lower : tail_lower)
|
||||
lower_bboxes.emplace_back(get_extents(lower));
|
||||
for (const ExPolygon& expoly : curr_polys) {
|
||||
bool is_sharp_tail = false;
|
||||
// 1. nothing below
|
||||
// this is a sharp tail region if it's floating and non-ignorable
|
||||
const ExPolygons expanded = offset_ex(expoly, 0.1 * extrusion_width_scaled);
|
||||
const BoundingBox bbox = get_extents(expanded);
|
||||
ExPolygons lower_nearby;
|
||||
for (size_t i = 0; i < tail_lower.size(); ++i)
|
||||
if (lower_bboxes[i].overlap(bbox))
|
||||
lower_nearby.emplace_back(tail_lower[i]);
|
||||
// As overlaps(expanded, lower_nearby), with each lower island cut to the island's box first:
|
||||
// below a fine relief the lower layer is a few islands with thousands of holes, and the whole
|
||||
// of that boundary would otherwise be intersected once per island above.
|
||||
const auto overlaps_nearby = [&]() {
|
||||
for (const ExPolygon &a : expanded) {
|
||||
if (a.empty())
|
||||
continue;
|
||||
const BoundingBox a_bbox = get_extents(a);
|
||||
for (const ExPolygon &b : lower_nearby) {
|
||||
if (b.empty() || !get_extents(b).overlap(a_bbox))
|
||||
continue;
|
||||
const Polygons b_near = ClipperUtils::clip_clipper_polygons_with_subject_bbox(b, a_bbox.inflated(SCALED_EPSILON));
|
||||
if (!intersection_pl(to_polylines(b_near), a).empty() || b.contains(a.contour.points.front()))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (!overlaps_nearby()) {
|
||||
// this is a sharp tail region if it's floating and non-ignorable.
|
||||
// On a belt, "below" has to include the belt itself and the
|
||||
// shear-advanced lower layer, or every belt-contact island reads as
|
||||
// a sharp tail -- which is what the empty-predecessor skip above was
|
||||
// really masking. effective_lower is exactly that notion of below.
|
||||
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
|
||||
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) {
|
||||
is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public:
|
||||
thickness(other.thickness), thickness_layers(other.thickness_layers),
|
||||
bridge_angle(other.bridge_angle), extra_perimeters(other.extra_perimeters)
|
||||
{};
|
||||
Surface(Surface &&rhs) noexcept
|
||||
Surface(Surface &&rhs)
|
||||
: surface_type(rhs.surface_type), expolygon(std::move(rhs.expolygon)),
|
||||
thickness(rhs.thickness), thickness_layers(rhs.thickness_layers),
|
||||
bridge_angle(rhs.bridge_angle), extra_perimeters(rhs.extra_perimeters)
|
||||
@@ -94,7 +94,7 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
Surface& operator=(Surface &&rhs) noexcept
|
||||
Surface& operator=(Surface &&rhs)
|
||||
{
|
||||
surface_type = rhs.surface_type;
|
||||
expolygon = std::move(rhs.expolygon);
|
||||
|
||||
@@ -1495,6 +1495,40 @@ float its_volume(const indexed_triangle_set &its)
|
||||
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)
|
||||
{
|
||||
if (its.indices.empty())
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <array>
|
||||
#include <cereal/specialize.hpp>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "BoundingBox.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);
|
||||
// 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);
|
||||
|
||||
void its_merge(indexed_triangle_set &A, const indexed_triangle_set &B);
|
||||
|
||||
@@ -167,10 +167,10 @@ inline void append(std::vector<T, Alloc> &dest, std::vector<T, Alloc> &&src)
|
||||
{
|
||||
if (dest.empty())
|
||||
dest = std::move(src);
|
||||
else
|
||||
// insert() grows the capacity geometrically; reserving exactly the new size reallocated on every call, which
|
||||
// made appending piece by piece quadratic.
|
||||
dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
|
||||
else {
|
||||
dest.reserve(dest.size() + src.size());
|
||||
std::move(std::begin(src), std::end(src), std::back_inserter(dest));
|
||||
}
|
||||
src.clear();
|
||||
src.shrink_to_fit();
|
||||
}
|
||||
|
||||
@@ -1441,6 +1441,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
||||
wxGetApp().plater()->schedule_background_process();
|
||||
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.
|
||||
// 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)
|
||||
{
|
||||
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);
|
||||
for (auto& move_type_times : m_move_type_times)
|
||||
@@ -1955,6 +1963,10 @@ void GCodeViewer::reset()
|
||||
m_move_type_distances.fill(0.0f);
|
||||
m_print_statistics.reset();
|
||||
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_right_extruder_filament.clear();
|
||||
m_sequential_view.gcode_window.reset();
|
||||
|
||||
@@ -260,6 +260,10 @@ private:
|
||||
GCodeProcessorResult::SettingsIds m_settings_ids;
|
||||
|
||||
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 };
|
||||
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(); }
|
||||
|
||||
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_layers_count() const { return m_viewer.get_layers_count(); }
|
||||
// ORCA: realistic view. Changes whenever the toolpaths casting shadows do.
|
||||
|
||||
@@ -85,7 +85,9 @@
|
||||
#include "3DScene.hpp"
|
||||
#include "BackgroundSlicingProcess.hpp"
|
||||
#include "CameraUtils.hpp"
|
||||
#include "GLModel.hpp"
|
||||
#include "GLShader.hpp"
|
||||
#include "libslic3r/ConnectedBodies.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "Tab.hpp"
|
||||
#include "GUI_Preview.hpp"
|
||||
@@ -136,6 +138,7 @@
|
||||
#include <tbb/spin_mutex.h>
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.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)
|
||||
{
|
||||
// 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");
|
||||
}
|
||||
|
||||
// After the occlusion pass, which would shade it as the surface behind it.
|
||||
m_center_of_mass.render(*this);
|
||||
|
||||
if (_is_fxaa_enabled()) {
|
||||
_render_fxaa_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height()));
|
||||
m_frame_profiler.mark("fxaa");
|
||||
@@ -4513,6 +4838,12 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
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;
|
||||
|
||||
std::map<MouseButton, MouseAction> button_mappings;
|
||||
@@ -9363,6 +9694,7 @@ void GLCanvas3D::_render_overlays()
|
||||
}*/
|
||||
}
|
||||
m_labels.render(sorted_instances);
|
||||
m_center_of_mass.render_details(*this);
|
||||
|
||||
_render_3d_navigator();
|
||||
|
||||
@@ -10350,6 +10682,12 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
[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
|
||||
// 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
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#define slic3r_GLCanvas3D_hpp_
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/ConnectedBodies.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "slic3r/GUI/3DScene.hpp"
|
||||
#include <cstdlib>
|
||||
#include <imgui.h>
|
||||
@@ -30,6 +33,7 @@
|
||||
#include "Gizmos/GLGizmosManager.hpp"
|
||||
#include "GUI_ObjectLayers.hpp"
|
||||
#include "GLSelectionRectangle.hpp"
|
||||
#include "GLModel.hpp"
|
||||
#include "MeshUtils.hpp"
|
||||
#include "GCodeViewer.hpp"
|
||||
#include "Camera.hpp"
|
||||
@@ -477,6 +481,69 @@ class GLCanvas3D
|
||||
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
|
||||
{
|
||||
std::string m_text;
|
||||
@@ -733,6 +800,7 @@ private:
|
||||
int m_selected_extruder;
|
||||
|
||||
Labels m_labels;
|
||||
CenterOfMass m_center_of_mass;
|
||||
Tooltip m_tooltip;
|
||||
bool m_tooltip_enabled{ true };
|
||||
Slope m_slope;
|
||||
|
||||
@@ -435,6 +435,9 @@ public:
|
||||
bool show_outline() const { return app_config->get_bool("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;};
|
||||
|
||||
std::vector<std::string> split_str(std::string src, std::string separator);
|
||||
|
||||
@@ -21230,6 +21230,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config)
|
||||
opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id") {
|
||||
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)
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
@@ -18,6 +20,7 @@
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <sstream>
|
||||
@@ -99,6 +102,22 @@ TEST_CASE("Reserved keyword detection reports every offending line", "[GCodeProc
|
||||
|
||||
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
|
||||
// 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)
|
||||
@@ -114,18 +133,42 @@ void process_squares(int squares, GCodeProcessorResult &result, bool virtual_mov
|
||||
if (virtual_moves)
|
||||
gcode << "VG1 X20 Y30 F12000\n";
|
||||
}
|
||||
FullPrintConfig config;
|
||||
config.gcode_flavor.value = gcfMarlinFirmware;
|
||||
// s_IsBBLPrinter selects the "; FEATURE: " role tags this 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.str();
|
||||
GCodeProcessor processor;
|
||||
processor.apply_config(config);
|
||||
processor.process_file(temp.string());
|
||||
result = std::move(processor.extract_result());
|
||||
process_gcode(gcode.str(), result);
|
||||
}
|
||||
|
||||
// Objects A and B on the first layer and A again on the second, with A's brim and support. The skirt and the
|
||||
// prime tower belong to neither.
|
||||
void process_two_objects(GCodeProcessorResult &result)
|
||||
{
|
||||
std::ostringstream gcode;
|
||||
gcode << "M83\nG90\n"
|
||||
<< "; CHANGE_LAYER\n; LAYER_HEIGHT: 0.2\nG1 Z0.2 F12000\n"
|
||||
<< "; FEATURE: Skirt\nG1 X0 Y100 E5 F3000\n"
|
||||
<< "; 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)
|
||||
@@ -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());
|
||||
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(), [¢er](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(), [¢er](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(), [¢er](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));
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_fill_plane_path.cpp
|
||||
test_fill_tpms_adaptive.cpp
|
||||
test_geometry.cpp
|
||||
test_kdtree.cpp
|
||||
test_multimaterial_segmentation.cpp
|
||||
test_placeholder_parser.cpp
|
||||
test_png_read_write.cpp
|
||||
@@ -66,6 +65,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_ordering_strategies.cpp
|
||||
# test_png_io.cpp
|
||||
test_indexed_triangle_set.cpp
|
||||
test_connected_bodies.cpp
|
||||
test_texture_displacement.cpp
|
||||
test_instance_lock.cpp
|
||||
../libnest2d/printer_parts.cpp
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <numeric>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -301,90 +300,3 @@ TEST_CASE("Top level expolygons of an even-odd union", "[ClipperUtils]") {
|
||||
REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested)));
|
||||
REQUIRE(top_level_expolygons(reference).size() == 1);
|
||||
}
|
||||
|
||||
// Rings flattened to x,y,x,y... and sorted, with each ring rotated to start at its lowest point: two
|
||||
// encodings of the same geometry compare equal however the pieces came back or wherever a ring started.
|
||||
static std::vector<std::vector<coord_t>> canonical_rings(const ExPolygons &expolygons)
|
||||
{
|
||||
std::vector<std::vector<coord_t>> rings;
|
||||
const auto add = [&rings](const Polygon &poly) {
|
||||
if (poly.points.empty())
|
||||
return;
|
||||
Points pts = poly.points;
|
||||
std::rotate(pts.begin(),
|
||||
std::min_element(pts.begin(), pts.end(), [](const Point &a, const Point &b) {
|
||||
return std::make_pair(a.x(), a.y()) < std::make_pair(b.x(), b.y());
|
||||
}),
|
||||
pts.end());
|
||||
std::vector<coord_t> flat;
|
||||
flat.reserve(pts.size() * 2);
|
||||
for (const Point &p : pts) {
|
||||
flat.emplace_back(p.x());
|
||||
flat.emplace_back(p.y());
|
||||
}
|
||||
rings.emplace_back(std::move(flat));
|
||||
};
|
||||
for (const ExPolygon &expoly : expolygons) {
|
||||
add(expoly.contour);
|
||||
for (const Polygon &hole : expoly.holes)
|
||||
add(hole);
|
||||
}
|
||||
std::sort(rings.begin(), rings.end());
|
||||
return rings;
|
||||
}
|
||||
|
||||
// The same rings, every coordinate within `tolerance`.
|
||||
static bool same_rings(const ExPolygons &a, const ExPolygons &b, coord_t tolerance)
|
||||
{
|
||||
const std::vector<std::vector<coord_t>> ra = canonical_rings(a), rb = canonical_rings(b);
|
||||
return std::equal(ra.begin(), ra.end(), rb.begin(), rb.end(), [tolerance](const std::vector<coord_t> &x, const std::vector<coord_t> &y) {
|
||||
return std::equal(x.begin(), x.end(), y.begin(), y.end(), [tolerance](coord_t u, coord_t v) { return std::abs(u - v) <= tolerance; });
|
||||
});
|
||||
}
|
||||
|
||||
TEST_CASE("Tiled diff and intersection return the same polygons as the plain calls", "[ClipperUtils]") {
|
||||
// A grid of disjoint framed squares, enough of them to be split into several tiles.
|
||||
const int n = 40;
|
||||
const coord_t cell = scaled<coord_t>(2.), side = scaled<coord_t>(1.5), frame = scaled<coord_t>(0.3);
|
||||
ExPolygons subject;
|
||||
for (int y = 0; y < n; ++ y)
|
||||
for (int x = 0; x < n; ++ x) {
|
||||
const Point o(x * cell, y * cell);
|
||||
ExPolygon square(Polygon({ o, o + Point(side, 0), o + Point(side, side), o + Point(0, side) }));
|
||||
Polygon hole({ o + Point(frame, frame), o + Point(frame, side - frame), o + Point(side - frame, side - frame), o + Point(side - frame, frame) });
|
||||
square.holes.emplace_back(std::move(hole));
|
||||
subject.emplace_back(std::move(square));
|
||||
}
|
||||
// Clip polygons crossing many squares, one of them large with holes of its own.
|
||||
Polygons clip;
|
||||
const coord_t span = n * cell;
|
||||
for (int i = 0; i < 8; ++ i) {
|
||||
const coord_t y0 = coord_t(i) * span / 8, y1 = y0 + scaled<coord_t>(0.9);
|
||||
clip.emplace_back(Polygon({ Point(- cell, y0), Point(span, y0 + cell * 3), Point(span, y1 + cell * 3), Point(- cell, y1) }));
|
||||
}
|
||||
ExPolygon big(Polygon({ Point(span / 4, span / 4), Point(3 * span / 4, span / 4), Point(3 * span / 4, 3 * span / 4), Point(span / 4, 3 * span / 4) }));
|
||||
for (int i = 0; i < 4; ++ i) {
|
||||
const Point o(span / 4 + scaled<coord_t>(3.1) + i * scaled<coord_t>(9.7), span / 4 + scaled<coord_t>(5.3));
|
||||
big.holes.emplace_back(Polygon({ o, o + Point(0, scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), 0) }));
|
||||
}
|
||||
polygons_append(clip, to_polygons(big));
|
||||
|
||||
const ApplySafetyOffset safety = GENERATE(ApplySafetyOffset::No, ApplySafetyOffset::Yes);
|
||||
|
||||
// The point of the fixture: below 128 pieces the helpers fall back to a single tile and the tiled
|
||||
// path under test is never taken.
|
||||
REQUIRE(ClipperUtils::tile_expolygons(subject, 32).size() > 1);
|
||||
|
||||
// With the safety offset a tile unites fewer clip polygons, so Clipper2 can round a crossing 1 unit differently.
|
||||
const coord_t tolerance = safety == ApplySafetyOffset::Yes ? 1 : 0;
|
||||
|
||||
const ExPolygons diff_plain = diff_ex(subject, clip, safety);
|
||||
const ExPolygons diff_tiled = diff_ex_by_piece(subject, clip, safety);
|
||||
REQUIRE(area(diff_plain) > 0.);
|
||||
CHECK(same_rings(diff_tiled, diff_plain, tolerance));
|
||||
|
||||
const ExPolygons intersection_plain = intersection_ex(subject, clip, safety);
|
||||
const ExPolygons intersection_tiled = intersection_ex_by_piece(subject, clip, safety);
|
||||
REQUIRE(area(intersection_plain) > 0.);
|
||||
CHECK(same_rings(intersection_tiled, intersection_plain, tolerance));
|
||||
}
|
||||
|
||||
@@ -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. }, { { ¬ch, 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));
|
||||
}
|
||||
@@ -12,11 +12,16 @@
|
||||
#include <string>
|
||||
|
||||
#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 "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
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);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/KDTreeIndirect.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Visiting the nearby points gives what collecting them gives", "[KDTree]") {
|
||||
std::mt19937 rng(19937);
|
||||
std::uniform_real_distribution<float> coord(-50.f, 50.f);
|
||||
// Points in a box, so that a radius search returns anything from none of them to all of them.
|
||||
std::vector<Vec3f> points(2000);
|
||||
for (Vec3f &p : points)
|
||||
p = Vec3f(coord(rng), coord(rng), coord(rng));
|
||||
|
||||
auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); };
|
||||
KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate);
|
||||
std::vector<size_t> indices(points.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
tree.build(indices);
|
||||
|
||||
const float radius = GENERATE(0.5f, 5.f, 25.f, 200.f);
|
||||
for (int i = 0; i < 20; ++ i) {
|
||||
const Vec3f center(coord(rng), coord(rng), coord(rng));
|
||||
|
||||
const std::vector<size_t> collected = find_nearby_points(tree, center, radius);
|
||||
std::vector<size_t> visited;
|
||||
visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); });
|
||||
|
||||
// Same points, and in the same order: a caller that keeps the first of several equally good ones
|
||||
// must get the same answer either way.
|
||||
REQUIRE(visited == collected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A radius search returns every point within the radius and no other", "[KDTree]") {
|
||||
std::mt19937 rng(2024);
|
||||
std::uniform_real_distribution<float> coord(-20.f, 20.f);
|
||||
std::vector<Vec3f> points(500);
|
||||
for (Vec3f &p : points)
|
||||
p = Vec3f(coord(rng), coord(rng), coord(rng));
|
||||
|
||||
auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); };
|
||||
KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate);
|
||||
std::vector<size_t> indices(points.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
tree.build(indices);
|
||||
|
||||
const Vec3f center(1.f, -2.f, 3.f);
|
||||
const float radius = 7.f;
|
||||
|
||||
std::vector<size_t> expected;
|
||||
for (size_t i = 0; i < points.size(); ++ i)
|
||||
if ((points[i] - center).squaredNorm() < radius * radius)
|
||||
expected.emplace_back(i);
|
||||
|
||||
std::vector<size_t> visited;
|
||||
visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); });
|
||||
std::sort(visited.begin(), visited.end());
|
||||
|
||||
REQUIRE(! expected.empty());
|
||||
REQUIRE(visited == expected);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
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]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
|
||||
Reference in New Issue
Block a user