diff --git a/resources/icons/measure.svg b/resources/icons/measure.svg
new file mode 100644
index 0000000000..275c522251
--- /dev/null
+++ b/resources/icons/measure.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index 01a6a3aa2a..a7ba046477 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -180,6 +180,8 @@ set(SLIC3R_SOURCES
MultiMaterialSegmentation.hpp
MeshNormals.hpp
MeshNormals.cpp
+ Measure.hpp
+ Measure.cpp
CustomGCode.cpp
CustomGCode.hpp
Arrange.hpp
@@ -251,6 +253,7 @@ set(SLIC3R_SOURCES
Surface.hpp
SurfaceCollection.cpp
SurfaceCollection.hpp
+ SurfaceMesh.hpp
SVG.cpp
SVG.hpp
Technologies.hpp
diff --git a/src/libslic3r/Measure.cpp b/src/libslic3r/Measure.cpp
new file mode 100644
index 0000000000..056178bc4e
--- /dev/null
+++ b/src/libslic3r/Measure.cpp
@@ -0,0 +1,440 @@
+#include "Measure.hpp"
+
+#include "libslic3r/Geometry/Circle.hpp"
+#include "libslic3r/SurfaceMesh.hpp"
+
+
+
+namespace Slic3r {
+namespace Measure {
+
+
+
+static std::pair get_center_and_radius(const std::vector& border, int start_idx, int end_idx, const Transform3d& trafo)
+{
+ Vec2ds pts;
+ double z = 0.;
+ for (int i=start_idx; i<=end_idx; ++i) {
+ Vec3d pt_transformed = trafo * border[i];
+ z = pt_transformed.z();
+ pts.emplace_back(pt_transformed.x(), pt_transformed.y());
+ }
+
+ auto circle = Geometry::circle_ransac(pts, 20); // FIXME: iterations?
+
+ return std::make_pair(trafo.inverse() * Vec3d(circle.center.x(), circle.center.y(), z), circle.radius);
+}
+
+
+
+
+class MeasuringImpl {
+public:
+ explicit MeasuringImpl(const indexed_triangle_set& its);
+ struct PlaneData {
+ std::vector facets;
+ std::vector> borders; // FIXME: should be in fact local in update_planes()
+ std::vector> surface_features;
+ Vec3d normal;
+ float area;
+ };
+
+ const std::vector& get_features() const;
+ const SurfaceFeature* get_feature(size_t face_idx, const Vec3d& point) const;
+ const std::vector> get_planes_triangle_indices() const;
+
+private:
+ void update_planes();
+ void extract_features();
+ void save_features();
+
+
+ std::vector m_planes;
+ std::vector m_face_to_plane;
+ std::vector m_features;
+ const indexed_triangle_set& m_its;
+};
+
+
+
+
+
+
+MeasuringImpl::MeasuringImpl(const indexed_triangle_set& its)
+: m_its{its}
+{
+ update_planes();
+ extract_features();
+ save_features();
+}
+
+
+void MeasuringImpl::update_planes()
+{
+ m_planes.clear();
+
+ // Now we'll go through all the facets and append Points of facets sharing the same normal.
+ // This part is still performed in mesh coordinate system.
+ const size_t num_of_facets = m_its.indices.size();
+ m_face_to_plane.resize(num_of_facets, size_t(-1));
+ const std::vector face_normals = its_face_normals(m_its);
+ const std::vector face_neighbors = its_face_neighbors(m_its);
+ std::vector facet_queue(num_of_facets, 0);
+ int facet_queue_cnt = 0;
+ const stl_normal* normal_ptr = nullptr;
+ size_t seed_facet_idx = 0;
+
+ auto is_same_normal = [](const stl_normal& a, const stl_normal& b) -> bool {
+ return (std::abs(a(0) - b(0)) < 0.001 && std::abs(a(1) - b(1)) < 0.001 && std::abs(a(2) - b(2)) < 0.001);
+ };
+
+ while (1) {
+ // Find next unvisited triangle:
+ for (; seed_facet_idx < num_of_facets; ++ seed_facet_idx)
+ if (m_face_to_plane[seed_facet_idx] == size_t(-1)) {
+ facet_queue[facet_queue_cnt ++] = seed_facet_idx;
+ normal_ptr = &face_normals[seed_facet_idx];
+ m_face_to_plane[seed_facet_idx] = m_planes.size();
+ m_planes.emplace_back();
+ break;
+ }
+ if (seed_facet_idx == num_of_facets)
+ break; // Everything was visited already
+
+ while (facet_queue_cnt > 0) {
+ int facet_idx = facet_queue[-- facet_queue_cnt];
+ const stl_normal& this_normal = face_normals[facet_idx];
+ if (is_same_normal(this_normal, *normal_ptr)) {
+ const Vec3i& face = m_its.indices[facet_idx];
+
+ m_face_to_plane[facet_idx] = m_planes.size() - 1;
+ m_planes.back().facets.emplace_back(facet_idx);
+ for (int j = 0; j < 3; ++ j)
+ if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && m_face_to_plane[neighbor_idx] == size_t(-1))
+ facet_queue[facet_queue_cnt ++] = neighbor_idx;
+ }
+ }
+
+ m_planes.back().normal = normal_ptr->cast();
+ std::sort(m_planes.back().facets.begin(), m_planes.back().facets.end());
+ }
+
+ assert(std::none_of(m_face_to_plane.begin(), m_face_to_plane.end(), [](size_t val) { return val == size_t(-1); }));
+
+ SurfaceMesh sm(m_its);
+ for (int plane_id=0; plane_id < int(m_planes.size()); ++plane_id) {
+ //int plane_id = 5; {
+ const auto& facets = m_planes[plane_id].facets;
+ m_planes[plane_id].borders.clear();
+ std::vector> visited(facets.size(), {false, false, false});
+
+ for (int face_id=0; face_id& last_border = m_planes[plane_id].borders.back();
+ last_border.emplace_back(sm.point(sm.source(he)).cast());
+ //Vertex_index target = sm.target(he);
+ const Halfedge_index he_start = he;
+
+ Face_index fi = he.face();
+ auto face_it = std::lower_bound(facets.begin(), facets.end(), int(fi));
+ assert(face_it != facets.end());
+ assert(*face_it == int(fi));
+ visited[face_it - facets.begin()][he.side()] = true;
+
+ do {
+ const Halfedge_index he_orig = he;
+ he = sm.next_around_target(he);
+ while ( m_face_to_plane[sm.face(he)] == plane_id && he != he_orig)
+ he = sm.next_around_target(he);
+ he = sm.opposite(he);
+
+ Face_index fi = he.face();
+ auto face_it = std::lower_bound(facets.begin(), facets.end(), int(fi));
+ assert(face_it != facets.end());
+ assert(*face_it == int(fi));
+ if (visited[face_it - facets.begin()][he.side()] && he != he_start) {
+ last_border.resize(1);
+ break;
+ }
+ visited[face_it - facets.begin()][he.side()] = true;
+
+ last_border.emplace_back(sm.point(sm.source(he)).cast());
+ } while (he != he_start);
+
+ if (last_border.size() == 1)
+ m_planes[plane_id].borders.pop_back();
+ }
+ }
+ }
+
+ m_planes.erase(std::remove_if(m_planes.begin(), m_planes.end(),
+ [](const PlaneData& p) { return p.borders.empty(); }),
+ m_planes.end());
+}
+
+
+
+
+
+
+void MeasuringImpl::extract_features()
+{
+ auto N_to_angle = [](double N) -> double { return 2.*M_PI / N; };
+ constexpr double polygon_upper_threshold = N_to_angle(4.5);
+ constexpr double polygon_lower_threshold = N_to_angle(8.5);
+ std::vector angles;
+ std::vector lengths;
+
+
+ for (int i=0; i& border : plane.borders) {
+ assert(border.size() > 1);
+ int start_idx = -1;
+
+ // First calculate angles at all the vertices.
+ angles.clear();
+ lengths.clear();
+ for (int i=0; i M_PI)
+ angle = 2*M_PI - angle;
+
+ angles.push_back(angle);
+ lengths.push_back(v2.squaredNorm());
+ }
+ assert(border.size() == angles.size());
+ assert(border.size() == lengths.size());
+
+
+ bool circle = false;
+ std::vector> circles;
+ std::vector> circles_idxs;
+ for (int i=1; i(
+ new Circle(center, radius, plane.normal)));
+ circle = false;
+ }
+ }
+ }
+
+ // Some of the "circles" may actually be polygons. We want them detected as
+ // edges, but also to remember the center and save it into those edges.
+ // We will add all such edges manually and delete the detected circles,
+ // leaving it in circles_idxs so they are not picked again:
+ assert(circles.size() == circles_idxs.size());
+ for (int i=circles.size()-1; i>=0; --i) {
+ assert(circles_idxs[i].first + 1 < angles.size() - 1); // Check that this is internal point of the circle, not the first, not the last.
+ double angle = angles[circles_idxs[i].first + 1];
+ if (angle > polygon_lower_threshold) {
+ if (angle < polygon_upper_threshold) {
+ const Vec3d center = static_cast(circles[i].get())->get_center();
+ for (int j=circles_idxs[i].first + 1; j<=circles_idxs[i].second; ++j)
+ plane.surface_features.emplace_back(std::unique_ptr(
+ new Edge(border[j-1], border[j], center)));
+ } else {
+ // This will be handled just like a regular edge.
+ circles_idxs.erase(circles_idxs.begin() + i);
+ }
+ circles.erase(circles.begin() + i);
+ }
+ }
+
+
+
+
+
+
+ // We have the circles. Now go around again and pick edges.
+ int cidx = 0; // index of next circle in the way
+ for (int i=1; i circles_idxs[cidx].first)
+ i = circles_idxs[cidx++].second;
+ else plane.surface_features.emplace_back(std::unique_ptr(
+ new Edge(border[i-1], border[i])));
+ }
+
+ // FIXME Throw away / do not create edges which are parts of circles or
+ // which lead to circle points (unless they belong to the same plane.)
+
+ // FIXME Check and merge first and last circle if needed.
+
+ // Now move the circles into the feature list.
+ assert(std::all_of(circles.begin(), circles.end(), [](const std::unique_ptr& f) { return f->get_type() == SurfaceFeatureType::Circle; }));
+ plane.surface_features.insert(plane.surface_features.end(), std::make_move_iterator(circles.begin()),
+ std::make_move_iterator(circles.end()));
+ }
+
+ // The last surface feature is the plane itself.
+ plane.surface_features.emplace_back(std::unique_ptr(
+ new Plane(i)));
+
+ plane.borders.clear();
+ plane.borders.shrink_to_fit();
+ }
+}
+
+
+
+void MeasuringImpl::save_features()
+{
+ m_features.clear();
+ for (PlaneData& plane : m_planes)
+ //PlaneData& plane = m_planes[0];
+ {
+ for (const std::unique_ptr& feature : plane.surface_features) {
+ m_features.emplace_back(feature.get());
+ }
+ }
+}
+
+
+
+const SurfaceFeature* MeasuringImpl::get_feature(size_t face_idx, const Vec3d& point) const
+{
+ if (face_idx >= m_face_to_plane.size())
+ return nullptr;
+
+ const PlaneData& plane = m_planes[m_face_to_plane[face_idx]];
+
+ const SurfaceFeature* closest_feature = nullptr;
+ double min_dist = std::numeric_limits::max();
+
+ for (const std::unique_ptr& feature : plane.surface_features) {
+ double dist = Measuring::get_distance(feature.get(), &point);
+ if (dist < 0.5 && dist < min_dist) {
+ min_dist = std::min(dist, min_dist);
+ closest_feature = feature.get();
+ }
+ }
+
+ if (closest_feature)
+ return closest_feature;
+
+ // Nothing detected, return the plane as a whole.
+ assert(plane.surface_features.back().get()->get_type() == SurfaceFeatureType::Plane);
+ return plane.surface_features.back().get();
+}
+
+
+
+const std::vector& MeasuringImpl::get_features() const
+{
+ return m_features;
+}
+
+
+
+const std::vector> MeasuringImpl::get_planes_triangle_indices() const
+{
+ std::vector> out;
+ for (const PlaneData& plane : m_planes)
+ out.emplace_back(plane.facets);
+ return out;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+Measuring::Measuring(const indexed_triangle_set& its)
+: priv{std::make_unique(its)}
+{}
+
+Measuring::~Measuring() {}
+
+
+const std::vector& Measuring::get_features() const
+{
+ return priv->get_features();
+}
+
+
+const SurfaceFeature* Measuring::get_feature(size_t face_idx, const Vec3d& point) const
+{
+ return priv->get_feature(face_idx, point);
+}
+
+
+
+const std::vector> Measuring::get_planes_triangle_indices() const
+{
+ return priv->get_planes_triangle_indices();
+}
+
+
+
+double Measuring::get_distance(const SurfaceFeature* feature, const Vec3d* pt)
+{
+ if (feature->get_type() == SurfaceFeatureType::Edge) {
+ const Edge* edge = static_cast(feature);
+ const auto& [s,e] = edge->get_edge();
+ Eigen::ParametrizedLine line(s, (e-s).normalized());
+ return line.distance(*pt);
+ }
+ else if (feature->get_type() == SurfaceFeatureType::Circle) {
+ const Circle* circle = static_cast(feature);
+ // Find a plane containing normal, center and the point.
+ const Vec3d& c = circle->get_center();
+ const Vec3d& n = circle->get_normal();
+ Eigen::Hyperplane circle_plane(n, c);
+ Vec3d proj = circle_plane.projection(*pt);
+ return std::sqrt( std::pow((proj - c).norm() - circle->get_radius(), 2.) + (*pt - proj).squaredNorm());
+ }
+
+ return std::numeric_limits::max();
+}
+
+
+
+
+
+
+} // namespace Measure
+} // namespace Slic3r
diff --git a/src/libslic3r/Measure.hpp b/src/libslic3r/Measure.hpp
new file mode 100644
index 0000000000..1db35d9fcd
--- /dev/null
+++ b/src/libslic3r/Measure.hpp
@@ -0,0 +1,109 @@
+#ifndef Slic3r_Measure_hpp_
+#define Slic3r_Measure_hpp_
+
+#include
+
+#include "Point.hpp"
+
+
+struct indexed_triangle_set;
+
+
+
+namespace Slic3r {
+namespace Measure {
+
+
+enum class SurfaceFeatureType {
+ Edge = 1 << 0,
+ Circle = 1 << 1,
+ Plane = 1 << 2
+ };
+
+class SurfaceFeature {
+public:
+ virtual SurfaceFeatureType get_type() const = 0;
+};
+
+class Edge : public SurfaceFeature {
+public:
+ Edge(const Vec3d& start, const Vec3d& end) : m_start{start}, m_end{end} {}
+ Edge(const Vec3d& start, const Vec3d& end, const Vec3d& pin) : m_start{start}, m_end{end},
+ m_pin{std::unique_ptr(new Vec3d(pin))} {}
+ SurfaceFeatureType get_type() const override { return SurfaceFeatureType::Edge; }
+ std::pair get_edge() const { return std::make_pair(m_start, m_end); }
+ const Vec3d* get_point_of_interest() const { return m_pin.get(); }
+private:
+ Vec3d m_start;
+ Vec3d m_end;
+ std::unique_ptr m_pin;
+};
+
+class Circle : public SurfaceFeature {
+public:
+ Circle(const Vec3d& center, double radius, const Vec3d& normal)
+ : m_center{center}, m_radius{radius}, m_normal{normal} {}
+ SurfaceFeatureType get_type() const override { return SurfaceFeatureType::Circle; }
+ Vec3d get_center() const { return m_center; }
+ double get_radius() const { return m_radius; }
+ Vec3d get_normal() const { return m_normal; }
+private:
+ Vec3d m_center;
+ double m_radius;
+ Vec3d m_normal;
+};
+
+class Plane : public SurfaceFeature {
+public:
+ Plane(int idx) : m_idx(idx) {}
+ SurfaceFeatureType get_type() const override { return SurfaceFeatureType::Plane; }
+ int get_plane_idx() const { return m_idx; } // index into vector provided by Measuring::get_plane_triangle_indices
+
+private:
+ int m_idx;
+};
+
+
+class MeasuringImpl;
+
+
+class Measuring {
+public:
+ // Construct the measurement object on a given its. The its must remain
+ // valid and unchanged during the whole lifetime of the object.
+ explicit Measuring(const indexed_triangle_set& its);
+ ~Measuring();
+
+ // Return a reference to a list of all features identified on the its.
+ [[deprecated]]const std::vector& get_features() const;
+
+ // Given a face_idx where the mouse cursor points, return a feature that
+ // should be highlighted or nullptr.
+ const SurfaceFeature* get_feature(size_t face_idx, const Vec3d& point) const;
+
+ // Returns a list of triangle indices for each identified plane. Each
+ // Plane object contains an index into this vector.
+ const std::vector> get_planes_triangle_indices() const;
+
+
+
+ // Returns distance between two SurfaceFeatures.
+ static double get_distance(const SurfaceFeature* a, const SurfaceFeature* b);
+
+ // Returns distance between a SurfaceFeature and a point.
+ static double get_distance(const SurfaceFeature* a, const Vec3d* pt);
+
+ // Returns true if measuring angles between features makes sense.
+ // If so, result contains the angle in radians.
+ static bool get_angle(const SurfaceFeature* a, const SurfaceFeature* b, double& result);
+
+
+private:
+ std::unique_ptr priv;
+};
+
+
+} // namespace Measure
+} // namespace Slic3r
+
+#endif // Slic3r_Measure_hpp_
diff --git a/src/libslic3r/SurfaceMesh.hpp b/src/libslic3r/SurfaceMesh.hpp
new file mode 100644
index 0000000000..a4b261ceb9
--- /dev/null
+++ b/src/libslic3r/SurfaceMesh.hpp
@@ -0,0 +1,153 @@
+#ifndef slic3r_SurfaceMesh_hpp_
+#define slic3r_SurfaceMesh_hpp_
+
+#include
+
+namespace Slic3r {
+
+class TriangleMesh;
+
+
+
+enum Face_index : int;
+
+class Halfedge_index {
+ friend class SurfaceMesh;
+
+public:
+ Halfedge_index() : m_face(Face_index(-1)), m_side(0) {}
+ Face_index face() const { return m_face; }
+ unsigned char side() const { return m_side; }
+ bool is_invalid() const { return int(m_face) < 0; }
+ bool operator!=(const Halfedge_index& rhs) const { return ! ((*this) == rhs); }
+ bool operator==(const Halfedge_index& rhs) const { return m_face == rhs.m_face && m_side == rhs.m_side; }
+
+private:
+ Halfedge_index(int face_idx, unsigned char side_idx) : m_face(Face_index(face_idx)), m_side(side_idx) {}
+
+ Face_index m_face;
+ unsigned char m_side;
+};
+
+
+
+class Vertex_index {
+ friend class SurfaceMesh;
+
+public:
+ Vertex_index() : m_face(Face_index(-1)), m_vertex_idx(0) {}
+ bool is_invalid() const { return int(m_face) < 0; }
+ bool operator==(const Vertex_index& rhs) const = delete; // Use SurfaceMesh::is_same_vertex.
+
+private:
+ Vertex_index(int face_idx, unsigned char vertex_idx) : m_face(Face_index(face_idx)), m_vertex_idx(vertex_idx) {}
+
+ Face_index m_face;
+ unsigned char m_vertex_idx;
+};
+
+
+
+class SurfaceMesh {
+public:
+ explicit SurfaceMesh(const indexed_triangle_set& its)
+ : m_its(its),
+ m_face_neighbors(its_face_neighbors_par(its))
+ {}
+ SurfaceMesh(const SurfaceMesh&) = delete;
+ SurfaceMesh& operator=(const SurfaceMesh&) = delete;
+
+ Vertex_index source(Halfedge_index h) const { assert(! h.is_invalid()); return Vertex_index(h.m_face, h.m_side); }
+ Vertex_index target(Halfedge_index h) const { assert(! h.is_invalid()); return Vertex_index(h.m_face, h.m_side == 2 ? 0 : h.m_side + 1); }
+ Face_index face(Halfedge_index h) const { assert(! h.is_invalid()); return h.m_face; }
+
+ Halfedge_index next(Halfedge_index h) const { assert(! h.is_invalid()); h.m_side = (h.m_side + 1) % 3; return h; }
+ Halfedge_index prev(Halfedge_index h) const { assert(! h.is_invalid()); h.m_side = (h.m_side == 0 ? 2 : h.m_side - 1); return h; }
+ Halfedge_index halfedge(Vertex_index v) const { return Halfedge_index(v.m_face, (v.m_vertex_idx == 0 ? 2 : v.m_vertex_idx - 1)); }
+ Halfedge_index halfedge(Face_index f) const { return Halfedge_index(f, 0); }
+ Halfedge_index opposite(Halfedge_index h) const {
+ if (h.is_invalid())
+ return h;
+
+ int face_idx = m_face_neighbors[h.m_face][h.m_side];
+ Halfedge_index h_candidate = halfedge(Face_index(face_idx));
+
+ if (h_candidate.is_invalid())
+ return Halfedge_index(); // invalid
+
+ for (int i=0; i<3; ++i) {
+ if (is_same_vertex(source(h_candidate), target(h))) {
+ // Meshes in PrusaSlicer should be fixed enough for the following not to happen.
+ assert(is_same_vertex(target(h_candidate), source(h)));
+ return h_candidate;
+ }
+ h_candidate = next(h_candidate);
+ }
+ return Halfedge_index(); // invalid
+ }
+
+ Halfedge_index next_around_target(Halfedge_index h) const { return opposite(next(h)); }
+ Halfedge_index prev_around_target(Halfedge_index h) const { Halfedge_index op = opposite(h); return (op.is_invalid() ? Halfedge_index() : prev(op)); }
+ Halfedge_index next_around_source(Halfedge_index h) const { Halfedge_index op = opposite(h); return (op.is_invalid() ? Halfedge_index() : next(op)); }
+ Halfedge_index prev_around_source(Halfedge_index h) const { return opposite(prev(h)); }
+ Halfedge_index halfedge(Vertex_index source, Vertex_index target) const
+ {
+ Halfedge_index hi(source.m_face, source.m_vertex_idx);
+ assert(! hi.is_invalid());
+
+ const Vertex_index orig_target = this->target(hi);
+ Vertex_index current_target = orig_target;
+
+ while (! is_same_vertex(current_target, target)) {
+ hi = next_around_source(hi);
+ if (hi.is_invalid())
+ break;
+ current_target = this->target(hi);
+ if (is_same_vertex(current_target, orig_target))
+ return Halfedge_index(); // invalid
+ }
+
+ return hi;
+ }
+
+ const stl_vertex& point(Vertex_index v) const { return m_its.vertices[m_its.indices[v.m_face][v.m_vertex_idx]]; }
+
+ size_t degree(Vertex_index v) const
+ {
+ Halfedge_index h_first = halfedge(v);
+ Halfedge_index h = next_around_target(h_first);
+ size_t degree = 2;
+ while (! h.is_invalid() && h != h_first) {
+ h = next_around_target(h);
+ ++degree;
+ }
+ return h.is_invalid() ? 0 : degree - 1;
+ }
+
+ size_t degree(Face_index f) const {
+ size_t total = 0;
+ for (unsigned char i=0; i<3; ++i) {
+ size_t d = degree(Vertex_index(f, i));
+ if (d == 0)
+ return 0;
+ total += d;
+ }
+ assert(total - 6 >= 0);
+ return total - 6; // we counted 3 halfedges from f, and one more for each neighbor
+ }
+
+ bool is_border(Halfedge_index h) const { return m_face_neighbors[h.m_face][h.m_side] == -1; }
+
+ bool is_same_vertex(const Vertex_index& a, const Vertex_index& b) const { return m_its.indices[a.m_face][a.m_vertex_idx] == m_its.indices[b.m_face][b.m_vertex_idx]; }
+ Vec3i get_face_neighbors(Face_index face_id) const { assert(int(face_id) < int(m_face_neighbors.size())); return m_face_neighbors[face_id]; }
+
+
+
+private:
+ const std::vector m_face_neighbors;
+ const indexed_triangle_set& m_its;
+};
+
+} //namespace Slic3r
+
+#endif // slic3r_SurfaceMesh_hpp_
diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp
index da6fcf28f6..8dc22f1490 100644
--- a/src/libslic3r/TriangleMesh.cpp
+++ b/src/libslic3r/TriangleMesh.cpp
@@ -877,13 +877,20 @@ Polygon its_convex_hull_2d_above(const indexed_triangle_set &its, const Transfor
indexed_triangle_set its_make_cube(double xd, double yd, double zd)
{
auto x = float(xd), y = float(yd), z = float(zd);
- return {
+ /*return {
{ {0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7},
{0, 4, 7}, {0, 7, 1}, {1, 7, 6}, {1, 6, 2},
{2, 6, 5}, {2, 5, 3}, {4, 0, 3}, {4, 3, 5} },
{ {x, y, 0}, {x, 0, 0}, {0, 0, 0}, {0, y, 0},
{x, y, z}, {0, y, z}, {0, 0, z}, {x, 0, z} }
- };
+ };*/
+ return {
+ { {0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7},
+ {0, 4, 7}, {0, 7, 1}, {1, 7, 6}, {1, 6, 2},
+ {2, 5, 6}, {2, 5, 3}, {4, 0, 3}, /*{4, 3, 5}*/ },
+ { {x, y, 0}, {x, 0, 0}, {0, 0, 0}, {0, y, 0},
+ {x, y, z}, {0, y, z}, {0, 0, z}, {x, 0, z} }
+ };
}
indexed_triangle_set its_make_prism(float width, float length, float height)
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 71a4941a4d..d016e3d4be 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -63,6 +63,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoSimplify.hpp
GUI/Gizmos/GLGizmoMmuSegmentation.cpp
GUI/Gizmos/GLGizmoMmuSegmentation.hpp
+ GUI/Gizmos/GLGizmoMeasure.cpp
+ GUI/Gizmos/GLGizmoMeasure.hpp
GUI/GLSelectionRectangle.cpp
GUI/GLSelectionRectangle.hpp
GUI/GLModel.hpp
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp
index 0c2fb9c398..72b08273a2 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp
@@ -25,6 +25,8 @@ class GLGizmoFlatten : public GLGizmoBase
private:
+ GLModel arrow;
+
struct PlaneData {
std::vector vertices; // should be in fact local in update_planes()
#if ENABLE_LEGACY_OPENGL_REMOVAL
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp
new file mode 100644
index 0000000000..3f12597987
--- /dev/null
+++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp
@@ -0,0 +1,310 @@
+// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro.
+#include "GLGizmoMeasure.hpp"
+#include "slic3r/GUI/GLCanvas3D.hpp"
+#include "slic3r/GUI/GUI_App.hpp"
+#include "slic3r/GUI/Plater.hpp"
+
+#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
+
+#include "libslic3r/Model.hpp"
+#include "libslic3r/Measure.hpp"
+
+#include
+
+#include
+
+namespace Slic3r {
+namespace GUI {
+
+static const Slic3r::ColorRGBA DEFAULT_PLANE_COLOR = { 0.9f, 0.9f, 0.9f, 0.9f };
+static const Slic3r::ColorRGBA DEFAULT_HOVER_PLANE_COLOR = { 0.9f, 0.2f, 0.2f, 1.f };
+
+GLGizmoMeasure::GLGizmoMeasure(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
+ : GLGizmoBase(parent, icon_filename, sprite_id)
+{
+ m_vbo_sphere.init_from(its_make_sphere(1., M_PI/32.));
+ m_vbo_cylinder.init_from(its_make_cylinder(1., 1.));
+}
+
+bool GLGizmoMeasure::on_mouse(const wxMouseEvent &mouse_event)
+{
+ m_mouse_pos_x = mouse_event.GetX();
+ m_mouse_pos_y = mouse_event.GetY();
+
+
+ if (mouse_event.Moving()) {
+ // only for sure
+ m_mouse_left_down = false;
+ return false;
+ }
+ if (mouse_event.LeftDown()) {
+ if (m_hover_id != -1) {
+ m_mouse_left_down = true;
+
+ return true;
+ }
+
+ // fix: prevent restart gizmo when reselect object
+ // take responsibility for left up
+ if (m_parent.get_first_hover_volume_idx() >= 0) m_mouse_left_down = true;
+
+ } else if (mouse_event.LeftUp()) {
+ if (m_mouse_left_down) {
+ // responsible for mouse left up after selecting plane
+ m_mouse_left_down = false;
+ return true;
+ }
+ } else if (mouse_event.Leaving()) {
+ m_mouse_left_down = false;
+ }
+ return false;
+}
+
+
+
+void GLGizmoMeasure::data_changed()
+{
+ const Selection & selection = m_parent.get_selection();
+ const ModelObject *model_object = nullptr;
+ if (selection.is_single_full_instance() ||
+ selection.is_from_single_object() ) {
+ model_object = selection.get_model()->objects[selection.get_object_idx()];
+ }
+ set_flattening_data(model_object);
+}
+
+
+
+bool GLGizmoMeasure::on_init()
+{
+ // FIXME m_shortcut_key = WXK_CONTROL_F;
+ return true;
+}
+
+
+
+void GLGizmoMeasure::on_set_state()
+{
+}
+
+
+
+CommonGizmosDataID GLGizmoMeasure::on_get_requirements() const
+{
+ return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::Raycaster));
+}
+
+
+
+std::string GLGizmoMeasure::on_get_name() const
+{
+ return _u8L("Measure");
+}
+
+
+
+bool GLGizmoMeasure::on_is_activable() const
+{
+ // This is assumed in GLCanvas3D::do_rotate, do not change this
+ // without updating that function too.
+ return m_parent.get_selection().is_single_full_instance();
+}
+
+
+
+void GLGizmoMeasure::on_render()
+{
+ const Selection& selection = m_parent.get_selection();
+
+ GLShaderProgram* shader = wxGetApp().get_shader("flat");
+ if (shader == nullptr)
+ return;
+
+ shader->start_using();
+
+ glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
+
+ glsafe(::glEnable(GL_DEPTH_TEST));
+ glsafe(::glEnable(GL_BLEND));
+ glsafe(::glLineWidth(2.f));
+
+ if (selection.is_single_full_instance()) {
+ const Transform3d& m = selection.get_volume(*selection.get_volume_idxs().begin())->get_instance_transformation().get_matrix();
+ const Camera& camera = wxGetApp().plater()->get_camera();
+ const Transform3d view_model_matrix = camera.get_view_matrix() *
+ Geometry::assemble_transform(selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z() * Vec3d::UnitZ()) * m;
+
+ shader->set_uniform("view_model_matrix", view_model_matrix);
+ shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+
+
+ update_if_needed();
+
+
+ m_imgui->begin(std::string("DEBUG"));
+
+ m_imgui->checkbox(wxString("Show all features"), m_show_all);
+ m_imgui->checkbox(wxString("Show all planes"), m_show_planes);
+
+ Vec3f pos;
+ Vec3f normal;
+ size_t facet_idx;
+ m_c->raycaster()->raycasters().front()->unproject_on_mesh(Vec2d(m_mouse_pos_x, m_mouse_pos_y), m, camera, pos, normal, nullptr, &facet_idx);
+ ImGui::Separator();
+ m_imgui->text(std::string("face_idx: ") + std::to_string(facet_idx));
+ m_imgui->text(std::string("pos_x: ") + std::to_string(pos.x()));
+ m_imgui->text(std::string("pos_y: ") + std::to_string(pos.y()));
+ m_imgui->text(std::string("pos_z: ") + std::to_string(pos.z()));
+
+
+
+ std::vector features = {m_measuring->get_feature(facet_idx, pos.cast())};
+ if (m_show_all) {
+ features = m_measuring->get_features();
+ features.erase(std::remove_if(features.begin(), features.end(),
+ [](const Measure::SurfaceFeature* f) {
+ return f->get_type() == Measure::SurfaceFeatureType::Plane;
+ }), features.end());
+ }
+
+
+ for (const Measure::SurfaceFeature* feature : features) {
+ if (! feature)
+ continue;
+
+ if (feature->get_type() == Measure::SurfaceFeatureType::Circle) {
+ const auto* circle = static_cast(feature);
+ const Vec3d& c = circle->get_center();
+ const Vec3d& n = circle->get_normal();
+ Transform3d view_feature_matrix = view_model_matrix * Transform3d(Eigen::Translation3d(c));
+ view_feature_matrix.scale(0.5);
+ shader->set_uniform("view_model_matrix", view_feature_matrix);
+ m_vbo_sphere.set_color(ColorRGBA(0.8f, 0.2f, 0.2f, 1.f));
+ m_vbo_sphere.render();
+
+ // Now draw the circle itself - let's take a funny shortcut:
+ Vec3d rad = n.cross(Vec3d::UnitX());
+ if (rad.squaredNorm() < 0.1)
+ rad = n.cross(Vec3d::UnitY());
+ rad *= circle->get_radius() * rad.norm();
+ const int N = 20;
+ for (int i=0; iset_uniform("view_model_matrix", view_feature_matrix);
+ m_vbo_sphere.render();
+ }
+ }
+ else if (feature->get_type() == Measure::SurfaceFeatureType::Edge) {
+ const auto* edge = static_cast(feature);
+ auto& [start, end] = edge->get_edge();
+ Transform3d view_feature_matrix = view_model_matrix * Transform3d(Eigen::Translation3d(start));
+ auto q = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitZ(), end - start);
+ view_feature_matrix *= q;
+ view_feature_matrix.scale(Vec3d(0.075, 0.075, (end - start).norm()));
+ shader->set_uniform("view_model_matrix", view_feature_matrix);
+ m_vbo_cylinder.set_color(ColorRGBA(0.8f, 0.2f, 0.2f, 1.f));
+ m_vbo_cylinder.render();
+ if (edge->get_point_of_interest()) {
+ Vec3d pin = *edge->get_point_of_interest();
+ view_feature_matrix = view_model_matrix * Transform3d(Eigen::Translation3d(pin));
+ view_feature_matrix.scale(0.5);
+ shader->set_uniform("view_model_matrix", view_feature_matrix);
+ m_vbo_sphere.set_color(ColorRGBA(0.8f, 0.2f, 0.2f, 1.f));
+ m_vbo_sphere.render();
+ }
+ }
+ else if (feature->get_type() == Measure::SurfaceFeatureType::Plane) {
+ const auto* plane = static_cast(feature);
+ assert(plane->get_plane_idx() < m_plane_models.size());
+ m_plane_models[plane->get_plane_idx()]->render();
+ }
+ }
+ shader->set_uniform("view_model_matrix", view_model_matrix);
+ if (m_show_planes)
+ for (const auto& glmodel : m_plane_models)
+ glmodel->render();
+
+ m_imgui->end();
+ }
+
+ glsafe(::glEnable(GL_CULL_FACE));
+ glsafe(::glDisable(GL_BLEND));
+
+ shader->stop_using();
+}
+
+
+
+
+
+#if ! ENABLE_LEGACY_OPENGL_REMOVAL
+ #error NOT IMPLEMENTED
+#endif
+
+void GLGizmoMeasure::set_flattening_data(const ModelObject* model_object)
+{
+ if (model_object != m_old_model_object)
+ update_if_needed();
+}
+
+
+void GLGizmoMeasure::update_if_needed()
+{
+ const ModelObject* mo = m_c->selection_info()->model_object();
+ if (m_state != On || ! mo || mo->instances.empty())
+ return;
+
+ if (! m_measuring || mo != m_old_model_object
+ || mo->volumes.size() != m_volumes_matrices.size())
+ goto UPDATE;
+
+ // We want to recalculate when the scale changes - some planes could (dis)appear.
+ if (! mo->instances.front()->get_scaling_factor().isApprox(m_first_instance_scale)
+ || ! mo->instances.front()->get_mirror().isApprox(m_first_instance_mirror))
+ goto UPDATE;
+
+ for (unsigned int i=0; i < mo->volumes.size(); ++i)
+ if (! mo->volumes[i]->get_matrix().isApprox(m_volumes_matrices[i])
+ || mo->volumes[i]->type() != m_volumes_types[i])
+ goto UPDATE;
+
+ return;
+
+UPDATE:
+ const indexed_triangle_set& its = mo->volumes.front()->mesh().its;
+ m_measuring.reset(new Measure::Measuring(its));
+ m_plane_models.clear();
+ const std::vector> planes_triangles = m_measuring->get_planes_triangle_indices();
+ for (const std::vector& triangle_indices : planes_triangles) {
+ m_plane_models.emplace_back(std::unique_ptr(new GLModel()));
+ GUI::GLModel::Geometry init_data;
+ init_data.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3 };
+ init_data.color = ColorRGBA(0.9f, 0.9f, 0.9f, 0.5f);
+ int i = 0;
+ for (int idx : triangle_indices) {
+ init_data.add_vertex(its.vertices[its.indices[idx][0]]);
+ init_data.add_vertex(its.vertices[its.indices[idx][1]]);
+ init_data.add_vertex(its.vertices[its.indices[idx][2]]);
+ init_data.add_triangle(i, i+1, i+2);
+ i+=3;
+ }
+ m_plane_models.back()->init_from(std::move(init_data));
+ }
+
+ // Let's save what we calculated it from:
+ m_volumes_matrices.clear();
+ m_volumes_types.clear();
+ for (const ModelVolume* vol : mo->volumes) {
+ m_volumes_matrices.push_back(vol->get_matrix());
+ m_volumes_types.push_back(vol->type());
+ }
+ m_first_instance_scale = mo->instances.front()->get_scaling_factor();
+ m_first_instance_mirror = mo->instances.front()->get_mirror();
+ m_old_model_object = mo;
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp
new file mode 100644
index 0000000000..9094007b5d
--- /dev/null
+++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp
@@ -0,0 +1,78 @@
+#ifndef slic3r_GLGizmoMeasure_hpp_
+#define slic3r_GLGizmoMeasure_hpp_
+
+#include "GLGizmoBase.hpp"
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+#include "slic3r/GUI/GLModel.hpp"
+#else
+#include "slic3r/GUI/3DScene.hpp"
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+
+
+#include
+
+
+namespace Slic3r {
+
+enum class ModelVolumeType : int;
+
+namespace Measure { class Measuring; }
+
+
+namespace GUI {
+
+
+class GLGizmoMeasure : public GLGizmoBase
+{
+// This gizmo does not use grabbers. The m_hover_id relates to polygon managed by the class itself.
+
+private:
+ std::unique_ptr m_measuring;
+
+ GLModel m_vbo_sphere;
+ GLModel m_vbo_cylinder;
+
+ // This holds information to decide whether recalculation is necessary:
+ std::vector m_volumes_matrices;
+ std::vector m_volumes_types;
+ Vec3d m_first_instance_scale;
+ Vec3d m_first_instance_mirror;
+
+ bool m_mouse_left_down = false; // for detection left_up of this gizmo
+ bool m_planes_valid = false;
+ const ModelObject* m_old_model_object = nullptr;
+ std::vector instances_matrices;
+
+ int m_mouse_pos_x;
+ int m_mouse_pos_y;
+ bool m_show_all = false;
+ bool m_show_planes = false;
+ std::vector> m_plane_models;
+
+ void update_if_needed();
+ void set_flattening_data(const ModelObject* model_object);
+
+public:
+ GLGizmoMeasure(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
+
+ ///
+ /// Apply rotation on select plane
+ ///
+ /// Keep information about mouse click
+ /// Return True when use the information otherwise False.
+ bool on_mouse(const wxMouseEvent &mouse_event) override;
+
+ void data_changed() override;
+protected:
+ bool on_init() override;
+ std::string on_get_name() const override;
+ bool on_is_activable() const override;
+ void on_render() override;
+ void on_set_state() override;
+ CommonGizmosDataID on_get_requirements() const override;
+};
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // slic3r_GLGizmoMeasure_hpp_
diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
index 8759e880d7..6d25f84fbc 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
@@ -21,6 +21,7 @@
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
+#include "slic3r/GUI/Gizmos/GLGizmoMeasure.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/Model.hpp"
@@ -106,6 +107,7 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoSeam(m_parent, "seam.svg", 8));
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, "mmu_segmentation.svg", 9));
m_gizmos.emplace_back(new GLGizmoSimplify(m_parent, "cut.svg", 10));
+ m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 11));
m_common_gizmos_data.reset(new CommonGizmosDataPool(&m_parent));
diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp
index 8a708f62a6..a759d911a9 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp
@@ -80,6 +80,7 @@ public:
Seam,
MmuSegmentation,
Simplify,
+ Measure,
Undefined
};
diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt
index 770e990a5a..d39d889ef7 100644
--- a/tests/libslic3r/CMakeLists.txt
+++ b/tests/libslic3r/CMakeLists.txt
@@ -25,6 +25,7 @@ add_executable(${_TEST_NAME}_tests
test_voronoi.cpp
test_optimizers.cpp
test_png_io.cpp
+ test_surface_mesh.cpp
test_timeutils.cpp
test_indexed_triangle_set.cpp
test_astar.cpp
diff --git a/tests/libslic3r/test_surface_mesh.cpp b/tests/libslic3r/test_surface_mesh.cpp
new file mode 100644
index 0000000000..34ff356679
--- /dev/null
+++ b/tests/libslic3r/test_surface_mesh.cpp
@@ -0,0 +1,122 @@
+#include
+#include
+
+
+#include
+
+using namespace Slic3r;
+
+
+// Generate a broken cube mesh. Face 8 is inverted, face 11 is missing.
+indexed_triangle_set its_make_cube_broken(double xd, double yd, double zd)
+{
+ auto x = float(xd), y = float(yd), z = float(zd);
+ return {
+ { {0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7},
+ {0, 4, 7}, {0, 7, 1}, {1, 7, 6}, {1, 6, 2},
+ {2, 5, 6}, {2, 5, 3}, {4, 0, 3} /*missing face*/ },
+ { {x, y, 0}, {x, 0, 0}, {0, 0, 0}, {0, y, 0},
+ {x, y, z}, {0, y, z}, {0, 0, z}, {x, 0, z} }
+ };
+}
+
+
+
+TEST_CASE("SurfaceMesh on a cube", "[SurfaceMesh]") {
+ indexed_triangle_set cube = its_make_cube(1., 1., 1.);
+ SurfaceMesh sm(cube);
+ const Halfedge_index hi_first = sm.halfedge(Face_index(0));
+ Halfedge_index hi = hi_first;
+
+ REQUIRE(! hi_first.is_invalid());
+
+ SECTION("next / prev halfedge") {
+ hi = sm.next(hi);
+ REQUIRE(hi != hi_first);
+ hi = sm.next(hi);
+ hi = sm.next(hi);
+ REQUIRE(hi == hi_first);
+ hi = sm.prev(hi);
+ REQUIRE(hi != hi_first);
+ hi = sm.prev(hi);
+ hi = sm.prev(hi);
+ REQUIRE(hi == hi_first);
+ }
+
+ SECTION("next_around_target") {
+ // Check that we get to the same halfedge after applying next_around_target
+ // four times.
+ const Vertex_index target_vert = sm.target(hi_first);
+ for (int i=0; i<4;++i) {
+ hi = sm.next_around_target(hi);
+ REQUIRE((hi == hi_first) == (i == 3));
+ REQUIRE(sm.is_same_vertex(sm.target(hi), target_vert));
+ REQUIRE(! sm.is_border(hi));
+ }
+ }
+
+ SECTION("iterate around target and source") {
+ hi = sm.next_around_target(hi);
+ hi = sm.prev_around_target(hi);
+ hi = sm.prev_around_source(hi);
+ hi = sm.next_around_source(hi);
+ REQUIRE(hi == hi_first);
+ }
+
+ SECTION("opposite") {
+ const Vertex_index target = sm.target(hi);
+ const Vertex_index source = sm.source(hi);
+ hi = sm.opposite(hi);
+ REQUIRE(sm.is_same_vertex(target, sm.source(hi)));
+ REQUIRE(sm.is_same_vertex(source, sm.target(hi)));
+ hi = sm.opposite(hi);
+ REQUIRE(hi == hi_first);
+ }
+
+ SECTION("halfedges walk") {
+ for (int i=0; i<4; ++i) {
+ hi = sm.next(hi);
+ hi = sm.opposite(hi);
+ }
+ REQUIRE(hi == hi_first);
+ }
+
+ SECTION("point accessor") {
+ Halfedge_index hi = sm.halfedge(Face_index(0));
+ hi = sm.opposite(hi);
+ hi = sm.prev(hi);
+ hi = sm.opposite(hi);
+ REQUIRE(hi.face() == Face_index(6));
+ REQUIRE(sm.point(sm.target(hi)).isApprox(cube.vertices[7]));
+ }
+}
+
+
+
+
+TEST_CASE("SurfaceMesh on a broken cube", "[SurfaceMesh]") {
+ indexed_triangle_set cube = its_make_cube_broken(1., 1., 1.);
+ SurfaceMesh sm(cube);
+
+ SECTION("Check inverted face") {
+ Halfedge_index hi = sm.halfedge(Face_index(8));
+ for (int i=0; i<3; ++i) {
+ REQUIRE(! hi.is_invalid());
+ REQUIRE(sm.is_border(hi));
+ }
+ REQUIRE(hi == sm.halfedge(Face_index(8)));
+ hi = sm.opposite(hi);
+ REQUIRE(hi.is_invalid());
+ }
+
+ SECTION("missing face") {
+ Halfedge_index hi = sm.halfedge(Face_index(0));
+ for (int i=0; i<3; ++i)
+ hi = sm.next_around_source(hi);
+ hi = sm.next(hi);
+ REQUIRE(sm.is_border(hi));
+ REQUIRE(! hi.is_invalid());
+ hi = sm.opposite(hi);
+ REQUIRE(hi.is_invalid());
+ }
+}