mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-07-22 06:04:01 -06:00
Renamed create_face_neighbors_index() to its_face_edge_ids().
Renamed its_create_neighbors_index() / its_create_neighbors_index_par() to its_face_neighbors() / its_face_neighbors_par(). New variant of its_face_edge_ids() to create edge IDs from face neighbors. Fixed some incorrect use of _NDEBUG, it should be NDEBUG. PrintObject::slice_support_volumes() returns newly Polygons, which are cheaper than ExPolygons. Updated SeamPlacer and SupportMaterial to use regions defined as Polygons, not ExPolygons. TriangleSelector::get_facets_strict() returning a patch with T-joints retriangulated. New slice_mesh_slabs() - slicing projections of a triangle patch into top / bottom layers of slices, for MMU top / bottom segmentation. TriangleMeshSlicer - use 64 mutexes instead of one when scattering sliced triangles into layers. This makes a big difference on modern many core desktop computers. When applying MM segmented regions to input regions, the split regions are now re-merged with 10x higher positive offset epsilon to avoid creating gaps. When testing for existence of paint-on supports or seam, use a more efficient has_facets() test, which does not deserialize into the expensive TriangleSelector tree structure. GLIndexedVertexArray newly uses Eigen::AlignedBox<float, 3> for efficiency instead of our double based BoundingBoxf3. Improved MMU painting refresh speed by optimizing generation of the vertex buffers. Refactored MMU segmentation - projection of painted surfaces from top / bottom. 1) Parallelized. 2) Using the new slice_mesh_slabs() instead of projecting one triangle by the other and merging them with Clipper.
This commit is contained in:
parent
d08a70478e
commit
0d70a2be69
23 changed files with 1357 additions and 489 deletions
|
@ -3,6 +3,7 @@
|
|||
#include "Tesselate.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
#include "TriangleMeshSlicer.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
@ -14,6 +15,10 @@
|
|||
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#ifndef NDEBUG
|
||||
// #define EXPENSIVE_DEBUG_CHECKS
|
||||
#endif // NDEBUG
|
||||
|
||||
#if 0
|
||||
#define DEBUG
|
||||
#define _DEBUG
|
||||
|
@ -64,6 +69,8 @@ public:
|
|||
|
||||
bool is_seed_candidate() const { return (this->flags & NO_SEED) == 0 && ! this->skip(); }
|
||||
void set_no_seed(bool set) { if (set) this->flags |= NO_SEED; else this->flags &= ~NO_SEED; }
|
||||
|
||||
void reverse() { std::swap(a, b); std::swap(a_id, b_id); std::swap(edge_a_id, edge_b_id); }
|
||||
|
||||
// Inherits Point a, b
|
||||
// For each line end point, either {a,b}_id or {a,b}edge_a_id is set, the other is left to -1.
|
||||
|
@ -82,7 +89,9 @@ public:
|
|||
// Two vertices are aligned with the cutting plane, the third vertex is above the cutting plane.
|
||||
Bottom,
|
||||
// All three vertices of a face are aligned with the cutting plane.
|
||||
Horizontal
|
||||
Horizontal,
|
||||
// Edge
|
||||
Slab,
|
||||
};
|
||||
|
||||
// feGeneral, feTop, feBottom, feHorizontal
|
||||
|
@ -102,6 +111,15 @@ public:
|
|||
SKIP = 0x200,
|
||||
};
|
||||
uint32_t flags { 0 };
|
||||
|
||||
#ifndef NDEBUG
|
||||
enum class Source {
|
||||
BottomPlane,
|
||||
TopPlane,
|
||||
Slab,
|
||||
};
|
||||
Source source { Source::BottomPlane };
|
||||
#endif // NDEBUG
|
||||
};
|
||||
|
||||
using IntersectionLines = std::vector<IntersectionLine>;
|
||||
|
@ -119,7 +137,7 @@ static FacetSliceType slice_facet(
|
|||
// 3 vertices of the triangle, XY scaled. Z scaled or unscaled (same as slice_z).
|
||||
const stl_vertex *vertices,
|
||||
const stl_triangle_vertex_indices &indices,
|
||||
const Vec3i &edge_neighbor,
|
||||
const Vec3i &edge_ids,
|
||||
const int idx_vertex_lowest,
|
||||
const bool horizontal,
|
||||
IntersectionLine &line_out)
|
||||
|
@ -138,7 +156,7 @@ static FacetSliceType slice_facet(
|
|||
{
|
||||
int k = (idx_vertex_lowest + j) % 3;
|
||||
int l = (k + 1) % 3;
|
||||
edge_id = edge_neighbor(k);
|
||||
edge_id = edge_ids(k);
|
||||
a_id = indices[k];
|
||||
a = vertices + k;
|
||||
b_id = indices[l];
|
||||
|
@ -284,11 +302,11 @@ void slice_facet_at_zs(
|
|||
const std::vector<Vec3f> &mesh_vertices,
|
||||
const TransformVertex &transform_vertex_fn,
|
||||
const stl_triangle_vertex_indices &indices,
|
||||
const Vec3i &facet_neighbors,
|
||||
const Vec3i &edge_ids,
|
||||
// Scaled or unscaled zs. If vertices have their zs scaled or transform_vertex_fn scales them, then zs have to be scaled as well.
|
||||
const std::vector<float> &zs,
|
||||
std::vector<IntersectionLines> &lines,
|
||||
boost::mutex &lines_mutex)
|
||||
std::array<std::mutex, 64> &lines_mutex)
|
||||
{
|
||||
stl_vertex vertices[3] { transform_vertex_fn(mesh_vertices[indices(0)]), transform_vertex_fn(mesh_vertices[indices(1)]), transform_vertex_fn(mesh_vertices[indices(2)]) };
|
||||
|
||||
|
@ -299,43 +317,376 @@ void slice_facet_at_zs(
|
|||
// find layer extents
|
||||
auto min_layer = std::lower_bound(zs.begin(), zs.end(), min_z); // first layer whose slice_z is >= min_z
|
||||
auto max_layer = std::upper_bound(min_layer, zs.end(), max_z); // first layer whose slice_z is > max_z
|
||||
int idx_vertex_lowest = (vertices[1].z() == min_z) ? 1 : ((vertices[2].z() == min_z) ? 2 : 0);
|
||||
|
||||
for (auto it = min_layer; it != max_layer; ++ it) {
|
||||
IntersectionLine il;
|
||||
int idx_vertex_lowest = (vertices[1].z() == min_z) ? 1 : ((vertices[2].z() == min_z) ? 2 : 0);
|
||||
if (slice_facet(*it, vertices, indices, facet_neighbors, idx_vertex_lowest, min_z == max_z, il) == FacetSliceType::Slicing &&
|
||||
il.edge_type != IntersectionLine::FacetEdgeType::Horizontal) {
|
||||
// Ignore horizontal triangles. Any valid horizontal triangle must have a vertical triangle connected, otherwise the part has zero volume.
|
||||
boost::lock_guard<boost::mutex> l(lines_mutex);
|
||||
lines[it - zs.begin()].emplace_back(il);
|
||||
// Ignore horizontal triangles. Any valid horizontal triangle must have a vertical triangle connected, otherwise the part has zero volume.
|
||||
if (min_z != max_z && slice_facet(*it, vertices, indices, edge_ids, idx_vertex_lowest, false, il) == FacetSliceType::Slicing) {
|
||||
assert(il.edge_type != IntersectionLine::FacetEdgeType::Horizontal);
|
||||
size_t slice_id = it - zs.begin();
|
||||
boost::lock_guard<std::mutex> l(lines_mutex[slice_id >> 6]);
|
||||
lines[slice_id].emplace_back(il);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename TransformVertex, typename ThrowOnCancel>
|
||||
inline std::vector<IntersectionLines> slice_make_lines(
|
||||
static inline std::vector<IntersectionLines> slice_make_lines(
|
||||
const std::vector<stl_vertex> &vertices,
|
||||
const TransformVertex &transform_vertex_fn,
|
||||
const std::vector<stl_triangle_vertex_indices> &indices,
|
||||
const std::vector<Vec3i> &face_neighbors,
|
||||
const std::vector<Vec3i> &face_edge_ids,
|
||||
const std::vector<float> &zs,
|
||||
const ThrowOnCancel throw_on_cancel_fn)
|
||||
{
|
||||
std::vector<IntersectionLines> lines(zs.size(), IntersectionLines());
|
||||
boost::mutex lines_mutex;
|
||||
std::array<std::mutex, 64> lines_mutex;
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<int>(0, int(indices.size())),
|
||||
[&vertices, &transform_vertex_fn, &indices, &face_neighbors, &zs, &lines, &lines_mutex, throw_on_cancel_fn](const tbb::blocked_range<int> &range) {
|
||||
[&vertices, &transform_vertex_fn, &indices, &face_edge_ids, &zs, &lines, &lines_mutex, throw_on_cancel_fn](const tbb::blocked_range<int> &range) {
|
||||
for (int face_idx = range.begin(); face_idx < range.end(); ++ face_idx) {
|
||||
if ((face_idx & 0x0ffff) == 0)
|
||||
throw_on_cancel_fn();
|
||||
slice_facet_at_zs(vertices, transform_vertex_fn, indices[face_idx], face_neighbors[face_idx], zs, lines, lines_mutex);
|
||||
slice_facet_at_zs(vertices, transform_vertex_fn, indices[face_idx], face_edge_ids[face_idx], zs, lines, lines_mutex);
|
||||
}
|
||||
}
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
// For projecting triangle sets onto slice slabs.
|
||||
struct SlabLines {
|
||||
// Intersection lines of a slice with a triangle set, CCW oriented.
|
||||
std::vector<IntersectionLines> at_slice;
|
||||
// Projections of triangle set boundary lines into layer below (for projection from the top)
|
||||
// or into layer above (for projection from the bottom).
|
||||
// In both cases the intersection liens are CCW oriented.
|
||||
std::vector<IntersectionLines> between_slices;
|
||||
};
|
||||
|
||||
// Orientation of the face normal in regard to a XY plane pointing upwards.
|
||||
enum class FaceOrientation : char {
|
||||
// Z component of the normal is positive.
|
||||
Up,
|
||||
// Z component of the normal is negative.
|
||||
Down,
|
||||
// Z component of the normal is zero.
|
||||
Vertical,
|
||||
// Triangle is degenerate, thus its normal is undefined. We may want to slice the degenerate triangles
|
||||
// because of the connectivity information they carry.
|
||||
Degenerate
|
||||
};
|
||||
|
||||
template<bool ProjectionFromTop>
|
||||
void slice_facet_with_slabs(
|
||||
// Scaled or unscaled vertices. transform_vertex_fn may scale zs.
|
||||
const std::vector<Vec3f> &mesh_vertices,
|
||||
const stl_triangle_vertex_indices &indices,
|
||||
const Vec3i &facet_neighbors,
|
||||
const Vec3i &facet_edge_ids,
|
||||
// Increase edge_ids at the top plane of the slab edges by num_edges to allow chaining
|
||||
// from bottom plane of the slab to the top plane of the slab and vice versa.
|
||||
const int num_edges,
|
||||
const std::vector<float> &zs,
|
||||
SlabLines &lines,
|
||||
std::array<std::mutex, 64> &lines_mutex)
|
||||
{
|
||||
stl_vertex vertices[3] { mesh_vertices[indices(0)], mesh_vertices[indices(1)], mesh_vertices[indices(2)] };
|
||||
|
||||
// find facet extents
|
||||
const float min_z = fminf(vertices[0].z(), fminf(vertices[1].z(), vertices[2].z()));
|
||||
const float max_z = fmaxf(vertices[0].z(), fmaxf(vertices[1].z(), vertices[2].z()));
|
||||
const bool horizontal = min_z == max_z;
|
||||
|
||||
// find layer extents
|
||||
auto min_layer = std::lower_bound(zs.begin(), zs.end(), min_z); // first layer whose slice_z is >= min_z
|
||||
auto max_layer = std::upper_bound(min_layer, zs.end(), max_z); // first layer whose slice_z is > max_z
|
||||
assert(min_layer == zs.end() ? max_layer == zs.end() : *min_layer >= min_z);
|
||||
assert(max_layer == zs.end() || *max_layer > max_z);
|
||||
|
||||
auto emit_slab_edge = [&lines, &lines_mutex, num_edges](IntersectionLine il, size_t slab_id, bool reverse) {
|
||||
if (reverse)
|
||||
il.reverse();
|
||||
boost::lock_guard<std::mutex> l(lines_mutex[(slab_id + 32) >> 6]);
|
||||
lines.between_slices[slab_id].emplace_back(il);
|
||||
};
|
||||
|
||||
if (min_layer == max_layer || horizontal) {
|
||||
// Horizontal face or a nearly horizontal face that fits between two layers or below the bottom most or above the top most layer.
|
||||
assert(horizontal || zs.empty() || max_z < zs.front() || min_z > zs.back() ||
|
||||
(min_layer == max_layer && min_layer != zs.end() && min_layer != zs.begin() && *(min_layer - 1) < min_z && *min_layer > max_z));
|
||||
size_t slab_id;
|
||||
if (horizontal && min_layer != zs.end() && *min_layer == min_z) {
|
||||
// slicing the triangle.
|
||||
assert(min_layer != max_layer);
|
||||
slab_id = min_layer - zs.begin();
|
||||
} else {
|
||||
if (ProjectionFromTop) {
|
||||
if (max_layer == zs.begin()) {
|
||||
// Not slicing the triangle and it is below the lowest layer.
|
||||
return;
|
||||
} else {
|
||||
// Not slicing the triangle and it could be projected into a slab.
|
||||
slab_id = max_layer - zs.begin();
|
||||
}
|
||||
} else {
|
||||
// projection from bottom
|
||||
if (min_layer == zs.end()) {
|
||||
// Not slicing the triangle and it is above the highest layer.
|
||||
return;
|
||||
} else {
|
||||
// Not slicing the triangle and it could be projected into a slab.
|
||||
slab_id = min_layer - zs.begin();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ProjectionFromTop)
|
||||
-- slab_id;
|
||||
for (int iedge = 0; iedge < 3; ++ iedge)
|
||||
if (facet_neighbors(iedge) == -1) {
|
||||
int i = iedge;
|
||||
int j = next_idx_modulo(i, 3);
|
||||
assert(ProjectionFromTop ? vertices[i].z() >= zs[slab_id] : vertices[i].z() <= zs[slab_id]);
|
||||
assert(ProjectionFromTop ? vertices[j].z() >= zs[slab_id] : vertices[j].z() <= zs[slab_id]);
|
||||
emit_slab_edge(
|
||||
IntersectionLine {
|
||||
{ to_2d(vertices[i]).cast<coord_t>(), to_2d(vertices[j]).cast<coord_t>() },
|
||||
indices(i), indices(j), -1, -1, IntersectionLine::FacetEdgeType::Slab
|
||||
},
|
||||
slab_id, ! ProjectionFromTop);
|
||||
}
|
||||
} else {
|
||||
int idx_vertex_lowest = (vertices[1].z() == min_z) ? 1 : ((vertices[2].z() == min_z) ? 2 : 0);
|
||||
IntersectionLine il_prev;
|
||||
for (auto it = min_layer; it != max_layer; ++ it) {
|
||||
IntersectionLine il;
|
||||
auto type = slice_facet(*it, vertices, indices, facet_edge_ids, idx_vertex_lowest, false, il);
|
||||
if (type == FacetSliceType::Slicing) {
|
||||
if (! ProjectionFromTop)
|
||||
il.reverse();
|
||||
size_t line_id = it - zs.begin();
|
||||
boost::lock_guard<std::mutex> l(lines_mutex[line_id >> 6]);
|
||||
lines.at_slice[line_id].emplace_back(il);
|
||||
} else if (type == FacetSliceType::Cutting) {
|
||||
// One edge is in plane, the 3rd vertex is above the plane. In case this edge has a neighbor,
|
||||
// its opposite edge is added by slicing the neighboring triangle. Only if this edge has no neighbor,
|
||||
// add this edge to lines.
|
||||
assert(il.a_id != -1 && il.b_id != -1);
|
||||
assert(il.edge_a_id == -1 && il.edge_b_id == -1);
|
||||
// Identify edge ID from the edge vertices.
|
||||
int edge_id = il.a_id == indices(0) ? 0 : il.a_id == indices(1) ? 1 : 2;
|
||||
assert(il.a_id == indices(edge_id));
|
||||
assert(il.b_id == indices(next_idx_modulo(edge_id, 3)));
|
||||
if (facet_neighbors(edge_id) == -1) {
|
||||
// Open edge.
|
||||
if (! ProjectionFromTop)
|
||||
il.reverse();
|
||||
size_t line_id = it - zs.begin();
|
||||
boost::lock_guard<std::mutex> l(lines_mutex[line_id >> 6]);
|
||||
lines.at_slice[line_id].emplace_back(il);
|
||||
}
|
||||
}
|
||||
if (! ProjectionFromTop || it != zs.begin()) {
|
||||
size_t slab_id = it - zs.begin();
|
||||
if (ProjectionFromTop)
|
||||
-- slab_id;
|
||||
// Try to project unbound edges.
|
||||
for (int iedge = 0; iedge < 3; ++ iedge)
|
||||
if (facet_neighbors(iedge) == -1) {
|
||||
// Unbound edge.
|
||||
int edge_id = facet_edge_ids(iedge);
|
||||
bool intersects_this = il.edge_a_id == edge_id || il.edge_b_id == edge_id;
|
||||
bool intersects_prev = il_prev.edge_a_id == edge_id || il_prev.edge_b_id == edge_id;
|
||||
int i = iedge;
|
||||
int j = next_idx_modulo(i, 3);
|
||||
assert((! intersects_this && ! intersects_prev) || vertices[j].z() != vertices[i].z());
|
||||
bool edge_up = vertices[j].z() > vertices[i].z();
|
||||
if (intersects_this && intersects_prev) {
|
||||
// Intersects both, emit the segment between these intersections.
|
||||
Line l(il_prev.edge_a_id == edge_id ? il_prev.a : il_prev.b,
|
||||
il.edge_a_id == edge_id ? il.a : il.b);
|
||||
emit_slab_edge(
|
||||
IntersectionLine { l, -1, -1, edge_id, edge_id + num_edges, IntersectionLine::FacetEdgeType::Slab },
|
||||
slab_id, ProjectionFromTop != edge_up);
|
||||
} else if (intersects_this) {
|
||||
// Intersects just the top plane, may touch the bottom plane.
|
||||
assert((vertices[i].z() > *it && vertices[j].z() < *it) || (vertices[i].z() < *it && vertices[j].z() > *it));
|
||||
assert(il.edge_a_id == edge_id || il.edge_b_id == edge_id);
|
||||
emit_slab_edge(
|
||||
IntersectionLine { {
|
||||
to_2d(edge_up ? vertices[i] : vertices[j]).cast<coord_t>(),
|
||||
il.edge_a_id == edge_id ? il.a : il.b
|
||||
},
|
||||
edge_up ? indices(i) : indices(j), -1, -1, edge_id + num_edges, IntersectionLine::FacetEdgeType::Slab
|
||||
},
|
||||
slab_id, ProjectionFromTop != edge_up);
|
||||
} else if (intersects_prev) {
|
||||
// Intersects just the bottom plane, may touch the top vertex.
|
||||
assert(*it <= max_z);
|
||||
#ifndef NDEBUG
|
||||
{
|
||||
auto it_prev = it;
|
||||
-- it_prev;
|
||||
assert((vertices[i].z() > *it_prev && vertices[j].z() < *it_prev) || (vertices[i].z() < *it_prev && vertices[j].z() > *it_prev));
|
||||
}
|
||||
#endif // NDEBUG
|
||||
emit_slab_edge(
|
||||
IntersectionLine { {
|
||||
il_prev.edge_a_id == edge_id ? il_prev.a : il_prev.b,
|
||||
to_2d(edge_up ? vertices[j] : vertices[i]).cast<coord_t>()
|
||||
},
|
||||
-1, edge_up ? indices(j) : indices(i), edge_id, -1, IntersectionLine::FacetEdgeType::Slab
|
||||
},
|
||||
slab_id, ProjectionFromTop != edge_up);
|
||||
} else if (float zi = vertices[i].z(), zj = vertices[j].z(); zi < *it || zj < *it) {
|
||||
// The edge does not intersect the current plane and it does not intersect the previous plane either.
|
||||
// Both points have to be inside the slab.
|
||||
assert(zi <= *it && zj <= *it);
|
||||
#ifndef NDEBUG
|
||||
if (type == FacetSliceType::Slicing || type == FacetSliceType::Cutting) {
|
||||
// Such edge should already be processed in the code above, it shall be skipped here.
|
||||
assert(indices(i) != il.b_id || indices(j) != il.a_id);
|
||||
assert(indices(i) != il.a_id || indices(j) != il.b_id);
|
||||
}
|
||||
#endif // NDEBUG
|
||||
// Is it inside the slab?
|
||||
bool inside_slab = true;
|
||||
if (it != min_layer) {
|
||||
auto it_prev = it;
|
||||
-- it_prev;
|
||||
assert(*it_prev >= *min_layer && *it_prev < *it);
|
||||
// One point may touch the plane below, the other must not.
|
||||
inside_slab = zi > *it_prev || zj > *it_prev;
|
||||
// Both points have to be inside the slab.
|
||||
assert(! inside_slab || (zi >= *it_prev && zj >= *it_prev));
|
||||
}
|
||||
if (inside_slab) {
|
||||
assert(ProjectionFromTop ? vertices[i].z() >= zs[slab_id] : vertices[i].z() <= zs[slab_id]);
|
||||
assert(ProjectionFromTop ? vertices[j].z() >= zs[slab_id] : vertices[j].z() <= zs[slab_id]);
|
||||
emit_slab_edge(
|
||||
IntersectionLine {
|
||||
{ to_2d(vertices[i]).cast<coord_t>(), to_2d(vertices[j]).cast<coord_t>() },
|
||||
indices(i), indices(j), -1, -1, IntersectionLine::FacetEdgeType::Slab
|
||||
},
|
||||
slab_id, ! ProjectionFromTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
il_prev = il;
|
||||
}
|
||||
if (ProjectionFromTop || max_layer != zs.end()) {
|
||||
// Try to project unbound edges above the last slicing plane to the last slab.
|
||||
// Last layer slicing this triangle.
|
||||
auto it = max_layer - 1;
|
||||
size_t slab_id = max_layer - zs.begin();
|
||||
if (ProjectionFromTop)
|
||||
-- slab_id;
|
||||
for (int iedge = 0; iedge < 3; ++ iedge)
|
||||
if (facet_neighbors(iedge) == -1) {
|
||||
// Unbound edge.
|
||||
int edge_id = facet_edge_ids(iedge);
|
||||
int i = iedge;
|
||||
int j = next_idx_modulo(i, 3);
|
||||
if (il_prev.edge_a_id == edge_id || il_prev.edge_b_id == edge_id) {
|
||||
// Intersects just the bottom plane, may touch the top vertex.
|
||||
assert((vertices[i].z() > *it && vertices[j].z() < *it) || (vertices[i].z() < *it && vertices[j].z() > *it));
|
||||
bool edge_up = vertices[j].z() > vertices[i].z();
|
||||
emit_slab_edge(
|
||||
IntersectionLine{ {
|
||||
il_prev.edge_a_id == edge_id ? il_prev.a : il_prev.b,
|
||||
to_2d(edge_up ? vertices[j] : vertices[i]).cast<coord_t>()
|
||||
},
|
||||
-1, edge_up ? indices(j) : indices(i), edge_id, -1, IntersectionLine::FacetEdgeType::Slab
|
||||
},
|
||||
slab_id, ProjectionFromTop != edge_up);
|
||||
} else if (float zi = vertices[i].z(), zj = vertices[j].z(); zi > *it || zj > *it) {
|
||||
// The edge does not intersect the current plane and it does not intersect the previous plane either.
|
||||
// Both points have to be inside the slab.
|
||||
assert(zi >= *it && zj >= *it);
|
||||
assert(max_layer == zs.end() || (zi < *max_layer && zj < *max_layer));
|
||||
emit_slab_edge(
|
||||
IntersectionLine{
|
||||
{ to_2d(vertices[i]).cast<coord_t>(), to_2d(vertices[j]).cast<coord_t>() },
|
||||
indices(i), indices(j), -1, -1, IntersectionLine::FacetEdgeType::Slab
|
||||
},
|
||||
slab_id, ! ProjectionFromTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// used by slice_mesh_slabs() to produce on-slice lines and between-slices lines.
|
||||
// Returning top / bottom SlabLines.
|
||||
template<typename ThrowOnCancel>
|
||||
inline std::pair<SlabLines, SlabLines> slice_slabs_make_lines(
|
||||
const std::vector<stl_vertex> &vertices,
|
||||
const std::vector<stl_triangle_vertex_indices> &indices,
|
||||
const std::vector<Vec3i> &face_neighbors,
|
||||
const std::vector<Vec3i> &face_edge_ids,
|
||||
// Total number of edges. All face_edge_ids are lower than num_edges.
|
||||
// num_edges will be used to distinguish between intersections with the top and bottom plane.
|
||||
const int num_edges,
|
||||
const std::vector<FaceOrientation> &face_orientation,
|
||||
const std::vector<float> &zs,
|
||||
bool top,
|
||||
bool bottom,
|
||||
const ThrowOnCancel throw_on_cancel_fn)
|
||||
{
|
||||
std::pair<SlabLines, SlabLines> out;
|
||||
SlabLines &lines_top = out.first;
|
||||
SlabLines &lines_bottom = out.second;
|
||||
std::array<std::mutex, 64> lines_mutex_top;
|
||||
std::array<std::mutex, 64> lines_mutex_bottom;
|
||||
|
||||
if (top) {
|
||||
lines_top.at_slice.assign(zs.size(), IntersectionLines());
|
||||
lines_top.between_slices.assign(zs.size(), IntersectionLines());
|
||||
}
|
||||
if (bottom) {
|
||||
lines_bottom.at_slice.assign(zs.size(), IntersectionLines());
|
||||
lines_bottom.between_slices.assign(zs.size(), IntersectionLines());
|
||||
}
|
||||
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<int>(0, int(indices.size())),
|
||||
[&vertices, &indices, &face_neighbors, &face_edge_ids, num_edges, &face_orientation, &zs, top, bottom, &lines_top, &lines_bottom, &lines_mutex_top, &lines_mutex_bottom, throw_on_cancel_fn]
|
||||
(const tbb::blocked_range<int> &range) {
|
||||
for (int face_idx = range.begin(); face_idx < range.end(); ++ face_idx) {
|
||||
if ((face_idx & 0x0ffff) == 0)
|
||||
throw_on_cancel_fn();
|
||||
FaceOrientation fo = face_orientation[face_idx];
|
||||
Vec3i edge_ids = face_edge_ids[face_idx];
|
||||
if (top && (fo == FaceOrientation::Up || fo == FaceOrientation::Degenerate)) {
|
||||
Vec3i neighbors = face_neighbors[face_idx];
|
||||
// Reset neighborship of this triangle in case the other triangle is oriented backwards from this one.
|
||||
for (int i = 0; i < 3; ++ i)
|
||||
if (neighbors(i) != -1) {
|
||||
FaceOrientation fo2 = face_orientation[neighbors(i)];
|
||||
if (fo2 != FaceOrientation::Up && fo2 != FaceOrientation::Degenerate)
|
||||
neighbors(i) = -1;
|
||||
}
|
||||
slice_facet_with_slabs<true>(vertices, indices[face_idx], neighbors, edge_ids, num_edges, zs, lines_top, lines_mutex_top);
|
||||
}
|
||||
if (bottom && (fo == FaceOrientation::Down || fo == FaceOrientation::Degenerate)) {
|
||||
Vec3i neighbors = face_neighbors[face_idx];
|
||||
// Reset neighborship of this triangle in case the other triangle is oriented backwards from this one.
|
||||
for (int i = 0; i < 3; ++ i)
|
||||
if (neighbors(i) != -1) {
|
||||
FaceOrientation fo2 = face_orientation[neighbors(i)];
|
||||
if (fo2 != FaceOrientation::Down && fo2 != FaceOrientation::Degenerate)
|
||||
neighbors(i) = -1;
|
||||
}
|
||||
slice_facet_with_slabs<false>(vertices, indices[face_idx], neighbors, edge_ids, num_edges, zs, lines_bottom, lines_mutex_bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
#if 0
|
||||
//FIXME Should this go away? For valid meshes the function slice_facet() returns Slicing
|
||||
// and sets edges of vertical triangles to produce only a single edge per pair of neighbor faces.
|
||||
|
@ -495,6 +846,7 @@ static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, Polyg
|
|||
if ((first_line->edge_a_id != -1 && first_line->edge_a_id == last_line->edge_b_id) ||
|
||||
(first_line->a_id != -1 && first_line->a_id == last_line->b_id)) {
|
||||
// The current loop is complete. Add it to the output.
|
||||
assert(first_line->a == last_line->b);
|
||||
loops.emplace_back(std::move(loop_pts));
|
||||
#ifdef SLIC3R_TRIANGLEMESH_DEBUG
|
||||
printf(" Discovered %s polygon of %d points\n", (p.is_counter_clockwise() ? "ccw" : "cw"), (int)p.points.size());
|
||||
|
@ -513,6 +865,7 @@ static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, Polyg
|
|||
next_line->edge_a_id, next_line->edge_b_id, next_line->a_id, next_line->b_id,
|
||||
next_line->a.x, next_line->a.y, next_line->b.x, next_line->b.y);
|
||||
*/
|
||||
assert(last_line->b == next_line->a);
|
||||
loop_pts.emplace_back(next_line->a);
|
||||
last_line = next_line;
|
||||
next_line->set_skip();
|
||||
|
@ -892,6 +1245,106 @@ static std::vector<Polygons> make_loops(
|
|||
return layers;
|
||||
}
|
||||
|
||||
// used by slice_mesh_slabs() to produce loops from on-slice lines and between-slices lines.
|
||||
template<bool ProjectionFromTop, typename ThrowOnCancel>
|
||||
static std::vector<Polygons> make_slab_loops(
|
||||
// Lines will have their flags modified.
|
||||
SlabLines &lines,
|
||||
// To differentiate edge IDs of the top plane from the edge IDs of the bottom plane for chaining.
|
||||
int num_edges,
|
||||
ThrowOnCancel throw_on_cancel)
|
||||
{
|
||||
assert(! lines.at_slice.empty() && lines.at_slice.size() == lines.between_slices.size());
|
||||
std::vector<Polygons> layers;
|
||||
layers.resize(lines.at_slice.size());
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<int>(0, int(lines.at_slice.size())),
|
||||
[&lines, num_edges, &layers, throw_on_cancel](const tbb::blocked_range<int> &range) {
|
||||
for (int line_idx = range.begin(); line_idx < range.end(); ++ line_idx) {
|
||||
if ((line_idx & 0x0ffff) == 0)
|
||||
throw_on_cancel();
|
||||
IntersectionLines in;
|
||||
size_t nlines = lines.between_slices[line_idx].size();
|
||||
int slice_below = ProjectionFromTop ? line_idx : line_idx - 1;
|
||||
int slice_above = ProjectionFromTop ? line_idx + 1 : line_idx;
|
||||
bool has_slice_below = ProjectionFromTop || line_idx > 0;
|
||||
bool has_slice_above = ! ProjectionFromTop || line_idx + 1 < int(lines.at_slice.size());
|
||||
if (has_slice_below)
|
||||
nlines += lines.at_slice[slice_below].size();
|
||||
if (has_slice_above)
|
||||
nlines += lines.at_slice[slice_above].size();
|
||||
if (nlines) {
|
||||
in.reserve(nlines);
|
||||
if (has_slice_below) {
|
||||
for (const IntersectionLine &l : lines.at_slice[slice_below])
|
||||
if (l.edge_type != IntersectionLine::FacetEdgeType::Top) {
|
||||
in.emplace_back(l);
|
||||
#ifndef NDEBUG
|
||||
in.back().source = IntersectionLine::Source::BottomPlane;
|
||||
#endif // NDEBUG
|
||||
}
|
||||
}
|
||||
{
|
||||
// Edges in between slice_below and slice_above.
|
||||
#ifndef NDEBUG
|
||||
size_t old_size = in.size();
|
||||
#endif // NDEBUG
|
||||
// Edge IDs of end points on in-between lines that touch the layer above are already increased with num_edges.
|
||||
append(in, lines.between_slices[line_idx]);
|
||||
#ifndef NDEBUG
|
||||
for (auto it = in.begin() + old_size; it != in.end(); ++ it) {
|
||||
assert(it->edge_type == IntersectionLine::FacetEdgeType::Slab);
|
||||
it->source = IntersectionLine::Source::Slab;
|
||||
}
|
||||
#endif // NDEBUG
|
||||
}
|
||||
if (has_slice_above) {
|
||||
for (const IntersectionLine &lsrc : lines.at_slice[slice_above])
|
||||
if (lsrc.edge_type != IntersectionLine::FacetEdgeType::Bottom) {
|
||||
in.emplace_back(lsrc);
|
||||
auto &l = in.back();
|
||||
l.reverse();
|
||||
// Differentiate edge IDs of the top plane from the edge IDs of the bottom plane for chaining.
|
||||
if (l.edge_a_id >= 0)
|
||||
l.edge_a_id += num_edges;
|
||||
if (l.edge_b_id >= 0)
|
||||
l.edge_b_id += num_edges;
|
||||
#ifndef NDEBUG
|
||||
l.source = IntersectionLine::Source::TopPlane;
|
||||
#endif // NDEBUG
|
||||
}
|
||||
}
|
||||
if (! in.empty()) {
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
BoundingBox bbox_svg;
|
||||
{
|
||||
static int iRun = 0;
|
||||
for (const IntersectionLine &line : in) {
|
||||
bbox_svg.merge(line.a);
|
||||
bbox_svg.merge(line.b);
|
||||
}
|
||||
SVG svg(debug_out_path("make_slab_loops-%d.svg", iRun++).c_str(), bbox_svg);
|
||||
for (const IntersectionLine& line : in) {
|
||||
const char* color = line.source == IntersectionLine::Source::BottomPlane ? "red" : line.source == IntersectionLine::Source::TopPlane ? "blue" : "green";
|
||||
svg.draw(line, color, scaled(0.1));
|
||||
}
|
||||
svg.Close();
|
||||
}
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
Polygons &loops = layers[line_idx];
|
||||
std::vector<OpenPolyline> open_polylines;
|
||||
chain_lines_by_triangle_connectivity(in, loops, open_polylines);
|
||||
assert(! loops.empty());
|
||||
assert(open_polylines.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
// Used to cut the mesh into two halves.
|
||||
static ExPolygons make_expolygons_simple(std::vector<IntersectionLine> &lines)
|
||||
{
|
||||
|
@ -1055,6 +1508,44 @@ static void make_expolygons(const Polygons &loops, const float closing_radius, c
|
|||
union_ex(loops, fill_type));
|
||||
}
|
||||
|
||||
// Make a trafo for transforming the vertices. Scale up in XY, not in Z.
|
||||
static inline Transform3f make_trafo_for_slicing(const Transform3d &trafo)
|
||||
{
|
||||
auto t = trafo;
|
||||
static constexpr const double s = 1. / SCALING_FACTOR;
|
||||
t.prescale(Vec3d(s, s, 1.));
|
||||
return t.cast<float>();
|
||||
}
|
||||
|
||||
static inline bool is_identity(const Transform3d &trafo)
|
||||
{
|
||||
return trafo.matrix() == Transform3d::Identity().matrix();
|
||||
}
|
||||
|
||||
static std::vector<stl_vertex> transform_mesh_vertices_for_slicing(const indexed_triangle_set &mesh, const Transform3d &trafo)
|
||||
{
|
||||
// Copy and scale vertices in XY, don't scale in Z.
|
||||
// Possibly apply the transformation.
|
||||
static constexpr const double s = 1. / SCALING_FACTOR;
|
||||
std::vector<stl_vertex> out(mesh.vertices);
|
||||
if (is_identity(trafo)) {
|
||||
// Identity.
|
||||
for (stl_vertex &v : out) {
|
||||
// Scale just XY, leave Z unscaled.
|
||||
v.x() *= float(s);
|
||||
v.y() *= float(s);
|
||||
}
|
||||
} else {
|
||||
// Transform the vertices, scale up in XY, not in Y.
|
||||
auto t = trafo;
|
||||
t.prescale(Vec3d(s, s, 1.));
|
||||
auto tf = t.cast<float>();
|
||||
for (stl_vertex &v : out)
|
||||
v = tf * v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<Polygons> slice_mesh(
|
||||
const indexed_triangle_set &mesh,
|
||||
// Unscaled Zs
|
||||
|
@ -1071,41 +1562,23 @@ std::vector<Polygons> slice_mesh(
|
|||
// Instead of edge identifiers, one shall use a sorted pair of edge vertex indices.
|
||||
// However facets_edges assigns a single edge ID to two triangles only, thus when factoring facets_edges out, one will have
|
||||
// to make sure that no code relies on it.
|
||||
std::vector<Vec3i> facets_edges = create_face_neighbors_index(mesh);
|
||||
const bool identity = params.trafo.matrix() == Transform3d::Identity().matrix();
|
||||
static constexpr const double s = 1. / SCALING_FACTOR;
|
||||
std::vector<Vec3i> face_edge_ids = its_face_edge_ids(mesh);
|
||||
if (zs.size() <= 1) {
|
||||
// It likely is not worthwile to copy the vertices. Apply the transformation in place.
|
||||
if (identity)
|
||||
if (is_identity(params.trafo)) {
|
||||
lines = slice_make_lines(
|
||||
mesh.vertices, [](const Vec3f &p) { return Vec3f(scaled<float>(p.x()), scaled<float>(p.y()), p.z()); },
|
||||
mesh.indices, facets_edges, zs, throw_on_cancel);
|
||||
else {
|
||||
// Transform the vertices, scale up in XY, not in Y.
|
||||
auto t = params.trafo;
|
||||
t.prescale(Vec3d(s, s, 1.));
|
||||
auto tf = t.cast<float>();
|
||||
lines = slice_make_lines(mesh.vertices, [tf](const Vec3f &p) { return tf * p; }, mesh.indices, facets_edges, zs, throw_on_cancel);
|
||||
mesh.indices, face_edge_ids, zs, throw_on_cancel);
|
||||
} else {
|
||||
// Transform the vertices, scale up in XY, not in Z.
|
||||
Transform3f tf = make_trafo_for_slicing(params.trafo);
|
||||
lines = slice_make_lines(mesh.vertices, [tf](const Vec3f &p) { return tf * p; }, mesh.indices, face_edge_ids, zs, throw_on_cancel);
|
||||
}
|
||||
} else {
|
||||
// Copy and scale vertices in XY, don't scale in Z.
|
||||
// Possibly apply the transformation.
|
||||
std::vector<stl_vertex> vertices(mesh.vertices);
|
||||
if (identity) {
|
||||
for (stl_vertex &v : vertices) {
|
||||
// Scale just XY, leave Z unscaled.
|
||||
v.x() *= float(s);
|
||||
v.y() *= float(s);
|
||||
}
|
||||
} else {
|
||||
// Transform the vertices, scale up in XY, not in Y.
|
||||
auto t = params.trafo;
|
||||
t.prescale(Vec3d(s, s, 1.));
|
||||
auto tf = t.cast<float>();
|
||||
for (stl_vertex &v : vertices)
|
||||
v = tf * v;
|
||||
}
|
||||
lines = slice_make_lines(vertices, [](const Vec3f &p) { return p; }, mesh.indices, facets_edges, zs, throw_on_cancel);
|
||||
// Copy and scale vertices in XY, don't scale in Z. Possibly apply the transformation.
|
||||
lines = slice_make_lines(
|
||||
transform_mesh_vertices_for_slicing(mesh, params.trafo),
|
||||
[](const Vec3f &p) { return p; }, mesh.indices, face_edge_ids, zs, throw_on_cancel);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1194,6 +1667,105 @@ std::vector<ExPolygons> slice_mesh_ex(
|
|||
return layers;
|
||||
}
|
||||
|
||||
// Slice a triangle set with a set of Z slabs (thick layers).
|
||||
// The effect is similar to producing the usual top / bottom layers from a sliced mesh by
|
||||
// subtracting layer[i] from layer[i - 1] for the top surfaces resp.
|
||||
// subtracting layer[i] from layer[i + 1] for the bottom surfaces,
|
||||
// with the exception that the triangle set this function processes may not cover the whole top resp. bottom surface.
|
||||
// top resp. bottom surfaces are calculated only if out_top resp. out_bottom is not null.
|
||||
void slice_mesh_slabs(
|
||||
const indexed_triangle_set &mesh,
|
||||
// Unscaled Zs
|
||||
const std::vector<float> &zs,
|
||||
const Transform3d &trafo,
|
||||
std::vector<Polygons> *out_top,
|
||||
std::vector<Polygons> *out_bottom,
|
||||
std::function<void()> throw_on_cancel)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << "slice_mesh_slabs to polygons";
|
||||
|
||||
#ifdef EXPENSIVE_DEBUG_CHECKS
|
||||
{
|
||||
// Verify that the vertices are unique.
|
||||
auto v = mesh.vertices;
|
||||
std::sort(v.begin(), v.end(), [](auto &l, auto &r) {
|
||||
return l.x() < r.x() || (l.x() == r.x() && (l.y() < r.y() || (l.y() == r.y() && l.z() < r.z())));
|
||||
});
|
||||
size_t num_duplicates = v.end() - std::unique(v.begin(), v.end());
|
||||
assert(num_duplicates == 0);
|
||||
}
|
||||
{
|
||||
// Verify that there are no T-joints.
|
||||
for (const auto &tri : mesh.indices)
|
||||
for (int i = 0; i < 3; ++ i) {
|
||||
int j = next_idx_modulo(i, 3);
|
||||
int k = next_idx_modulo(j, 3);
|
||||
auto &v1 = mesh.vertices[tri(i)];
|
||||
auto &v2 = mesh.vertices[tri(j)];
|
||||
auto &v3 = mesh.vertices[tri(k)];
|
||||
for (auto &pt : mesh.vertices)
|
||||
if (&pt != &v1 && &pt != &v2) {
|
||||
assert(pt != v1 && pt != v2);
|
||||
assert((pt - v1).norm() > EPSILON);
|
||||
assert((pt - v2).norm() > EPSILON);
|
||||
auto l2 = (v2 - v1).squaredNorm();
|
||||
assert(l2 > 0);
|
||||
auto t = (pt - v1).dot(v2 - v1);
|
||||
if (t > 0 && t < l2) {
|
||||
auto d2 = (pt - v1).squaredNorm() - sqr(t) / l2;
|
||||
auto d = sqrt(std::max(d2, 0.f));
|
||||
if (&pt == &v3) {
|
||||
if (d < EPSILON)
|
||||
printf("Degenerate triangle!\n");
|
||||
} else {
|
||||
assert(d > EPSILON);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // EXPENSIVE_DEBUG_CHECKS
|
||||
|
||||
std::vector<stl_vertex> vertices_transformed = transform_mesh_vertices_for_slicing(mesh, trafo);
|
||||
|
||||
std::vector<FaceOrientation> face_orientation(mesh.indices.size(), FaceOrientation::Up);
|
||||
for (const stl_triangle_vertex_indices &tri : mesh.indices) {
|
||||
const Vec3f fa = vertices_transformed[tri(0)];
|
||||
const Vec3f fb = vertices_transformed[tri(1)];
|
||||
const Vec3f fc = vertices_transformed[tri(2)];
|
||||
assert(fa != fb && fa != fc && fb != fc);
|
||||
const Point a = to_2d(fa).cast<coord_t>();
|
||||
const Point b = to_2d(fb).cast<coord_t>();
|
||||
const Point c = to_2d(fc).cast<coord_t>();
|
||||
const int64_t d = cross2((b - a).cast<int64_t>(), (c - b).cast<int64_t>());
|
||||
FaceOrientation fo = FaceOrientation::Vertical;
|
||||
if (d > 0)
|
||||
fo = FaceOrientation::Up;
|
||||
else if (d < 0)
|
||||
fo = FaceOrientation::Down;
|
||||
else {
|
||||
// Is the triangle vertical or degenerate?
|
||||
assert(d == 0);
|
||||
fo = fa == fb || fa == fc || fb == fc ? FaceOrientation::Degenerate : FaceOrientation::Vertical;
|
||||
}
|
||||
face_orientation[&tri - mesh.indices.data()] = fo;
|
||||
}
|
||||
|
||||
std::vector<Vec3i> face_neighbors = its_face_neighbors_par(mesh);
|
||||
int num_edges;
|
||||
std::vector<Vec3i> face_edge_ids = its_face_edge_ids(mesh, face_neighbors, true, &num_edges);
|
||||
std::pair<SlabLines, SlabLines> lines = slice_slabs_make_lines(
|
||||
vertices_transformed, mesh.indices, face_neighbors, face_edge_ids, num_edges, face_orientation, zs,
|
||||
out_top != nullptr, out_bottom != nullptr, throw_on_cancel);
|
||||
|
||||
throw_on_cancel();
|
||||
|
||||
if (out_top)
|
||||
*out_top = make_slab_loops<true>(lines.first, num_edges, throw_on_cancel);
|
||||
if (out_bottom)
|
||||
*out_bottom = make_slab_loops<false>(lines.second, num_edges, throw_on_cancel);
|
||||
}
|
||||
|
||||
// Remove duplicates of slice_vertices, optionally triangulate the cut.
|
||||
static void triangulate_slice(
|
||||
indexed_triangle_set &its,
|
||||
|
@ -1308,7 +1880,7 @@ void cut_mesh(const indexed_triangle_set &mesh, float z, indexed_triangle_set *u
|
|||
// To triangulate the caps after slicing.
|
||||
IntersectionLines upper_lines, lower_lines;
|
||||
std::vector<int> upper_slice_vertices, lower_slice_vertices;
|
||||
std::vector<Vec3i> facets_edges = create_face_neighbors_index(mesh);
|
||||
std::vector<Vec3i> facets_edge_ids = its_face_edge_ids(mesh);
|
||||
|
||||
for (int facet_idx = 0; facet_idx < int(mesh.indices.size()); ++ facet_idx) {
|
||||
const stl_triangle_vertex_indices &facet = mesh.indices[facet_idx];
|
||||
|
@ -1329,7 +1901,7 @@ void cut_mesh(const indexed_triangle_set &mesh, float z, indexed_triangle_set *u
|
|||
dst.y() = scale_(src.y());
|
||||
dst.z() = src.z();
|
||||
}
|
||||
slice_type = slice_facet(z, vertices_scaled, mesh.indices[facet_idx], facets_edges[facet_idx], idx_vertex_lowest, min_z == max_z, line);
|
||||
slice_type = slice_facet(z, vertices_scaled, mesh.indices[facet_idx], facets_edge_ids[facet_idx], idx_vertex_lowest, min_z == max_z, line);
|
||||
}
|
||||
|
||||
if (slice_type != FacetSliceType::NoSlice) {
|
||||
|
@ -1371,8 +1943,8 @@ void cut_mesh(const indexed_triangle_set &mesh, float z, indexed_triangle_set *u
|
|||
// get vertices starting from the isolated one
|
||||
int iv = isolated_vertex;
|
||||
stl_vertex v0v1, v2v0;
|
||||
assert(facets_edges[facet_idx](iv) == line.edge_a_id ||facets_edges[facet_idx](iv) == line.edge_b_id);
|
||||
if (facets_edges[facet_idx](iv) == line.edge_a_id) {
|
||||
assert(facets_edge_ids[facet_idx](iv) == line.edge_a_id ||facets_edge_ids[facet_idx](iv) == line.edge_b_id);
|
||||
if (facets_edge_ids[facet_idx](iv) == line.edge_a_id) {
|
||||
v0v1 = to_3d(unscaled<float>(line.a), z);
|
||||
v2v0 = to_3d(unscaled<float>(line.b), z);
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue