mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ca182a98e | ||
|
|
5e19fc0e6c | ||
|
|
3f4709aaa7 | ||
|
|
c63bd5d474 | ||
|
|
6263502697 | ||
|
|
1a9201690d | ||
|
|
c6a338f35f | ||
|
|
4f2d2ae11a | ||
|
|
c57b5dc6c5 | ||
|
|
7d4ec3494c | ||
|
|
391c53a8da |
@@ -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 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2.5,2.5h11v11h-11z" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M8,2.5v11M2.5,8h11" style="fill:none;stroke:#949494;stroke-dasharray:1.5,1.5"/><path d="M4.5,11.5l3.5-6l3.5,6z" style="fill:#009688"/></svg>
|
||||
|
Before Width: | Height: | Size: 340 B |
@@ -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": [
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+43
-17
@@ -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
|
||||
|
||||
@@ -353,24 +353,6 @@ Model Model::read_from_file(const std::string&
|
||||
ObjInfo obj_info;
|
||||
ObjParser::MtlData mtl_data;
|
||||
result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data);
|
||||
// Keep the model's own UV channel on the volume, whatever the colour import then does with
|
||||
// the mesh: it is what TextureProjectionMethod::ModelUV maps a texture-displacement layer
|
||||
// through, so a model that arrives with a UV layout is textured on that layout instead of on
|
||||
// a fresh unwrap. load_obj() fills obj_info.uvs per face of the mesh it built, in the same
|
||||
// order (and only when every face named a known material, hence the size check). V is flipped
|
||||
// on the way in, to the convention DecodedHeightTexture samples in - the same flip
|
||||
// obj_to_textured_mesh() does for the colour importer.
|
||||
if (result && !obj_info.uvs.empty() && !model.objects.empty() && !model.objects.back()->volumes.empty()) {
|
||||
ModelVolume &volume = *model.objects.back()->volumes[0];
|
||||
const indexed_triangle_set &its = volume.mesh().its;
|
||||
if (obj_info.uvs.size() == its.indices.size()) {
|
||||
volume.texture_uv.resize(its.indices.size() * 3);
|
||||
for (size_t fi = 0; fi < obj_info.uvs.size(); ++fi)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
volume.texture_uv[fi * 3 + size_t(k)] =
|
||||
Vec2f(obj_info.uvs[fi][size_t(k)].x(), 1.f - obj_info.uvs[fi][size_t(k)].y());
|
||||
}
|
||||
}
|
||||
if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) {
|
||||
// Textured OBJ: hand the mesh + materials to the texture-to-color importer
|
||||
// instead of the flat per-face colour dialog.
|
||||
@@ -2151,10 +2133,6 @@ void ModelVolume::reset_extra_facets()
|
||||
// than left referring to a mesh that no longer matches it.
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
this->texture_displacement_facet(i).reset();
|
||||
// Same reasoning for the model's own UV channel: it is indexed by triangle, so against a mesh
|
||||
// this no longer matches it would map the texture by another mesh's triangle numbering. A caller
|
||||
// that can carry it across - see transfer_uv_channel() - puts it back after this.
|
||||
this->reset_texture_uv();
|
||||
}
|
||||
|
||||
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const
|
||||
|
||||
+2
-23
@@ -974,26 +974,6 @@ public:
|
||||
// they are not a property of any single layer.
|
||||
TextureDisplacementOptions texture_displacement_options;
|
||||
|
||||
// The model's own texture coordinates, as the file it was imported from carried them: three per
|
||||
// triangle of mesh().its, in its triangle order (corner k of triangle i at 3i+k), V already
|
||||
// flipped to the sampling convention (V = 0 is the image's top row). Empty when the source had
|
||||
// none, which is every STL and every 3mf - only a textured OBJ brings one in today.
|
||||
//
|
||||
// Per corner rather than per vertex because that is what a UV layout is: the two sides of a seam
|
||||
// are the same vertex with two different coordinates, and collapsing them would smear the texture
|
||||
// across every island boundary. TextureProjectionMethod::ModelUV maps through it, which is what
|
||||
// lets a model that already carries a UV channel be textured on the artist's own layout instead
|
||||
// of on a fresh unwrap.
|
||||
//
|
||||
// Indexed by triangle, so anything that *replaces* the mesh has to bring it across (or drop it):
|
||||
// see transfer_uv_channel(), and reset_texture_uv() for the cases that cannot.
|
||||
std::vector<Vec2f> texture_uv;
|
||||
|
||||
bool has_texture_uv() const { return !texture_uv.empty() && texture_uv.size() == mesh().its.indices.size() * 3; }
|
||||
// Forgets the channel. For a mesh change that nothing can carry it across - a split, a boolean -
|
||||
// where leaving it would map the texture by another mesh's triangle numbering.
|
||||
void reset_texture_uv() { texture_uv.clear(); }
|
||||
|
||||
// Save painting data before reset_extra_facets() discards it.
|
||||
// Used for replacing mesh without losing painting data.
|
||||
// Only for model parts (not modifiers/connectors).
|
||||
@@ -1256,7 +1236,6 @@ private:
|
||||
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
|
||||
texture_displacement_layers(other.texture_displacement_layers),
|
||||
texture_displacement_options(other.texture_displacement_options),
|
||||
texture_uv(other.texture_uv),
|
||||
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
|
||||
{
|
||||
assert(this->id().valid());
|
||||
@@ -1355,7 +1334,7 @@ private:
|
||||
cereal::load_by_value(ar, f);
|
||||
mesh_changed |= tf != f.timestamp();
|
||||
}
|
||||
ar(texture_displacement_layers, texture_displacement_options, texture_uv);
|
||||
ar(texture_displacement_layers, texture_displacement_options);
|
||||
cereal::load_by_value(ar, config);
|
||||
cereal::load(ar, text_configuration);
|
||||
cereal::load(ar, emboss_shape);
|
||||
@@ -1379,7 +1358,7 @@ private:
|
||||
cereal::save_by_value(ar, fuzzy_skin_facets);
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
cereal::save_by_value(ar, texture_displacement_facet(i));
|
||||
ar(texture_displacement_layers, texture_displacement_options, texture_uv);
|
||||
ar(texture_displacement_layers, texture_displacement_options);
|
||||
cereal::save_by_value(ar, config);
|
||||
cereal::save(ar, text_configuration);
|
||||
cereal::save(ar, emboss_shape);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1571,61 +1571,6 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
return islands;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
|
||||
const TriangleSelector::TriangleSplittingData &src_paint,
|
||||
const indexed_triangle_set &dst_mesh)
|
||||
{
|
||||
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_paint.bitstream.empty())
|
||||
return {};
|
||||
|
||||
const TriangleMesh src(src_mesh);
|
||||
TriangleSelector selector(src);
|
||||
selector.deserialize(src_paint, false);
|
||||
|
||||
// Every sub-triangle of the source, each carrying its own state - NONE included, so the soup
|
||||
// covers the whole surface and a foot point always lands on something. One soup rather than one
|
||||
// tree per state: a foot point has one nearest triangle, and that triangle's state is the answer.
|
||||
std::vector<indexed_triangle_set> per_state;
|
||||
selector.get_facets(per_state);
|
||||
indexed_triangle_set soup;
|
||||
std::vector<uint8_t> label;
|
||||
for (size_t st = 0; st < per_state.size(); ++st) {
|
||||
const indexed_triangle_set &part = per_state[st];
|
||||
if (part.indices.empty())
|
||||
continue;
|
||||
const int base = int(soup.vertices.size());
|
||||
soup.vertices.insert(soup.vertices.end(), part.vertices.begin(), part.vertices.end());
|
||||
for (const stl_triangle_vertex_indices &t : part.indices) {
|
||||
soup.indices.emplace_back(t[0] + base, t[1] + base, t[2] + base);
|
||||
label.push_back(uint8_t(st));
|
||||
}
|
||||
}
|
||||
if (soup.indices.empty())
|
||||
return {};
|
||||
|
||||
const AABBTreeIndirect::Tree3f tree =
|
||||
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(soup.vertices, soup.indices);
|
||||
|
||||
std::vector<uint8_t> out(dst_mesh.indices.size(), 0);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t j = range.begin(); j < range.end(); ++j) {
|
||||
const stl_triangle_vertex_indices &t = dst_mesh.indices[j];
|
||||
// The centroid, not a corner: a facet takes one state, and its middle is the point that
|
||||
// is inside it whatever the triangle looks like.
|
||||
const Vec3f centroid = (dst_mesh.vertices[size_t(t[0])] + dst_mesh.vertices[size_t(t[1])] +
|
||||
dst_mesh.vertices[size_t(t[2])]) / 3.f;
|
||||
size_t hit = 0;
|
||||
Vec3f foot;
|
||||
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(soup.vertices, soup.indices,
|
||||
tree, centroid, hit, foot);
|
||||
if (d2 >= 0.f && hit < label.size())
|
||||
out[j] = label[hit];
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
void average_island_scales(std::vector<TextureIsland> &islands)
|
||||
{
|
||||
if (islands.empty())
|
||||
@@ -1728,110 +1673,6 @@ std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, co
|
||||
return corner;
|
||||
}
|
||||
|
||||
// Barycentric weights of `p` inside triangle (a, b, c), as the pair that belongs with the edges
|
||||
// (b - a) and (c - a): p ~= (1 - w1 - w2) * a + w1 * b + w2 * c. `p` is projected onto the triangle's
|
||||
// plane, so a point a hair off the surface still reports the weights of its foot point. A degenerate
|
||||
// triangle reports its centroid, the only coordinate that means anything there.
|
||||
static void barycentric_weights(const Vec3f &a, const Vec3f &b, const Vec3f &c, const Vec3f &p, float &w1, float &w2)
|
||||
{
|
||||
const Vec3f e0 = b - a, e1 = c - a, ep = p - a;
|
||||
const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
|
||||
const float den = d00 * d11 - d01 * d01;
|
||||
if (std::abs(den) <= 1e-20f) {
|
||||
w1 = w2 = 1.f / 3.f;
|
||||
return;
|
||||
}
|
||||
w1 = (d11 * dp0 - d01 * dp1) / den;
|
||||
w2 = (d00 * dp1 - d01 * dp0) / den;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<Vec2f> &model_uv,
|
||||
const indexed_triangle_set &patch,
|
||||
const std::vector<int> &patch_source)
|
||||
{
|
||||
if (patch.indices.empty() || model_uv.size() != base_mesh.indices.size() * 3 ||
|
||||
patch_source.size() != patch.indices.size())
|
||||
return {};
|
||||
|
||||
std::vector<Vec2f> corner(patch.indices.size() * 3, Vec2f::Zero());
|
||||
for (size_t j = 0; j < patch.indices.size(); ++j) {
|
||||
const int S = patch_source[j];
|
||||
if (S < 0 || size_t(S) >= base_mesh.indices.size())
|
||||
continue; // no source triangle: leave (0, 0), which the callers read as "no placement"
|
||||
const stl_triangle_vertex_indices &src = base_mesh.indices[size_t(S)];
|
||||
const Vec3f &a = base_mesh.vertices[size_t(src[0])];
|
||||
const Vec3f &b = base_mesh.vertices[size_t(src[1])];
|
||||
const Vec3f &c = base_mesh.vertices[size_t(src[2])];
|
||||
const Vec2f *uv = &model_uv[size_t(S) * 3];
|
||||
const stl_triangle_vertex_indices &tri = patch.indices[j];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
// An unsplit patch triangle is its source triangle, so its corners land exactly on a, b
|
||||
// and c and this reproduces their uvs unchanged; a sub-triangle the brush cut gets the
|
||||
// interpolated coordinate, which is the whole point of going through the barycentric.
|
||||
float w1 = 0.f, w2 = 0.f;
|
||||
barycentric_weights(a, b, c, patch.vertices[size_t(tri[k])], w1, w2);
|
||||
corner[j * 3 + size_t(k)] = (1.f - w1 - w2) * uv[0] + w1 * uv[1] + w2 * uv[2];
|
||||
}
|
||||
}
|
||||
return corner;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner)
|
||||
{
|
||||
if (patch.indices.empty() || corner.size() != patch.indices.size() * 3)
|
||||
return {};
|
||||
|
||||
std::vector<Vec2f> per_vertex(patch.vertices.size(), Vec2f::Zero());
|
||||
std::vector<char> seen(patch.vertices.size(), 0);
|
||||
for (size_t j = 0; j < patch.indices.size(); ++j)
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const int vi = patch.indices[j][k];
|
||||
if (vi < 0 || size_t(vi) >= per_vertex.size() || seen[size_t(vi)])
|
||||
continue;
|
||||
seen[size_t(vi)] = 1;
|
||||
per_vertex[size_t(vi)] = corner[j * 3 + size_t(k)];
|
||||
}
|
||||
return per_vertex;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
|
||||
const std::vector<Vec2f> &src_uv,
|
||||
const indexed_triangle_set &dst_mesh,
|
||||
float max_distance)
|
||||
{
|
||||
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_uv.size() != src_mesh.indices.size() * 3)
|
||||
return {};
|
||||
|
||||
const AABBTreeIndirect::Tree3f tree =
|
||||
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(src_mesh.vertices, src_mesh.indices);
|
||||
const float limit2 = max_distance >= 0.f ? max_distance * max_distance : std::numeric_limits<float>::max();
|
||||
|
||||
std::vector<Vec2f> out(dst_mesh.indices.size() * 3, Vec2f::Zero());
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t j = range.begin(); j < range.end(); ++j) {
|
||||
const stl_triangle_vertex_indices &tri = dst_mesh.indices[j];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const Vec3f &p = dst_mesh.vertices[size_t(tri[k])];
|
||||
size_t hit = 0;
|
||||
Vec3f foot;
|
||||
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
|
||||
src_mesh.vertices, src_mesh.indices, tree, p, hit, foot);
|
||||
if (d2 < 0.f || d2 > limit2 || hit >= src_mesh.indices.size())
|
||||
continue; // nowhere near the source surface: (0, 0), as documented
|
||||
const stl_triangle_vertex_indices &st = src_mesh.indices[hit];
|
||||
float w1 = 0.f, w2 = 0.f;
|
||||
barycentric_weights(src_mesh.vertices[size_t(st[0])], src_mesh.vertices[size_t(st[1])],
|
||||
src_mesh.vertices[size_t(st[2])], foot, w1, w2);
|
||||
const Vec2f *uv = &src_uv[hit * 3];
|
||||
out[j * 3 + size_t(k)] = (1.f - w1 - w2) * uv[0] + w1 * uv[1] + w2 * uv[2];
|
||||
}
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// apply_uv_transform()'s per-layer constants, worked out once. Triplanar sampling runs the transform
|
||||
// three times per point, and recomputing the rotation's cos/sin and the tiling reciprocal on every one
|
||||
@@ -1841,16 +1682,13 @@ struct UVTransform
|
||||
float scale, cs, sn, aspect;
|
||||
Vec2f offset;
|
||||
|
||||
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
|
||||
// model - the millimetre tiling scale and the non-square aspect correction - and keeps the
|
||||
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
|
||||
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
|
||||
UVTransform(const TextureDisplacementLayer &layer, float aspect_)
|
||||
{
|
||||
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
cs = std::cos(rad);
|
||||
sn = std::sin(rad);
|
||||
aspect = normalized_uv ? 1.f : aspect_;
|
||||
aspect = aspect_;
|
||||
offset = layer.offset;
|
||||
}
|
||||
Vec2f operator()(const Vec2f &planar) const
|
||||
@@ -1875,11 +1713,9 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
|
||||
{
|
||||
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
|
||||
// see the header. UVTransform does the same, and the two must stay in step.
|
||||
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
||||
const Vec2f scaled = planar * scale;
|
||||
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
@@ -1897,7 +1733,7 @@ Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &la
|
||||
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
||||
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
||||
// turning it.
|
||||
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
|
||||
if (aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
|
||||
return rotated + layer.offset;
|
||||
@@ -1922,9 +1758,6 @@ float blend_displacement(float accumulated, float value, TextureBlendMode mode)
|
||||
value;
|
||||
return accumulated / divisor;
|
||||
}
|
||||
// Replaces instead of combining, so `accumulated` is deliberately unused: whatever the layers
|
||||
// below put here is gone where this one is painted.
|
||||
case TextureBlendMode::Override: return value;
|
||||
case TextureBlendMode::Add:
|
||||
default: return accumulated + value;
|
||||
}
|
||||
@@ -1954,21 +1787,14 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
||||
// apply_uv_transform()). Every projection except the projective "from view" one funnels through
|
||||
// here, so this one line is what makes them all aspect-correct.
|
||||
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
|
||||
// A ModelUV layer's precomputed uv is the model's own, already normalized; every other coordinate
|
||||
// reaching the transform - the unwrap's, and every analytic projection's - is in millimetres. The
|
||||
// `lscm_uv` test is what keeps the two apart: a ModelUV layer with no channel gets none, falls
|
||||
// through to the millimetre methods below, and must be scaled like them.
|
||||
const bool normalized_uv = layer.projection_method == TextureProjectionMethod::ModelUV &&
|
||||
lscm_uv != nullptr;
|
||||
const UVTransform xf(layer, aspect, normalized_uv);
|
||||
const UVTransform xf(layer, aspect);
|
||||
auto sample_at = [&](const Vec2f &planar) {
|
||||
return texture.sample(xf(planar), layer.tile_enabled, layer.tile_method);
|
||||
};
|
||||
|
||||
// A precomputed per-patch uv - the LSCM solve, or the model's own channel - wins over the layer's
|
||||
// own method (see the header): the caller only passes it when the patch actually has one, so a
|
||||
// patch that failed to unwrap, or a volume with no UV channel, falls through to the analytic
|
||||
// methods below as its documented fallback.
|
||||
// Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the
|
||||
// caller only passes it when the patch actually parameterized successfully, so a patch that
|
||||
// failed to unwrap falls through to the analytic methods below as its documented fallback.
|
||||
if (lscm_uv != nullptr)
|
||||
return sample_at(*lscm_uv);
|
||||
|
||||
@@ -2026,12 +1852,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
// the two differ in what "nothing here" means. Height returns 0, which is a perfectly good height
|
||||
// (no displacement); colour has no such neutral value - black is a colour - so every path that
|
||||
// returns 0 there has to report false here instead, and the caller leaves the triangle uncoloured.
|
||||
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
|
||||
// Same reasoning as sample_layer_height(), and it has to match it exactly or a layer's colour and
|
||||
// its relief would be read from different places in the image.
|
||||
const bool normalized_uv = layer.projection_method == TextureProjectionMethod::ModelUV &&
|
||||
lscm_uv != nullptr;
|
||||
const UVTransform xf(layer, aspect, normalized_uv);
|
||||
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
|
||||
const UVTransform xf(layer, aspect);
|
||||
auto sample_at = [&](const Vec2f &planar) {
|
||||
return texture.sample_color(xf(planar), layer.tile_enabled, layer.tile_method);
|
||||
};
|
||||
@@ -2045,8 +1867,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
};
|
||||
|
||||
if (lscm_uv != nullptr) {
|
||||
// The coverage test goes through the same transform, so an untiled ModelUV layer is "there"
|
||||
// exactly where the model's own uv lands inside the image.
|
||||
if (!covered(*lscm_uv))
|
||||
return false;
|
||||
out = sample_at(*lscm_uv);
|
||||
@@ -2391,10 +2211,9 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
const TextureColorRequest *color,
|
||||
bool flip_normals,
|
||||
BakeStageRecorder *debug,
|
||||
TextureBakeStats *stats,
|
||||
const std::vector<Vec2f> *model_uv)
|
||||
TextureBakeStats *stats)
|
||||
{
|
||||
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data, model_uv);
|
||||
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data);
|
||||
if (!combined)
|
||||
return mesh; // nothing decodable to displace with
|
||||
|
||||
@@ -2564,8 +2383,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
// part of it (half a tile in the neighbour's colour, a tile edge that wanders).
|
||||
const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize);
|
||||
const ColorFieldSampler color_sampler =
|
||||
want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure, model_uv) :
|
||||
ColorFieldSampler{};
|
||||
want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure) : ColorFieldSampler{};
|
||||
//
|
||||
// The *palette* index, not the printed filament. The decimation treats any edge whose two faces
|
||||
// differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived**
|
||||
@@ -2697,8 +2515,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
const TextureColorRequest *color,
|
||||
bool flip_normals,
|
||||
BakeStageRecorder *debug,
|
||||
TextureBakeStats *stats,
|
||||
const std::vector<Vec2f> *model_uv)
|
||||
TextureBakeStats *stats)
|
||||
{
|
||||
// The classic path moves the vertices the mesh already has, so there is no budget to report on.
|
||||
(void) stats;
|
||||
@@ -2735,7 +2552,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
|
||||
if (options.pipeline_v2)
|
||||
return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, color, flip_normals,
|
||||
debug, stats, model_uv);
|
||||
debug, stats);
|
||||
|
||||
// Layers are combined in slot order, like stacked layers in an image editor: each one folds its
|
||||
// own displacement into the running total via its blend mode (see TextureBlendMode).
|
||||
@@ -2853,19 +2670,13 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
selector_dirty = true;
|
||||
|
||||
const bool color_this_layer = want_color && layer->color_enabled;
|
||||
// The model's own channel is per base-mesh *triangle*, so reading it needs to know which base
|
||||
// triangle each (possibly brush-split) patch triangle came from - the colour pass's map, asked
|
||||
// for here as well.
|
||||
const bool model_uv_layer = layer->projection_method == TextureProjectionMethod::ModelUV &&
|
||||
model_uv != nullptr && model_uv->size() == mesh.indices.size() * 3;
|
||||
// A flat-colour image is matched against the filaments alone (see TextureColorRequest).
|
||||
const ColorQuantizeFn &layer_quantize =
|
||||
(color_this_layer && color->quantize_pure && analyze_texture_detail(*layer).flat_colors) ? color->quantize_pure
|
||||
: color->quantize;
|
||||
std::vector<int> patch_source; // sub-triangle -> base mesh triangle; built when colouring or mapping by ModelUV
|
||||
std::vector<int> patch_source; // sub-triangle -> base mesh triangle, only built when colouring
|
||||
const indexed_triangle_set patch =
|
||||
selector.get_facets_strict(EnforcerBlockerType::ENFORCER,
|
||||
(color_this_layer || model_uv_layer) ? &patch_source : nullptr);
|
||||
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, color_this_layer ? &patch_source : nullptr);
|
||||
if (patch.indices.empty())
|
||||
continue;
|
||||
// get_facets_strict() returns the same vertex array whichever state is asked for (only the
|
||||
@@ -2896,25 +2707,18 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
// A real unwrap of the whole patch, computed once here rather than per vertex - it is a
|
||||
// per-chart solve over the whole patch, not a per-point formula. Cached, so repeating this
|
||||
// for every slider tweak costs a hash rather than a re-solve (see compute_patch_unwrap()).
|
||||
// ModelUV takes its corners from the volume's own channel instead of solving anything, and
|
||||
// collapses them to one per vertex the same way the unwrap does.
|
||||
const std::vector<Vec2f> model_corner_uvs = model_uv_layer ?
|
||||
compute_model_corner_uvs(mesh, *model_uv, patch, patch_source) :
|
||||
std::vector<Vec2f>{};
|
||||
const std::vector<Vec2f> patch_uvs = model_uv_layer ? collapse_corner_uvs(patch, model_corner_uvs) :
|
||||
(layer->projection_method == TextureProjectionMethod::LSCM) ?
|
||||
const std::vector<Vec2f> lscm_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
|
||||
compute_lscm_uvs(patch, *layer) :
|
||||
std::vector<Vec2f>{};
|
||||
// The colour pass below samples per *triangle*, so it takes the per-corner unwrap instead: the
|
||||
// per-vertex collapse above would hand a triangle at a seam the island layout did not join its
|
||||
// neighbour's placement, painting one triangle per face from the wrong part of the texture.
|
||||
// (The displacement itself stays on patch_uvs - a vertex has one position, so one height.)
|
||||
const std::vector<Vec2f> patch_corner_uvs = model_uv_layer ? model_corner_uvs :
|
||||
(layer->projection_method == TextureProjectionMethod::LSCM) ?
|
||||
// (The displacement itself stays on lscm_uvs - a vertex has one position, so one height.)
|
||||
const std::vector<Vec2f> lscm_corner_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
|
||||
compute_lscm_corner_uvs(patch, *layer) :
|
||||
std::vector<Vec2f>{};
|
||||
const bool corner_uv_ok = !patch_corner_uvs.empty() &&
|
||||
patch_corner_uvs.size() == patch.indices.size() * 3;
|
||||
const bool corner_uv_ok = !lscm_corner_uvs.empty() &&
|
||||
lscm_corner_uvs.size() == patch.indices.size() * 3;
|
||||
|
||||
// Colour, if this layer carries any. Area-weighted over each base triangle's *painted* part,
|
||||
// so a triangle the brush only clipped a corner off takes the colour of that corner rather
|
||||
@@ -2944,7 +2748,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
// the per-vertex height sampling uses, evaluated at the centroid instead.
|
||||
Vec3f n = Vec3f::Zero();
|
||||
Vec2f uv = Vec2f::Zero();
|
||||
bool have_uv = !patch_uvs.empty();
|
||||
bool have_uv = !lscm_uvs.empty();
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const int vi = t[k];
|
||||
if (vi < int(vertex_normals.size()))
|
||||
@@ -2952,9 +2756,9 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
if (!have_uv)
|
||||
continue;
|
||||
if (corner_uv_ok)
|
||||
uv += patch_corner_uvs[j * 3 + size_t(k)];
|
||||
else if (size_t(vi) < patch_uvs.size())
|
||||
uv += patch_uvs[size_t(vi)];
|
||||
uv += lscm_corner_uvs[j * 3 + size_t(k)];
|
||||
else if (size_t(vi) < lscm_uvs.size())
|
||||
uv += lscm_uvs[size_t(vi)];
|
||||
else
|
||||
have_uv = false;
|
||||
}
|
||||
@@ -3028,7 +2832,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t k = range.begin(); k < range.end(); ++k) {
|
||||
const size_t vi = size_t(layer_vertices[k]);
|
||||
const Vec2f *lscm_uv = patch_uvs.empty() ? nullptr : &patch_uvs[vi];
|
||||
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[vi];
|
||||
const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi],
|
||||
patch_centroid, patch_axis, lscm_uv);
|
||||
// midlevel is the height that means "stay put", so anything below it displaces
|
||||
@@ -3161,8 +2965,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
const TextureColorRequest *color,
|
||||
const Transform3d &volume_to_world,
|
||||
BakeStageRecorder *debug,
|
||||
TextureBakeStats *stats,
|
||||
const std::vector<Vec2f> *model_uv)
|
||||
TextureBakeStats *stats)
|
||||
{
|
||||
// An untransformed volume on an untransformed instance is by far the common case, and the round
|
||||
// trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take
|
||||
@@ -3172,7 +2975,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
const Transform3d frame = texture_displacement_bake_frame(volume_to_world);
|
||||
if (frame.matrix().isApprox(Transform3d::Identity().matrix()))
|
||||
return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color,
|
||||
false, debug, stats, model_uv);
|
||||
false, debug, stats);
|
||||
|
||||
const Transform3d to_local = frame.inverse();
|
||||
// A mirroring placement leaves the positions correct but every winding-derived normal pointing
|
||||
@@ -3186,10 +2989,8 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
v = (frame * v.cast<double>()).cast<float>();
|
||||
|
||||
const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0;
|
||||
// The UV channel rides along untouched: a uv is a coordinate in the texture, not on the model, so
|
||||
// the frame the mesh is measured in does not reach it.
|
||||
indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options,
|
||||
progress, color, mirrored, debug, stats, model_uv);
|
||||
progress, color, mirrored, debug, stats);
|
||||
// Everything the bake recorded is in world millimetres, like `out` itself. The debug view draws in
|
||||
// the volume's local frame, so the stages are brought back the same way the result is.
|
||||
if (debug != nullptr)
|
||||
@@ -3261,8 +3062,7 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
|
||||
|
||||
return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data,
|
||||
volume.texture_displacement_options, {}, nullptr,
|
||||
texture_displacement_volume_to_world(volume), nullptr, nullptr,
|
||||
volume.has_texture_uv() ? &volume.texture_uv : nullptr);
|
||||
texture_displacement_volume_to_world(volume));
|
||||
}
|
||||
|
||||
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
||||
@@ -3326,18 +3126,16 @@ void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t>
|
||||
namespace {
|
||||
// One decoded texture + placement per sampleable layer, in blend (slot) order. Held by shared_ptr so
|
||||
// the returned closure owns it for as long as the subdivider keeps calling back.
|
||||
// A per-triangle uv map turned into something a *point* sampler can use. Neither of the two maps that
|
||||
// are not a formula from position to uv - the LSCM unwrap and the model's own UV channel - can be
|
||||
// evaluated at an arbitrary point directly, so a point is placed on the painted patch (the nearest
|
||||
// patch triangle, and its barycentric coordinates there) and the uv is interpolated from that
|
||||
// triangle's own per-corner uvs. Exact for a point on the base surface, which is where both samplers
|
||||
// are queried: the displacement samples refined positions before moving them, and the colour pass
|
||||
// samples the foot point on the painted pieces.
|
||||
// An unwrap turned into something a *point* sampler can use. LSCM has no formula from position to
|
||||
// uv - it is a per-triangle map - so a point is placed on the painted patch (the nearest patch
|
||||
// triangle, and its barycentric coordinates there) and the uv is interpolated from that triangle's own
|
||||
// per-corner uvs. Exact for a point on the base surface, which is where both samplers are queried: the
|
||||
// displacement samples refined positions before moving them, and the colour pass samples the foot
|
||||
// point on the painted pieces.
|
||||
struct LscmLookup {
|
||||
indexed_triangle_set patch; // the layer's painted patch, as the uvs were computed on
|
||||
indexed_triangle_set patch; // the layer's painted patch, as the unwrap was solved on
|
||||
AABBTreeIndirect::Tree3f tree;
|
||||
// compute_lscm_corner_uvs(patch, layer), or compute_model_corner_uvs() for a ModelUV layer
|
||||
std::vector<Vec2f> corner;
|
||||
std::vector<Vec2f> corner; // compute_lscm_corner_uvs(patch, layer)
|
||||
|
||||
// False when `pos` is not on this layer's patch (farther than `tol`): there is no uv there, so the
|
||||
// layer contributes nothing - the same as a non-tiled texture outside its placement.
|
||||
@@ -3350,9 +3148,15 @@ struct LscmLookup {
|
||||
if (d2 < 0.f || d2 > tol * tol || hit >= patch.indices.size())
|
||||
return false;
|
||||
const stl_triangle_vertex_indices &t = patch.indices[hit];
|
||||
float w1 = 0.f, w2 = 0.f; // a degenerate triangle takes its centroid's uv
|
||||
barycentric_weights(patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])], patch.vertices[size_t(t[2])],
|
||||
foot, w1, w2);
|
||||
const Vec3f &a = patch.vertices[size_t(t[0])], &b = patch.vertices[size_t(t[1])], &c = patch.vertices[size_t(t[2])];
|
||||
const Vec3f e0 = b - a, e1 = c - a, ep = foot - a;
|
||||
const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
|
||||
const float den = d00 * d11 - d01 * d01;
|
||||
float w1 = 1.f / 3.f, w2 = 1.f / 3.f; // a degenerate triangle takes its centroid's uv
|
||||
if (std::abs(den) > 1e-20f) {
|
||||
w1 = (d11 * dp0 - d01 * dp1) / den;
|
||||
w2 = (d00 * dp1 - d01 * dp0) / den;
|
||||
}
|
||||
const Vec2f *c3 = &corner[hit * 3];
|
||||
uv = (1.f - w1 - w2) * c3[0] + w1 * c3[1] + w2 * c3[2];
|
||||
return true;
|
||||
@@ -3411,7 +3215,7 @@ struct PreparedLayer {
|
||||
// where a layer is placed. `need_color` additionally drops layers that cannot contribute colour.
|
||||
std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
|
||||
const indexed_triangle_set &base_mesh, const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data, bool need_color, const std::vector<Vec2f> *model_uv)
|
||||
const TextureDisplacementFacetsData &facets_data, bool need_color)
|
||||
{
|
||||
auto prepared = std::make_shared<std::vector<PreparedLayer>>();
|
||||
|
||||
@@ -3442,14 +3246,9 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
|
||||
if (tex.empty() || (need_color && !tex.has_color()))
|
||||
continue;
|
||||
|
||||
const bool model_uv_layer = layer->projection_method == TextureProjectionMethod::ModelUV &&
|
||||
model_uv != nullptr && model_uv->size() == base_mesh.indices.size() * 3;
|
||||
|
||||
TriangleSelector selector(selector_mesh);
|
||||
selector.deserialize(data, false);
|
||||
std::vector<int> patch_source; // only the model's own channel needs it; see the classic path
|
||||
const indexed_triangle_set patch =
|
||||
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, model_uv_layer ? &patch_source : nullptr);
|
||||
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
|
||||
if (patch.indices.empty())
|
||||
continue;
|
||||
|
||||
@@ -3459,16 +3258,12 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
|
||||
texture_displacement_patch_frame(patch, vertex_normals, centroid, axis, average_normal);
|
||||
|
||||
std::shared_ptr<const LscmLookup> lscm;
|
||||
if (model_uv_layer || layer->projection_method == TextureProjectionMethod::LSCM) {
|
||||
if (layer->projection_method == TextureProjectionMethod::LSCM) {
|
||||
// Solved on the very patch the classic path and the GUI solve it on (same geometry, seam
|
||||
// angle and edges), so it hits the unwrap cache and lands exactly where the UV editor
|
||||
// shows it, hand-placed islands and UV edits included. A ModelUV layer needs no solve at
|
||||
// all - its corners are read straight out of the volume's channel - but it is looked up
|
||||
// through the same per-triangle machinery, for the same reason: there is no formula from a
|
||||
// point to its uv.
|
||||
// shows it, hand-placed islands and UV edits included.
|
||||
auto l = std::make_shared<LscmLookup>();
|
||||
l->corner = model_uv_layer ? compute_model_corner_uvs(base_mesh, *model_uv, patch, patch_source) :
|
||||
compute_lscm_corner_uvs(patch, *layer);
|
||||
l->corner = compute_lscm_corner_uvs(patch, *layer);
|
||||
if (l->corner.size() == patch.indices.size() * 3) {
|
||||
l->patch = patch;
|
||||
l->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(l->patch.vertices, l->patch.indices);
|
||||
@@ -3494,12 +3289,11 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
ColorQuantizeFn quantize,
|
||||
ColorQuantizeFn quantize_pure,
|
||||
const std::vector<Vec2f> *model_uv)
|
||||
ColorQuantizeFn quantize_pure)
|
||||
{
|
||||
if (!quantize)
|
||||
return nullptr;
|
||||
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true, model_uv);
|
||||
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true);
|
||||
if (prepared->empty())
|
||||
return nullptr;
|
||||
// Per layer: a flat-colour image is matched against the filaments alone, when that quantizer
|
||||
@@ -3530,10 +3324,9 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
|
||||
|
||||
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const std::vector<Vec2f> *model_uv)
|
||||
const TextureDisplacementFacetsData &facets_data)
|
||||
{
|
||||
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false, model_uv);
|
||||
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false);
|
||||
if (prepared->empty())
|
||||
return nullptr;
|
||||
|
||||
|
||||
@@ -86,17 +86,6 @@ enum class TextureProjectionMethod : int
|
||||
// that is inherent to view projection and is the user's call, not a bug. The projector's two
|
||||
// in-plane axes live in TextureDisplacementLayer::view_project_right/up.
|
||||
ViewProjected = 4,
|
||||
// The model's own texture coordinates, as the file it was imported from carried them (see
|
||||
// ModelVolume::texture_uv). Nothing is unwrapped or projected: the artist's layout *is* the
|
||||
// placement, so a texture authored against that layout - the model's own albedo map, say - lands
|
||||
// exactly where it was painted. Automatically picked for a new layer on a volume that has a UV
|
||||
// channel, since re-unwrapping a model that already carries one can only be worse than using it.
|
||||
//
|
||||
// The uv is already normalized, so unlike every method above it is not measured in millimetres:
|
||||
// "Tile size" and the non-square aspect correction do not apply to it (the layer's rotation and
|
||||
// offset still do). Falls back to Triplanar when the volume has no channel, exactly as LSCM falls
|
||||
// back when a patch cannot be flattened.
|
||||
ModelUV = 5,
|
||||
};
|
||||
|
||||
// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam
|
||||
@@ -132,11 +121,6 @@ enum class TextureBlendMode : int
|
||||
Subtract = 1, // acc - value (carve this layer's relief out of the layers below)
|
||||
Multiply = 2, // acc * (value / 1mm) (mask/modulate the layers below by this layer)
|
||||
Divide = 3, // acc / (value / 1mm) (inverse mask; guarded against a zero/near-zero divisor)
|
||||
// acc = value: this layer replaces what is under it instead of combining with it, so where it is
|
||||
// painted only its own relief remains. The image editor's "Normal" mode, under the name the panel
|
||||
// uses. Note the stack's *colour* already behaves this way - a higher slot's colour overwrites a
|
||||
// lower one's where they overlap - so this is what makes the displacement agree with it.
|
||||
Override = 4,
|
||||
};
|
||||
|
||||
// Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`.
|
||||
@@ -584,13 +568,7 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
|
||||
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
|
||||
// square case and leaves the coordinate exactly as it always was.
|
||||
//
|
||||
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
|
||||
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
|
||||
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
|
||||
// the image's proportions) applies, and only the rotation and offset are left.
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
|
||||
bool normalized_uv = false);
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
|
||||
|
||||
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
|
||||
// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched -
|
||||
@@ -627,31 +605,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
|
||||
const Vec2f *lscm_uv = nullptr);
|
||||
|
||||
// Carries a per-facet paint annotation - the colour paint above all - from a mesh onto the mesh a
|
||||
// bake produced from it: every triangle of `dst_mesh` takes the state painted at its own foot point on
|
||||
// `src_mesh`, i.e. on the nearest point of the surface it was built from. Returned as one
|
||||
// EnforcerBlockerType per `dst_mesh` triangle, 0 (NONE) where that foot point carried no paint.
|
||||
//
|
||||
// ModelVolume::restore_painting() cannot do this. Its remap pairs a source paint triangle with a
|
||||
// target triangle that *overlaps* it to within 0.01 mm, which is right for a remesh or a
|
||||
// simplification - they leave the surface where it was - and useless after a bake, which moves the
|
||||
// surface by the relief depth. Measured on a model displaced by a tenth of its own size, that remap
|
||||
// kept a quarter of the colour and left the rest unpainted; this keeps essentially all of it, because
|
||||
// a foot point is exactly as far away as the relief is deep and the lookup does not care how far that
|
||||
// is.
|
||||
//
|
||||
// The source's *sub*-triangles are what is looked up, not its triangles, so the part of a triangle a
|
||||
// brush stroke only clipped a corner off keeps its own state rather than rounding to the whole
|
||||
// triangle's. No distance limit is needed or wanted: the nearest point on the source surface is where
|
||||
// a baked triangle came from however far the relief carried it, and an unpainted origin reports NONE
|
||||
// on its own.
|
||||
//
|
||||
// Returns an empty vector when `src_paint` is empty or either mesh is - the caller then has nothing to
|
||||
// carry, which is not the same as "carry nothing".
|
||||
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
|
||||
const TriangleSelector::TriangleSplittingData &src_paint,
|
||||
const indexed_triangle_set &dst_mesh);
|
||||
|
||||
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
|
||||
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant
|
||||
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
|
||||
@@ -765,55 +718,6 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
|
||||
// Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map.
|
||||
std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
|
||||
|
||||
// Three UVs per patch triangle, read out of the model's *own* UV channel - the
|
||||
// TextureProjectionMethod::ModelUV counterpart of compute_lscm_corner_uvs(), and laid out the same way
|
||||
// (corner k of triangle i at index 3i+k).
|
||||
//
|
||||
// `model_uv` is the volume's channel: three uvs per triangle of `base_mesh`, in its triangle order
|
||||
// (see ModelVolume::texture_uv). `patch_source` maps each patch triangle back to the base-mesh
|
||||
// triangle it was cut from - TriangleSelector::get_facets_strict()'s out_source. A patch triangle is
|
||||
// always a piece of exactly one base triangle, so each of its corners is interpolated barycentrically
|
||||
// inside that triangle's three uvs. That is what keeps a sub-triangle the brush split (whose corners
|
||||
// are not base-mesh vertices at all) on the texture, and what keeps the two sides of a UV seam
|
||||
// reading from their own island instead of from one averaged coordinate.
|
||||
//
|
||||
// Returns an empty vector when the channel does not match `base_mesh`, when `patch_source` does not
|
||||
// match `patch`, or when either is empty - the callers treat that as "no placement" and fall back,
|
||||
// exactly as they do for an unwrap that failed.
|
||||
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<Vec2f> &model_uv,
|
||||
const indexed_triangle_set &patch,
|
||||
const std::vector<int> &patch_source);
|
||||
|
||||
// One UV per patch vertex, collapsed from a per-corner array: the first corner to reach a vertex wins.
|
||||
// This is the per-corner/per-vertex split compute_lscm_uvs() documents, applied to any corner array -
|
||||
// displacement is per-vertex, so a vertex on a seam has to settle on one of its copies.
|
||||
// Returns an empty vector if `corner` is not three per patch triangle.
|
||||
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner);
|
||||
|
||||
// Resamples a per-corner UV channel from `src_mesh` onto `dst_mesh`, for when something replaces a
|
||||
// volume's mesh and the model's own UV channel has to come along: the colour import's painted mesh,
|
||||
// the gizmo's remesh/subdivide, the default bake pipeline's rebuild. Each destination corner takes the
|
||||
// uv of its closest point on the source surface, interpolated barycentrically inside the source
|
||||
// triangle it lands on - the same transfer LscmLookup does per point, run over every corner.
|
||||
//
|
||||
// Exact wherever the two meshes describe the same surface, which is the case for every caller here
|
||||
// (they subdivide, repair or displace it, they do not reshape it). A corner that lands nowhere near
|
||||
// the source - farther than `max_distance` (negative: no limit) - takes (0, 0).
|
||||
//
|
||||
// One thing a positional transfer cannot reproduce: a *seam* vertex, where one position carries a
|
||||
// different uv in each island that meets there. Position alone does not say which island a corner
|
||||
// belongs to, so corners on a seam all take whichever island the nearest-triangle lookup lands on,
|
||||
// and the texture can be off by up to one triangle in a band along the island borders. Doing better
|
||||
// needs a triangle-to-triangle correspondence, which only some of the stages that replace a mesh
|
||||
// have (a remesh has none at all).
|
||||
//
|
||||
// Returns an empty vector when `src_uv` is not three per source triangle, or when either mesh is empty.
|
||||
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
|
||||
const std::vector<Vec2f> &src_uv,
|
||||
const indexed_triangle_set &dst_mesh,
|
||||
float max_distance = -1.f);
|
||||
|
||||
// The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs).
|
||||
inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); }
|
||||
|
||||
@@ -872,10 +776,6 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
|
||||
//
|
||||
// `color`, when given, also reports which filament each triangle should print in - see
|
||||
// TextureColorRequest.
|
||||
//
|
||||
// `model_uv`, when given, is the volume's own UV channel (three uvs per base-mesh triangle, see
|
||||
// ModelVolume::texture_uv) and is what a TextureProjectionMethod::ModelUV layer maps through. A layer
|
||||
// set to ModelUV without it falls back to Triplanar.
|
||||
struct TextureColorRequest
|
||||
{
|
||||
// RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list
|
||||
@@ -950,9 +850,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
// stage, already brought back into `base_mesh`'s frame.
|
||||
BakeStageRecorder *debug = nullptr,
|
||||
// When given, receives what the bake spent.
|
||||
TextureBakeStats *stats = nullptr,
|
||||
// The model's own UV channel; see above.
|
||||
const std::vector<Vec2f> *model_uv = nullptr);
|
||||
TextureBakeStats *stats = nullptr);
|
||||
|
||||
// `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own.
|
||||
// The mesh is shared by every instance, so a multi-instance object can only be baked for one of
|
||||
@@ -1027,15 +925,11 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
ColorQuantizeFn quantize,
|
||||
ColorQuantizeFn quantize_pure = nullptr,
|
||||
// The model's own UV channel, for ModelUV layers; see
|
||||
// build_texture_displacement().
|
||||
const std::vector<Vec2f> *model_uv = nullptr);
|
||||
ColorQuantizeFn quantize_pure = nullptr);
|
||||
|
||||
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const std::vector<Vec2f> *model_uv = nullptr);
|
||||
const TextureDisplacementFacetsData &facets_data);
|
||||
|
||||
// Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a
|
||||
// shared cache so a midpoint is computed once and reused by both triangles on either side of that
|
||||
@@ -1240,10 +1134,6 @@ struct TextureDisplacementPrepareResult
|
||||
{
|
||||
indexed_triangle_set mesh;
|
||||
TextureDisplacementFacetsData masks;
|
||||
// The model's own UV channel, resampled onto `mesh` (see transfer_uv_channel()). Empty when the
|
||||
// volume had none. Like the masks it is handed back rather than left to the caller to recompute,
|
||||
// because only the run itself knows which stages replaced the mesh.
|
||||
std::vector<Vec2f> uv;
|
||||
// The remesh landed but no layer's paint survived being carried onto it. Nothing is committed:
|
||||
// everything downstream is driven by that paint, so baking on would bake a flat mesh.
|
||||
bool paint_lost = false;
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include "TextureToColor/ColorUtils.hpp"
|
||||
|
||||
#include "Model.hpp"
|
||||
#include "TextureDisplacement.hpp"
|
||||
#include "TextureToColor/TriMesh.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
#include "TriangleSelector.hpp"
|
||||
@@ -555,29 +554,9 @@ bool apply_painted_mesh_to_volume(
|
||||
<< ", unit_scale=" << unit_scale << ")";
|
||||
}
|
||||
|
||||
// The model's own UV channel is indexed by triangle, and this replaces the triangles, so it has
|
||||
// to be resampled onto them. Kept rather than dropped because it is the layout the model was
|
||||
// authored against: a texture-displacement layer set to ModelUV maps through it, and the colour
|
||||
// import is exactly the path a model that carries one arrives by.
|
||||
const indexed_triangle_set old_its = volume.has_texture_uv() ? volume.mesh().its : indexed_triangle_set{};
|
||||
const std::vector<Vec2f> old_uv = volume.texture_uv;
|
||||
|
||||
volume.set_mesh(std::move(new_mesh));
|
||||
volume.calculate_convex_hull();
|
||||
|
||||
// Resampled here and not further down, while the two meshes still share a frame: the painted mesh
|
||||
// was just shifted by the same source.mesh_offset the old one was centred by, so the only thing
|
||||
// between them is whatever drift the repair introduced. The re-centering below translates the mesh
|
||||
// again, and a translation is exactly what a closest-point transfer cannot see through - it would
|
||||
// read every uv off a point that far away. A uv is a coordinate in the texture, so it does not care
|
||||
// about either shift; it just has to be read before the second one happens.
|
||||
if (!old_its.indices.empty())
|
||||
volume.texture_uv = transfer_uv_channel(old_its, old_uv, volume.mesh().its);
|
||||
else
|
||||
// No channel to carry, so make sure none is left behind either: a leftover of the right length
|
||||
// would read as a valid channel against a mesh it was never measured on.
|
||||
volume.reset_texture_uv();
|
||||
|
||||
// Re-center the replaced mesh so its bbox center sits at the origin,
|
||||
// matching what center_geometry_after_creation did for the original mesh.
|
||||
// CGAL repair / subdivision may shift the bbox center (drift); without
|
||||
|
||||
@@ -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
|
||||
@@ -1231,33 +1231,9 @@ indexed_triangle_set GLGizmoTextureDisplacement::patch_in_world(const indexed_tr
|
||||
return world;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> GLGizmoTextureDisplacement::model_uv_corners(const indexed_triangle_set &local_patch,
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source) const
|
||||
{
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (layer.projection_method != TextureProjectionMethod::ModelUV || patch_source == nullptr || mv == nullptr ||
|
||||
!mv->has_texture_uv())
|
||||
return {};
|
||||
// The channel is indexed against the volume's *local* mesh, and a uv is a coordinate in the
|
||||
// texture rather than on the model, so unlike every projected method this never goes through
|
||||
// patch_in_world() - there is no millimetre measurement here for a world scale to change.
|
||||
std::vector<Vec2f> uv = compute_model_corner_uvs(mv->mesh().its, mv->texture_uv, local_patch, *patch_source);
|
||||
for (Vec2f &p : uv)
|
||||
// Already a uv: no tiling scale, no aspect correction - see apply_uv_transform().
|
||||
p = apply_uv_transform(p, layer, 1.f, /* normalized_uv */ true);
|
||||
return uv;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &local_patch,
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source) const
|
||||
const TextureDisplacementLayer &layer) const
|
||||
{
|
||||
if (layer.projection_method == TextureProjectionMethod::ModelUV)
|
||||
// Collapsed to one per vertex the same way the unwrap is - a vertex has one position, so the
|
||||
// displacement can only read one height for it.
|
||||
return collapse_corner_uvs(local_patch, model_uv_corners(local_patch, layer, patch_source));
|
||||
|
||||
// The bake maps the texture in world millimetres, so "Tile size (mm)" means the same thing on a
|
||||
// scaled instance as it does on an untouched one (see build_texture_displacement()). Everything
|
||||
// that has to agree with the bake - the fast preview's uvs, the checker/distortion overlays -
|
||||
@@ -1317,11 +1293,8 @@ int GLGizmoTextureDisplacement::layer_projection_frame(const indexed_triangle_se
|
||||
}
|
||||
|
||||
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const indexed_triangle_set &local_patch,
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source) const
|
||||
const TextureDisplacementLayer &layer) const
|
||||
{
|
||||
if (layer.projection_method == TextureProjectionMethod::ModelUV)
|
||||
return model_uv_corners(local_patch, layer, patch_source);
|
||||
if (layer.projection_method == TextureProjectionMethod::LSCM) {
|
||||
const indexed_triangle_set patch = patch_in_world(local_patch);
|
||||
const float aspect = layer_texture_aspect(layer);
|
||||
@@ -1331,7 +1304,7 @@ std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const in
|
||||
return uv;
|
||||
}
|
||||
// Single-valued per point: fan the per-vertex result out over the corners.
|
||||
const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer, patch_source);
|
||||
const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer);
|
||||
if (per_vertex.size() != local_patch.vertices.size())
|
||||
return {};
|
||||
std::vector<Vec2f> corner(local_patch.indices.size() * 3);
|
||||
@@ -1352,11 +1325,7 @@ void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh()
|
||||
// Uses the *live* selector (not the flushed model facet data), so this reflects an in-progress
|
||||
// stroke immediately rather than only once it ends - the point of this preview mode is to be
|
||||
// the fast, no-CPU-meshing one.
|
||||
// The source map comes along because a ModelUV layer's uvs are read per base-mesh triangle; every
|
||||
// other mapping ignores it.
|
||||
std::vector<int> patch_source;
|
||||
const indexed_triangle_set patch =
|
||||
m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER, &patch_source);
|
||||
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
|
||||
if (patch.indices.empty())
|
||||
return;
|
||||
|
||||
@@ -1380,8 +1349,7 @@ void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh()
|
||||
// Per *corner*, not per vertex: the mesh below is flat (unshared) anyway, so each triangle can
|
||||
// carry its own chart's UVs - see compute_layer_corner_uvs().
|
||||
const TextureDisplacementLayer *active = active_layer();
|
||||
std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active, &patch_source) :
|
||||
std::vector<Vec2f>{};
|
||||
std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active) : std::vector<Vec2f>{};
|
||||
m_shaded_preview_uses_vertex_uv = corner_uv.size() == patch.indices.size() * 3;
|
||||
if (!m_shaded_preview_uses_vertex_uv)
|
||||
corner_uv.clear();
|
||||
@@ -1552,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;
|
||||
@@ -1958,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();
|
||||
@@ -2038,9 +2016,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || m_triangle_selectors.empty())
|
||||
return;
|
||||
std::vector<int> patch_source; // only ModelUV reads it; see compute_layer_vertex_uvs()
|
||||
const indexed_triangle_set patch =
|
||||
m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER, &patch_source);
|
||||
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
|
||||
if (patch.indices.empty())
|
||||
return;
|
||||
// Everything below works in the *patch's* vertex space, not the mesh's. Those agree only until a
|
||||
@@ -2054,7 +2030,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
|
||||
// The checker samples wherever the projection puts it; the projections the shader can't
|
||||
// reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader.
|
||||
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer, &patch_source);
|
||||
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer);
|
||||
const bool have_uvs = uv.size() == patch.vertices.size();
|
||||
m_uvcheck_uses_vertex_uv = have_uvs;
|
||||
m_uvcheck_projection_mode = have_uvs ? 0 :
|
||||
@@ -2063,7 +2039,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
// different uv in each island it borders, so the shared-vertex form drew one triangle per face from
|
||||
// a neighbouring island's placement. Only the *drawing* needs this; the distortion metric below is
|
||||
// a per-vertex average by construction and keeps using `uv`.
|
||||
const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer, &patch_source);
|
||||
const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer);
|
||||
const bool have_corner_uvs = have_uvs && corner_uv.size() == patch.indices.size() * 3;
|
||||
|
||||
// Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible.
|
||||
@@ -2339,8 +2315,6 @@ void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
if (input.options.pipeline_v2)
|
||||
input.options.v2_max_triangles_k = 0;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(*mv);
|
||||
if (mv->has_texture_uv())
|
||||
input.model_uv = mv->texture_uv;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||
// Captured here rather than read in the handler: the palette is main-thread state, and the preview
|
||||
@@ -2687,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();
|
||||
@@ -2927,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();
|
||||
}
|
||||
|
||||
@@ -3750,8 +3756,7 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
|
||||
// Paint / Erase, brush cursor shape, mapping, tiling, placement and the dock toggle.
|
||||
"texture_displacement_add.svg", "texture_displacement_add_negative.svg", "circle_paint.svg",
|
||||
"menu_obj_sphere.svg", "menu_obj_cube.svg", "menu_obj_cylinder.svg", "texture_displacement_map_unwrap.svg",
|
||||
"texture_displacement_map_view.svg", "texture_displacement_map_model_uv.svg",
|
||||
"texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
|
||||
"texture_displacement_map_view.svg", "texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
|
||||
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
|
||||
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
|
||||
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
|
||||
@@ -3800,11 +3805,6 @@ void GLGizmoTextureDisplacement::add_texture_layer()
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = free_slot;
|
||||
// A model that came with a UV channel is mapped by it. Re-unwrapping such a model throws away the
|
||||
// layout it was authored against and can only be worse than using it, so the default follows the
|
||||
// model rather than the other way round; the Mapping row still offers every other method.
|
||||
if (mv->has_texture_uv())
|
||||
layer.projection_method = TextureProjectionMethod::ModelUV;
|
||||
// Start the layer off on the first library texture rather than on nothing at all: a textureless
|
||||
// layer looks broken (painting on it appears to do nothing, because there is no height map to
|
||||
// displace by). The user swaps it for another from the layer's own picker.
|
||||
@@ -4080,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;
|
||||
@@ -4255,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();
|
||||
@@ -4278,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();
|
||||
@@ -4950,8 +4950,7 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
|
||||
|
||||
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
|
||||
const std::vector<TextureDisplacementLayer> &layers, const std::vector<Vec2f> &model_uv,
|
||||
const TextureDisplacementPrepareParams ¶ms,
|
||||
const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams ¶ms,
|
||||
const TextureColorSettings &color_settings, const DisplacementProgressFn &progress,
|
||||
BakeStageRecorder *debug)
|
||||
{
|
||||
@@ -4978,13 +4977,6 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
bool had_paint = false;
|
||||
for (const TriangleSelector::TriangleSplittingData &m : masks)
|
||||
had_paint = had_paint || TriangleSelector::has_facets(m, EnforcerBlockerType::ENFORCER);
|
||||
// The model's own UV channel for whatever `mesh` currently is. Every stage below keeps the input's
|
||||
// surface - it evens out, refines or cuts the triangles lying on it - so each corner reads its uv
|
||||
// straight off `base`'s channel at its own position instead of being carried stage by stage, which
|
||||
// would compound each stage's approximation. Untouched while nothing has replaced the mesh yet.
|
||||
const auto uv_now = [&]() {
|
||||
return (model_uv.empty() || !changed) ? model_uv : transfer_uv_channel(base, model_uv, mesh.its);
|
||||
};
|
||||
if (!report(1))
|
||||
return out;
|
||||
|
||||
@@ -5031,11 +5023,9 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
// Feature-adaptive: sample the combined displacement so refinement follows texture
|
||||
// curvature. A null sampler (only LSCM layers, or nothing decodable) falls back to the
|
||||
// length baseline alone.
|
||||
const std::vector<Vec2f> uv_stage = uv_now();
|
||||
const std::vector<Vec2f> *uv_ptr = uv_stage.empty() ? nullptr : &uv_stage;
|
||||
HeightFieldSampler sampler;
|
||||
if (params.subdiv_feature)
|
||||
sampler = make_combined_displacement_sampler(mesh.its, layers, current, uv_ptr);
|
||||
sampler = make_combined_displacement_sampler(mesh.its, layers, current);
|
||||
// Colour boundaries need triangles of their own - the chord test cannot see them, since
|
||||
// the height field is perfectly smooth across a change of filament.
|
||||
ColorFieldSampler color;
|
||||
@@ -5043,7 +5033,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
// boundaries are refined where the bake will actually change filament.
|
||||
if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty())
|
||||
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette),
|
||||
make_palette_quantizer(color_settings.palette_pure), uv_ptr);
|
||||
make_palette_quantizer(color_settings.palette_pure));
|
||||
// The refinement follows perceived colour: a mix is one colour here, however the slicer
|
||||
// interleaves its filaments layer by layer.
|
||||
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
|
||||
@@ -5102,10 +5092,8 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
std::vector<uint8_t> cut_region(mesh.its.indices.size(), 0);
|
||||
for (size_t i = 0; i < cut_region.size() && i < source.size(); ++i)
|
||||
cut_region[i] = (region[size_t(source[i])] & REFINE_PAINTED) ? 1 : 0;
|
||||
const std::vector<Vec2f> uv_refined = uv_now();
|
||||
const HeightFieldSampler refined_sampler =
|
||||
make_combined_displacement_sampler(mesh.its, layers, current,
|
||||
uv_refined.empty() ? nullptr : &uv_refined);
|
||||
make_combined_displacement_sampler(mesh.its, layers, current);
|
||||
std::vector<int> cut_source;
|
||||
size_t cuts = 0;
|
||||
indexed_triangle_set cut = cut_mesh_at_steps(mesh.its, cut_region, refined_sampler,
|
||||
@@ -5129,7 +5117,6 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
if (!report(100) || !changed)
|
||||
return out; // an empty result: nothing to commit, which is not a failure
|
||||
|
||||
out.uv = uv_now();
|
||||
out.mesh = std::move(mesh.its);
|
||||
out.masks = std::move(current);
|
||||
return out;
|
||||
@@ -5397,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();
|
||||
|
||||
@@ -5410,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();
|
||||
}
|
||||
@@ -5419,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();
|
||||
}
|
||||
@@ -5441,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);
|
||||
@@ -5449,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();
|
||||
}
|
||||
@@ -5781,8 +5768,6 @@ void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareP
|
||||
input.layers = mv->texture_displacement_layers;
|
||||
input.params = params;
|
||||
input.snapshot_name = snapshot_name;
|
||||
if (mv->has_texture_uv())
|
||||
input.model_uv = mv->texture_uv;
|
||||
if (params.subdiv_color_edge_mm > 0.f)
|
||||
input.color = color_settings_for(*mv);
|
||||
|
||||
@@ -5892,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;
|
||||
|
||||
@@ -6338,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();
|
||||
@@ -6380,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
|
||||
@@ -6406,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);
|
||||
@@ -6426,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);
|
||||
@@ -6450,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) {
|
||||
@@ -6641,19 +6630,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad,
|
||||
_L("Height of the relief: how far white in the texture lifts the surface, in "
|
||||
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
|
||||
// Mapped by the model's own UV layout there is no millimetre scale to set: the layout is the
|
||||
// placement (see TextureProjectionMethod::ModelUV), so the slider would move nothing. The row
|
||||
// states why rather than hiding: float_row()'s tooltip is shown for a disabled slider too.
|
||||
const bool tile_size_na = layer.projection_method == TextureProjectionMethod::ModelUV;
|
||||
m_imgui->disabled_begin(tile_size_na);
|
||||
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad,
|
||||
tile_size_na ?
|
||||
_L("Mapped by the model's own UV layout, which is already the placement - one "
|
||||
"copy of the texture covers it exactly, so there is no tile size to set.") :
|
||||
_L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
|
||||
"more often and makes its detail finer; with Tile off, this is the size of the "
|
||||
"single copy."));
|
||||
m_imgui->disabled_end();
|
||||
_L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
|
||||
"more often and makes its detail finer; with Tile off, this is the size of the "
|
||||
"single copy."));
|
||||
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad,
|
||||
_L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
|
||||
"edge of the model."));
|
||||
@@ -6759,7 +6739,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
}
|
||||
|
||||
// Mapping, as one icon per TextureProjectionMethod, in the enum's own order.
|
||||
// Mapping, as five icons in TextureProjectionMethod's own order.
|
||||
{
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
@@ -6787,26 +6767,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
{ "texture_displacement_map_view.svg", _L("From view"),
|
||||
_L("From view - projects straight onto the painted area from where you are looking, like a slide "
|
||||
"projector.") },
|
||||
{ "texture_displacement_map_model_uv.svg", _L("Model UV"),
|
||||
_L("Model UV - uses the texture coordinates the model itself came with, so a texture made for "
|
||||
"that model lands exactly where it was painted. Nothing is unwrapped, and Tile size does "
|
||||
"not apply: the model's own layout is the placement.") },
|
||||
};
|
||||
// Only a volume that actually carries a UV channel can be mapped by it. Shown
|
||||
// greyed out rather than hidden, so the reason is visible instead of the control
|
||||
// simply being missing.
|
||||
const wxString model_uv_na =
|
||||
(mv != nullptr && mv->has_texture_uv()) ?
|
||||
wxString() :
|
||||
_L("This model carries no texture coordinates of its own. Only a model imported with a UV "
|
||||
"channel - a textured OBJ, say - can be mapped by them.");
|
||||
for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) {
|
||||
if (mi > 0)
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
const auto method = static_cast<TextureProjectionMethod>(mi);
|
||||
const wxString na = (method == TextureProjectionMethod::ModelUV) ? model_uv_na : wxString();
|
||||
if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label,
|
||||
mappings[mi].tip, na) &&
|
||||
mappings[mi].tip) &&
|
||||
layer.projection_method != method) {
|
||||
// Capture the current view the moment "From view" is chosen, so it does something
|
||||
// sensible immediately rather than projecting from a stale/default direction.
|
||||
@@ -6936,33 +6903,18 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement");
|
||||
const std::string blend_override = _u8L_CONTEXT("Override", "Texture Displacement");
|
||||
const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement");
|
||||
const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement");
|
||||
const std::string blend_divide = _u8L("Divide");
|
||||
const char *blend_items[] = { blend_add.c_str(), blend_override.c_str(), blend_subtract.c_str(),
|
||||
blend_multiply.c_str(), blend_divide.c_str() };
|
||||
// The combo's order is the panel's, not the enum's - Override reads as the sibling of Add and
|
||||
// belongs next to it, while TextureBlendMode's numbering is fixed by what projects already
|
||||
// store. Hence the explicit mapping rather than a cast of the index.
|
||||
static const TextureBlendMode blend_modes[] = { TextureBlendMode::Add, TextureBlendMode::Override,
|
||||
TextureBlendMode::Subtract, TextureBlendMode::Multiply,
|
||||
TextureBlendMode::Divide };
|
||||
static_assert(IM_ARRAYSIZE(blend_items) == IM_ARRAYSIZE(blend_modes),
|
||||
"every Blend entry needs its mode");
|
||||
const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() };
|
||||
// The first layer has nothing before it to combine with - build_texture_displacement() makes it
|
||||
// add regardless - so its combo shows Add and cannot be changed.
|
||||
const bool base_layer = li == 0;
|
||||
int blend_mode = 0;
|
||||
if (!base_layer)
|
||||
for (int bi = 0; bi < int(IM_ARRAYSIZE(blend_modes)); ++bi)
|
||||
if (blend_modes[bi] == layer.blend_mode)
|
||||
blend_mode = bi;
|
||||
int blend_mode = base_layer ? 0 : static_cast<int>(layer.blend_mode);
|
||||
ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm));
|
||||
m_imgui->disabled_begin(base_layer);
|
||||
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer &&
|
||||
blend_mode >= 0 && blend_mode < int(IM_ARRAYSIZE(blend_modes))) {
|
||||
layer.blend_mode = blend_modes[blend_mode];
|
||||
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) {
|
||||
layer.blend_mode = static_cast<TextureBlendMode>(blend_mode);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
@@ -6970,11 +6922,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->tooltip(base_layer ?
|
||||
_u8L("The base layer has nothing beneath it to combine with, so it always adds.") :
|
||||
_u8L("What this layer does where it overlaps the ones below. Add and "
|
||||
"Subtract pile relief on or carve it away. Override replaces them, "
|
||||
"so only this layer's relief is left where it is painted. Multiply "
|
||||
"and Divide scale what is underneath, which turns this layer into a "
|
||||
"mask over it - there, Depth acts as a strength, and 1 mm leaves "
|
||||
"white areas untouched."),
|
||||
"Subtract pile relief on or carve it away. Multiply and Divide scale "
|
||||
"what is underneath, which turns this layer into a mask over it - "
|
||||
"there, Depth acts as a strength, and 1 mm leaves white areas "
|
||||
"untouched."),
|
||||
wrap_w);
|
||||
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
|
||||
@@ -61,12 +61,6 @@ public:
|
||||
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
|
||||
const TextureDisplacementFacetsData &masks,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
// `base`'s own UV channel, when it has one
|
||||
// (ModelVolume::texture_uv). Resampled onto
|
||||
// the result and reported back in it, and fed
|
||||
// to the samplers so a ModelUV layer drives
|
||||
// the refinement through its own layout.
|
||||
const std::vector<Vec2f> &model_uv,
|
||||
const TextureDisplacementPrepareParams ¶ms,
|
||||
const TextureColorSettings &color_settings,
|
||||
const DisplacementProgressFn &progress,
|
||||
@@ -203,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/
|
||||
@@ -519,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;
|
||||
@@ -750,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
|
||||
@@ -833,31 +855,18 @@ private:
|
||||
// Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first).
|
||||
static void join_island_groups(std::vector<int> &groups, int a, int b, int chart_count);
|
||||
// Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an
|
||||
// unwrap), ModelUV (the model's own channel) and ViewProjected (a projector plane the shader
|
||||
// doesn't know). One entry per patch/base vertex, already through apply_uv_transform(). Empty for
|
||||
// Triplanar/Cylindrical/Spherical, which the shader projects on its own. Shared by the shaded
|
||||
// preview and the UV-check overlay.
|
||||
//
|
||||
// `patch_source` maps each patch triangle back to its base-mesh triangle
|
||||
// (TriangleSelector::get_facets_strict()'s out_source). Only ModelUV needs it - its channel is per
|
||||
// base triangle - and without it a ModelUV layer reports no uvs and the preview falls back.
|
||||
// unwrap) and ViewProjected (a projector plane the shader doesn't know). One entry per patch/base
|
||||
// vertex, already through apply_uv_transform(). Empty for Triplanar/Cylindrical/Spherical, which
|
||||
// the shader projects on its own. Shared by the shaded preview and the UV-check overlay.
|
||||
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source = nullptr) const;
|
||||
const TextureDisplacementLayer &layer) const;
|
||||
// The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what
|
||||
// the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different
|
||||
// UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined
|
||||
// seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is
|
||||
// single-valued per point, so there a corner's UV is just its vertex's.
|
||||
std::vector<Vec2f> compute_layer_corner_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source = nullptr) const;
|
||||
// The model's own channel over `local_patch`, already through apply_uv_transform() - three per
|
||||
// patch triangle. Empty when the volume has no channel, or when `patch_source` is missing. Shared
|
||||
// by the two functions above so the per-vertex and per-corner forms can never disagree.
|
||||
std::vector<Vec2f> model_uv_corners(const indexed_triangle_set &local_patch,
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source) const;
|
||||
const TextureDisplacementLayer &layer) const;
|
||||
// The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two
|
||||
// wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the
|
||||
// shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview
|
||||
@@ -943,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;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include <exception>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
#include <memory>
|
||||
|
||||
@@ -27,7 +26,6 @@
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include <array>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
@@ -76,8 +74,7 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
}
|
||||
return true;
|
||||
},
|
||||
color, m_input.volume_to_world, nullptr, &m_stats,
|
||||
m_input.model_uv.empty() ? nullptr : &m_input.model_uv));
|
||||
color, m_input.volume_to_world, nullptr, &m_stats));
|
||||
|
||||
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
|
||||
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
|
||||
@@ -129,71 +126,16 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
{ mix.a_percent(), 100 - mix.a_percent() });
|
||||
});
|
||||
|
||||
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
|
||||
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
|
||||
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
|
||||
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
|
||||
// the triangles matter - the annotations are per facet and reference no vertex index - so a
|
||||
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
|
||||
// reason to remap anything.
|
||||
const bool topology_changed = m_input.options.pipeline_v2 ||
|
||||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
|
||||
|
||||
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
|
||||
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
|
||||
// no longer existed, which is what scrambled the colours of an imported coloured model even
|
||||
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
|
||||
//
|
||||
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
|
||||
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
|
||||
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
|
||||
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
|
||||
// could only lose detail.
|
||||
std::array<TriangleSelector::TriangleSplittingData, 4> before;
|
||||
if (topology_changed) {
|
||||
before[0] = volume->supported_facets.get_data();
|
||||
before[1] = volume->seam_facets.get_data();
|
||||
before[2] = volume->mmu_segmentation_facets.get_data();
|
||||
before[3] = volume->fuzzy_skin_facets.get_data();
|
||||
}
|
||||
|
||||
volume->set_mesh(std::move(m_result));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
|
||||
if (topology_changed) {
|
||||
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
|
||||
&volume->mmu_segmentation_facets,
|
||||
&volume->fuzzy_skin_facets };
|
||||
for (size_t ch = 0; ch < target.size(); ++ch) {
|
||||
const std::vector<uint8_t> state =
|
||||
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
|
||||
// Nothing was painted in this channel, or there is nothing to paint it onto: either
|
||||
// way the annotation must not keep describing the mesh that is gone.
|
||||
if (state.empty()) {
|
||||
target[ch]->reset();
|
||||
continue;
|
||||
}
|
||||
TriangleSelector selector(volume->mesh());
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < state.size(); ++i)
|
||||
if (state[i] != 0) {
|
||||
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
|
||||
any = true;
|
||||
}
|
||||
if (any)
|
||||
target[ch]->set(selector);
|
||||
else
|
||||
target[ch]->reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
|
||||
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
|
||||
// and one the user never painted at all stays at NONE, which already means "the volume's own
|
||||
// filament". That is what confines the effect to the painted area without having to invent a
|
||||
// colour for everything outside it. What was already painted is whatever the transfer above
|
||||
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
|
||||
// colour for everything outside it. Safe to index straight onto the new mesh - the bake is
|
||||
// topology-preserving, so triangle i is still triangle i.
|
||||
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
|
||||
TriangleSelector selector(volume->mesh());
|
||||
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
|
||||
@@ -217,6 +159,7 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
|
||||
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
|
||||
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
|
||||
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
|
||||
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
|
||||
@@ -224,14 +167,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
volume->texture_displacement_facet(slot).reset();
|
||||
}
|
||||
|
||||
// The model's own UV channel. Resampled onto a rebuilt mesh from the surface the bake started
|
||||
// from: a displaced vertex came from its foot point on that surface, which is the position its
|
||||
// uv belongs to - so no distance limit, the relief depth is exactly how far it has moved.
|
||||
if (!m_input.model_uv.empty())
|
||||
volume->texture_uv = topology_changed ? transfer_uv_channel(m_input.base_mesh, m_input.model_uv,
|
||||
volume->mesh().its) :
|
||||
m_input.model_uv;
|
||||
|
||||
ModelObject *object = volume->get_object();
|
||||
if (object == nullptr)
|
||||
return;
|
||||
@@ -284,8 +219,6 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
|
||||
input.take_snapshot = take_snapshot;
|
||||
input.volume_id = volume.id();
|
||||
input.base_mesh = volume.mesh().its;
|
||||
if (volume.has_texture_uv())
|
||||
input.model_uv = volume.texture_uv;
|
||||
input.layers = volume.texture_displacement_layers;
|
||||
input.options = volume.texture_displacement_options;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
|
||||
@@ -34,9 +34,6 @@ struct TextureDisplacementBakeInput
|
||||
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
||||
// paint gizmo writes - alongside the displaced geometry.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none: what a ModelUV
|
||||
// layer maps through, and what the commit puts back on the baked mesh.
|
||||
std::vector<Vec2f> model_uv;
|
||||
// Whether this job pushes its own undo step when it commits. False when the caller has already
|
||||
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
|
||||
// and subdivides first and has to undo as a single action.
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
#include <memory>
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
@@ -53,14 +52,13 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
|
||||
|
||||
indexed_triangle_set mesh = m_input.base_mesh;
|
||||
TextureDisplacementFacetsData masks = m_input.facets_data;
|
||||
std::vector<Vec2f> uv = m_input.model_uv;
|
||||
|
||||
// The default pipeline can only move vertices the mesh already has, so its recipe starts with the
|
||||
// preparation - which is where "first it remeshes, then it refines" actually happens. The
|
||||
// experimental pipeline refines as part of the bake and has nothing to prepare.
|
||||
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
|
||||
const TextureDisplacementPrepareResult prepared =
|
||||
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, uv, m_input.prepare_params,
|
||||
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
|
||||
m_input.color,
|
||||
// Preparation is roughly half the run; the bake
|
||||
// takes the progress bar from there.
|
||||
@@ -79,14 +77,12 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
|
||||
if (!prepared.mesh.indices.empty()) {
|
||||
mesh = prepared.mesh;
|
||||
masks = prepared.masks;
|
||||
uv = prepared.uv; // resampled onto the prepared mesh, as the prepare commit stores it
|
||||
}
|
||||
}
|
||||
|
||||
build_texture_displacement(mesh, m_input.layers, masks, m_input.options,
|
||||
[&report](int pct) { return report(50 + pct / 2); },
|
||||
/* color */ nullptr, m_input.volume_to_world, &recorder, /* stats */ nullptr,
|
||||
uv.empty() ? nullptr : &uv);
|
||||
/* color */ nullptr, m_input.volume_to_world, &recorder);
|
||||
|
||||
m_stages = recorder.take();
|
||||
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
|
||||
@@ -119,8 +115,6 @@ void queue_texture_displacement_debug(const ModelVolume &volume, const TextureCo
|
||||
input.check_topology = check_topology;
|
||||
input.color = color;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
if (volume.has_texture_uv())
|
||||
input.model_uv = volume.texture_uv;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
|
||||
|
||||
@@ -47,9 +47,6 @@ struct TextureDisplacementDebugInput
|
||||
// Only the quantizer is used, and only by the refinement's colour criterion - the debug run
|
||||
// produces no colour of its own.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), so the recipe captured here is the one a
|
||||
// ModelUV layer actually bakes through. Empty when the volume has none.
|
||||
std::vector<Vec2f> model_uv;
|
||||
|
||||
// The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a
|
||||
// multi-million triangle stage costs more than the stage did. On by default because a stage that
|
||||
|
||||
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
|
||||
// idle loop.
|
||||
int last_reported = 1;
|
||||
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
|
||||
m_input.model_uv, m_input.params, m_input.color,
|
||||
m_input.params, m_input.color,
|
||||
[&ctl, &status, &last_reported](int percent) {
|
||||
if (ctl.was_canceled())
|
||||
return false;
|
||||
@@ -106,10 +106,6 @@ void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &
|
||||
volume->restore_painting(saved_painting);
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)]));
|
||||
// The model's own UV channel, resampled onto the new mesh by the run. Also put back after
|
||||
// restore_painting(), which resets it along with the extra facets for the same reason: it is
|
||||
// indexed by triangle and the triangles have changed.
|
||||
volume->texture_uv = std::move(m_result.uv);
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "Job.hpp"
|
||||
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -48,10 +47,6 @@ struct TextureDisplacementPrepareInput
|
||||
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
|
||||
// perceived colour, never the interleaving that realises a mix.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none. prepare_mesh()
|
||||
// resamples it onto the mesh it produces, and the commit puts it back - like the masks, it is
|
||||
// indexed by triangle and so cannot survive the stages on its own.
|
||||
std::vector<Vec2f> model_uv;
|
||||
// The undo step the commit opens. Standard mode's Bake names it after the bake, because the
|
||||
// displacement job that follows commits into this same step rather than pushing its own.
|
||||
std::string snapshot_name;
|
||||
|
||||
@@ -48,8 +48,7 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
(!m_current_generation ||
|
||||
m_current_generation->load() == m_generation);
|
||||
},
|
||||
color, m_input.volume_to_world, nullptr, nullptr,
|
||||
m_input.model_uv.empty() ? nullptr : &m_input.model_uv);
|
||||
color, m_input.volume_to_world);
|
||||
}
|
||||
|
||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
|
||||
@@ -29,9 +29,6 @@ struct TextureDisplacementPreviewInput
|
||||
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
|
||||
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), so a ModelUV layer previews through the
|
||||
// same layout it will bake through. Empty when the volume has none.
|
||||
std::vector<Vec2f> model_uv;
|
||||
};
|
||||
|
||||
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)>;
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -2346,353 +2346,3 @@ TEST_CASE("whether a layer's texture has colour agrees with its decode", "[Textu
|
||||
|
||||
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The model's own UV channel, and the Override blend mode
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// A grayscale image that is black in its left half and white in its right half, so where a sample
|
||||
// lands along u is readable straight off the height it comes back with.
|
||||
static std::shared_ptr<std::vector<unsigned char>> make_left_right_split_png(size_t w = 16, size_t h = 16)
|
||||
{
|
||||
std::vector<uint8_t> pixels(w * h);
|
||||
for (size_t y = 0; y < h; ++y)
|
||||
for (size_t x = 0; x < w; ++x)
|
||||
pixels[y * w + x] = (x < w / 2) ? 0 : 255;
|
||||
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
|
||||
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
|
||||
REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels));
|
||||
|
||||
std::vector<unsigned char> bytes;
|
||||
{
|
||||
std::ifstream ifs(tmp_path.string(), std::ios::binary);
|
||||
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(tmp_path, ec);
|
||||
|
||||
REQUIRE_FALSE(bytes.empty());
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||
}
|
||||
|
||||
// Where a sheet coordinate lands in uv: across [UV_LO, UV_LO + UV_SPAN] rather than the full [0, 1].
|
||||
// The inset keeps the tests off the exact edge of an untiled placement, where there is deliberately no
|
||||
// texture at all rather than its border texel (see DecodedHeightTexture::sample()) - a real model's
|
||||
// channel does reach 1.0, and a vertex sitting exactly there displaces by nothing on purpose.
|
||||
static constexpr float UV_LO = 0.1f;
|
||||
static constexpr float UV_SPAN = 0.8f;
|
||||
|
||||
static float sheet_uv_at(float coord, float size, bool mirror = false)
|
||||
{
|
||||
const float t = coord / size;
|
||||
return UV_LO + UV_SPAN * (mirror ? 1.f - t : t);
|
||||
}
|
||||
|
||||
// One UV per triangle corner: u follows x, v follows y. `mirror_u` mirrors u, which is what the tests
|
||||
// use to show the bake is really reading the channel rather than projecting the position.
|
||||
static std::vector<Vec2f> sheet_uv_channel(const indexed_triangle_set &sheet, float size, bool mirror_u = false)
|
||||
{
|
||||
std::vector<Vec2f> uv(sheet.indices.size() * 3);
|
||||
for (size_t f = 0; f < sheet.indices.size(); ++f)
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const Vec3f &p = sheet.vertices[size_t(sheet.indices[f][k])];
|
||||
uv[f * 3 + size_t(k)] = Vec2f(sheet_uv_at(p.x(), size, mirror_u), sheet_uv_at(p.y(), size));
|
||||
}
|
||||
return uv;
|
||||
}
|
||||
|
||||
TEST_CASE("Override blend replaces the displacement under it instead of combining with it",
|
||||
"[TextureDisplacement]")
|
||||
{
|
||||
CHECK_THAT(blend_displacement(3.f, 1.f, TextureBlendMode::Override), WithinAbs(1.f, 1e-6f));
|
||||
// Nothing underneath yet is still just the value, so an Override layer at the bottom of the
|
||||
// stack behaves exactly like the Add the bake forces there.
|
||||
CHECK_THAT(blend_displacement(0.f, 1.f, TextureBlendMode::Override), WithinAbs(1.f, 1e-6f));
|
||||
// The sign carries, and a zero sample really does flatten what is under it - that is the point.
|
||||
CHECK_THAT(blend_displacement(3.f, -1.f, TextureBlendMode::Override), WithinAbs(-1.f, 1e-6f));
|
||||
CHECK_THAT(blend_displacement(3.f, 0.f, TextureBlendMode::Override), WithinAbs(0.f, 1e-6f));
|
||||
}
|
||||
|
||||
TEST_CASE("an Override layer leaves only its own relief where two layers overlap", "[TextureDisplacement]")
|
||||
{
|
||||
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
|
||||
|
||||
TextureDisplacementLayer base;
|
||||
base.slot = 0;
|
||||
base.depth_mm = 2.0f;
|
||||
base.tiling_scale = 5.0f;
|
||||
base.image_data = make_flat_gray_png(255); // full depth everywhere
|
||||
|
||||
TextureDisplacementLayer second = base;
|
||||
second.slot = 1;
|
||||
second.depth_mm = 0.5f;
|
||||
second.blend_mode = TextureBlendMode::Override;
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(cube);
|
||||
facets[1] = facets[0];
|
||||
|
||||
const indexed_triangle_set overridden = build_texture_displacement(cube, {base, second}, facets, classic_options());
|
||||
REQUIRE(overridden.vertices.size() == cube.vertices.size());
|
||||
|
||||
// Add would have piled the two up to 2.5 mm; Override keeps the upper layer's 0.5 mm alone.
|
||||
TextureDisplacementLayer added = second;
|
||||
added.blend_mode = TextureBlendMode::Add;
|
||||
const indexed_triangle_set summed = build_texture_displacement(cube, {base, added}, facets, classic_options());
|
||||
REQUIRE(summed.vertices.size() == cube.vertices.size());
|
||||
|
||||
for (size_t i = 0; i < cube.vertices.size(); ++i) {
|
||||
if (!above_resting_plane(cube, i))
|
||||
continue;
|
||||
CHECK_THAT((overridden.vertices[i] - cube.vertices[i]).norm(), WithinAbs(0.5f, 1e-3f));
|
||||
CHECK_THAT((summed.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.5f, 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("the ModelUV mapping displaces through the model's own UV channel", "[TextureDisplacement]")
|
||||
{
|
||||
constexpr float size = 10.f;
|
||||
const indexed_triangle_set sheet = make_flat_sheet(size, 1.f);
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 1.0f;
|
||||
layer.image_data = make_left_right_split_png();
|
||||
layer.tile_enabled = false; // the layout is the placement; nothing should repeat
|
||||
layer.projection_method = TextureProjectionMethod::ModelUV;
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(sheet);
|
||||
|
||||
const std::vector<Vec2f> uv = sheet_uv_channel(sheet, size);
|
||||
const std::vector<Vec2f> flipped = sheet_uv_channel(sheet, size, /* mirror_u */ true);
|
||||
|
||||
const indexed_triangle_set out = build_texture_displacement(sheet, {layer}, facets, classic_options(), {}, nullptr,
|
||||
Transform3d::Identity(), nullptr, nullptr, &uv);
|
||||
REQUIRE(out.vertices.size() == sheet.vertices.size());
|
||||
|
||||
// The image is black for u < 0.5 and white above it, and the channel's u rises with x across the
|
||||
// sheet, so it comes out flat on its left half and raised by the full depth on its right.
|
||||
size_t low = 0, high = 0;
|
||||
for (size_t i = 0; i < sheet.vertices.size(); ++i) {
|
||||
const float x = sheet.vertices[i].x();
|
||||
const float dz = out.vertices[i].z() - sheet.vertices[i].z();
|
||||
if (x < 0.4f * size) {
|
||||
CHECK_THAT(dz, WithinAbs(0.f, 1e-3f));
|
||||
++low;
|
||||
} else if (x > 0.6f * size) {
|
||||
CHECK_THAT(dz, WithinAbs(layer.depth_mm, 1e-3f));
|
||||
++high;
|
||||
}
|
||||
}
|
||||
REQUIRE(low > 0);
|
||||
REQUIRE(high > 0);
|
||||
|
||||
// Mirroring the channel mirrors the relief: the mapping really is the channel's, not a planar
|
||||
// projection of the position (which would be unchanged by it).
|
||||
const indexed_triangle_set mirrored = build_texture_displacement(sheet, {layer}, facets, classic_options(), {},
|
||||
nullptr, Transform3d::Identity(), nullptr, nullptr,
|
||||
&flipped);
|
||||
REQUIRE(mirrored.vertices.size() == sheet.vertices.size());
|
||||
for (size_t i = 0; i < sheet.vertices.size(); ++i) {
|
||||
const float x = sheet.vertices[i].x();
|
||||
if (x < 0.4f * size)
|
||||
CHECK_THAT(mirrored.vertices[i].z() - sheet.vertices[i].z(), WithinAbs(layer.depth_mm, 1e-3f));
|
||||
else if (x > 0.6f * size)
|
||||
CHECK_THAT(mirrored.vertices[i].z() - sheet.vertices[i].z(), WithinAbs(0.f, 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("a ModelUV layer falls back to Triplanar when the volume carries no UV channel",
|
||||
"[TextureDisplacement]")
|
||||
{
|
||||
constexpr float size = 10.f;
|
||||
const indexed_triangle_set sheet = make_flat_sheet(size, 2.f);
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 1.0f;
|
||||
layer.tiling_scale = 5.0f;
|
||||
layer.image_data = make_flat_gray_png(255);
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(sheet);
|
||||
|
||||
layer.projection_method = TextureProjectionMethod::Triplanar;
|
||||
const indexed_triangle_set as_triplanar = build_texture_displacement(sheet, {layer}, facets, classic_options());
|
||||
layer.projection_method = TextureProjectionMethod::ModelUV;
|
||||
const indexed_triangle_set no_channel = build_texture_displacement(sheet, {layer}, facets, classic_options());
|
||||
|
||||
REQUIRE(no_channel.vertices.size() == as_triplanar.vertices.size());
|
||||
for (size_t i = 0; i < as_triplanar.vertices.size(); ++i)
|
||||
CHECK_THAT((no_channel.vertices[i] - as_triplanar.vertices[i]).norm(), WithinAbs(0.f, 1e-5f));
|
||||
}
|
||||
|
||||
TEST_CASE("a bake with colour off on every layer reports no colour at all", "[TextureDisplacement]")
|
||||
{
|
||||
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 1.0f;
|
||||
layer.tiling_scale = 5.0f;
|
||||
layer.image_data = make_flat_rgb_png(200, 40, 10); // has colour to give...
|
||||
layer.color_enabled = false; // ...but is not asked for it
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(cube);
|
||||
|
||||
std::vector<uint8_t> triangle_color;
|
||||
TextureColorRequest request;
|
||||
request.quantize = [](const Vec3f &) { return 0; }; // would colour everything, if asked
|
||||
request.out_triangle = &triangle_color;
|
||||
|
||||
const indexed_triangle_set out = build_texture_displacement(cube, {layer}, facets, classic_options(), {}, &request);
|
||||
REQUIRE_FALSE(out.vertices.empty());
|
||||
// Every entry 0 ("this triangle takes no colour from the texture"), so the commit leaves whatever
|
||||
// the model was already painted with exactly as it was.
|
||||
for (const uint8_t entry : triangle_color)
|
||||
CHECK(entry == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("transfer_uv_channel resamples a UV channel onto a subdivided copy of the same surface",
|
||||
"[TextureDisplacement]")
|
||||
{
|
||||
constexpr float size = 10.f;
|
||||
const indexed_triangle_set coarse = make_flat_sheet(size, 5.f);
|
||||
const indexed_triangle_set fine = make_flat_sheet(size, 1.f);
|
||||
const std::vector<Vec2f> uv = sheet_uv_channel(coarse, size);
|
||||
|
||||
const std::vector<Vec2f> moved = transfer_uv_channel(coarse, uv, fine);
|
||||
REQUIRE(moved.size() == fine.indices.size() * 3);
|
||||
|
||||
// The channel is affine in position here (u = x/size, v = y/size), so a correct transfer
|
||||
// reproduces it exactly at every corner of the finer mesh.
|
||||
for (size_t f = 0; f < fine.indices.size(); ++f)
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const Vec3f &p = fine.vertices[size_t(fine.indices[f][k])];
|
||||
CHECK_THAT(moved[f * 3 + size_t(k)].x(), WithinAbs(sheet_uv_at(p.x(), size), 1e-4f));
|
||||
CHECK_THAT(moved[f * 3 + size_t(k)].y(), WithinAbs(sheet_uv_at(p.y(), size), 1e-4f));
|
||||
}
|
||||
|
||||
// A mismatched channel is refused rather than read past its end.
|
||||
CHECK(transfer_uv_channel(coarse, {Vec2f::Zero()}, fine).empty());
|
||||
CHECK(transfer_uv_channel(coarse, uv, indexed_triangle_set{}).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("the model's per-corner UVs survive a brush split and collapse to one per vertex",
|
||||
"[TextureDisplacement]")
|
||||
{
|
||||
constexpr float size = 10.f;
|
||||
const indexed_triangle_set sheet = make_flat_sheet(size, 5.f);
|
||||
const std::vector<Vec2f> uv = sheet_uv_channel(sheet, size);
|
||||
|
||||
// The whole sheet as a patch, with the source map the channel is read through.
|
||||
const TriangleMesh tm(sheet);
|
||||
TriangleSelector selector(tm);
|
||||
for (int f = 0; f < int(sheet.indices.size()); ++f)
|
||||
selector.set_facet(f, EnforcerBlockerType::ENFORCER);
|
||||
std::vector<int> source;
|
||||
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER, &source);
|
||||
REQUIRE(patch.indices.size() == source.size());
|
||||
|
||||
const std::vector<Vec2f> corner = compute_model_corner_uvs(sheet, uv, patch, source);
|
||||
REQUIRE(corner.size() == patch.indices.size() * 3);
|
||||
// An unsplit patch triangle *is* its source triangle, so its corners reproduce the channel.
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f)
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const Vec3f &p = patch.vertices[size_t(patch.indices[f][k])];
|
||||
CHECK_THAT(corner[f * 3 + size_t(k)].x(), WithinAbs(sheet_uv_at(p.x(), size), 1e-4f));
|
||||
CHECK_THAT(corner[f * 3 + size_t(k)].y(), WithinAbs(sheet_uv_at(p.y(), size), 1e-4f));
|
||||
}
|
||||
|
||||
const std::vector<Vec2f> per_vertex = collapse_corner_uvs(patch, corner);
|
||||
REQUIRE(per_vertex.size() == patch.vertices.size());
|
||||
for (size_t i = 0; i < patch.vertices.size(); ++i) {
|
||||
CHECK_THAT(per_vertex[i].x(), WithinAbs(sheet_uv_at(patch.vertices[i].x(), size), 1e-4f));
|
||||
CHECK_THAT(per_vertex[i].y(), WithinAbs(sheet_uv_at(patch.vertices[i].y(), size), 1e-4f));
|
||||
}
|
||||
|
||||
// Mismatched inputs report nothing rather than guessing.
|
||||
CHECK(compute_model_corner_uvs(sheet, {Vec2f::Zero()}, patch, source).empty());
|
||||
CHECK(compute_model_corner_uvs(sheet, uv, patch, {}).empty());
|
||||
CHECK(collapse_corner_uvs(patch, {Vec2f::Zero()}).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("facet paint is carried onto a rebuilt mesh by each facet's foot point", "[TextureDisplacement]")
|
||||
{
|
||||
constexpr float size = 10.f;
|
||||
const indexed_triangle_set coarse = make_flat_sheet(size, 2.5f);
|
||||
|
||||
// Two filaments, split down the middle - the pattern the transfer has to reproduce.
|
||||
const TriangleMesh coarse_mesh(coarse);
|
||||
TriangleSelector painter(coarse_mesh);
|
||||
for (size_t t = 0; t < coarse.indices.size(); ++t) {
|
||||
const stl_triangle_vertex_indices &tri = coarse.indices[t];
|
||||
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
|
||||
coarse.vertices[size_t(tri[2])].x()) / 3.f;
|
||||
painter.set_facet(int(t), x < 0.5f * size ? EnforcerBlockerType::Extruder1 : EnforcerBlockerType::Extruder2);
|
||||
}
|
||||
const TriangleSelector::TriangleSplittingData paint = painter.serialize();
|
||||
|
||||
// A finer mesh, lifted clear of the original surface the way a bake's relief lifts it. This is
|
||||
// what ModelVolume::restore_painting()'s 0.01 mm overlap remap cannot follow.
|
||||
indexed_triangle_set lifted = make_flat_sheet(size, 1.f);
|
||||
for (Vec3f &v : lifted.vertices)
|
||||
v.z() += 0.5f;
|
||||
|
||||
const std::vector<uint8_t> state = transfer_facet_paint(coarse, paint, lifted);
|
||||
REQUIRE(state.size() == lifted.indices.size());
|
||||
|
||||
size_t left = 0, right = 0;
|
||||
for (size_t t = 0; t < lifted.indices.size(); ++t) {
|
||||
const stl_triangle_vertex_indices &tri = lifted.indices[t];
|
||||
const float x = (lifted.vertices[size_t(tri[0])].x() + lifted.vertices[size_t(tri[1])].x() +
|
||||
lifted.vertices[size_t(tri[2])].x()) / 3.f;
|
||||
// Only away from the split: a facet straddling it may legitimately land on either side.
|
||||
if (x < 0.4f * size) {
|
||||
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder1));
|
||||
++left;
|
||||
} else if (x > 0.6f * size) {
|
||||
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder2));
|
||||
++right;
|
||||
}
|
||||
}
|
||||
REQUIRE(left > 0);
|
||||
REQUIRE(right > 0);
|
||||
|
||||
// An unpainted source has nothing to carry, which is not the same as carrying NONE everywhere:
|
||||
// the caller has to tell those apart to decide between resetting the annotation and writing one.
|
||||
CHECK(transfer_facet_paint(coarse, TriangleSelector::TriangleSplittingData{}, lifted).empty());
|
||||
CHECK(transfer_facet_paint(coarse, paint, indexed_triangle_set{}).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("an unpainted part of the source carries no paint onto the rebuilt mesh", "[TextureDisplacement]")
|
||||
{
|
||||
constexpr float size = 10.f;
|
||||
const indexed_triangle_set coarse = make_flat_sheet(size, 2.5f);
|
||||
|
||||
// Only the left half is painted at all; the right half must come back NONE rather than borrowing
|
||||
// the nearest painted colour from across the model.
|
||||
const TriangleMesh coarse_mesh(coarse);
|
||||
TriangleSelector painter(coarse_mesh);
|
||||
for (size_t t = 0; t < coarse.indices.size(); ++t) {
|
||||
const stl_triangle_vertex_indices &tri = coarse.indices[t];
|
||||
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
|
||||
coarse.vertices[size_t(tri[2])].x()) / 3.f;
|
||||
if (x < 0.5f * size)
|
||||
painter.set_facet(int(t), EnforcerBlockerType::Extruder3);
|
||||
}
|
||||
|
||||
const std::vector<uint8_t> state = transfer_facet_paint(coarse, painter.serialize(), coarse);
|
||||
REQUIRE(state.size() == coarse.indices.size());
|
||||
for (size_t t = 0; t < coarse.indices.size(); ++t) {
|
||||
const stl_triangle_vertex_indices &tri = coarse.indices[t];
|
||||
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
|
||||
coarse.vertices[size_t(tri[2])].x()) / 3.f;
|
||||
if (x < 0.4f * size)
|
||||
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder3));
|
||||
else if (x > 0.6f * size)
|
||||
CHECK(state[t] == uint8_t(EnforcerBlockerType::NONE));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user