Improve adaptive subdivision at border & fix some visual bugs

This commit is contained in:
ExPikaPaka
2026-08-26 09:13:50 +02:00
parent 165a1e9f4c
commit 6166bac17a
13 changed files with 914 additions and 153 deletions
+19
View File
@@ -356,6 +356,25 @@ std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &
if (border == halfedge_descriptor())
return std::nullopt; // no boundary at all -- a closed patch, which isn't a disk either
// ...and exactly one boundary loop. One connected component is not enough on its own: a patch with
// a hole in it (paint a ring, or erase the middle of a stroke) is a single component with two
// loops, and LSCM will happily "parameterize" it into an overlapping, folded-over chart rather
// than fail. Walk the border halfedges and check every one of them belongs to the longest loop.
{
std::size_t border_halfedges = 0;
for (halfedge_descriptor h : halfedges(cgal_mesh))
if (is_border(h, cgal_mesh))
++border_halfedges;
std::size_t loop_length = 0;
halfedge_descriptor h = border;
do {
++loop_length;
h = next(h, cgal_mesh);
} while (h != border && loop_length <= border_halfedges);
if (loop_length != border_halfedges)
return std::nullopt; // more than one boundary loop -- not a topological disk
}
using Point_2 = EpicKernel::Point_2;
using UV_pmap = _EpicMesh::Property_map<vertex_descriptor, Point_2>;
UV_pmap uv_map = cgal_mesh.add_property_map<vertex_descriptor, Point_2>("h:uv", Point_2(0, 0)).first;
+145 -40
View File
@@ -13,6 +13,9 @@
#include <unordered_map>
#include <unordered_set>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include "MeshBoolean.hpp"
#include "Model.hpp"
#include "PNGReadWrite.hpp"
@@ -897,7 +900,7 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
return per_vertex;
}
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer)
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
{
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
const Vec2f scaled = planar * scale;
@@ -905,7 +908,20 @@ Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &la
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
const float cs = std::cos(rad);
const float sn = std::sin(rad);
const Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
//
// 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 (aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect;
return rotated + layer.offset;
}
@@ -954,8 +970,12 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
if (texture.empty())
return 0.f;
// width / height of the height map, so a non-square image keeps its proportions (see
// 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;
auto sample_at = [&](const Vec2f &planar) {
return texture.sample(apply_uv_transform(planar, layer), layer.tile_enabled, layer.tile_method);
return texture.sample(apply_uv_transform(planar, layer, aspect), layer.tile_enabled, layer.tile_method);
};
// Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the
@@ -1118,8 +1138,13 @@ std::vector<float> patch_boundary_distance(const indexed_triangle_set &patch, co
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
const TextureDisplacementOptions &options)
const TextureDisplacementOptions &options,
const DisplacementProgressFn &progress)
{
// Returns true to keep going. An aborted run returns {} (see the header): an empty mesh is the
// one result no caller can mistake for a finished bake and commit onto the volume.
const auto report = [&progress](int percent) { return !progress || progress(percent); };
indexed_triangle_set mesh = base_mesh;
// TriangleSelector's vertex array starts with the mesh's own vertices (any extra ones, created
// where a brush stroke split a triangle, are appended after them), and get_facets_strict()
@@ -1183,8 +1208,14 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
}
}
if (!report(5))
return {};
std::vector<float> displacement(mesh.vertices.size(), 0.f);
std::vector<bool> displaced(mesh.vertices.size(), false);
// uint8_t rather than std::vector<bool>: the sampling loop below writes these from several
// threads at once, and vector<bool>'s bit packing makes writes to *distinct* elements a data
// race on the shared word.
std::vector<uint8_t> displaced(mesh.vertices.size(), 0);
// Union, over every layer, of that layer's patch border - the vertices the post-process smoothing
// holds when TextureDisplacementOptions::smooth_skip_border is set. A vertex on any patch's edge
// counts, which is the conservative choice: hold it rather than let one layer's smoothing melt the
@@ -1193,7 +1224,21 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
bool any_displacement = false;
const TriangleMesh selector_mesh(mesh);
// One selector for the whole stack, re-deserialized per layer. Its constructor computes
// its_face_neighbors() and its_face_normals() over the *entire* mesh, which on a subdivided model
// is by far the most expensive thing here - building a fresh one per layer paid that cost up to
// eight times over. reset() (what deserialize(..., true) calls) only rebuilds the vertex/triangle
// arrays; the neighbour and face-normal tables are immutable members and survive it.
TriangleSelector selector(selector_mesh);
bool selector_dirty = false;
const int layer_count = std::max(int(ordered_layers.size()), 1);
int layer_index = 0;
for (const TextureDisplacementLayer *layer : ordered_layers) {
// Progress spans 5..65% across the layers; the apply and smoothing passes take it from there.
if (!report(5 + (60 * layer_index++) / layer_count))
return {};
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
if (data.triangles_to_split.empty())
continue;
@@ -1202,8 +1247,9 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
if (height.empty())
continue;
TriangleSelector selector(selector_mesh);
selector.deserialize(data, false);
// needs_reset only from the second layer on: the selector is already pristine on the first.
selector.deserialize(data, selector_dirty);
selector_dirty = true;
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
if (patch.indices.empty())
@@ -1287,8 +1333,13 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
const float sign = layer->invert ? -1.f : 1.f;
// A vertex may be reached by several of the patch's triangles; each must fold into the
// running total exactly once, or a Multiply/Subtract layer would apply two or three times
// over depending on how many painted triangles happen to share the vertex.
std::vector<bool> visited(patch.vertices.size(), false);
// over depending on how many painted triangles happen to share the vertex. Collecting the
// unique list up front (cheap, one pass) is also what lets the expensive part - the texture
// sampling, which is three bilinear fetches plus three pow()s per vertex for triplanar - run
// in parallel below, instead of serially inside the triangle walk.
std::vector<int> layer_vertices;
std::vector<char> visited(patch.vertices.size(), 0);
layer_vertices.reserve(patch.vertices.size());
for (const stl_triangle_vertex_indices &tri : patch.indices)
for (int i = 0; i < 3; ++i) {
const int vi = tri[i];
@@ -1296,28 +1347,53 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
// they carry no displacement of their own and are not part of the output mesh.
if (vi >= int(mesh.vertices.size()) || (pin_boundary && is_boundary[vi]) || visited[vi])
continue;
visited[vi] = true;
visited[vi] = 1;
layer_vertices.push_back(vi);
}
if (layer_vertices.empty())
continue;
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[size_t(vi)];
std::vector<float> sampled(layer_vertices.size(), 0.f);
tbb::parallel_for(tbb::blocked_range<size_t>(0, layer_vertices.size()),
[&](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 = 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
// *inwards* - see TextureDisplacementLayer::midlevel. At the default of 0 this is
// exactly the old outward-only behaviour.
const float edge_w = edge_weight.empty() ? 1.f : edge_weight[size_t(vi)];
const float signed_height = (h - layer->midlevel) * layer->depth_mm * sign * edge_w;
displacement[size_t(vi)] = blend_displacement(displacement[size_t(vi)], signed_height,
displaced[size_t(vi)] ? layer->blend_mode : TextureBlendMode::Add);
// The first layer to reach a vertex has nothing underneath it to blend with, so it
// always starts the total off additively - a Multiply/Divide against an implicit
// zero base would otherwise annihilate (or blow up) it, which is never what the
// user means by putting a mask on the bottom of the stack.
displaced[size_t(vi)] = true;
any_displacement = true;
sampled[k] = (h - layer->midlevel) * layer->depth_mm * sign;
}
});
for (size_t k = 0; k < layer_vertices.size(); ++k) {
const size_t vi = size_t(layer_vertices[k]);
// The first layer to reach a vertex has nothing underneath it to blend with, so it
// always starts the total off additively - a Multiply/Divide against an implicit
// zero base would otherwise annihilate (or blow up) it, which is never what the
// user means by putting a mask on the bottom of the stack.
const float accumulated = displacement[vi];
const float blended = blend_displacement(accumulated, sampled[k],
displaced[vi] ? layer->blend_mode : TextureBlendMode::Add);
// Edge smoothing fades this layer's *effect*, not its input. Scaling the input instead is
// only correct for Add/Subtract, whose neutral value is 0: on a Multiply layer a faded
// input approaches 0, which annihilates everything beneath it at the rim rather than
// leaving it alone, and on a Divide layer it approaches the 0.05 divisor floor, which
// amplifies the relief underneath by up to 20x exactly where it was meant to fade out.
// Interpolating the blended result back toward the accumulated total is the neutral
// element for every mode at once, and reduces to the old formula exactly for Add.
const float edge_w = edge_weight.empty() ? 1.f : edge_weight[vi];
displacement[vi] = accumulated + (blended - accumulated) * edge_w;
displaced[vi] = 1;
}
any_displacement = true;
}
if (!report(65))
return {};
if (!any_displacement)
return mesh;
@@ -1332,12 +1408,22 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
// neighbours are those pinned outsiders: relaxing it would drag the rim of the relief back down and
// leave the pattern looking half-melted right where it meets the edge.
if (options.smooth_enabled && options.smooth_strength > 0.f && options.smooth_iterations > 0) {
if (!report(70))
return {};
std::vector<uint8_t> movable(mesh.vertices.size(), 0);
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
movable[vi] = (displaced[vi] && !(options.smooth_skip_border && on_patch_border[vi])) ? 1 : 0;
smooth_mesh_vertices(mesh, movable, options.smooth_strength, options.smooth_iterations);
// The pass hook only *stops* the relaxation early; the report(99) below is what turns a
// cancellation into an empty (uncommittable) result, since a cancelled run keeps reporting
// cancelled.
smooth_mesh_vertices(mesh, movable, options.smooth_strength, options.smooth_iterations,
progress ? DisplacementProgressFn([&report, it = options.smooth_iterations](int pass) {
return report(70 + (29 * (pass + 1)) / std::max(it, 1));
}) : DisplacementProgressFn{});
}
if (!report(99))
return {};
return mesh;
}
@@ -1352,7 +1438,7 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
}
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
int iterations)
int iterations, const DisplacementProgressFn &on_pass)
{
if (iterations <= 0 || mesh.vertices.empty() || movable.size() != mesh.vertices.size())
return;
@@ -1391,15 +1477,21 @@ void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t>
std::vector<Vec3f> prev;
for (int it = 0; it < iterations; ++it) {
prev = mesh.vertices;
for (size_t v = 0; v < nv; ++v) {
if (!movable[v] || start[v] == start[v + 1])
continue;
Vec3f sum = Vec3f::Zero();
for (int k = start[v]; k < start[v + 1]; ++k)
sum += prev[size_t(nbr[size_t(k)])];
const Vec3f avg = sum / float(start[v + 1] - start[v]);
mesh.vertices[v] = prev[v] + (avg - prev[v]) * strength;
}
// Each vertex reads only from `prev` and writes only its own slot, so the sweep parallelises
// with no synchronisation at all.
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &range) {
for (size_t v = range.begin(); v < range.end(); ++v) {
if (!movable[v] || start[v] == start[v + 1])
continue;
Vec3f sum = Vec3f::Zero();
for (int k = start[v]; k < start[v + 1]; ++k)
sum += prev[size_t(nbr[size_t(k)])];
const Vec3f avg = sum / float(start[v + 1] - start[v]);
mesh.vertices[v] = prev[v] + (avg - prev[v]) * strength;
}
});
if (on_pass && !on_pass(it))
return; // cancelled: leave the passes done so far in place, the caller decides what to do
}
}
@@ -1552,7 +1644,8 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
const std::vector<uint8_t> &refine_region,
float target_edge_length_mm, int max_triangles,
std::vector<int> *out_source, const HeightFieldSampler &sampler,
float chord_tolerance_mm, float min_edge_length_mm)
float chord_tolerance_mm, float min_edge_length_mm,
float border_edge_length_mm)
{
// Neighbour slots that are not a triangle index.
constexpr int NB_BOUNDARY = -1; // open edge: terminal on its own, bisected from this side alone
@@ -1590,12 +1683,13 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
const bool feature_mode = bool(sampler) && chord_tolerance_mm > 0.f;
const float min_floor_sq = min_edge_length_mm > 0.f ? min_edge_length_mm * min_edge_length_mm : 0.f;
const float target_sq = target_edge_length_mm > 0.f ? target_edge_length_mm * target_edge_length_mm : 0.f;
const float border_sq = border_edge_length_mm > 0.f ? border_edge_length_mm * border_edge_length_mm : 0.f;
// refine_region is indexed by input-triangle index, and every triangle's src stays in that range
// (children inherit their parent's src), so a wrong size would be an out-of-bounds read. Guard it.
if (refine_region.size() != mesh.indices.size() || int(tris.size()) + 2 > max_triangles)
return emit();
if (!feature_mode && target_sq <= 0.f)
if (!feature_mode && target_sq <= 0.f && border_sq <= 0.f)
return emit(); // no criterion at all
if (std::none_of(refine_region.begin(), refine_region.end(), [](uint8_t v) { return v != 0; }))
return emit(); // nothing flagged: no-op
@@ -1720,16 +1814,27 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
// reach. Triangles outside the region always score 0 - they are only ever touched by the conformal
// closure below, never refined on their own account.
auto priority = [&](int ti) -> float {
const Tri &t = tris[ti];
if (refine_region[t.src] == 0)
const Tri &t = tris[ti];
const uint8_t flags = refine_region[t.src];
if (flags == 0)
return 0.f;
const int le = longest_local(ti);
const float ll = elen_sq(t.v[le], t.v[(le + 1) % 3]);
if (ll <= min_floor_sq)
return 0.f; // at the resolution floor - also what stops a sharp texture step going forever
float p = (target_sq > 0.f) ? ll / target_sq : 0.f;
if (feature_mode)
p = std::max(p, detail_error(ti) / chord_tolerance_mm);
float p = 0.f;
if (flags & REFINE_PAINTED) {
p = (target_sq > 0.f) ? ll / target_sq : 0.f;
if (feature_mode)
p = std::max(p, detail_error(ti) / chord_tolerance_mm);
}
// The band straddling the paint's edge, refined by plain edge length. Deliberately *not* run
// through detail_error(): outside the paint the sampler still reports full relief (it has no
// per-point paint test), so the chord test there would chase texture detail on a surface the
// bake is going to leave flat. Length alone is what this band needs - the error it is fixing
// is the size of the triangles spanning the displacement step, not the curvature of anything.
if ((flags & REFINE_BORDER) && border_sq > 0.f)
p = std::max(p, ll / border_sq);
return p;
};
+44 -4
View File
@@ -22,6 +22,14 @@ namespace Slic3r {
class ModelVolume;
// Bits of subdivide_mesh_adaptive()'s per-triangle `refine_region` mask. See that function.
static constexpr uint8_t REFINE_PAINTED = 1;
static constexpr uint8_t REFINE_BORDER = 2;
// Progress/cancellation hook for the (potentially multi-second) bake. Called with a 0..100
// percentage; return false to abort. See build_texture_displacement().
using DisplacementProgressFn = std::function<bool(int)>;
// Maximum number of simultaneous texture-displacement layers a single ModelVolume can hold.
// Each layer owns its own paint mask (ModelVolume::texture_displacement_facet(slot)), so this
// is also the number of independent EnforcerBlockerType selectors kept per volume.
@@ -370,7 +378,11 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// LSCM's per-patch UV solve through the same scale/rotate/offset controls as every other
// projection method, without going through project_texture_displacement_uv()'s own dispatch
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer);
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
// 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.
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 -
@@ -532,10 +544,18 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
// mesh-boolean ops) the way TriangleSelector::remap_painting() does for the other paint channels.
// Such operations will silently drop any unbaked texture-displacement paint on the affected
// volume. This is an explicit extension point for a later phase, not an oversight.
//
// `progress`, when set, is called from the worker thread with a 0..100 completion percentage as the
// bake proceeds. Returning false from it aborts the run, which then returns an *empty* mesh - never
// a partially displaced one, so a cancelled bake can never be mistaken for a finished result and
// committed. It exists because this is the one call in the feature that can take seconds on a
// subdivided mesh, and without it the progress notification the Job framework puts on screen sits at
// 0% for the whole run and offers no way to close it (its close button only appears at 100%).
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
const TextureDisplacementOptions &options = {});
const TextureDisplacementOptions &options = {},
const DisplacementProgressFn &progress = {});
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
// `volume` and forwards to the overload above.
@@ -551,8 +571,10 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
// after displacing it" runs, and it is also safe to run standalone on an already baked mesh.
// `strength` is clamped to [0, 1]; 0 iterations, an empty/mis-sized `movable`, or an all-false one
// leave the mesh untouched.
// `on_pass`, when set, is called with the 0-based index of each completed pass; returning false stops
// the relaxation there, leaving the passes already done in place.
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
int iterations);
int iterations, const DisplacementProgressFn &on_pass = {});
// Returns a scalar height (in mm - a displacement magnitude) at a surface point, given that point's
// position and interpolated normal. This is what feature-adaptive subdivision samples to decide
@@ -638,12 +660,30 @@ indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, fl
// the input triangle that output triangle i descends from (children inherit their parent's index), so
// a caller can carry per-triangle data - e.g. a paint mask - across the topology change without a
// geometric remap.
//
// `refine_region` is a **bitmask** per input triangle, not a plain flag:
// bit 0 (REFINE_PAINTED) - inside the painted area: refine by the length baseline and, in feature
// mode, by the chord-error test.
// bit 1 (REFINE_BORDER) - inside the band straddling the paint's edge: refine by
// `border_edge_length_mm` alone.
// A value of 1 therefore means exactly what a plain 1 always meant, and 0 still means "never touch
// this triangle except through the conformal closure".
//
// The border band exists because the chord-error test is blind to the one discontinuity the bake
// actually creates. `make_combined_displacement_sampler()` evaluates the height field everywhere,
// with no per-point paint test, so where the paint *stops* it keeps reporting full relief - smooth
// and low-curvature - while the baked surface steps from full displacement to zero. The test sees no
// error there and leaves the transition at whatever density the input had, which is what turns the
// rim of an unpainted island into a ring of large, steeply tilted triangles. Refining that band by
// plain edge length is bounded (it is a thin ring, and a length target always terminates) and needs
// no paint-aware sampler.
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
const std::vector<uint8_t> &refine_region,
float target_edge_length_mm, int max_triangles = 1000000,
std::vector<int> *out_source = nullptr,
const HeightFieldSampler &sampler = nullptr,
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f);
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f,
float border_edge_length_mm = 0.f);
} // namespace Slic3r
+9 -2
View File
@@ -1519,9 +1519,11 @@ void TriangleSelector::get_facets(std::vector<indexed_triangle_set>& facets_per_
}
}
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state) const
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source) const
{
indexed_triangle_set out;
if (out_source)
out_source->clear();
size_t num_vertices = 0;
for (const Vertex &v : m_vertices)
@@ -1535,8 +1537,13 @@ indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType sta
out.vertices.emplace_back(v.v);
}
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle)
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle) {
this->get_facets_strict_recursive(m_triangles[itriangle], m_neighbors[itriangle], state, out.indices);
// Everything the recursion just appended came from this original triangle, whatever depth it
// was split to. Recording it here keeps the recursive helpers untouched.
if (out_source)
out_source->resize(out.indices.size(), itriangle);
}
for (auto &triangle : out.indices)
for (int i = 0; i < 3; ++ i)
+8 -1
View File
@@ -332,7 +332,14 @@ public:
// Get facets at a given state. Don't triangulate T-joints.
indexed_triangle_set get_facets(EnforcerBlockerType state) const;
// Get facets at a given state. Triangulate T-joints.
indexed_triangle_set get_facets_strict(EnforcerBlockerType state) const;
// Sub-triangles in `state`, with the *whole* mesh's referenced vertex array (only .indices is
// filtered by state, so two calls with different states share one indexing).
//
// `out_source`, when given, is filled parallel to the returned .indices with the index of the
// original mesh triangle each sub-triangle came from. That is what lets a caller carry partial
// paint - the pieces of a triangle a brush stroke only partly covered - across a refinement of
// the same surface, instead of having to round each source triangle to wholly painted or not.
indexed_triangle_set get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source = nullptr) const;
// Get edges around the selected area by seed fill.
std::vector<Vec2i32> get_seed_fill_contour() const;