Compare commits

...
Author SHA1 Message Date
SoftFever 3ca182a98e Fix H2C start G-code record-data comments (#16406)
This fixes the H2C start G-code template by commenting the two
`========== record data ==========` separator lines.

Those lines are currently emitted as raw text inside
`machine_start_gcode`. Since they are not comments or G-code commands,
they can make the generated start sequence invalid or firmware-dependent
when the template is used. The fix preserves the separators as comments.

Fixes #16218.

Validation:
- Parsed the modified JSON profile with Python `json.loads`.
- Checked both `record data` separator lines now start with `;` after
unescaping the G-code string.
2026-10-13 01:07:25 +08:00
SoftFever 5e19fc0e6c Stop leaking layers on re-slice and honor each object's slice closing radius (#16066)
# Description

Re-slicing no longer leaks layers in two places. Tree supports dropped
the support layers above the topmost overhang without freeing them. And
an object that had sliced itself, then became identical to another
object (for example a copy whose original lost a per-object setting or
modifier), leaked its own layers when it switched to sharing that
object's slices; it now frees them once it takes the shared layers over.
If slicing is cancelled before that point, they still leak, as before.
Tree support also stops copying its collision and avoidance areas on
every lookup, and releases its working cache once supports are generated
instead of keeping it until the next slice. Elephant-foot compensation
now uses each object's own slice closing radius, where the first object
sliced in a session used to fix the value for every later slice.

Slicing output changes only on layers with more than one region printed
with elephant-foot compensation and no raft, and only where an object's
slice closing radius differs from the value the session had fixed.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

Adds a regression test for an object that switches to sharing another
object's slices. The `fff_print` suite passes.

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

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-10-13 00:28:26 +08:00
SoftFever 3f4709aaa7 Fix stale and hidden highlights in the texture displacement gizmo (#16409)
# Description

Fixes highlights in the texture displacement gizmo that stayed on screen
after the state behind them changed, or were drawn where they could not
be seen. Erase whole model, Select whole model and Capture current view
update the paint highlight at once instead of on the next right-click. A
stroke released outside the 3D view ends on the next mouse move, in
every paint gizmo, rather than leaving the brush stuck in its pressed
colour. The layer textures stop showing an image that has been replaced.
Selecting islands in the UV editor switches a Normal view to Fast, where
their highlight is visible instead of buried under the displaced relief,
and Normal comes back once no island is selected unless another view was
picked in the meantime.

No change to slicing output; the changes are confined to the paint
gizmos and the UV editor.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

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

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

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-13 00:23:33 +08:00
SoftFever c63bd5d474 Switch to the Fast view while UV islands are selected in the texture gizmo
The island highlight was hidden under the Normal view's relief. Normal comes back once no island is selected, unless another view was picked in the meantime.
2026-10-13 00:18:51 +08:00
SoftFever 6263502697 Free a sharing object's own layers only when it takes over the shared ones 2026-10-13 00:15:07 +08:00
HanifKoh 1a9201690d Draw Preview Segments With 16-Bit Indices (#16389)
The segment draw switched to an element buffer of 8-bit indices. GCN 1/2
GPUs cannot read 8-bit indices in hardware, so the driver converts them,
and on a Mac Pro 2013 FirePro under Windows Boot Camp the toolpaths come
out as scattered spikes. 16-bit indices are supported everywhere and keep
the eight shader runs per segment.
2026-10-13 00:09:18 +08:00
SoftFever c6a338f35f Fix stale paint highlights and textures in the texture displacement gizmo
Erase whole model, Select whole model and Capture current view update the
highlight at once. A stroke released outside the 3D view ends on the next mouse
move, in every paint gizmo, instead of leaving the brush in its pressed colour.
The layer textures no longer show an image that was replaced.
2026-10-12 23:18:38 +08:00
HanifKoh 4f2d2ae11a Wait for the PR's Deps Cache in clang-tidy When deps/ Changes (#16374)
* Wait for the PR's Deps Cache in clang-tidy When deps/ Changes

* Fail the clang-tidy Deps Wait When Listing Caches Keeps Failing

* Wait for Deps Through ci_deps_cache.sh in clang-tidy

Reuse the macOS deps wait for Linux: the job names, workflow and cache
ref are passed in, and a new 'own' mode waits for the PR's own build
when it changes deps/. A PR that keeps deps/ now waits for a running base
build with the same deps, and warns when it falls back to older ones. The
wait ends as soon as the deps job fails.
2026-10-12 23:17:41 +08:00
SoftFever c57b5dc6c5 Merge branch 'main' into perf/slicing-core-memory 2026-10-12 22:16:10 +08:00
chenshj73 7d4ec3494c Fix H2C start G-code record-data comments 2026-10-12 19:30:32 +08:00
ExPikaPaka 391c53a8da Take the print config by reference while simplifying extrusions
simplify_path, simplify_multi_path and simplify_loop each copied the whole
PrintConfig, 10520 bytes with around 200 heap owning members, once per extrusion
entity, from inside the parallel_for over all layers. LayerRegion.cpp already
binds a reference a few hundred lines above. Three more sites in Layer.cpp do the
same thing.

The tree support changes in the same area: draw_circles() dropped layers from an
owning vector without deleting them, so every re-slice leaked the layers above
the topmost overhang. std::stable_partition rather than std::remove_if, because
remove_if leaves moved-from duplicates of kept pointers in the tail and deleting
those would be a double free. The collision and avoidance accessors returned
ExPolygons by value out of a cache that hands them out by reference, several
times per node per layer. The preview cache is released when the support step
finishes instead of at the start of the next slice; nothing reads it in between.

set_shared_object() now releases the layers the object still owns before it
starts aliasing another object's. Both clear functions are no-ops once the alias
is set, so this cannot free the sharee's layers.

PrintObjectSlice.cpp: eps was a function local static inside a parallel_for, so
slice_closing_radius was latched once per process. This one changes output for
projects that override it per object, or on a re-slice after editing it. The old
behaviour was undefined: whichever object won the race set it for the session.
2026-10-01 09:15:15 +02:00
18 changed files with 462 additions and 106 deletions
+70 -4
View File
@@ -16,6 +16,15 @@ on:
permissions:
contents: read
# Listing caches and builds while waiting for a deps cache.
actions: read
# Read by scripts/ci_deps_cache.sh.
env:
REPO: ${{ github.repository }}
WORKFLOW: build_all.yml
BASE_REF: ${{ github.base_ref }}
JOB: build_linux (${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }})
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -35,7 +44,7 @@ jobs:
# The PR merge commit plus its first parent, the base it is diffed against.
fetch-depth: 2
- name: Look for changed C++ files
- name: Look for changed files
id: changes
run: |
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then
@@ -43,18 +52,75 @@ jobs:
else
echo "No C++ changes under src/ or tests/."
fi
if ! git diff --quiet HEAD^1 -- deps; then
echo "deps=true" >> "$GITHUB_OUTPUT"
fi
echo "deps-key=linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}" >> "$GITHUB_OUTPUT"
# Parsing needs the dependency headers, not a build of this PR's deps/, so
# a PR that changes deps/ falls back to the newest cache main has.
# Parsing needs the dependency headers, not a build of this PR's deps/.
# Without an exact match the job waits for the build that saves it: this
# PR's own when it changes deps/, otherwise a running push build of the
# base branch with the same deps. With neither, a PR that keeps deps/
# falls back to the newest cache.
- name: Restore cached deps
id: deps_cache
if: steps.changes.outputs.cpp == 'true'
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
key: ${{ steps.changes.outputs.deps-key }}
- name: Look for a build to wait for
id: wait_check
if: steps.changes.outputs.cpp == 'true' && steps.deps_cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bash scripts/ci_deps_cache.sh ${{ steps.changes.outputs.deps == 'true' && 'own' || 'check' }}
# Linux Build Deps takes about 35 minutes on GitHub-hosted runners. The
# wait ends early when the deps job fails.
- name: Wait for the deps cache
if: steps.wait_check.outputs.wait-for != ''
timeout-minutes: 100
env:
GH_TOKEN: ${{ github.token }}
KEY: ${{ steps.changes.outputs.deps-key }}
CACHE_REF: ${{ steps.changes.outputs.deps == 'true' && github.ref || format('refs/heads/{0}', github.base_ref) }}
KEY_INPUTS: ${{ steps.wait_check.outputs.key-inputs }}
WAIT_FOR: ${{ steps.wait_check.outputs.wait-for }}
WAIT_SINCE: ${{ steps.wait_check.outputs.wait-since }}
WAIT_MINUTES: 90
run: bash scripts/ci_deps_cache.sh wait
- name: Restore the deps cache after waiting
id: waited_cache
if: steps.wait_check.outputs.wait-for != ''
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
- name: Fail without this PR's deps cache
if: steps.changes.outputs.deps == 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
run: |
echo "::error::No deps cache ${{ steps.changes.outputs.deps-key }}. Check this PR's Linux Build Deps job, then rerun clang-tidy."
exit 1
- name: Restore the newest cached deps
id: newest_cache
if: steps.changes.outputs.deps != 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
restore-keys: linux-clang-cache-orcaslicer_deps-build-
fail-on-cache-miss: true
- name: Warn about older deps
if: steps.newest_cache.outcome == 'success' && steps.newest_cache.outputs.cache-hit != 'true'
run: echo "::warning title=Older deps::No cache matches this PR's deps/, so clang-tidy parsed against ${{ steps.newest_cache.outputs.cache-matched-key }}. If it reports errors in code that uses a dependency, rerun clang-tidy once the base branch's Build all has saved the new deps."
- name: Apt-Install Dependencies
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "Bambulab",
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
"version": "02.08.00.14",
"version": "02.08.00.15",
"force_update": "0",
"description": "the initial version of BBL configurations",
"machine_model_list": [
File diff suppressed because one or more lines are too long
+43 -17
View File
@@ -1,21 +1,27 @@
#!/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.
# Lets a pull request's macOS deps build, or its clang-tidy job, wait for a
# build that saves the deps cache it needs, from a Linux job. `check` runs after
# a cache miss and names a queued or running push build of the base branch with
# the same deps/ and .github/ to wait for, unless the pull request changes either.
# `own` names the pull request's own build instead, for clang-tidy when the pull
# request changes deps/. `wait` waits until that build saves KEY under CACHE_REF,
# or its deps job fails. 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`.
# Environment: GH_TOKEN, REPO, WORKFLOW_REF (or WORKFLOW, the build workflow's
# file name) and BASE_REF, plus HEAD_SHA for `own`, and KEY_INPUTS, WAIT_FOR,
# WAIT_SINCE, WAIT_MINUTES, KEY, CACHE_REF (default the base branch) and ARCH
# (or JOB, the matrix job's name) for `wait`.
# Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT.
set -uo pipefail
workflow=${WORKFLOW_REF%%@*}
workflow=${workflow##*/}
workflow=${WORKFLOW_REF:-}
workflow=${workflow%%@*}
workflow=${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"
job=${JOB:-build_macos_arch (${ARCH:-})}
check_job_name="$job / Check Cache"
deps_job_name="$job / Build Deps / Build Deps"
out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; }
@@ -63,8 +69,27 @@ check() {
fi
}
own() {
out "key-inputs=$(local_key_inputs HEAD)"
local id
# The build can be created a moment after this job.
for _ in 1 2 3 4 5; do
id=$(gh api -X GET "repos/$REPO/actions/workflows/$workflow/runs" \
-f head_sha="$HEAD_SHA" -f event=pull_request -f per_page=1 --jq '.workflow_runs[0].id // empty' < /dev/null)
[ -n "$id" ] && break
sleep 30
done
if [ -n "$id" ]; then
echo "Waiting for the deps cache of build $id of this pull request."
out "wait-for=$id"
out "wait-since=$(date +%s)"
else
echo "No build of this pull request is building its deps."
fi
}
key_saved() {
gh cache list -R "$REPO" --ref "refs/heads/$BASE_REF" --key "$KEY" --json key < /dev/null \
gh cache list -R "$REPO" --ref "${CACHE_REF:-refs/heads/$BASE_REF}" --key "$KEY" --json key < /dev/null \
| jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null
}
@@ -87,11 +112,11 @@ wait_for() {
key_saved && { echo "Found $KEY."; return; }
sleep 20
done
echo "Build $id of $BASE_REF finished its deps without saving $KEY."
echo "Build $id finished its deps without saving $KEY."
return ;;
"completed cancelled") ;;
completed*)
echo "The deps job of build $id of $BASE_REF did not succeed."
echo "The deps job of build $id did not succeed."
return ;;
# The deps job exists only once the build's cache check has succeeded.
"")
@@ -100,7 +125,7 @@ wait_for() {
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."
echo "::warning title=Deps wait::Build $id 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 ;;
*)
@@ -118,11 +143,12 @@ wait_for() {
seen="$seen $id"
echo "Following build $id of $BASE_REF."
done
echo "No build of $BASE_REF saved $KEY."
echo "No build saved $KEY."
}
case "${1:-}" in
check) check ;;
own) own ;;
wait) wait_for ;;
*) echo "usage: $0 check|wait" >&2; exit 2 ;;
*) echo "usage: $0 check|own|wait" >&2; exit 2 ;;
esac
+9
View File
@@ -2991,6 +2991,15 @@ void PrintObject::clear_shared_object()
void PrintObject::copy_layers_from_shared_object()
{
if (m_shared_object) {
// Orca: Print::process() calls this once per slice for each sharing object, after clear_shared_object() has
// emptied any earlier alias, so the vectors hold only layers this object sliced itself before it started sharing
// (or nothing). Free them here, as clear_support_layers() / clear_layers() would; those skip deletion while
// m_shared_object is set. The belt-only data they also free is always empty here, because belt printers never
// share layers.
for (SupportLayer *layer : m_support_layers)
delete layer;
for (Layer *layer : m_layers)
delete layer;
m_layers.clear();
m_support_layers.clear();
+3
View File
@@ -982,6 +982,9 @@ void PrintObject::generate_support_material(bool with_belt_brim)
this->_generate_support_material();
m_print->throw_if_canceled();
}
// Orca: the tree support collision/avoidance caches and support nodes are only used while this step runs
// (detect_overhangs() rebuilds them from scratch), so don't keep them resident until the next slice.
this->clear_tree_support_preview_cache();
// Belt brim rides here rather than in the brim step because its apron
// prologue introduces print_z values below the object's first layer, and
// those must exist before ToolOrdering is built at psWipeTower - one step
+1 -1
View File
@@ -1532,7 +1532,7 @@ void PrintObject::slice_volumes()
if (min_growth < 0.f || elfoot > 0.f) {
// Apply the negative XY compensation. (the ones that is <0)
ExPolygons trimming;
static const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5);
const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5);
if (elfoot > 0.f) {
ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps);
lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate;
+13 -30
View File
@@ -1976,37 +1976,15 @@ coordf_t TreeSupport::get_radius(const SupportNode* node)
return node->radius;
}
ExPolygons TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
// Orca: drop_nodes() calls these several times per node per layer, so they return a reference into the
// TreeSupportData cache. The cache only ever inserts, so its entries never move while nodes drop in parallel.
const ExPolygons& TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
{
#if USE_SUPPORT_3D
if (m_model_volumes) {
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
const Polygons& avoid_polys = m_model_volumes->getAvoidance(radius, obj_layer_nr, TreeSupport3D::TreeModelVolumes::AvoidanceType::FastSafe, on_build_plate, true);
ExPolygons expolys;
for (auto& poly : avoid_polys)
expolys.emplace_back(std::move(poly));
return expolys;
}
return ExPolygons();
#else
return m_ts_data->get_avoidance(radius, obj_layer_nr);
#endif
}
ExPolygons TreeSupport::get_collision(coordf_t radius, size_t layer_nr)
const ExPolygons& TreeSupport::get_collision(coordf_t radius, size_t layer_nr)
{
#if USE_SUPPORT_3D
if (m_model_volumes) {
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
const Polygons& collision_polys = m_model_volumes->getCollision(radius, layer_nr, true);
ExPolygons expolys;
for (auto& poly : collision_polys)
expolys.emplace_back(std::move(poly));
return expolys;
}
#else
return m_ts_data->get_collision(radius, layer_nr);
#endif
return ExPolygons();
}
Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr)
{
@@ -2827,7 +2805,12 @@ void TreeSupport::draw_circles()
#endif // SUPPORT_TREE_DEBUG_TO_SVG
SupportLayerPtrs& ts_layers = m_object->support_layers();
auto iter = std::remove_if(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return ts_layer->height < EPSILON; });
// Orca: the vector owns its layers, so the dropped ones are deleted before they are erased.
// std::stable_partition (unlike std::remove_if) leaves exactly the dropped layers in the tail.
// Only heights below EPSILON are dropped; written as !(height < EPSILON), a NaN height is kept.
auto iter = std::stable_partition(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return !(ts_layer->height < EPSILON); });
for (auto it = iter; it != ts_layers.end(); ++it)
delete *it;
ts_layers.erase(iter, ts_layers.end());
for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) {
ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr;
@@ -3071,7 +3054,7 @@ void TreeSupport::drop_nodes()
//Insert a completely new node and let both original nodes fade.
Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes.
coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next);
auto avoid_layer = get_avoidance(next_radius, obj_layer_nr_next);
const ExPolygons& avoid_layer = get_avoidance(next_radius, obj_layer_nr_next);
if (group_index == 0)
{
//Avoid collisions.
@@ -3275,7 +3258,7 @@ void TreeSupport::drop_nodes()
}
#endif
coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next);
auto avoidance_next = get_avoidance(next_radius, obj_layer_nr_next);
const ExPolygons& avoidance_next = get_avoidance(next_radius, obj_layer_nr_next);
Point to_outside = projection_onto(avoidance_next, node.position);
Point direction_to_outer = to_outside - node.position;
@@ -3337,7 +3320,7 @@ void TreeSupport::drop_nodes()
p_node->to_buildplate = false;
return; // from parallel_for_each lambda
}
auto next_collision = get_collision(0, obj_layer_nr_next);
const ExPolygons& next_collision = get_collision(0, obj_layer_nr_next);
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
to_outside = projection_onto(next_collision, next_layer_vertex);
+2 -2
View File
@@ -536,9 +536,9 @@ private:
coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true);
coordf_t calc_radius(coordf_t mm_to_top);
coordf_t get_radius(const SupportNode* node);
ExPolygons get_avoidance(coordf_t radius, size_t obj_layer_nr);
const ExPolygons& get_avoidance(coordf_t radius, size_t obj_layer_nr);
// layer's expolygon expanded by radius+m_xy_distance
ExPolygons get_collision(coordf_t radius, size_t layer_nr);
const ExPolygons& get_collision(coordf_t radius, size_t layer_nr);
// get Polygons instead of ExPolygons
Polygons get_collision_polys(coordf_t radius, size_t layer_nr);
+6 -4
View File
@@ -21,7 +21,9 @@ namespace libvgcode {
// the triangles that share it: the shader runs 8 times per segment instead of 24.
static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
static constexpr const std::array<uint8_t, 24> INDEX_DATA = {
// 16-bit indices: GCN 1/2 GPUs have no 8-bit index support, and some of their drivers
// emulate it wrongly in instanced draws.
static constexpr const std::array<uint16_t, 24> INDEX_DATA = {
0, 1, 2, // front spike
0, 2, 3, // front spike
0, 3, 4, // right/bottom body
@@ -37,7 +39,7 @@ void SegmentTemplate::init()
if (m_vao_id != 0)
return;
m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t);
m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t) + INDEX_DATA.size() * sizeof(uint16_t);
int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
@@ -61,7 +63,7 @@ void SegmentTemplate::init()
// and restored together with the vao.
glsafe(glGenBuffers(1, &m_ibo_id));
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id));
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW));
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint16_t), INDEX_DATA.data(), GL_STATIC_DRAW));
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
glsafe(glBindVertexArray(curr_vertex_array));
@@ -94,7 +96,7 @@ void SegmentTemplate::render(size_t count)
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
glsafe(glBindVertexArray(m_vao_id));
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE,
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_SHORT,
nullptr, static_cast<GLsizei>(count)));
glsafe(glBindVertexArray(curr_vertex_array));
}
@@ -1036,6 +1036,11 @@ bool GLGizmoPainterBase::on_mouse(const wxMouseEvent &mouse_event)
Vec2d mouse_pos = mouse_coord.cast<double>();
if (mouse_event.Moving()) {
// No button is down, yet a stroke is still open: its release went elsewhere, as a paint drag does not
// capture the mouse. End the stroke as that release would have.
if (m_button_down != Button::None)
gizmo_event(m_button_down == Button::Left ? SLAGizmoEventType::LeftUp : SLAGizmoEventType::RightUp, mouse_pos,
mouse_event.ShiftDown(), mouse_event.AltDown(), false);
gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false);
return false;
}
@@ -1205,6 +1210,10 @@ void GLGizmoPainterBase::on_set_state()
m_old_mo_id = -1;
//m_iva.release_geometry();
m_triangle_selectors.clear();
// A stroke still open goes with the selectors. Its state must not carry into the next session, where the
// first move would end it as if released and the first press would paint a line from its last position.
m_button_down = Button::None;
m_last_mouse_click = Vec2d::Zero();
//Camera& camera = wxGetApp().plater()->get_camera();
//camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ());
@@ -562,6 +562,11 @@ void GLGizmoTextureDisplacement::on_shutdown()
m_shaded_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
m_island_drag_active = false;
m_island_move_set.clear();
// The pane closes below, taking the highlight with it, and the view must not open the next session in a
// Fast the user never picked. Set directly: nothing is to be queued against a gizmo that is closing.
if (m_island_view.switched)
m_use_shaded_preview = false;
m_island_view = IslandViewFollow{};
m_adjust_texture_mode = false;
m_seam_edit_mode = false;
m_adjust_drag_handle = AdjustHandle::None;
@@ -727,18 +732,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane
// changes its selection in its own mouse handling, and a compare of a few ints per frame is free.
{
const TextureDisplacementLayer *al = active_layer();
const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas();
if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM &&
uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) {
if (uv_canvas->selected_islands() != m_island_overlay_selection)
rebuild_island_overlay(uv_canvas->selected_islands());
render_island_overlay();
} else if (m_island_overlay_glmodel.is_initialized()) {
m_island_overlay_glmodel.reset();
m_island_overlay_selection.clear();
}
if (const std::vector<int> *islands = island_highlight_selection()) {
if (*islands != m_island_overlay_selection)
rebuild_island_overlay(*islands);
render_island_overlay();
} else if (m_island_overlay_glmodel.is_initialized()) {
m_island_overlay_glmodel.reset();
m_island_overlay_selection.clear();
}
// Diagnostic overlays, drawn on top of whatever preview is active (both pull toward the camera
@@ -1520,7 +1520,7 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
// is box-filtered down to 128 px for a ~48 px row, and feeding it to the shader cost the preview
// three quarters of the height map's detail - and, since height_tex_texel is derived from it, also
// flattened the shading gradient and made the parallax march skip itself at angles where it should
// run. Cached on the image_data pointer + smoothing, so no PNG is decoded per frame.
// run. Cached per image_data and smoothing, so no PNG is decoded per frame.
GLTexture *tex = get_layer_height_texture(*layer);
if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0)
return;
@@ -1926,6 +1926,16 @@ void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay)
shader->stop_using();
}
const std::vector<int> *GLGizmoTextureDisplacement::island_highlight_selection()
{
const TextureDisplacementLayer *al = active_layer();
const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas();
if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM &&
uv_canvas != nullptr && !m_uv_editor_unwrap.empty())
return &uv_canvas->selected_islands();
return nullptr;
}
void GLGizmoTextureDisplacement::rebuild_island_overlay(const std::vector<int> &selection)
{
m_island_overlay_glmodel.reset();
@@ -2651,6 +2661,37 @@ void GLGizmoTextureDisplacement::apply_view_mode(int mode)
m_parent.set_as_dirty();
}
int GLGizmoTextureDisplacement::view_mode() const
{
return m_use_shaded_preview ? 1 :
m_uv_check_mode == UVCheckMode::Checker ? 2 :
m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0;
}
int GLGizmoTextureDisplacement::IslandViewFollow::step(bool shown_now, int view, bool debug_open)
{
shown = shown_now;
if (!shown) {
// The view is back to Normal already if a debug stage took it, and Normal applied over a stage would
// queue a preview job that replaces the stage being inspected.
const bool restore = switched && view == 1;
switched = keep_normal = false;
return restore ? 0 : -1;
}
if (view != 0 || keep_normal || debug_open)
return -1;
switched = true;
return 1;
}
void GLGizmoTextureDisplacement::follow_island_highlight()
{
const std::vector<int> *islands = island_highlight_selection();
const int view = m_island_view.step(islands != nullptr && !islands->empty(), view_mode(), m_debug_stage >= 0);
if (view >= 0)
apply_view_mode(view);
}
void GLGizmoTextureDisplacement::push_uv_pane_state()
{
UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas();
@@ -2891,7 +2932,8 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value)
rebuild_preview();
}
}
// FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet.
// FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet. SelectionChanged
// only asks for this frame: follow_island_highlight() and the overlay read the selection back themselves.
m_parent.set_as_dirty();
}
@@ -4038,8 +4080,8 @@ void GLGizmoTextureDisplacement::remove_texture_layer(int slot)
if (slot >= 0 && slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) {
mv->texture_displacement_facet(slot).reset();
m_thumbnails[size_t(slot)].reset();
m_thumbnail_source[size_t(slot)] = nullptr;
m_layer_expanded[size_t(slot)] = false;
m_thumbnail_source[size_t(slot)].reset();
m_layer_expanded[size_t(slot)] = false;
}
if (m_picker_slot == slot)
m_picker_slot = -1;
@@ -4213,7 +4255,7 @@ int GLGizmoTextureDisplacement::select_visible_faces(const std::array<float, 12>
selector.reset();
for (unsigned i : unobscured)
selector.set_facet(int(front_facing[i]), EnforcerBlockerType::ENFORCER);
selector.request_update_render_data();
selector.request_update_render_data(true);
update_model_object();
m_parent.set_as_dirty();
@@ -4236,7 +4278,7 @@ void GLGizmoTextureDisplacement::select_whole_model()
const size_t facet_count = v->mesh().its.indices.size();
for (size_t i = 0; i < facet_count; ++i)
m_triangle_selectors[idx]->set_facet(int(i), EnforcerBlockerType::ENFORCER);
m_triangle_selectors[idx]->request_update_render_data();
m_triangle_selectors[idx]->request_update_render_data(true);
}
update_model_object();
m_parent.set_as_dirty();
@@ -5342,7 +5384,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
return nullptr;
const size_t slot = size_t(layer.slot);
if (m_thumbnails[slot] && m_thumbnail_source[slot] == layer.image_data.get() &&
if (m_thumbnails[slot] && m_thumbnail_source[slot].lock() == layer.image_data &&
m_thumbnail_smoothing[slot] == layer.smoothing)
return m_thumbnails[slot].get();
@@ -5355,7 +5397,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
return nullptr;
m_thumbnails[slot] = std::move(texture);
m_thumbnail_source[slot] = layer.image_data.get();
m_thumbnail_source[slot] = layer.image_data;
m_thumbnail_smoothing[slot] = layer.smoothing;
return m_thumbnails[slot].get();
}
@@ -5364,17 +5406,17 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp
{
if (layer.empty() || !layer.color_enabled)
return nullptr;
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing)
if (m_color_tex && m_color_tex_source.lock() == layer.image_data && m_color_tex_smoothing == layer.smoothing)
return m_color_tex.get();
std::unique_ptr<GLTexture> texture = upload_color_texture(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
if (!texture) {
m_color_tex.reset();
m_color_tex_source = nullptr;
m_color_tex_source.reset();
return nullptr;
}
m_color_tex = std::move(texture);
m_color_tex_source = layer.image_data.get();
m_color_tex_source = layer.image_data;
m_color_tex_smoothing = layer.smoothing;
return m_color_tex.get();
}
@@ -5386,7 +5428,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis
// One slot, not one per layer: the preview shader only ever shades the *active* layer, so a single
// full-resolution upload is enough and the VRAM cost stays at one texture rather than eight.
if (m_height_tex && m_height_tex_source == layer.image_data.get() && m_height_tex_smoothing == layer.smoothing)
if (m_height_tex && m_height_tex_source.lock() == layer.image_data && m_height_tex_smoothing == layer.smoothing)
return m_height_tex.get();
std::unique_ptr<GLTexture> texture = upload_height_thumbnail(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
@@ -5394,7 +5436,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis
return nullptr;
m_height_tex = std::move(texture);
m_height_tex_source = layer.image_data.get();
m_height_tex_source = layer.image_data;
m_height_tex_smoothing = layer.smoothing;
return m_height_tex.get();
}
@@ -5835,6 +5877,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
GizmoImguiBegin(get_name(), flags);
ensure_panel_icons();
process_uv_commands(); // clicks from the UV editor pane, run here where the GL context is current
// After the pane's commands, which can close it or change the view, and before the View row, so the row
// lights the view this frame ends up in and a click on it has the last word.
follow_island_highlight();
const float previous_body_h = m_panel_body_h;
const float previous_footer_h = m_panel_footer_h;
@@ -6281,7 +6326,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
if (v->is_model_part()) {
++idx;
m_triangle_selectors[idx]->reset();
m_triangle_selectors[idx]->request_update_render_data();
m_triangle_selectors[idx]->request_update_render_data(true);
}
update_model_object();
m_parent.set_as_dirty();
@@ -6323,10 +6368,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// ---- View: Normal / Fast / Checker / Distortion as one group, Wireframe on its own ----
// The underlying state stays m_use_shaded_preview + m_uv_check_mode.
{
const int cur_mode = m_use_shaded_preview ? 1 :
m_uv_check_mode == UVCheckMode::Checker ? 2 :
m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0;
const int cur_mode = view_mode();
int new_mode = cur_mode;
int picked = -1; // the button clicked, the active one included
bool wf_toggle = false;
bool open_uv_editor = false;
// Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second
@@ -6349,19 +6393,19 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f)));
if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"),
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update")))
new_mode = 0;
new_mode = picked = 0;
ImGui::SameLine(0.f, gap_s);
if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"),
_L("Fast - the relief is only shaded on, not built, and only for the layer you are "
"editing. Updates instantly while you paint")))
new_mode = 1;
new_mode = picked = 1;
ImGui::SameLine(0.f, gap_s);
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
"it is closed"),
uv_view_na)) {
new_mode = 2;
new_mode = picked = 2;
open_uv_editor = true;
}
ImGui::SameLine(0.f, gap_s);
@@ -6369,7 +6413,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
"it is closed"),
uv_view_na)) {
new_mode = 3;
new_mode = picked = 3;
open_uv_editor = true;
}
vsep(icon_md);
@@ -6393,6 +6437,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
if (new_mode == cur_mode)
update_uv_editor(); // otherwise apply_view_mode() below does it
}
if (picked >= 0)
m_island_view.picked(picked);
if (new_mode != cur_mode)
apply_view_mode(new_mode);
if (wf_toggle) {
@@ -197,6 +197,27 @@ public:
};
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
// The view while the UV editor's selected islands are highlighted on the model. The highlight lies on the
// undisplaced surface, so the Normal view's relief hides it wherever it rises: Normal gives way to Fast
// while islands are selected and comes back once none are. A view the user clicks in the View row is
// theirs - the pane's background buttons are not a pick, Height only meaning "the texture view".
struct IslandViewFollow
{
bool shown = false; // the highlight, as of the last step()
bool switched = false; // Fast was switched on for the highlight, so Normal comes back once it is gone
bool keep_normal = false; // the user picked Normal in the View row while the highlight was shown
// The view to apply this frame (0 Normal, 1 Fast), or -1 for none. `view` is the one on screen, numbered as
// apply_view_mode() takes it; `debug_open` a debug stage on screen, which holds Normal itself.
int step(bool shown_now, int view, bool debug_open);
// A View row button clicked, the active one included. The row is drawn after step() in the same frame,
// so `shown` is this frame's.
void picked(int view)
{
switched = false;
keep_normal = shown && view == 0;
}
};
void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
@@ -513,6 +534,11 @@ private:
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
// background buttons, so both switch views the same way.
void apply_view_mode(int mode);
// The view on screen, numbered as apply_view_mode() takes it.
int view_mode() const;
// Run once a frame, after the pane's commands: switches the view as m_island_view says.
void follow_island_highlight();
IslandViewFollow m_island_view;
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
static constexpr int UV_THUMB_PX = 64;
const void *m_uv_thumb_source = nullptr;
@@ -744,6 +770,8 @@ private:
// in place. Rebuilt whenever the pane's selection differs from the one it was built for.
GLModel m_island_overlay_glmodel;
std::vector<int> m_island_overlay_selection;
// The pane's selection while the overlay applies - the pane open over an unwrapped LSCM layer - else null.
const std::vector<int> *island_highlight_selection();
void rebuild_island_overlay(const std::vector<int> &selection);
void render_island_overlay();
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
@@ -924,25 +952,26 @@ private:
bool m_preview_job_pending = false;
void queue_preview_job();
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
// was current the last time each thumbnail was built (see get_layer_thumbnail()).
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
// Per-slot GPU thumbnail cache for the layer list panel, each keyed by the image_data it was built from
// (see get_layer_thumbnail()). Held weakly rather than by address, as decode_height_texture()'s cache is:
// an image freed and replaced by another at the same address must not pass for it.
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<std::weak_ptr<const std::vector<unsigned char>>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source;
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
// Full-resolution height texture for the preview shader, keyed the same way (see
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
// blur the bake will apply.
std::unique_ptr<GLTexture> m_height_tex;
const void *m_height_tex_source = nullptr;
float m_height_tex_smoothing = -1.f;
std::unique_ptr<GLTexture> m_height_tex;
std::weak_ptr<const std::vector<unsigned char>> m_height_tex_source;
float m_height_tex_smoothing = -1.f;
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
std::unique_ptr<GLTexture> m_color_tex;
const void *m_color_tex_source = nullptr;
float m_color_tex_smoothing = -1.f;
std::unique_ptr<GLTexture> m_color_tex;
std::weak_ptr<const std::vector<unsigned char>> m_color_tex_source;
float m_color_tex_smoothing = -1.f;
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures;
+6 -1
View File
@@ -939,7 +939,8 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair<int, int>{ -1, -1 };
m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan;
} else {
const int hit = island_at(uv);
const std::vector<int> selection_before = m_selection;
const int hit = island_at(uv);
if (hit >= 0) {
if (evt.ShiftDown()) {
// Shift adds to the selection (and makes the clicked one the new primary).
@@ -969,6 +970,10 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_selected_island = -1;
}
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
// The owner shows the selection on the model and would not hear of this click otherwise in time: a
// click that clears the selection starts no gesture, and one that sets it reports only on release.
if (m_selection != selection_before)
run_command(Command::SelectionChanged);
}
grab_mouse();
Refresh();
+3 -1
View File
@@ -108,10 +108,12 @@ public:
// the command callback - the canvas has the selection and the view, the gizmo has the layer.
// `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into
// SelectMode / Background) and is 0 otherwise.
// SelectionChanged is sent by the canvas itself when a click changes the island selection. It carries
// nothing: the owner reads selected_islands() back, and only needs telling that it should look.
enum class Command {
FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected,
Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits,
SetBackground, PickTexture, PaneClosed
SetBackground, PickTexture, PaneClosed, SelectionChanged
};
void run_command(Command cmd, float value = 0.f);
using CommandFn = std::function<void(Command, float)>;
+76
View File
@@ -43,6 +43,7 @@
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Exception.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/Utils.hpp"
#include "test_helpers.hpp"
#include "test_utils.hpp"
@@ -156,6 +157,81 @@ SCENARIO("Changing the number of solid shell layers does not make all surfaces i
}
}
// Two copies of one object share a single slice. When a copy that has sliced itself becomes identical to an
// earlier object, it keeps its own layers while that object slices and only then takes the shared layers over.
TEST_CASE("An object that starts sharing another object's slices keeps its own layers until it takes them over", "[Print][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "raft_layers", 1 }, // each object gets support layers of its own
{ "sparse_infill_density", "20%" }, // object_a's override below differs from this
});
// Activates the slicing-pipeline hook installed for the second slice below.
config.set_key_value("slicing_pipeline_plugin", new ConfigOptionStrings({"probe"}));
Print print;
Model model;
init_print({cube(20)}, print, model, config);
ModelObject *object_a = model.objects.front();
// add_object() gives the copy extruder 1, and objects only share slices when their configs match.
object_a->config.set_key_value("extruder", new ConfigOptionInt(1));
ModelObject *object_b = model.add_object(*object_a); // shares object_a's mesh
for (ModelInstance *instance : object_b->instances)
instance->set_offset(instance->get_offset() + Vec3d(40., 0., 0.));
// A per-object override makes the objects differ, so each slices itself.
object_a->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(40));
print.apply(model, config);
print.process();
REQUIRE(print.objects().size() == 2);
// The non-const accessors hand out the layer vectors themselves, which the checks below compare.
PrintObject *print_b = print.get_object(1);
REQUIRE(print_b->model_object()->id() == object_b->id()); // Print slices its own copy of the model
REQUIRE(print_b->get_shared_object() == nullptr);
const LayerPtrs own_layers = print_b->layers();
const SupportLayerPtrs own_support_layers = print_b->support_layers();
REQUIRE(!own_layers.empty());
REQUIRE(!own_support_layers.empty());
// Without the override the objects are identical again: print_a redoes the steps the setting invalidated,
// and print_b shares it.
object_a->config.erase("sparse_infill_density");
print.apply(model, config);
REQUIRE(print.objects().size() == 2);
PrintObject *print_a = print.get_object(0);
// Erasing the override invalidates print_a's posPrepareInfill and the steps after it. Its hook is the first one
// print_a fires, and it runs after print_b starts sharing but before print_b takes the shared layers over.
bool hook_fired = false;
bool shared_while_a_runs = false;
bool kept_layers = false;
bool kept_support_layers = false;
{
Print::set_slicing_pipeline_hook_fn([&](Print &, const PrintObject *object, SlicingPipelineStepPlugin) {
if (object != print_a || hook_fired)
return;
hook_fired = true;
shared_while_a_runs = print_b->get_shared_object() == print_a;
kept_layers = print_b->layers() == own_layers;
kept_support_layers = print_b->support_layers() == own_support_layers;
});
const ScopeGuard reset_hook([] { Print::set_slicing_pipeline_hook_fn(nullptr); });
print.process();
}
REQUIRE(hook_fired);
CHECK(print.objects()[1] == print_b);
CHECK(shared_while_a_runs);
CHECK(kept_layers);
CHECK(kept_support_layers);
CHECK(print_b->get_shared_object() == print_a);
REQUIRE(!print_a->layers().empty());
CHECK(print_b->layers() == print_a->layers());
CHECK(print_b->support_layers() == print_a->support_layers());
}
// ---------------------------------------------------------------------------
// Print::validate() warning collection
//
+1
View File
@@ -37,6 +37,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_json_depth.cpp
test_shortcuts.cpp
test_texture_color_palette.cpp
test_texture_island_view.cpp
test_file_url.cpp
test_user_manager.cpp
../fff_print/test_helpers.cpp
@@ -0,0 +1,99 @@
// How the texture displacement gizmo's view follows the UV editor's island highlight. The rules live in
// libslic3r_gui; this is the suite that links it. Same Windows include prologue as
// test_texture_color_palette.cpp (wx pulls in <windows.h>; keep WIN32_LEAN_AND_MEAN / NOMINMAX ahead of it).
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
using Follow = Slic3r::GUI::GLGizmoTextureDisplacement::IslandViewFollow;
namespace {
// The View row's indices, and step()'s "apply nothing".
constexpr int NORMAL = 0, FAST = 1, CHECKER = 2, DISTORTION = 3, NONE = -1;
} // namespace
TEST_CASE("a Normal view gives way to Fast while islands are selected and comes back once none are", "[TextureIslandView][TextureDisplacement]")
{
Follow f;
CHECK(f.step(false, NORMAL, false) == NONE);
CHECK(f.step(true, NORMAL, false) == FAST);
CHECK(f.step(true, FAST, false) == NONE);
CHECK(f.step(false, FAST, false) == NORMAL);
CHECK(f.step(false, NORMAL, false) == NONE);
}
TEST_CASE("a Fast, Checker or Distortion view is left alone by the island highlight", "[TextureIslandView][TextureDisplacement]")
{
const int view = GENERATE(FAST, CHECKER, DISTORTION);
Follow f;
CHECK(f.step(true, view, false) == NONE);
CHECK(f.step(false, view, false) == NONE);
}
TEST_CASE("arriving in Normal with islands already selected switches to Fast", "[TextureIslandView][TextureDisplacement]")
{
// The pane's Height button over Checker lands in Normal with the islands still selected.
Follow f;
CHECK(f.step(true, CHECKER, false) == NONE);
CHECK(f.step(true, NORMAL, false) == FAST);
CHECK(f.step(false, FAST, false) == NORMAL);
}
TEST_CASE("Normal picked while islands are selected stays until they are deselected", "[TextureIslandView][TextureDisplacement]")
{
Follow f;
REQUIRE(f.step(true, NORMAL, false) == FAST);
f.picked(NORMAL);
CHECK(f.step(true, NORMAL, false) == NONE);
// A debug stage opening and closing over it changes nothing.
CHECK(f.step(true, NORMAL, true) == NONE);
CHECK(f.step(true, NORMAL, false) == NONE);
CHECK(f.step(false, NORMAL, false) == NONE);
CHECK(f.step(true, NORMAL, false) == FAST);
}
TEST_CASE("Fast picked while it was switched on stays after deselecting", "[TextureIslandView][TextureDisplacement]")
{
Follow f;
REQUIRE(f.step(true, NORMAL, false) == FAST);
f.picked(FAST);
CHECK(f.step(false, FAST, false) == NONE);
}
TEST_CASE("Normal picked with nothing selected does not hold off the next switch", "[TextureIslandView][TextureDisplacement]")
{
Follow f;
CHECK(f.step(false, NORMAL, false) == NONE);
f.picked(NORMAL);
CHECK(f.step(true, NORMAL, false) == FAST);
}
TEST_CASE("a debug stage holds Normal while it is open, and the switch follows once it closes", "[TextureIslandView][TextureDisplacement]")
{
Follow f;
REQUIRE(f.step(true, NORMAL, false) == FAST);
// The stage takes Normal itself, with the islands still selected.
CHECK(f.step(true, NORMAL, true) == NONE);
CHECK(f.step(true, NORMAL, false) == FAST);
}
TEST_CASE("islands deselected while a debug stage is open apply no view over it", "[TextureIslandView][TextureDisplacement]")
{
Follow f;
REQUIRE(f.step(true, NORMAL, false) == FAST);
CHECK(f.step(false, NORMAL, true) == NONE);
CHECK(f.step(false, NORMAL, false) == NONE);
}