diff --git a/resources/images/copy_menu.svg b/resources/images/copy_menu.svg
new file mode 100644
index 0000000000..0d1af6a0a7
--- /dev/null
+++ b/resources/images/copy_menu.svg
@@ -0,0 +1,37 @@
+
+
+
diff --git a/resources/images/measure.svg b/resources/images/measure.svg
new file mode 100644
index 0000000000..3ea137a1ed
--- /dev/null
+++ b/resources/images/measure.svg
@@ -0,0 +1,93 @@
+
+
+
+
diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h
index 1774eb28ac..be78d7cd40 100644
--- a/src/imgui/imconfig.h
+++ b/src/imgui/imconfig.h
@@ -161,6 +161,7 @@ namespace ImGui
const wchar_t ClippyMarker = 0x0802;
const wchar_t InfoMarker = 0x0803;
const wchar_t SliderFloatEditBtnIcon = 0x0804;
+ const wchar_t ClipboardBtnIcon = 0x0805;
// BBS
const wchar_t CircleButtonIcon = 0x0810;
diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index 0a840db87f..c5d6463a39 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -208,6 +208,9 @@ set(lisbslic3r_sources
ModelArrange.cpp
MultiMaterialSegmentation.cpp
MultiMaterialSegmentation.hpp
+ Measure.hpp
+ Measure.cpp
+ MeasureUtils.hpp
CustomGCode.cpp
CustomGCode.hpp
Arrange.hpp
@@ -307,6 +310,7 @@ set(lisbslic3r_sources
Surface.hpp
SurfaceCollection.cpp
SurfaceCollection.hpp
+ SurfaceMesh.hpp
SVG.cpp
SVG.hpp
Technologies.hpp
diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp
index bafed3b9cf..0a3dcad7d5 100644
--- a/src/libslic3r/Geometry.hpp
+++ b/src/libslic3r/Geometry.hpp
@@ -1,3 +1,18 @@
+///|/ Copyright (c) Prusa Research 2016 - 2023 Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966, Tomáš Mészáros @tamasmeszaros, Lukáš Matěna @lukasmatena, Filip Sykala @Jony01, Lukáš Hejl @hejllukas
+///|/ Copyright (c) 2017 Eyal Soha @eyal0
+///|/ Copyright (c) Slic3r 2013 - 2016 Alessandro Ranellucci @alranel
+///|/
+///|/ ported from lib/Slic3r/Geometry.pm:
+///|/ Copyright (c) Prusa Research 2017 - 2022 Vojtěch Bubník @bubnikv
+///|/ Copyright (c) Slic3r 2011 - 2015 Alessandro Ranellucci @alranel
+///|/ Copyright (c) 2013 Jose Luis Perez Diez
+///|/ Copyright (c) 2013 Anders Sundman
+///|/ Copyright (c) 2013 Jesse Vincent
+///|/ Copyright (c) 2012 Mike Sheldrake @mesheldrake
+///|/ Copyright (c) 2012 Mark Hindess
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
#ifndef slic3r_Geometry_hpp_
#define slic3r_Geometry_hpp_
diff --git a/src/libslic3r/Geometry/Circle.cpp b/src/libslic3r/Geometry/Circle.cpp
index 4d7c38ccc2..012b240f8a 100644
--- a/src/libslic3r/Geometry/Circle.cpp
+++ b/src/libslic3r/Geometry/Circle.cpp
@@ -1,3 +1,7 @@
+///|/ Copyright (c) Prusa Research 2021 - 2022 Lukáš Matěna @lukasmatena, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
#include "Circle.hpp"
#include "../Polygon.hpp"
@@ -108,7 +112,7 @@ Circled circle_taubin_newton(const Vec2ds& input, size_t cycles)
return out;
}
-Circled circle_ransac(const Vec2ds& input, size_t iterations)
+Circled circle_ransac(const Vec2ds& input, size_t iterations, double* min_error)
{
if (input.size() < 3)
return Circled::make_invalid();
@@ -132,6 +136,8 @@ Circled circle_ransac(const Vec2ds& input, size_t iterations)
circle_best = c;
}
}
+ if (min_error)
+ *min_error = err_min;
return circle_best;
}
diff --git a/src/libslic3r/Geometry/Circle.hpp b/src/libslic3r/Geometry/Circle.hpp
index 39973d916d..a192cc2fd6 100644
--- a/src/libslic3r/Geometry/Circle.hpp
+++ b/src/libslic3r/Geometry/Circle.hpp
@@ -1,3 +1,7 @@
+///|/ Copyright (c) Prusa Research 2021 - 2022 Lukáš Matěna @lukasmatena, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
#ifndef slic3r_Geometry_Circle_hpp_
#define slic3r_Geometry_Circle_hpp_
@@ -102,7 +106,7 @@ inline Vec2d circle_center_taubin_newton(const Vec2ds& input, size_t cycles = 20
Circled circle_taubin_newton(const Vec2ds& input, size_t cycles = 20);
// Find circle using RANSAC randomized algorithm.
-Circled circle_ransac(const Vec2ds& input, size_t iterations = 20);
+Circled circle_ransac(const Vec2ds& input, size_t iterations = 20, double* min_error = nullptr);
// Randomized algorithm by Emo Welzl, working with squared radii for efficiency. The returned circle radius is inflated by epsilon.
template
diff --git a/src/libslic3r/Measure.cpp b/src/libslic3r/Measure.cpp
new file mode 100644
index 0000000000..2e6156a88e
--- /dev/null
+++ b/src/libslic3r/Measure.cpp
@@ -0,0 +1,1255 @@
+///|/ Copyright (c) Prusa Research 2022 - 2023 Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Vojtěch Bubník @bubnikv, Pavel Mikuš @Godrak
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
+#include "libslic3r/libslic3r.h"
+#include "Measure.hpp"
+#include "MeasureUtils.hpp"
+
+#include "libslic3r/Geometry/Circle.hpp"
+#include "libslic3r/SurfaceMesh.hpp"
+
+
+#include
+#include
+
+#define DEBUG_EXTRACT_ALL_FEATURES_AT_ONCE 0
+
+namespace Slic3r {
+namespace Measure {
+
+
+constexpr double feature_hover_limit = 0.5; // how close to a feature the mouse must be to highlight it
+
+static std::tuple get_center_and_radius(const std::vector& points, const Transform3d& trafo, const Transform3d& trafo_inv)
+{
+ Vec2ds out;
+ double z = 0.;
+ for (const Vec3d& pt : points) {
+ Vec3d pt_transformed = trafo * pt;
+ z = pt_transformed.z();
+ out.emplace_back(pt_transformed.x(), pt_transformed.y());
+ }
+
+ const int iter = points.size() < 10 ? 2 :
+ points.size() < 100 ? 4 :
+ 6;
+
+ double error = std::numeric_limits::max();
+ auto circle = Geometry::circle_ransac(out, iter, &error);
+
+ return std::make_tuple(trafo.inverse() * Vec3d(circle.center.x(), circle.center.y(), z), circle.radius, error);
+}
+
+
+
+static std::array orthonormal_basis(const Vec3d& v)
+{
+ std::array ret;
+ ret[2] = v.normalized();
+ int index;
+ ret[2].cwiseAbs().maxCoeff(&index);
+ switch (index)
+ {
+ case 0: { ret[0] = Vec3d(ret[2].y(), -ret[2].x(), 0.0).normalized(); break; }
+ case 1: { ret[0] = Vec3d(0.0, ret[2].z(), -ret[2].y()).normalized(); break; }
+ case 2: { ret[0] = Vec3d(-ret[2].z(), 0.0, ret[2].x()).normalized(); break; }
+ }
+ ret[1] = ret[2].cross(ret[0]).normalized();
+ return ret;
+}
+
+
+
+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;
+ bool features_extracted = false;
+ };
+
+ std::optional get_feature(size_t face_idx, const Vec3d& point);
+ int get_num_of_planes() const;
+ const std::vector& get_plane_triangle_indices(int idx) const;
+ const std::vector& get_plane_features(unsigned int plane_id);
+ const indexed_triangle_set& get_its() const;
+
+private:
+ void update_planes();
+ void extract_features(int plane_idx);
+
+ std::vector m_planes;
+ std::vector m_face_to_plane;
+ indexed_triangle_set m_its;
+};
+
+
+
+
+
+
+MeasuringImpl::MeasuringImpl(const indexed_triangle_set& its)
+: m_its(its)
+{
+ update_planes();
+
+ // Extracting features will be done as needed.
+ // To extract all planes at once, run the following:
+#if DEBUG_EXTRACT_ALL_FEATURES_AT_ONCE
+ for (int i=0; i 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);
+ };
+
+ m_planes.clear();
+ m_planes.reserve(num_of_facets / 5); // empty plane data object is quite lightweight, let's save the initial reallocations
+
+
+ // First go through all the triangles and fill in m_planes vector. For each "plane"
+ // detected on the model, it will contain list of facets that are part of it.
+ // We will also fill in m_face_to_plane, which contains index into m_planes
+ // for each of the source facets.
+ 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());
+ }
+
+ // Check that each facet is part of one of the planes.
+ assert(std::none_of(m_face_to_plane.begin(), m_face_to_plane.end(), [](size_t val) { return val == size_t(-1); }));
+
+ // Now we will walk around each of the planes and save vertices which form the border.
+ const SurfaceMesh sm(m_its);
+
+ const auto& face_to_plane = m_face_to_plane;
+ auto& planes = m_planes;
+
+ tbb::parallel_for(tbb::blocked_range(0, m_planes.size()),
+ [&planes, &face_to_plane, &face_neighbors, &sm](const tbb::blocked_range& range) {
+ for (size_t plane_id = range.begin(); plane_id != range.end(); ++plane_id) {
+
+ const auto& facets = planes[plane_id].facets;
+ planes[plane_id].borders.clear();
+ std::vector> visited(facets.size(), {false, false, false});
+
+ for (int face_id=0; face_id& last_border = planes[plane_id].borders.back();
+ last_border.reserve(4);
+ 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);
+ if (he.is_invalid())
+ goto PLANE_FAILURE;
+
+ // For broken meshes, the iteration might never get back to he_orig.
+ // Remember all halfedges we saw to break out of such infinite loops.
+ boost::container::small_vector he_seen;
+
+ while ( face_to_plane[sm.face(he)] == plane_id && he != he_orig) {
+ he_seen.emplace_back(he);
+ he = sm.next_around_target(he);
+ if (he.is_invalid() || std::find(he_seen.begin(), he_seen.end(), he) != he_seen.end())
+ goto PLANE_FAILURE;
+ }
+ he = sm.opposite(he);
+ if (he.is_invalid())
+ goto PLANE_FAILURE;
+
+ Face_index fi = he.face();
+ auto face_it = std::lower_bound(facets.begin(), facets.end(), int(fi));
+ if (face_it == facets.end() || *face_it != int(fi)) // This indicates a broken mesh.
+ goto PLANE_FAILURE;
+
+ 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());
+
+ // In case of broken meshes, this loop might be infinite. Break
+ // out in case it is clearly going bad.
+ if (last_border.size() > 3*facets.size()+1)
+ goto PLANE_FAILURE;
+
+ } while (he != he_start);
+
+ if (last_border.size() == 1)
+ planes[plane_id].borders.pop_back();
+ else {
+ assert(last_border.front() == last_border.back());
+ last_border.pop_back();
+ }
+ }
+ }
+ continue; // There was no failure.
+
+ PLANE_FAILURE:
+ planes[plane_id].borders.clear();
+ }});
+ m_planes.shrink_to_fit();
+}
+
+
+
+
+
+
+void MeasuringImpl::extract_features(int plane_idx)
+{
+ assert(! m_planes[plane_idx].features_extracted);
+
+ PlaneData& plane = m_planes[plane_idx];
+ plane.surface_features.clear();
+ const Vec3d& normal = plane.normal;
+
+ Eigen::Quaterniond q;
+ q.setFromTwoVectors(plane.normal, Vec3d::UnitZ());
+ Transform3d trafo = Transform3d::Identity();
+ trafo.rotate(q);
+ const Transform3d trafo_inv = trafo.inverse();
+
+ std::vector angles; // placed in outer scope to prevent reallocations
+ std::vector lengths;
+
+ for (const std::vector& border : plane.borders) {
+ if (border.size() <= 1)
+ continue;
+
+ bool done = false;
+
+ if (border.size() > 4) {
+ const auto& [center, radius, err] = get_center_and_radius(border, trafo, trafo_inv);
+
+ if (err < 0.05) {
+ // The whole border is one circle. Just add it into the list of features
+ // and we are done.
+
+ bool is_polygon = border.size()>4 && border.size()<=8;
+ bool lengths_match = std::all_of(border.begin()+2, border.end(), [is_polygon](const Vec3d& pt) {
+ return Slic3r::is_approx((pt - *((&pt)-1)).squaredNorm(), (*((&pt)-1) - *((&pt)-2)).squaredNorm(), is_polygon ? 0.01 : 0.01);
+ });
+
+ if (lengths_match && (is_polygon || border.size() > 8)) {
+ if (is_polygon) {
+ // This is a polygon, add the separate edges with the center.
+ for (int j=0; j int {
+ assert(std::abs(offset) < border_size);
+ int out = idx+offset;
+ if (out >= border_size)
+ out = out - border_size;
+ else if (out < 0)
+ out = border_size + out;
+
+ return out;
+ };
+
+ // First calculate angles at all the vertices.
+ angles.clear();
+ lengths.clear();
+ int first_different_angle_idx = 0;
+ for (int i=0; i M_PI)
+ angle = 2*M_PI - angle;
+
+ angles.push_back(angle);
+ lengths.push_back(v2.norm());
+ if (first_different_angle_idx == 0 && angles.size() > 1) {
+ if (! are_angles_same(angles.back(), angles[angles.size()-2]))
+ first_different_angle_idx = angles.size()-1;
+ }
+ }
+ assert(border.size() == angles.size());
+ assert(border.size() == lengths.size());
+
+ // First go around the border and pick what might be circular segments.
+ // Save pair of indices to where such potential segments start and end.
+ // Also remember the length of these segments.
+ int start_idx = -1;
+ bool circle = false;
+ bool first_iter = true;
+ std::vector circles;
+ std::vector edges;
+ std::vector> circles_idxs;
+ //std::vector circles_lengths;
+ std::vector single_circle; // could be in loop-scope, but reallocations
+ double single_circle_length = 0.;
+ int first_pt_idx = offset_to_index(first_different_angle_idx, 1);
+ int i = first_pt_idx;
+ while (i != first_pt_idx || first_iter) {
+ if (are_angles_same(angles[i], angles[offset_to_index(i,-1)])
+ && i != offset_to_index(first_pt_idx, -1) // not the last point
+ && i != start_idx ) {
+ // circle
+ if (! circle) {
+ circle = true;
+ single_circle.clear();
+ single_circle_length = 0.;
+ start_idx = offset_to_index(i, -2);
+ single_circle = { border[start_idx], border[offset_to_index(start_idx,1)] };
+ single_circle_length += lengths[offset_to_index(i, -1)];
+ }
+ single_circle.emplace_back(border[i]);
+ single_circle_length += lengths[i];
+ } else {
+ if (circle && single_circle.size() >= 5) { // Less than 5 vertices? Not a circle.
+ single_circle.emplace_back(border[i]);
+ single_circle_length += lengths[i];
+
+ bool accept_circle = true;
+ {
+ // Check that lengths of internal (!!!) edges match.
+ int j = offset_to_index(start_idx, 3);
+ while (j != i) {
+ if (! are_lengths_same(lengths[offset_to_index(j,-1)], lengths[j])) {
+ accept_circle = false;
+ break;
+ }
+ j = offset_to_index(j, 1);
+ }
+ }
+
+ if (accept_circle) {
+ const auto& [center, radius, err] = get_center_and_radius(single_circle, trafo, trafo_inv);
+
+ // Check that the fit went well. The tolerance is high, only to
+ // reject complete failures.
+ accept_circle &= err < 0.05;
+
+ // If the segment subtends less than 90 degrees, throw it away.
+ accept_circle &= single_circle_length / radius > 0.9*M_PI/2.;
+
+ if (accept_circle) {
+ // Add the circle and remember indices into borders.
+ circles_idxs.emplace_back(start_idx, i);
+ circles.emplace_back(SurfaceFeature(SurfaceFeatureType::Circle, center, plane.normal, std::nullopt, radius));
+ }
+ }
+ }
+ circle = false;
+ }
+ // Take care of the wrap around.
+ first_iter = false;
+ i = offset_to_index(i, 1);
+ }
+
+ // We have the circles. Now go around again and pick edges, while jumping over circles.
+ if (circles_idxs.empty()) {
+ // Just add all edges.
+ for (int i=1; i 1 || circles_idxs.front().first != circles_idxs.front().second) {
+ // There is at least one circular segment. Start at its end and add edges until the start of the next one.
+ int i = circles_idxs.front().second;
+ int circle_idx = 1;
+ while (true) {
+ i = offset_to_index(i, 1);
+ edges.emplace_back(SurfaceFeature(SurfaceFeatureType::Edge, border[offset_to_index(i,-1)], border[i]));
+ if (circle_idx < int(circles_idxs.size()) && i == circles_idxs[circle_idx].first) {
+ i = circles_idxs[circle_idx].second;
+ ++circle_idx;
+ }
+ if (i == circles_idxs.front().first)
+ break;
+ }
+ }
+
+ // Merge adjacent edges where needed.
+ assert(std::all_of(edges.begin(), edges.end(),
+ [](const SurfaceFeature& f) { return f.get_type() == SurfaceFeatureType::Edge; }));
+ for (int i=edges.size()-1; i>=0; --i) {
+ const auto& [first_start, first_end] = edges[i==0 ? edges.size()-1 : i-1].get_edge();
+ const auto& [second_start, second_end] = edges[i].get_edge();
+
+ if (Slic3r::is_approx(first_end, second_start)
+ && Slic3r::is_approx((first_end-first_start).normalized().dot((second_end-second_start).normalized()), 1.)) {
+ // The edges have the same direction and share a point. Merge them.
+ edges[i==0 ? edges.size()-1 : i-1] = SurfaceFeature(SurfaceFeatureType::Edge, first_start, second_end);
+ edges.erase(edges.begin() + i);
+ }
+ }
+
+ // Now move the circles and edges into the feature list for the plane.
+ assert(std::all_of(circles.begin(), circles.end(), [](const SurfaceFeature& f) {
+ return f.get_type() == SurfaceFeatureType::Circle;
+ }));
+ assert(std::all_of(edges.begin(), edges.end(), [](const SurfaceFeature& f) {
+ return f.get_type() == SurfaceFeatureType::Edge;
+ }));
+ plane.surface_features.insert(plane.surface_features.end(), std::make_move_iterator(circles.begin()),
+ std::make_move_iterator(circles.end()));
+ plane.surface_features.insert(plane.surface_features.end(), std::make_move_iterator(edges.begin()),
+ std::make_move_iterator(edges.end()));
+ }
+ }
+
+ // The last surface feature is the plane itself.
+ Vec3d cog = Vec3d::Zero();
+ size_t counter = 0;
+ for (const std::vector& b : plane.borders) {
+ for (size_t i = 1; i < b.size(); ++i) {
+ cog += b[i];
+ ++counter;
+ }
+ }
+ cog /= double(counter);
+ plane.surface_features.emplace_back(SurfaceFeature(SurfaceFeatureType::Plane,
+ plane.normal, cog, std::optional(), plane_idx + 0.0001));
+
+ plane.borders.clear();
+ plane.borders.shrink_to_fit();
+
+ plane.features_extracted = true;
+}
+
+
+
+
+
+
+
+
+std::optional MeasuringImpl::get_feature(size_t face_idx, const Vec3d& point)
+{
+ if (face_idx >= m_face_to_plane.size())
+ return std::optional();
+
+ const PlaneData& plane = m_planes[m_face_to_plane[face_idx]];
+
+ if (! plane.features_extracted)
+ extract_features(m_face_to_plane[face_idx]);
+
+ size_t closest_feature_idx = size_t(-1);
+ double min_dist = std::numeric_limits::max();
+
+ MeasurementResult res;
+ SurfaceFeature point_sf(point);
+
+ assert(plane.surface_features.empty() || plane.surface_features.back().get_type() == SurfaceFeatureType::Plane);
+
+ for (size_t i=0; idist;
+ if (dist < feature_hover_limit && dist < min_dist) {
+ min_dist = std::min(dist, min_dist);
+ closest_feature_idx = i;
+ }
+ }
+ }
+
+ if (closest_feature_idx != size_t(-1)) {
+ const SurfaceFeature& f = plane.surface_features[closest_feature_idx];
+ if (f.get_type() == SurfaceFeatureType::Edge) {
+ // If this is an edge, check if we are not close to the endpoint. If so,
+ // we will include the endpoint as well. Close = 10% of the lenghth of
+ // the edge, clamped between 0.025 and 0.5 mm.
+ const auto& [sp, ep] = f.get_edge();
+ double len_sq = (ep-sp).squaredNorm();
+ double limit_sq = std::max(0.025*0.025, std::min(0.5*0.5, 0.1 * 0.1 * len_sq));
+
+ if ((point-sp).squaredNorm() < limit_sq)
+ return std::make_optional(SurfaceFeature(sp));
+ if ((point-ep).squaredNorm() < limit_sq)
+ return std::make_optional(SurfaceFeature(ep));
+ }
+ return std::make_optional(f);
+ }
+
+ // Nothing detected, return the plane as a whole.
+ assert(plane.surface_features.back().get_type() == SurfaceFeatureType::Plane);
+ return std::make_optional(plane.surface_features.back());
+}
+
+
+
+
+
+int MeasuringImpl::get_num_of_planes() const
+{
+ return (m_planes.size());
+}
+
+
+
+const std::vector& MeasuringImpl::get_plane_triangle_indices(int idx) const
+{
+ assert(idx >= 0 && idx < int(m_planes.size()));
+ return m_planes[idx].facets;
+}
+
+const std::vector& MeasuringImpl::get_plane_features(unsigned int plane_id)
+{
+ assert(plane_id < m_planes.size());
+ if (! m_planes[plane_id].features_extracted)
+ extract_features(plane_id);
+ return m_planes[plane_id].surface_features;
+}
+
+const indexed_triangle_set& MeasuringImpl::get_its() const
+{
+ return this->m_its;
+}
+
+
+
+
+
+
+
+
+
+
+
+Measuring::Measuring(const indexed_triangle_set& its)
+: priv{std::make_unique(its)}
+{}
+
+Measuring::~Measuring() {}
+
+
+
+std::optional Measuring::get_feature(size_t face_idx, const Vec3d& point) const
+{
+ return priv->get_feature(face_idx, point);
+}
+
+
+int Measuring::get_num_of_planes() const
+{
+ return priv->get_num_of_planes();
+}
+
+
+const std::vector& Measuring::get_plane_triangle_indices(int idx) const
+{
+ return priv->get_plane_triangle_indices(idx);
+}
+
+const std::vector& Measuring::get_plane_features(unsigned int plane_id) const
+{
+ return priv->get_plane_features(plane_id);
+}
+
+const indexed_triangle_set& Measuring::get_its() const
+{
+ return priv->get_its();
+}
+
+const AngleAndEdges AngleAndEdges::Dummy = { 0.0, Vec3d::Zero(), { Vec3d::Zero(), Vec3d::Zero() }, { Vec3d::Zero(), Vec3d::Zero() }, 0.0, true };
+
+static AngleAndEdges angle_edge_edge(const std::pair& e1, const std::pair& e2)
+{
+ if (are_parallel(e1, e2))
+ return AngleAndEdges::Dummy;
+
+ Vec3d e1_unit = edge_direction(e1.first, e1.second);
+ Vec3d e2_unit = edge_direction(e2.first, e2.second);
+
+ // project edges on the plane defined by them
+ Vec3d normal = e1_unit.cross(e2_unit).normalized();
+ const Eigen::Hyperplane plane(normal, e1.first);
+ Vec3d e11_proj = plane.projection(e1.first);
+ Vec3d e12_proj = plane.projection(e1.second);
+ Vec3d e21_proj = plane.projection(e2.first);
+ Vec3d e22_proj = plane.projection(e2.second);
+
+ const bool coplanar = (e2.first - e21_proj).norm() < EPSILON && (e2.second - e22_proj).norm() < EPSILON;
+
+ // rotate the plane to become the XY plane
+ auto qp = Eigen::Quaternion::FromTwoVectors(normal, Vec3d::UnitZ());
+ auto qp_inverse = qp.inverse();
+ const Vec3d e11_rot = qp * e11_proj;
+ const Vec3d e12_rot = qp * e12_proj;
+ const Vec3d e21_rot = qp * e21_proj;
+ const Vec3d e22_rot = qp * e22_proj;
+
+ // discard Z
+ const Vec2d e11_rot_2d = Vec2d(e11_rot.x(), e11_rot.y());
+ const Vec2d e12_rot_2d = Vec2d(e12_rot.x(), e12_rot.y());
+ const Vec2d e21_rot_2d = Vec2d(e21_rot.x(), e21_rot.y());
+ const Vec2d e22_rot_2d = Vec2d(e22_rot.x(), e22_rot.y());
+
+ // find intersection (arc center) of edges in XY plane
+ const Eigen::Hyperplane e1_rot_2d_line = Eigen::Hyperplane::Through(e11_rot_2d, e12_rot_2d);
+ const Eigen::Hyperplane e2_rot_2d_line = Eigen::Hyperplane::Through(e21_rot_2d, e22_rot_2d);
+ const Vec2d center_rot_2d = e1_rot_2d_line.intersection(e2_rot_2d_line);
+
+ // arc center in original coordinate
+ const Vec3d center = qp_inverse * Vec3d(center_rot_2d.x(), center_rot_2d.y(), e11_rot.z());
+
+ // ensure the edges are pointing away from the center
+ std::pair out_e1 = e1;
+ std::pair out_e2 = e2;
+ if ((center_rot_2d - e11_rot_2d).squaredNorm() > (center_rot_2d - e12_rot_2d).squaredNorm()) {
+ std::swap(e11_proj, e12_proj);
+ std::swap(out_e1.first, out_e1.second);
+ e1_unit = -e1_unit;
+ }
+ if ((center_rot_2d - e21_rot_2d).squaredNorm() > (center_rot_2d - e22_rot_2d).squaredNorm()) {
+ std::swap(e21_proj, e22_proj);
+ std::swap(out_e2.first, out_e2.second);
+ e2_unit = -e2_unit;
+ }
+
+ // arc angle
+ const double angle = std::acos(std::clamp(e1_unit.dot(e2_unit), -1.0, 1.0));
+ // arc radius
+ const Vec3d e1_proj_mid = 0.5 * (e11_proj + e12_proj);
+ const Vec3d e2_proj_mid = 0.5 * (e21_proj + e22_proj);
+ const double radius = std::min((center - e1_proj_mid).norm(), (center - e2_proj_mid).norm());
+
+ return { angle, center, out_e1, out_e2, radius, coplanar };
+}
+
+static AngleAndEdges angle_edge_plane(const std::pair& e, const std::tuple& p)
+{
+ const auto& [idx, normal, origin] = p;
+ Vec3d e1e2_unit = edge_direction(e);
+ if (are_perpendicular(e1e2_unit, normal))
+ return AngleAndEdges::Dummy;
+
+ // ensure the edge is pointing away from the intersection
+ // 1st calculate instersection between edge and plane
+ const Eigen::Hyperplane plane(normal, origin);
+ const Eigen::ParametrizedLine line = Eigen::ParametrizedLine::Through(e.first, e.second);
+ const Vec3d inters = line.intersectionPoint(plane);
+
+ // then verify edge direction and revert it, if needed
+ Vec3d e1 = e.first;
+ Vec3d e2 = e.second;
+ if ((e1 - inters).squaredNorm() > (e2 - inters).squaredNorm()) {
+ std::swap(e1, e2);
+ e1e2_unit = -e1e2_unit;
+ }
+
+ if (are_parallel(e1e2_unit, normal)) {
+ const std::array basis = orthonormal_basis(e1e2_unit);
+ const double radius = (0.5 * (e1 + e2) - inters).norm();
+ const Vec3d edge_on_plane_dir = (basis[1].dot(origin - inters) >= 0.0) ? basis[1] : -basis[1];
+ std::pair edge_on_plane = std::make_pair(inters, inters + radius * edge_on_plane_dir);
+ if (!inters.isApprox(e1)) {
+ edge_on_plane.first += radius * edge_on_plane_dir;
+ edge_on_plane.second += radius * edge_on_plane_dir;
+ }
+ return AngleAndEdges(0.5 * double(PI), inters, std::make_pair(e1, e2), edge_on_plane, radius, inters.isApprox(e1));
+ }
+
+ const Vec3d e1e2 = e2 - e1;
+ const double e1e2_len = e1e2.norm();
+
+ // calculate 2nd edge (on the plane)
+ const Vec3d temp = normal.cross(e1e2);
+ const Vec3d edge_on_plane_unit = normal.cross(temp).normalized();
+ std::pair edge_on_plane = { origin, origin + e1e2_len * edge_on_plane_unit };
+
+ // ensure the 2nd edge is pointing in the correct direction
+ const Vec3d test_edge = (edge_on_plane.second - edge_on_plane.first).cross(e1e2);
+ if (test_edge.dot(temp) < 0.0)
+ edge_on_plane = { origin, origin - e1e2_len * edge_on_plane_unit };
+
+ AngleAndEdges ret = angle_edge_edge({ e1, e2 }, edge_on_plane);
+ ret.radius = (inters - 0.5 * (e1 + e2)).norm();
+ return ret;
+}
+
+static AngleAndEdges angle_plane_plane(const std::tuple& p1, const std::tuple& p2)
+{
+ const auto& [idx1, normal1, origin1] = p1;
+ const auto& [idx2, normal2, origin2] = p2;
+
+ // are planes parallel ?
+ if (are_parallel(normal1, normal2))
+ return AngleAndEdges::Dummy;
+
+ auto intersection_plane_plane = [](const Vec3d& n1, const Vec3d& o1, const Vec3d& n2, const Vec3d& o2) {
+ Eigen::MatrixXd m(2, 3);
+ m << n1.x(), n1.y(), n1.z(), n2.x(), n2.y(), n2.z();
+ Eigen::VectorXd b(2);
+ b << o1.dot(n1), o2.dot(n2);
+ Eigen::VectorXd x = m.colPivHouseholderQr().solve(b);
+ return std::make_pair(n1.cross(n2).normalized(), Vec3d(x(0), x(1), x(2)));
+ };
+
+ // Calculate intersection line between planes
+ const auto [intersection_line_direction, intersection_line_origin] = intersection_plane_plane(normal1, origin1, normal2, origin2);
+
+ // Project planes' origin on intersection line
+ const Eigen::ParametrizedLine intersection_line = Eigen::ParametrizedLine(intersection_line_origin, intersection_line_direction);
+ const Vec3d origin1_proj = intersection_line.projection(origin1);
+ const Vec3d origin2_proj = intersection_line.projection(origin2);
+
+ // Calculate edges on planes
+ const Vec3d edge_on_plane1_unit = (origin1 - origin1_proj).normalized();
+ const Vec3d edge_on_plane2_unit = (origin2 - origin2_proj).normalized();
+ const double radius = std::max(10.0, std::max((origin1 - origin1_proj).norm(), (origin2 - origin2_proj).norm()));
+ const std::pair edge_on_plane1 = { origin1_proj + radius * edge_on_plane1_unit, origin1_proj + 2.0 * radius * edge_on_plane1_unit };
+ const std::pair edge_on_plane2 = { origin2_proj + radius * edge_on_plane2_unit, origin2_proj + 2.0 * radius * edge_on_plane2_unit };
+
+ AngleAndEdges ret = angle_edge_edge(edge_on_plane1, edge_on_plane2);
+ ret.radius = radius;
+ return ret;
+}
+
+
+
+
+
+
+
+MeasurementResult get_measurement(const SurfaceFeature& a, const SurfaceFeature& b, const Measuring* measuring)
+{
+ assert(a.get_type() != SurfaceFeatureType::Undef && b.get_type() != SurfaceFeatureType::Undef);
+
+ const bool swap = int(a.get_type()) > int(b.get_type());
+ const SurfaceFeature& f1 = swap ? b : a;
+ const SurfaceFeature& f2 = swap ? a : b;
+
+ MeasurementResult result;
+
+ ///////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////
+ if (f1.get_type() == SurfaceFeatureType::Point) {
+ if (f2.get_type() == SurfaceFeatureType::Point) {
+ Vec3d diff = (f2.get_point() - f1.get_point());
+ result.distance_strict = std::make_optional(DistAndPoints{diff.norm(), f1.get_point(), f2.get_point()});
+ result.distance_xyz = diff.cwiseAbs();
+
+ ///////////////////////////////////////////////////////////////////////////
+ } else if (f2.get_type() == SurfaceFeatureType::Edge) {
+ const auto [s,e] = f2.get_edge();
+ const Eigen::ParametrizedLine line(s, (e-s).normalized());
+ const double dist_inf = line.distance(f1.get_point());
+ const Vec3d proj = line.projection(f1.get_point());
+ const double len_sq = (e-s).squaredNorm();
+ const double dist_start_sq = (proj-s).squaredNorm();
+ const double dist_end_sq = (proj-e).squaredNorm();
+ if (dist_start_sq < len_sq && dist_end_sq < len_sq) {
+ // projection falls on the line - the strict distance is the same as infinite
+ result.distance_strict = std::make_optional(DistAndPoints{dist_inf, f1.get_point(), proj});
+ } else { // the result is the closer of the endpoints
+ const bool s_is_closer = dist_start_sq < dist_end_sq;
+ result.distance_strict = std::make_optional(DistAndPoints{std::sqrt(std::min(dist_start_sq, dist_end_sq) + sqr(dist_inf)), f1.get_point(), s_is_closer ? s : e});
+ }
+ result.distance_infinite = std::make_optional(DistAndPoints{dist_inf, f1.get_point(), proj});
+ ///////////////////////////////////////////////////////////////////////////
+ } else if (f2.get_type() == SurfaceFeatureType::Circle) {
+ // Find a plane containing normal, center and the point.
+ const auto [c, radius, n] = f2.get_circle();
+ const Eigen::Hyperplane circle_plane(n, c);
+ const Vec3d proj = circle_plane.projection(f1.get_point());
+ if (proj.isApprox(c)) {
+ const Vec3d p_on_circle = c + radius * get_orthogonal(n, true);
+ result.distance_strict = std::make_optional(DistAndPoints{ radius, c, p_on_circle });
+ }
+ else {
+ const Eigen::Hyperplane circle_plane(n, c);
+ const Vec3d proj = circle_plane.projection(f1.get_point());
+ const double dist = std::sqrt(std::pow((proj - c).norm() - radius, 2.) +
+ (f1.get_point() - proj).squaredNorm());
+
+ const Vec3d p_on_circle = c + radius * (proj - c).normalized();
+ result.distance_strict = std::make_optional(DistAndPoints{ dist, f1.get_point(), p_on_circle }); // TODO
+ }
+ ///////////////////////////////////////////////////////////////////////////
+ } else if (f2.get_type() == SurfaceFeatureType::Plane) {
+ const auto [idx, normal, pt] = f2.get_plane();
+ Eigen::Hyperplane plane(normal, pt);
+ result.distance_infinite = std::make_optional(DistAndPoints{plane.absDistance(f1.get_point()), f1.get_point(), plane.projection(f1.get_point())}); // TODO
+ // TODO: result.distance_strict =
+ }
+ ///////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////
+ }
+ else if (f1.get_type() == SurfaceFeatureType::Edge) {
+ if (f2.get_type() == SurfaceFeatureType::Edge) {
+ std::vector distances;
+
+ auto add_point_edge_distance = [&distances](const Vec3d& v, const std::pair& e) {
+ const MeasurementResult res = get_measurement(SurfaceFeature(v), SurfaceFeature(SurfaceFeatureType::Edge, e.first, e.second));
+ double distance = res.distance_strict->dist;
+ Vec3d v2 = res.distance_strict->to;
+
+ const Vec3d e1e2 = e.second - e.first;
+ const Vec3d e1v2 = v2 - e.first;
+ if (e1v2.dot(e1e2) >= 0.0 && e1v2.norm() < e1e2.norm())
+ distances.emplace_back(distance, v, v2);
+ };
+
+ std::pair e1 = f1.get_edge();
+ std::pair e2 = f2.get_edge();
+
+ distances.emplace_back((e2.first - e1.first).norm(), e1.first, e2.first);
+ distances.emplace_back((e2.second - e1.first).norm(), e1.first, e2.second);
+ distances.emplace_back((e2.first - e1.second).norm(), e1.second, e2.first);
+ distances.emplace_back((e2.second - e1.second).norm(), e1.second, e2.second);
+ add_point_edge_distance(e1.first, e2);
+ add_point_edge_distance(e1.second, e2);
+ add_point_edge_distance(e2.first, e1);
+ add_point_edge_distance(e2.second, e1);
+ auto it = std::min_element(distances.begin(), distances.end(),
+ [](const DistAndPoints& item1, const DistAndPoints& item2) {
+ return item1.dist < item2.dist;
+ });
+ result.distance_infinite = std::make_optional(*it);
+
+ result.angle = angle_edge_edge(f1.get_edge(), f2.get_edge());
+ ///////////////////////////////////////////////////////////////////////////
+ } else if (f2.get_type() == SurfaceFeatureType::Circle) {
+ const std::pair e = f1.get_edge();
+ const auto& [center, radius, normal] = f2.get_circle();
+ const Vec3d e1e2 = (e.second - e.first);
+ const Vec3d e1e2_unit = e1e2.normalized();
+
+ std::vector distances;
+ distances.emplace_back(*get_measurement(SurfaceFeature(e.first), f2).distance_strict);
+ distances.emplace_back(*get_measurement(SurfaceFeature(e.second), f2).distance_strict);
+
+ const Eigen::Hyperplane plane(e1e2_unit, center);
+ const Eigen::ParametrizedLine line = Eigen::ParametrizedLine::Through(e.first, e.second);
+ const Vec3d inter = line.intersectionPoint(plane);
+ const Vec3d e1inter = inter - e.first;
+ if (e1inter.dot(e1e2) >= 0.0 && e1inter.norm() < e1e2.norm())
+ distances.emplace_back(*get_measurement(SurfaceFeature(inter), f2).distance_strict);
+
+ auto it = std::min_element(distances.begin(), distances.end(),
+ [](const DistAndPoints& item1, const DistAndPoints& item2) {
+ return item1.dist < item2.dist;
+ });
+ result.distance_infinite = std::make_optional(DistAndPoints{it->dist, it->from, it->to});
+ ///////////////////////////////////////////////////////////////////////////
+ } else if (f2.get_type() == SurfaceFeatureType::Plane) {
+ assert(measuring != nullptr);
+
+ const auto [from, to] = f1.get_edge();
+ const auto [idx, normal, origin] = f2.get_plane();
+
+ const Vec3d edge_unit = (to - from).normalized();
+ if (are_perpendicular(edge_unit, normal)) {
+ std::vector distances;
+ const Eigen::Hyperplane plane(normal, origin);
+ distances.push_back(DistAndPoints{ plane.absDistance(from), from, plane.projection(from) });
+ distances.push_back(DistAndPoints{ plane.absDistance(to), to, plane.projection(to) });
+ auto it = std::min_element(distances.begin(), distances.end(),
+ [](const DistAndPoints& item1, const DistAndPoints& item2) {
+ return item1.dist < item2.dist;
+ });
+ result.distance_infinite = std::make_optional(DistAndPoints{ it->dist, it->from, it->to });
+ }
+ else {
+ const std::vector& plane_features = measuring->get_plane_features(idx);
+ std::vector distances;
+ for (const SurfaceFeature& sf : plane_features) {
+ if (sf.get_type() == SurfaceFeatureType::Edge) {
+ const auto m = get_measurement(sf, f1);
+ if (!m.distance_infinite.has_value()) {
+ distances.clear();
+ break;
+ }
+ else
+ distances.push_back(*m.distance_infinite);
+ }
+ }
+ if (!distances.empty()) {
+ auto it = std::min_element(distances.begin(), distances.end(),
+ [](const DistAndPoints& item1, const DistAndPoints& item2) {
+ return item1.dist < item2.dist;
+ });
+ result.distance_infinite = std::make_optional(DistAndPoints{ it->dist, it->from, it->to });
+ }
+ }
+ result.angle = angle_edge_plane(f1.get_edge(), f2.get_plane());
+ }
+ ///////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////
+ } else if (f1.get_type() == SurfaceFeatureType::Circle) {
+ if (f2.get_type() == SurfaceFeatureType::Circle) {
+ const auto [c0, r0, n0] = f1.get_circle();
+ const auto [c1, r1, n1] = f2.get_circle();
+
+ // The following code is an adaptation of the algorithm found in:
+ // https://github.com/davideberly/GeometricTools/blob/master/GTE/Mathematics/DistCircle3Circle3.h
+ // and described in:
+ // https://www.geometrictools.com/Documentation/DistanceToCircle3.pdf
+
+ struct ClosestInfo
+ {
+ double sqrDistance{ 0.0 };
+ Vec3d circle0Closest{ Vec3d::Zero() };
+ Vec3d circle1Closest{ Vec3d::Zero() };
+
+ inline bool operator < (const ClosestInfo& other) const { return sqrDistance < other.sqrDistance; }
+ };
+ std::array candidates{};
+
+ const double zero = 0.0;
+
+ const Vec3d D = c1 - c0;
+
+ if (!are_parallel(n0, n1)) {
+ // Get parameters for constructing the degree-8 polynomial phi.
+ const double one = 1.0;
+ const double two = 2.0;
+ const double r0sqr = sqr(r0);
+ const double r1sqr = sqr(r1);
+
+ // Compute U1 and V1 for the plane of circle1.
+ const std::array basis = orthonormal_basis(n1);
+ const Vec3d U1 = basis[0];
+ const Vec3d V1 = basis[1];
+
+ // Construct the polynomial phi(cos(theta)).
+ const Vec3d N0xD = n0.cross(D);
+ const Vec3d N0xU1 = n0.cross(U1);
+ const Vec3d N0xV1 = n0.cross(V1);
+ const double a0 = r1 * D.dot(U1);
+ const double a1 = r1 * D.dot(V1);
+ const double a2 = N0xD.dot(N0xD);
+ const double a3 = r1 * N0xD.dot(N0xU1);
+ const double a4 = r1 * N0xD.dot(N0xV1);
+ const double a5 = r1sqr * N0xU1.dot(N0xU1);
+ const double a6 = r1sqr * N0xU1.dot(N0xV1);
+ const double a7 = r1sqr * N0xV1.dot(N0xV1);
+ Polynomial1 p0{ a2 + a7, two * a3, a5 - a7 };
+ Polynomial1 p1{ two * a4, two * a6 };
+ Polynomial1 p2{ zero, a1 };
+ Polynomial1 p3{ -a0 };
+ Polynomial1 p4{ -a6, a4, two * a6 };
+ Polynomial1 p5{ -a3, a7 - a5 };
+ Polynomial1 tmp0{ one, zero, -one };
+ Polynomial1 tmp1 = p2 * p2 + tmp0 * p3 * p3;
+ Polynomial1 tmp2 = two * p2 * p3;
+ Polynomial1 tmp3 = p4 * p4 + tmp0 * p5 * p5;
+ Polynomial1 tmp4 = two * p4 * p5;
+ Polynomial1 p6 = p0 * tmp1 + tmp0 * p1 * tmp2 - r0sqr * tmp3;
+ Polynomial1 p7 = p0 * tmp2 + p1 * tmp1 - r0sqr * tmp4;
+
+ // Parameters for polynomial root finding. The roots[] array
+ // stores the roots. We need only the unique ones, which is
+ // the responsibility of the set uniqueRoots. The pairs[]
+ // array stores the (cosine,sine) information mentioned in the
+ // PDF. TODO: Choose the maximum number of iterations for root
+ // finding based on specific polynomial data?
+ const uint32_t maxIterations = 128;
+ int32_t degree = 0;
+ size_t numRoots = 0;
+ std::array roots{};
+ std::set uniqueRoots{};
+ size_t numPairs = 0;
+ std::array, 16> pairs{};
+ double temp = zero;
+ double sn = zero;
+
+ if (p7.GetDegree() > 0 || p7[0] != zero) {
+ // H(cs,sn) = p6(cs) + sn * p7(cs)
+ Polynomial1 phi = p6 * p6 - tmp0 * p7 * p7;
+ degree = static_cast(phi.GetDegree());
+ assert(degree > 0);
+ numRoots = RootsPolynomial::Find(degree, &phi[0], maxIterations, roots.data());
+ for (size_t i = 0; i < numRoots; ++i) {
+ uniqueRoots.insert(roots[i]);
+ }
+
+ for (auto const& cs : uniqueRoots) {
+ if (std::fabs(cs) <= one) {
+ temp = p7(cs);
+ if (temp != zero) {
+ sn = -p6(cs) / temp;
+ pairs[numPairs++] = std::make_pair(cs, sn);
+ }
+ else {
+ temp = std::max(one - sqr(cs), zero);
+ sn = std::sqrt(temp);
+ pairs[numPairs++] = std::make_pair(cs, sn);
+ if (sn != zero)
+ pairs[numPairs++] = std::make_pair(cs, -sn);
+ }
+ }
+ }
+ }
+ else {
+ // H(cs,sn) = p6(cs)
+ degree = static_cast(p6.GetDegree());
+ assert(degree > 0);
+ numRoots = RootsPolynomial::Find(degree, &p6[0], maxIterations, roots.data());
+ for (size_t i = 0; i < numRoots; ++i) {
+ uniqueRoots.insert(roots[i]);
+ }
+
+ for (auto const& cs : uniqueRoots) {
+ if (std::fabs(cs) <= one) {
+ temp = std::max(one - sqr(cs), zero);
+ sn = std::sqrt(temp);
+ pairs[numPairs++] = std::make_pair(cs, sn);
+ if (sn != zero)
+ pairs[numPairs++] = std::make_pair(cs, -sn);
+ }
+ }
+ }
+
+ for (size_t i = 0; i < numPairs; ++i) {
+ ClosestInfo& info = candidates[i];
+ Vec3d delta = D + r1 * (pairs[i].first * U1 + pairs[i].second * V1);
+ info.circle1Closest = c0 + delta;
+ const double N0dDelta = n0.dot(delta);
+ const double lenN0xDelta = n0.cross(delta).norm();
+ if (lenN0xDelta > 0.0) {
+ const double diff = lenN0xDelta - r0;
+ info.sqrDistance = sqr(N0dDelta) + sqr(diff);
+ delta -= N0dDelta * n0;
+ delta.normalize();
+ info.circle0Closest = c0 + r0 * delta;
+ }
+ else {
+ const Vec3d r0U0 = r0 * get_orthogonal(n0, true);
+ const Vec3d diff = delta - r0U0;
+ info.sqrDistance = diff.dot(diff);
+ info.circle0Closest = c0 + r0U0;
+ }
+ }
+
+ std::sort(candidates.begin(), candidates.begin() + numPairs);
+ }
+ else {
+ ClosestInfo& info = candidates[0];
+
+ const double N0dD = n0.dot(D);
+ const Vec3d normProj = N0dD * n0;
+ const Vec3d compProj = D - normProj;
+ Vec3d U = compProj;
+ const double d = U.norm();
+ U.normalize();
+
+ // The configuration is determined by the relative location of the
+ // intervals of projection of the circles on to the D-line.
+ // Circle0 projects to [-r0,r0] and circle1 projects to
+ // [d-r1,d+r1].
+ const double dmr1 = d - r1;
+ double distance;
+ if (dmr1 >= r0) {
+ // d >= r0 + r1
+ // The circles are separated (d > r0 + r1) or tangent with one
+ // outside the other (d = r0 + r1).
+ distance = dmr1 - r0;
+ info.circle0Closest = c0 + r0 * U;
+ info.circle1Closest = c1 - r1 * U;
+ }
+ else {
+ // d < r0 + r1
+ // The cases implicitly use the knowledge that d >= 0.
+ const double dpr1 = d + r1;
+ if (dpr1 <= r0) {
+ // Circle1 is inside circle0.
+ distance = r0 - dpr1;
+ if (d > 0.0) {
+ info.circle0Closest = c0 + r0 * U;
+ info.circle1Closest = c1 + r1 * U;
+ }
+ else {
+ // The circles are concentric, so U = (0,0,0).
+ // Construct a vector perpendicular to N0 to use for
+ // closest points.
+ U = get_orthogonal(n0, true);
+ info.circle0Closest = c0 + r0 * U;
+ info.circle1Closest = c1 + r1 * U;
+ }
+ }
+ else if (dmr1 <= -r0) {
+ // Circle0 is inside circle1.
+ distance = -r0 - dmr1;
+ if (d > 0.0) {
+ info.circle0Closest = c0 - r0 * U;
+ info.circle1Closest = c1 - r1 * U;
+ }
+ else {
+ // The circles are concentric, so U = (0,0,0).
+ // Construct a vector perpendicular to N0 to use for
+ // closest points.
+ U = get_orthogonal(n0, true);
+ info.circle0Closest = c0 + r0 * U;
+ info.circle1Closest = c1 + r1 * U;
+ }
+ }
+ else {
+ distance = (c1 - c0).norm();
+ info.circle0Closest = c0;
+ info.circle1Closest = c1;
+ }
+ }
+
+ info.sqrDistance = distance * distance + N0dD * N0dD;
+ }
+
+ result.distance_infinite = std::make_optional(DistAndPoints{ std::sqrt(candidates[0].sqrDistance), candidates[0].circle0Closest, candidates[0].circle1Closest }); // TODO
+ ///////////////////////////////////////////////////////////////////////////
+ } else if (f2.get_type() == SurfaceFeatureType::Plane) {
+ assert(measuring != nullptr);
+
+ const auto [center, radius, normal1] = f1.get_circle();
+ const auto [idx2, normal2, origin2] = f2.get_plane();
+
+ const bool coplanar = are_parallel(normal1, normal2) && Eigen::Hyperplane(normal1, center).absDistance(origin2) < EPSILON;
+ if (!coplanar) {
+ const std::vector& plane_features = measuring->get_plane_features(idx2);
+ std::vector distances;
+ for (const SurfaceFeature& sf : plane_features) {
+ if (sf.get_type() == SurfaceFeatureType::Edge) {
+ const auto m = get_measurement(sf, f1);
+ if (!m.distance_infinite.has_value()) {
+ distances.clear();
+ break;
+ }
+ else
+ distances.push_back(*m.distance_infinite);
+ }
+ }
+ if (!distances.empty()) {
+ auto it = std::min_element(distances.begin(), distances.end(),
+ [](const DistAndPoints& item1, const DistAndPoints& item2) {
+ return item1.dist < item2.dist;
+ });
+ result.distance_infinite = std::make_optional(DistAndPoints{ it->dist, it->from, it->to });
+ }
+ }
+ }
+ ///////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////
+ } else if (f1.get_type() == SurfaceFeatureType::Plane) {
+ const auto [idx1, normal1, pt1] = f1.get_plane();
+ const auto [idx2, normal2, pt2] = f2.get_plane();
+
+ if (are_parallel(normal1, normal2)) {
+ // The planes are parallel, calculate distance.
+ const Eigen::Hyperplane plane(normal1, pt1);
+ result.distance_infinite = std::make_optional(DistAndPoints{ plane.absDistance(pt2), pt2, plane.projection(pt2) }); // TODO
+ }
+ else
+ result.angle = angle_plane_plane(f1.get_plane(), f2.get_plane());
+ }
+
+ return result;
+}
+
+
+
+
+
+
+
+
+} // namespace Measure
+} // namespace Slic3r
+
diff --git a/src/libslic3r/Measure.hpp b/src/libslic3r/Measure.hpp
new file mode 100644
index 0000000000..dcccafb70d
--- /dev/null
+++ b/src/libslic3r/Measure.hpp
@@ -0,0 +1,200 @@
+///|/ Copyright (c) Prusa Research 2022 - 2023 Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Vojtěch Bubník @bubnikv
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
+#ifndef Slic3r_Measure_hpp_
+#define Slic3r_Measure_hpp_
+
+#include
+#include
+
+#include "Point.hpp"
+
+
+struct indexed_triangle_set;
+
+
+
+namespace Slic3r {
+
+class TriangleMesh;
+
+namespace Measure {
+
+
+enum class SurfaceFeatureType : int {
+ Undef = 0,
+ Point = 1 << 0,
+ Edge = 1 << 1,
+ Circle = 1 << 2,
+ Plane = 1 << 3
+};
+
+class SurfaceFeature {
+public:
+ SurfaceFeature(SurfaceFeatureType type, const Vec3d& pt1, const Vec3d& pt2, std::optional pt3 = std::nullopt, double value = 0.0)
+ : m_type(type), m_pt1(pt1), m_pt2(pt2), m_pt3(pt3), m_value(value) {}
+
+ explicit SurfaceFeature(const Vec3d& pt)
+ : m_type{SurfaceFeatureType::Point}, m_pt1{pt} {}
+
+ // Get type of this feature.
+ SurfaceFeatureType get_type() const { return m_type; }
+
+ // For points, return the point.
+ Vec3d get_point() const { assert(m_type == SurfaceFeatureType::Point); return m_pt1; }
+
+ // For edges, return start and end.
+ std::pair get_edge() const { assert(m_type == SurfaceFeatureType::Edge); return std::make_pair(m_pt1, m_pt2); }
+
+ // For circles, return center, radius and normal.
+ std::tuple get_circle() const { assert(m_type == SurfaceFeatureType::Circle); return std::make_tuple(m_pt1, m_value, m_pt2); }
+
+ // For planes, return index into vector provided by Measuring::get_plane_triangle_indices, normal and point.
+ std::tuple get_plane() const { assert(m_type == SurfaceFeatureType::Plane); return std::make_tuple(int(m_value), m_pt1, m_pt2); }
+
+ // For anything, return an extra point that should also be considered a part of this.
+ std::optional get_extra_point() const { assert(m_type != SurfaceFeatureType::Undef); return m_pt3; }
+
+ bool operator == (const SurfaceFeature& other) const {
+ if (this->m_type != other.m_type) return false;
+ switch (this->m_type)
+ {
+ case SurfaceFeatureType::Undef: { break; }
+ case SurfaceFeatureType::Point: { return (this->m_pt1.isApprox(other.m_pt1)); }
+ case SurfaceFeatureType::Edge: {
+ return (this->m_pt1.isApprox(other.m_pt1) && this->m_pt2.isApprox(other.m_pt2)) ||
+ (this->m_pt1.isApprox(other.m_pt2) && this->m_pt2.isApprox(other.m_pt1));
+ }
+ case SurfaceFeatureType::Plane:
+ case SurfaceFeatureType::Circle: {
+ return (this->m_pt1.isApprox(other.m_pt1) && this->m_pt2.isApprox(other.m_pt2) && std::abs(this->m_value - other.m_value) < EPSILON);
+ }
+ }
+
+ return false;
+ }
+
+ bool operator != (const SurfaceFeature& other) const {
+ return !operator == (other);
+ }
+
+private:
+ SurfaceFeatureType m_type{ SurfaceFeatureType::Undef };
+ Vec3d m_pt1{ Vec3d::Zero() };
+ Vec3d m_pt2{ Vec3d::Zero() };
+ std::optional m_pt3;
+ double m_value{ 0.0 };
+};
+
+
+
+class MeasuringImpl;
+
+
+class Measuring {
+public:
+ // Construct the measurement object on a given its.
+ explicit Measuring(const indexed_triangle_set& its);
+ ~Measuring();
+
+
+ // Given a face_idx where the mouse cursor points, return a feature that
+ // should be highlighted (if any).
+ std::optional get_feature(size_t face_idx, const Vec3d& point) const;
+
+ // Return total number of planes.
+ int get_num_of_planes() const;
+
+ // Returns a list of triangle indices for given plane.
+ const std::vector& get_plane_triangle_indices(int idx) const;
+
+ // Returns the surface features of the plane with the given index
+ const std::vector& get_plane_features(unsigned int plane_id) const;
+
+ // Returns the mesh used for measuring
+ const indexed_triangle_set& get_its() const;
+
+private:
+ std::unique_ptr priv;
+};
+
+
+struct DistAndPoints {
+ DistAndPoints(double dist_, Vec3d from_, Vec3d to_) : dist(dist_), from(from_), to(to_) {}
+ double dist;
+ Vec3d from;
+ Vec3d to;
+};
+
+struct AngleAndEdges {
+ AngleAndEdges(double angle_, const Vec3d& center_, const std::pair& e1_, const std::pair& e2_, double radius_, bool coplanar_)
+ : angle(angle_), center(center_), e1(e1_), e2(e2_), radius(radius_), coplanar(coplanar_) {}
+ double angle;
+ Vec3d center;
+ std::pair e1;
+ std::pair e2;
+ double radius;
+ bool coplanar;
+
+ static const AngleAndEdges Dummy;
+};
+
+struct MeasurementResult {
+ std::optional angle;
+ std::optional distance_infinite;
+ std::optional distance_strict;
+ std::optional distance_xyz;
+
+ bool has_distance_data() const {
+ return distance_infinite.has_value() || distance_strict.has_value();
+ }
+
+ bool has_any_data() const {
+ return angle.has_value() || distance_infinite.has_value() || distance_strict.has_value() || distance_xyz.has_value();
+ }
+};
+
+// Returns distance/angle between two SurfaceFeatures.
+MeasurementResult get_measurement(const SurfaceFeature& a, const SurfaceFeature& b, const Measuring* measuring = nullptr);
+
+inline Vec3d edge_direction(const Vec3d& from, const Vec3d& to) { return (to - from).normalized(); }
+inline Vec3d edge_direction(const std::pair& e) { return edge_direction(e.first, e.second); }
+inline Vec3d edge_direction(const SurfaceFeature& edge) {
+ assert(edge.get_type() == SurfaceFeatureType::Edge);
+ return edge_direction(edge.get_edge());
+}
+
+inline Vec3d plane_normal(const SurfaceFeature& plane) {
+ assert(plane.get_type() == SurfaceFeatureType::Plane);
+ return std::get<1>(plane.get_plane());
+}
+
+inline bool are_parallel(const Vec3d& v1, const Vec3d& v2) { return std::abs(std::abs(v1.dot(v2)) - 1.0) < EPSILON; }
+inline bool are_perpendicular(const Vec3d& v1, const Vec3d& v2) { return std::abs(v1.dot(v2)) < EPSILON; }
+
+inline bool are_parallel(const std::pair& e1, const std::pair& e2) {
+ return are_parallel(e1.second - e1.first, e2.second - e2.first);
+}
+inline bool are_parallel(const SurfaceFeature& f1, const SurfaceFeature& f2) {
+ if (f1.get_type() == SurfaceFeatureType::Edge && f2.get_type() == SurfaceFeatureType::Edge)
+ return are_parallel(edge_direction(f1), edge_direction(f2));
+ else if (f1.get_type() == SurfaceFeatureType::Edge && f2.get_type() == SurfaceFeatureType::Plane)
+ return are_perpendicular(edge_direction(f1), plane_normal(f2));
+ else
+ return false;
+}
+
+inline bool are_perpendicular(const SurfaceFeature& f1, const SurfaceFeature& f2) {
+ if (f1.get_type() == SurfaceFeatureType::Edge && f2.get_type() == SurfaceFeatureType::Edge)
+ return are_perpendicular(edge_direction(f1), edge_direction(f2));
+ else if (f1.get_type() == SurfaceFeatureType::Edge && f2.get_type() == SurfaceFeatureType::Plane)
+ return are_parallel(edge_direction(f1), plane_normal(f2));
+ else
+ return false;
+}
+
+} // namespace Measure
+} // namespace Slic3r
+
+#endif // Slic3r_Measure_hpp_
diff --git a/src/libslic3r/MeasureUtils.hpp b/src/libslic3r/MeasureUtils.hpp
new file mode 100644
index 0000000000..8a63de5a1f
--- /dev/null
+++ b/src/libslic3r/MeasureUtils.hpp
@@ -0,0 +1,390 @@
+///|/ Copyright (c) Prusa Research 2022 Enrico Turri @enricoturri1966
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
+#ifndef Slic3r_MeasureUtils_hpp_
+#define Slic3r_MeasureUtils_hpp_
+
+#include
+
+namespace Slic3r {
+namespace Measure {
+
+// Utility class used to calculate distance circle-circle
+// Adaptation of code found in:
+// https://github.com/davideberly/GeometricTools/blob/master/GTE/Mathematics/Polynomial1.h
+
+class Polynomial1
+{
+public:
+ Polynomial1(std::initializer_list values)
+ {
+ // C++ 11 will call the default constructor for
+ // Polynomial1 p{}, so it is guaranteed that
+ // values.size() > 0.
+ m_coefficient.resize(values.size());
+ std::copy(values.begin(), values.end(), m_coefficient.begin());
+ EliminateLeadingZeros();
+ }
+
+ // Construction and destruction. The first constructor creates a
+ // polynomial of the specified degree but sets all coefficients to
+ // zero (to ensure initialization). You are responsible for setting
+ // the coefficients, presumably with the degree-term set to a nonzero
+ // number. In the second constructor, the degree is the number of
+ // initializers plus 1, but then adjusted so that coefficient[degree]
+ // is not zero (unless all initializer values are zero).
+ explicit Polynomial1(uint32_t degree)
+ : m_coefficient(static_cast(degree) + 1, 0.0)
+ {}
+
+ // Eliminate any leading zeros in the polynomial, except in the case
+ // the degree is 0 and the coefficient is 0. The elimination is
+ // necessary when arithmetic operations cause a decrease in the degree
+ // of the result. For example, (1 + x + x^2) + (1 + 2*x - x^2) =
+ // (2 + 3*x). The inputs both have degree 2, so the result is created
+ // with degree 2. After the addition we find that the degree is in
+ // fact 1 and resize the array of coefficients. This function is
+ // called internally by the arithmetic operators, but it is exposed in
+ // the public interface in case you need it for your own purposes.
+ void EliminateLeadingZeros()
+ {
+ const size_t size = m_coefficient.size();
+ if (size > 1) {
+ const double zero = 0.0;
+ int32_t leading;
+ for (leading = static_cast(size) - 1; leading > 0; --leading) {
+ if (m_coefficient[leading] != zero)
+ break;
+ }
+
+ m_coefficient.resize(++leading);
+ }
+ }
+
+ // Set all coefficients to the specified value.
+ void SetCoefficients(double value)
+ {
+ std::fill(m_coefficient.begin(), m_coefficient.end(), value);
+ }
+
+ inline uint32_t GetDegree() const
+ {
+ // By design, m_coefficient.size() > 0.
+ return static_cast(m_coefficient.size() - 1);
+ }
+
+ inline const double& operator[](uint32_t i) const { return m_coefficient[i]; }
+ inline double& operator[](uint32_t i) { return m_coefficient[i]; }
+
+ // Evaluate the polynomial. If the polynomial is invalid, the
+ // function returns zero.
+ double operator()(double t) const
+ {
+ int32_t i = static_cast(m_coefficient.size());
+ double result = m_coefficient[--i];
+ for (--i; i >= 0; --i) {
+ result *= t;
+ result += m_coefficient[i];
+ }
+ return result;
+ }
+
+protected:
+ // The class is designed so that m_coefficient.size() >= 1.
+ std::vector m_coefficient;
+};
+
+inline Polynomial1 operator * (const Polynomial1& p0, const Polynomial1& p1)
+{
+ const uint32_t p0Degree = p0.GetDegree();
+ const uint32_t p1Degree = p1.GetDegree();
+ Polynomial1 result(p0Degree + p1Degree);
+ result.SetCoefficients(0.0);
+ for (uint32_t i0 = 0; i0 <= p0Degree; ++i0) {
+ for (uint32_t i1 = 0; i1 <= p1Degree; ++i1) {
+ result[i0 + i1] += p0[i0] * p1[i1];
+ }
+ }
+ return result;
+}
+
+inline Polynomial1 operator + (const Polynomial1& p0, const Polynomial1& p1)
+{
+ const uint32_t p0Degree = p0.GetDegree();
+ const uint32_t p1Degree = p1.GetDegree();
+ uint32_t i;
+ if (p0Degree >= p1Degree) {
+ Polynomial1 result(p0Degree);
+ for (i = 0; i <= p1Degree; ++i) {
+ result[i] = p0[i] + p1[i];
+ }
+ for (/**/; i <= p0Degree; ++i) {
+ result[i] = p0[i];
+ }
+ result.EliminateLeadingZeros();
+ return result;
+ }
+ else {
+ Polynomial1 result(p1Degree);
+ for (i = 0; i <= p0Degree; ++i) {
+ result[i] = p0[i] + p1[i];
+ }
+ for (/**/; i <= p1Degree; ++i) {
+ result[i] = p1[i];
+ }
+ result.EliminateLeadingZeros();
+ return result;
+ }
+}
+
+inline Polynomial1 operator - (const Polynomial1& p0, const Polynomial1& p1)
+{
+ const uint32_t p0Degree = p0.GetDegree();
+ const uint32_t p1Degree = p1.GetDegree();
+ uint32_t i;
+ if (p0Degree >= p1Degree) {
+ Polynomial1 result(p0Degree);
+ for (i = 0; i <= p1Degree; ++i) {
+ result[i] = p0[i] - p1[i];
+ }
+ for (/**/; i <= p0Degree; ++i) {
+ result[i] = p0[i];
+ }
+ result.EliminateLeadingZeros();
+ return result;
+ }
+ else {
+ Polynomial1 result(p1Degree);
+ for (i = 0; i <= p0Degree; ++i) {
+ result[i] = p0[i] - p1[i];
+ }
+ for (/**/; i <= p1Degree; ++i) {
+ result[i] = -p1[i];
+ }
+ result.EliminateLeadingZeros();
+ return result;
+ }
+}
+
+inline Polynomial1 operator * (double scalar, const Polynomial1& p)
+{
+ const uint32_t degree = p.GetDegree();
+ Polynomial1 result(degree);
+ for (uint32_t i = 0; i <= degree; ++i) {
+ result[i] = scalar * p[i];
+ }
+ return result;
+}
+
+// Utility class used to calculate distance circle-circle
+// Adaptation of code found in:
+// https://github.com/davideberly/GeometricTools/blob/master/GTE/Mathematics/RootsPolynomial.h
+
+class RootsPolynomial
+{
+public:
+ // General equations: sum_{i=0}^{d} c(i)*t^i = 0. The input array 'c'
+ // must have at least d+1 elements and the output array 'root' must
+ // have at least d elements.
+
+ // Find the roots on (-infinity,+infinity).
+ static int32_t Find(int32_t degree, const double* c, uint32_t maxIterations, double* roots)
+ {
+ if (degree >= 0 && c != nullptr) {
+ const double zero = 0.0;
+ while (degree >= 0 && c[degree] == zero) {
+ --degree;
+ }
+
+ if (degree > 0) {
+ // Compute the Cauchy bound.
+ const double one = 1.0;
+ const double invLeading = one / c[degree];
+ double maxValue = zero;
+ for (int32_t i = 0; i < degree; ++i) {
+ const double value = std::fabs(c[i] * invLeading);
+ if (value > maxValue)
+ maxValue = value;
+ }
+ const double bound = one + maxValue;
+
+ return FindRecursive(degree, c, -bound, bound, maxIterations, roots);
+ }
+ else if (degree == 0)
+ // The polynomial is a nonzero constant.
+ return 0;
+ else {
+ // The polynomial is identically zero.
+ roots[0] = zero;
+ return 1;
+ }
+ }
+ else
+ // Invalid degree or c.
+ return 0;
+ }
+
+ // If you know that p(tmin) * p(tmax) <= 0, then there must be at
+ // least one root in [tmin, tmax]. Compute it using bisection.
+ static bool Find(int32_t degree, const double* c, double tmin, double tmax, uint32_t maxIterations, double& root)
+ {
+ const double zero = 0.0;
+ double pmin = Evaluate(degree, c, tmin);
+ if (pmin == zero) {
+ root = tmin;
+ return true;
+ }
+ double pmax = Evaluate(degree, c, tmax);
+ if (pmax == zero) {
+ root = tmax;
+ return true;
+ }
+
+ if (pmin * pmax > zero)
+ // It is not known whether the interval bounds a root.
+ return false;
+
+ if (tmin >= tmax)
+ // Invalid ordering of interval endpoitns.
+ return false;
+
+ for (uint32_t i = 1; i <= maxIterations; ++i) {
+ root = 0.5 * (tmin + tmax);
+
+ // This test is designed for 'float' or 'double' when tmin
+ // and tmax are consecutive floating-point numbers.
+ if (root == tmin || root == tmax)
+ break;
+
+ const double p = Evaluate(degree, c, root);
+ const double product = p * pmin;
+ if (product < zero) {
+ tmax = root;
+ pmax = p;
+ }
+ else if (product > zero) {
+ tmin = root;
+ pmin = p;
+ }
+ else
+ break;
+ }
+
+ return true;
+ }
+
+ // Support for the Find functions.
+ static int32_t FindRecursive(int32_t degree, double const* c, double tmin, double tmax, uint32_t maxIterations, double* roots)
+ {
+ // The base of the recursion.
+ const double zero = 0.0;
+ double root = zero;
+ if (degree == 1) {
+ int32_t numRoots;
+ if (c[1] != zero) {
+ root = -c[0] / c[1];
+ numRoots = 1;
+ }
+ else if (c[0] == zero) {
+ root = zero;
+ numRoots = 1;
+ }
+ else
+ numRoots = 0;
+
+ if (numRoots > 0 && tmin <= root && root <= tmax) {
+ roots[0] = root;
+ return 1;
+ }
+ return 0;
+ }
+
+ // Find the roots of the derivative polynomial scaled by 1/degree.
+ // The scaling avoids the factorial growth in the coefficients;
+ // for example, without the scaling, the high-order term x^d
+ // becomes (d!)*x through multiple differentiations. With the
+ // scaling we instead get x. This leads to better numerical
+ // behavior of the root finder.
+ const int32_t derivDegree = degree - 1;
+ std::vector derivCoeff(static_cast(derivDegree) + 1);
+ std::vector derivRoots(derivDegree);
+ for (int32_t i = 0, ip1 = 1; i <= derivDegree; ++i, ++ip1) {
+ derivCoeff[i] = c[ip1] * (double)(ip1) / (double)degree;
+ }
+ const int32_t numDerivRoots = FindRecursive(degree - 1, &derivCoeff[0], tmin, tmax, maxIterations, &derivRoots[0]);
+
+ int32_t numRoots = 0;
+ if (numDerivRoots > 0) {
+ // Find root on [tmin,derivRoots[0]].
+ if (Find(degree, c, tmin, derivRoots[0], maxIterations, root))
+ roots[numRoots++] = root;
+
+ // Find root on [derivRoots[i],derivRoots[i+1]].
+ for (int32_t i = 0, ip1 = 1; i <= numDerivRoots - 2; ++i, ++ip1) {
+ if (Find(degree, c, derivRoots[i], derivRoots[ip1], maxIterations, root))
+ roots[numRoots++] = root;
+ }
+
+ // Find root on [derivRoots[numDerivRoots-1],tmax].
+ if (Find(degree, c, derivRoots[static_cast(numDerivRoots) - 1], tmax, maxIterations, root))
+ roots[numRoots++] = root;
+ }
+ else {
+ // The polynomial is monotone on [tmin,tmax], so has at most one root.
+ if (Find(degree, c, tmin, tmax, maxIterations, root))
+ roots[numRoots++] = root;
+ }
+ return numRoots;
+ }
+
+ static double Evaluate(int32_t degree, const double* c, double t)
+ {
+ int32_t i = degree;
+ double result = c[i];
+ while (--i >= 0) {
+ result = t * result + c[i];
+ }
+ return result;
+ }
+};
+
+// Adaptation of code found in:
+// https://github.com/davideberly/GeometricTools/blob/master/GTE/Mathematics/Vector.h
+
+// Construct a single vector orthogonal to the nonzero input vector. If
+// the maximum absolute component occurs at index i, then the orthogonal
+// vector U has u[i] = v[i+1], u[i+1] = -v[i], and all other components
+// zero. The index addition i+1 is computed modulo N.
+inline Vec3d get_orthogonal(const Vec3d& v, bool unitLength)
+{
+ double cmax = std::fabs(v[0]);
+ int32_t imax = 0;
+ for (int32_t i = 1; i < 3; ++i) {
+ double c = std::fabs(v[i]);
+ if (c > cmax) {
+ cmax = c;
+ imax = i;
+ }
+ }
+
+ Vec3d result = Vec3d::Zero();
+ int32_t inext = imax + 1;
+ if (inext == 3)
+ inext = 0;
+
+ result[imax] = v[inext];
+ result[inext] = -v[imax];
+ if (unitLength) {
+ const double sqrDistance = result[imax] * result[imax] + result[inext] * result[inext];
+ const double invLength = 1.0 / std::sqrt(sqrDistance);
+ result[imax] *= invLength;
+ result[inext] *= invLength;
+ }
+ return result;
+}
+
+} // namespace Slic3r
+} // namespace Measure
+
+#endif // Slic3r_MeasureUtils_hpp_
diff --git a/src/libslic3r/SurfaceMesh.hpp b/src/libslic3r/SurfaceMesh.hpp
new file mode 100644
index 0000000000..976387c21f
--- /dev/null
+++ b/src/libslic3r/SurfaceMesh.hpp
@@ -0,0 +1,167 @@
+///|/ Copyright (c) Prusa Research 2022 Lukáš Matěna @lukasmatena
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
+#ifndef slic3r_SurfaceMesh_hpp_
+#define slic3r_SurfaceMesh_hpp_
+
+#include
+#include
+
+#include "boost/container/small_vector.hpp"
+
+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
+ {
+ // In case the mesh is broken badly, the loop might end up to be infinite,
+ // never getting back to the first halfedge. Remember list of all half-edges
+ // and trip if any is encountered for the second time.
+ Halfedge_index h_first = halfedge(v);
+ boost::container::small_vector he_visited;
+ Halfedge_index h = next_around_target(h_first);
+ size_t degree = 2;
+ while (! h.is_invalid() && h != h_first) {
+ he_visited.emplace_back(h);
+ h = next_around_target(h);
+ if (std::find(he_visited.begin(), he_visited.end(), h) == he_visited.end())
+ return 0;
+ ++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/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 4b9a967f9e..e1404b09a5 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -1,3 +1,12 @@
+#/|/ Copyright (c) Prusa Research 2018 - 2023 Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Vojtěch Bubník @bubnikv, Pavel Mikuš @Godrak, Filip Sykala @Jony01, Oleksandra Iushchenko @YuSanka, Lukáš Hejl @hejllukas, Vojtěch Král @vojtechkral
+#/|/ Copyright (c) 2023 Pedro Lamas @PedroLamas
+#/|/ Copyright (c) 2020 Sergey Kovalev @RandoMan70
+#/|/ Copyright (c) 2021 Boleslaw Ciesielski
+#/|/ Copyright (c) 2019 Spencer Owen @spuder
+#/|/ Copyright (c) 2019 Stephan Reichhelm @stephanr
+#/|/
+#/|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+#/|/
cmake_minimum_required(VERSION 3.13)
project(libslic3r_gui)
@@ -135,6 +144,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoMmuSegmentation.hpp
#GUI/Gizmos/GLGizmoFaceDetector.cpp
#GUI/Gizmos/GLGizmoFaceDetector.hpp
+ GUI/Gizmos/GLGizmoMeasure.cpp
+ GUI/Gizmos/GLGizmoMeasure.hpp
GUI/Gizmos/GLGizmoSeam.cpp
GUI/Gizmos/GLGizmoSeam.hpp
GUI/Gizmos/GLGizmoText.cpp
@@ -175,6 +186,8 @@ set(SLIC3R_GUI_SOURCES
GUI/GUI_App.hpp
GUI/GUI_Utils.cpp
GUI/GUI_Utils.hpp
+ GUI/GUI_Geometry.cpp
+ GUI/GUI_Geometry.hpp
GUI/I18N.cpp
GUI/I18N.hpp
GUI/MainFrame.cpp
diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp
index e04e71ab45..0d52ba4882 100644
--- a/src/slic3r/GUI/3DScene.hpp
+++ b/src/slic3r/GUI/3DScene.hpp
@@ -1,3 +1,14 @@
+///|/ Copyright (c) Prusa Research 2017 - 2023 Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka, Tomáš Mészáros @tamasmeszaros, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv, David Kocík @kocikdav, Vojtěch Král @vojtechkral
+///|/ Copyright (c) 2017 Eyal Soha @eyal0
+///|/ Copyright (c) Slic3r 2015 Alessandro Ranellucci @alranel
+///|/
+///|/ ported from lib/Slic3r/GUI/3DScene.pm:
+///|/ Copyright (c) Prusa Research 2016 - 2019 Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka
+///|/ Copyright (c) Slic3r 2013 - 2016 Alessandro Ranellucci @alranel
+///|/ Copyright (c) 2013 Guillaume Seguin @iXce
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
#ifndef slic3r_3DScene_hpp_
#define slic3r_3DScene_hpp_
diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp
index b00657faea..dcf2713fb5 100644
--- a/src/slic3r/GUI/GLCanvas3D.cpp
+++ b/src/slic3r/GUI/GLCanvas3D.cpp
@@ -2189,15 +2189,11 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
if (!m_initialized)
return;
-
+
_set_current();
m_hover_volume_idxs.clear();
- GLGizmoBase* curr_gizmo = m_gizmos.get_current();
- if (curr_gizmo != nullptr)
- curr_gizmo->unregister_raycasters_for_picking();
-
struct ModelVolumeState {
ModelVolumeState(const GLVolume* volume) :
model_volume(nullptr), geometry_id(volume->geometry_id), volume_idx(-1) {}
@@ -2656,6 +2652,8 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
else
m_selection.volumes_changed(map_glvolume_old_to_new);
+ // @Enrico suggest this solution to preven accessing pointer on caster without data
+ m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Volume);
m_gizmos.update_data();
m_gizmos.update_assemble_view_data();
m_gizmos.refresh_on_off_state();
@@ -2663,9 +2661,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
// Update the toolbar
//BBS: notify the PartPlateList to reload all objects
if (update_object_list)
- {
post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
- }
//BBS:exclude the assmble view
if (m_canvas_type != ECanvasType::CanvasAssembleView) {
@@ -2711,14 +2707,19 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
}
// refresh volume raycasters for picking
- m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Volume);
for (size_t i = 0; i < m_volumes.volumes.size(); ++i) {
- assert(m_volumes.volumes[i]->mesh_raycaster != nullptr);
- add_raycaster_for_picking(SceneRaycaster::EType::Volume, i, *m_volumes.volumes[i]->mesh_raycaster, m_volumes.volumes[i]->world_matrix());
+ const GLVolume* v = m_volumes.volumes[i];
+ assert(v->mesh_raycaster != nullptr);
+ std::shared_ptr raycaster = add_raycaster_for_picking(SceneRaycaster::EType::Volume, i, *v->mesh_raycaster, v->world_matrix());
+ raycaster->set_active(v->is_active);
}
// refresh gizmo elements raycasters for picking
+ GLGizmoBase* curr_gizmo = m_gizmos.get_current();
+ if (curr_gizmo != nullptr)
+ curr_gizmo->unregister_raycasters_for_picking();
m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Gizmo);
+ m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::FallbackGizmo);
if (curr_gizmo != nullptr && !m_selection.is_empty())
curr_gizmo->register_raycasters_for_picking();
@@ -6434,6 +6435,7 @@ void GLCanvas3D::_picking_pass()
break;
}
case SceneRaycaster::EType::Gizmo:
+ case SceneRaycaster::EType::FallbackGizmo:
{
const Size& cnv_size = get_canvas_size();
const bool inside = 0 <= m_mouse.position.x() && m_mouse.position.x() < cnv_size.get_width() &&
@@ -6474,42 +6476,94 @@ void GLCanvas3D::_picking_pass()
{
case SceneRaycaster::EType::Bed: { object_type = "Bed"; break; }
case SceneRaycaster::EType::Gizmo: { object_type = "Gizmo element"; break; }
+ case SceneRaycaster::EType::FallbackGizmo: { object_type = "Gizmo2 element"; break; }
case SceneRaycaster::EType::Volume:
{
if (m_volumes.volumes[hit.raycaster_id]->is_wipe_tower)
- object_type = "Wipe tower";
+ object_type = "Volume (Wipe tower)";
else if (m_volumes.volumes[hit.raycaster_id]->volume_idx() == -int(slaposPad))
- object_type = "SLA pad";
+ object_type = "Volume (SLA pad)";
else if (m_volumes.volumes[hit.raycaster_id]->volume_idx() == -int(slaposSupportTree))
- object_type = "SLA supports";
+ object_type = "Volume (SLA supports)";
+ else if (m_volumes.volumes[hit.raycaster_id]->is_modifier)
+ object_type = "Volume (Modifier)";
else
- object_type = "Volume";
+ object_type = "Volume (Part)";
break;
}
default: { break; }
}
+
+ auto add_strings_row_to_table = [&imgui](const std::string& col_1, const ImVec4& col_1_color, const std::string& col_2, const ImVec4& col_2_color,
+ const std::string& col_3 = "", const ImVec4& col_3_color = ImGui::GetStyleColorVec4(ImGuiCol_Text)) {
+ ImGui::TableNextRow();
+ ImGui::TableSetColumnIndex(0);
+ imgui.text_colored(col_1_color, col_1.c_str());
+ ImGui::TableSetColumnIndex(1);
+ imgui.text_colored(col_2_color, col_2.c_str());
+ if (!col_3.empty()) {
+ ImGui::TableSetColumnIndex(2);
+ imgui.text_colored(col_3_color, col_3.c_str());
+ }
+ };
+
char buf[1024];
if (hit.type != SceneRaycaster::EType::None) {
- sprintf(buf, "Object ID: %d", hit.raycaster_id);
- imgui.text(std::string(buf));
- sprintf(buf, "Type: %s", object_type.c_str());
- imgui.text(std::string(buf));
- sprintf(buf, "Position: %.3f, %.3f, %.3f", hit.position.x(), hit.position.y(), hit.position.z());
- imgui.text(std::string(buf));
- sprintf(buf, "Normal: %.3f, %.3f, %.3f", hit.normal.x(), hit.normal.y(), hit.normal.z());
- imgui.text(std::string(buf));
+ if (ImGui::BeginTable("Hit", 2)) {
+ add_strings_row_to_table("Object ID", ImGuiWrapper::COL_ORANGE_LIGHT, std::to_string(hit.raycaster_id), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ add_strings_row_to_table("Type", ImGuiWrapper::COL_ORANGE_LIGHT, object_type, ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ sprintf(buf, "%.3f, %.3f, %.3f", hit.position.x(), hit.position.y(), hit.position.z());
+ add_strings_row_to_table("Position", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ sprintf(buf, "%.3f, %.3f, %.3f", hit.normal.x(), hit.normal.y(), hit.normal.z());
+ add_strings_row_to_table("Normal", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ImGui::EndTable();
+ }
}
else
imgui.text("NO HIT");
ImGui::Separator();
imgui.text("Registered for picking:");
- sprintf(buf, "Beds: %d", (int)m_scene_raycaster.beds_count());
- imgui.text(std::string(buf));
- sprintf(buf, "Volumes: %d", (int)m_scene_raycaster.volumes_count());
- imgui.text(std::string(buf));
- sprintf(buf, "Gizmo elements: %d", (int)m_scene_raycaster.gizmos_count());
- imgui.text(std::string(buf));
+ if (ImGui::BeginTable("Raycasters", 2)) {
+ sprintf(buf, "%d (%d)", (int)m_scene_raycaster.beds_count(), (int)m_scene_raycaster.active_beds_count());
+ add_strings_row_to_table("Beds", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ sprintf(buf, "%d (%d)", (int)m_scene_raycaster.volumes_count(), (int)m_scene_raycaster.active_volumes_count());
+ add_strings_row_to_table("Volumes", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ sprintf(buf, "%d (%d)", (int)m_scene_raycaster.gizmos_count(), (int)m_scene_raycaster.active_gizmos_count());
+ add_strings_row_to_table("Gizmo elements", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ sprintf(buf, "%d (%d)", (int)m_scene_raycaster.fallback_gizmos_count(), (int)m_scene_raycaster.active_fallback_gizmos_count());
+ add_strings_row_to_table("Gizmo2 elements", ImGuiWrapper::COL_ORANGE_LIGHT, std::string(buf), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ImGui::EndTable();
+ }
+
+ std::vector>* gizmo_raycasters = m_scene_raycaster.get_raycasters(SceneRaycaster::EType::Gizmo);
+ if (gizmo_raycasters != nullptr && !gizmo_raycasters->empty()) {
+ ImGui::Separator();
+ imgui.text("Gizmo raycasters IDs:");
+ if (ImGui::BeginTable("GizmoRaycasters", 3)) {
+ for (size_t i = 0; i < gizmo_raycasters->size(); ++i) {
+ add_strings_row_to_table(std::to_string(i), ImGuiWrapper::COL_ORANGE_LIGHT,
+ std::to_string(SceneRaycaster::decode_id(SceneRaycaster::EType::Gizmo, (*gizmo_raycasters)[i]->get_id())), ImGui::GetStyleColorVec4(ImGuiCol_Text),
+ to_string(Geometry::Transformation((*gizmo_raycasters)[i]->get_transform()).get_offset()), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ }
+ ImGui::EndTable();
+ }
+ }
+
+ std::vector>* gizmo2_raycasters = m_scene_raycaster.get_raycasters(SceneRaycaster::EType::FallbackGizmo);
+ if (gizmo2_raycasters != nullptr && !gizmo2_raycasters->empty()) {
+ ImGui::Separator();
+ imgui.text("Gizmo2 raycasters IDs:");
+ if (ImGui::BeginTable("Gizmo2Raycasters", 3)) {
+ for (size_t i = 0; i < gizmo2_raycasters->size(); ++i) {
+ add_strings_row_to_table(std::to_string(i), ImGuiWrapper::COL_ORANGE_LIGHT,
+ std::to_string(SceneRaycaster::decode_id(SceneRaycaster::EType::FallbackGizmo, (*gizmo2_raycasters)[i]->get_id())), ImGui::GetStyleColorVec4(ImGuiCol_Text),
+ to_string(Geometry::Transformation((*gizmo2_raycasters)[i]->get_transform()).get_offset()), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ }
+ ImGui::EndTable();
+ }
+ }
+
imgui.end();
#endif // ENABLE_RAYCAST_PICKING_DEBUG
}
diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp
index b1f73fefab..afcd5bdcb8 100644
--- a/src/slic3r/GUI/GLCanvas3D.hpp
+++ b/src/slic3r/GUI/GLCanvas3D.hpp
@@ -1,3 +1,8 @@
+///|/ Copyright (c) Prusa Research 2018 - 2023 Enrico Turri @enricoturri1966, Tomáš Mészáros @tamasmeszaros, Lukáš Matěna @lukasmatena, Oleksandra Iushchenko @YuSanka, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv, Lukáš Hejl @hejllukas, David Kocík @kocikdav, Vojtěch Král @vojtechkral
+///|/ Copyright (c) BambuStudio 2023 manch1n @manch1n
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
#ifndef slic3r_GLCanvas3D_hpp_
#define slic3r_GLCanvas3D_hpp_
@@ -724,9 +729,9 @@ public:
bool init();
void post_event(wxEvent &&event);
-
+
std::shared_ptr add_raycaster_for_picking(SceneRaycaster::EType type, int id, const MeshRaycaster& raycaster,
- const Transform3d& trafo, bool use_back_faces = false) {
+ const Transform3d& trafo = Transform3d::Identity(), bool use_back_faces = false) {
return m_scene_raycaster.add_raycaster(type, id, raycaster, trafo, use_back_faces);
}
void remove_raycasters_for_picking(SceneRaycaster::EType type, int id) {
diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp
index 97ef65978f..959c9aa9ac 100644
--- a/src/slic3r/GUI/GLModel.cpp
+++ b/src/slic3r/GUI/GLModel.cpp
@@ -1,3 +1,7 @@
+///|/ Copyright (c) Prusa Research 2020 - 2023 Enrico Turri @enricoturri1966, Vojtěch Bubník @bubnikv, Filip Sykala @Jony01
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
#include "libslic3r/libslic3r.h"
#include "GLModel.hpp"
@@ -247,6 +251,21 @@ void GLModel::Geometry::remove_vertex(size_t id)
}
}
+indexed_triangle_set GLModel::Geometry::get_as_indexed_triangle_set() const
+{
+ indexed_triangle_set its;
+ its.vertices.reserve(vertices_count());
+ for (size_t i = 0; i < vertices_count(); ++i) {
+ its.vertices.emplace_back(extract_position_3(i));
+ }
+ its.indices.reserve(indices_count() / 3);
+ for (size_t i = 0; i < indices_count() / 3; ++i) {
+ const size_t tri_id = i * 3;
+ its.indices.emplace_back(extract_index(tri_id), extract_index(tri_id + 1), extract_index(tri_id + 2));
+ }
+ return its;
+}
+
size_t GLModel::Geometry::vertex_stride_floats(const Format& format)
{
switch (format.vertex_layout)
@@ -1212,5 +1231,190 @@ GLModel::Geometry diamond(unsigned int resolution)
return data;
}
+GLModel::Geometry smooth_sphere(unsigned int resolution, float radius)
+{
+ resolution = std::max(4, resolution);
+
+ const unsigned int sectorCount = resolution;
+ const unsigned int stackCount = resolution;
+
+ const float sectorStep = float(2.0 * M_PI / sectorCount);
+ const float stackStep = float(M_PI / stackCount);
+
+ GLModel::Geometry data;
+ data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
+ data.reserve_vertices((stackCount - 1) * sectorCount + 2);
+ data.reserve_indices((2 * (stackCount - 1) * sectorCount) * 3);
+
+ // vertices
+ for (unsigned int i = 0; i <= stackCount; ++i) {
+ // from pi/2 to -pi/2
+ const double stackAngle = 0.5 * M_PI - stackStep * i;
+ const double xy = double(radius) * ::cos(stackAngle);
+ const double z = double(radius) * ::sin(stackAngle);
+ if (i == 0 || i == stackCount) {
+ const Vec3f v(float(xy), 0.0f, float(z));
+ data.add_vertex(v, (Vec3f)v.normalized());
+ }
+ else {
+ for (unsigned int j = 0; j < sectorCount; ++j) {
+ // from 0 to 2pi
+ const double sectorAngle = sectorStep * j;
+ const Vec3f v(float(xy * std::cos(sectorAngle)), float(xy * std::sin(sectorAngle)), float(z));
+ data.add_vertex(v, (Vec3f)v.normalized());
+ }
+ }
+ }
+
+ // triangles
+ for (unsigned int i = 0; i < stackCount; ++i) {
+ // Beginning of current stack.
+ unsigned int k1 = (i == 0) ? 0 : (1 + (i - 1) * sectorCount);
+ const unsigned int k1_first = k1;
+ // Beginning of next stack.
+ unsigned int k2 = (i == 0) ? 1 : (k1 + sectorCount);
+ const unsigned int k2_first = k2;
+ for (unsigned int j = 0; j < sectorCount; ++j) {
+ // 2 triangles per sector excluding first and last stacks
+ unsigned int k1_next = k1;
+ unsigned int k2_next = k2;
+ if (i != 0) {
+ k1_next = (j + 1 == sectorCount) ? k1_first : (k1 + 1);
+ data.add_triangle(k1, k2, k1_next);
+ }
+ if (i + 1 != stackCount) {
+ k2_next = (j + 1 == sectorCount) ? k2_first : (k2 + 1);
+ data.add_triangle(k1_next, k2, k2_next);
+ }
+ k1 = k1_next;
+ k2 = k2_next;
+ }
+ }
+
+ return data;
+}
+
+GLModel::Geometry smooth_cylinder(unsigned int resolution, float radius, float height)
+{
+ resolution = std::max(4, resolution);
+
+ const unsigned int sectorCount = resolution;
+ const float sectorStep = 2.0f * float(M_PI) / float(sectorCount);
+
+ GLModel::Geometry data;
+ data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
+ data.reserve_vertices(sectorCount * 4 + 2);
+ data.reserve_indices(sectorCount * 4 * 3);
+
+ auto generate_vertices_on_circle = [sectorCount, sectorStep](float radius) {
+ std::vector ret;
+ ret.reserve(sectorCount);
+ for (unsigned int i = 0; i < sectorCount; ++i) {
+ // from 0 to 2pi
+ const float sectorAngle = sectorStep * i;
+ ret.emplace_back(radius * std::cos(sectorAngle), radius * std::sin(sectorAngle), 0.0f);
+ }
+ return ret;
+ };
+
+ const std::vector base_vertices = generate_vertices_on_circle(radius);
+ const Vec3f h = height * Vec3f::UnitZ();
+
+ // stem vertices
+ for (unsigned int i = 0; i < sectorCount; ++i) {
+ const Vec3f& v = base_vertices[i];
+ const Vec3f n = v.normalized();
+ data.add_vertex(v, n);
+ data.add_vertex(v + h, n);
+ }
+
+ // stem triangles
+ for (unsigned int i = 0; i < sectorCount; ++i) {
+ unsigned int v1 = i * 2;
+ unsigned int v2 = (i < sectorCount - 1) ? v1 + 2 : 0;
+ unsigned int v3 = v2 + 1;
+ unsigned int v4 = v1 + 1;
+ data.add_triangle(v1, v2, v3);
+ data.add_triangle(v1, v3, v4);
+ }
+
+ // bottom cap vertices
+ Vec3f cap_center = Vec3f::Zero();
+ unsigned int cap_center_id = data.vertices_count();
+ Vec3f normal = -Vec3f::UnitZ();
+
+ data.add_vertex(cap_center, normal);
+ for (unsigned int i = 0; i < sectorCount; ++i) {
+ data.add_vertex(base_vertices[i], normal);
+ }
+
+ // bottom cap triangles
+ for (unsigned int i = 0; i < sectorCount; ++i) {
+ data.add_triangle(cap_center_id, (i < sectorCount - 1) ? cap_center_id + i + 2 : cap_center_id + 1, cap_center_id + i + 1);
+ }
+
+ // top cap vertices
+ cap_center += h;
+ cap_center_id = data.vertices_count();
+ normal = -normal;
+
+ data.add_vertex(cap_center, normal);
+ for (unsigned int i = 0; i < sectorCount; ++i) {
+ data.add_vertex(base_vertices[i] + h, normal);
+ }
+
+ // top cap triangles
+ for (unsigned int i = 0; i < sectorCount; ++i) {
+ data.add_triangle(cap_center_id, cap_center_id + i + 1, (i < sectorCount - 1) ? cap_center_id + i + 2 : cap_center_id + 1);
+ }
+
+ return data;
+}
+
+GLModel::Geometry smooth_torus(unsigned int primary_resolution, unsigned int secondary_resolution, float radius, float thickness)
+{
+ const unsigned int torus_sector_count = std::max(4, primary_resolution);
+ const float torus_sector_step = 2.0f * float(M_PI) / float(torus_sector_count);
+ const unsigned int section_sector_count = std::max(4, secondary_resolution);
+ const float section_sector_step = 2.0f * float(M_PI) / float(section_sector_count);
+
+ GLModel::Geometry data;
+ data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
+ data.reserve_vertices(torus_sector_count * section_sector_count);
+ data.reserve_indices(torus_sector_count * section_sector_count * 2 * 3);
+
+ // vertices
+ for (unsigned int i = 0; i < torus_sector_count; ++i) {
+ const float section_angle = torus_sector_step * i;
+ const float csa = std::cos(section_angle);
+ const float ssa = std::sin(section_angle);
+ const Vec3f section_center(radius * csa, radius * ssa, 0.0f);
+ for (unsigned int j = 0; j < section_sector_count; ++j) {
+ const float circle_angle = section_sector_step * j;
+ const float thickness_xy = thickness * std::cos(circle_angle);
+ const float thickness_z = thickness * std::sin(circle_angle);
+ const Vec3f v(thickness_xy * csa, thickness_xy * ssa, thickness_z);
+ data.add_vertex(section_center + v, (Vec3f)v.normalized());
+ }
+ }
+
+ // triangles
+ for (unsigned int i = 0; i < torus_sector_count; ++i) {
+ const unsigned int ii = i * section_sector_count;
+ const unsigned int ii_next = ((i + 1) % torus_sector_count) * section_sector_count;
+ for (unsigned int j = 0; j < section_sector_count; ++j) {
+ const unsigned int j_next = (j + 1) % section_sector_count;
+ const unsigned int i0 = ii + j;
+ const unsigned int i1 = ii_next + j;
+ const unsigned int i2 = ii_next + j_next;
+ const unsigned int i3 = ii + j_next;
+ data.add_triangle(i0, i1, i2);
+ data.add_triangle(i0, i2, i3);
+ }
+ }
+
+ return data;
+}
+
} // namespace GUI
} // namespace Slic3r
diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp
index 7089f11e15..e90c263ece 100644
--- a/src/slic3r/GUI/GLModel.hpp
+++ b/src/slic3r/GUI/GLModel.hpp
@@ -1,3 +1,7 @@
+///|/ Copyright (c) Prusa Research 2020 - 2023 Enrico Turri @enricoturri1966, Vojtěch Bubník @bubnikv, Filip Sykala @Jony01, Lukáš Matěna @lukasmatena
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
#ifndef slic3r_GLModel_hpp_
#define slic3r_GLModel_hpp_
@@ -101,6 +105,8 @@ namespace GUI {
size_t vertices_size_bytes() const { return vertices_size_floats() * sizeof(float); }
size_t indices_size_bytes() const { return indices.size() * index_stride_bytes(*this); }
+ indexed_triangle_set get_as_indexed_triangle_set() const;
+
static size_t vertex_stride_floats(const Format& format);
static size_t vertex_stride_bytes(const Format& format) { return vertex_stride_floats(format) * sizeof(float); }
@@ -233,6 +239,18 @@ namespace GUI {
// the diamond is contained into a box with size [1, 1, 1]
GLModel::Geometry diamond(unsigned int resolution);
+ // create a sphere with smooth normals
+ // the origin of the sphere is in its center
+ GLModel::Geometry smooth_sphere(unsigned int resolution, float radius);
+ // create a cylinder with smooth normals
+ // the axis of the cylinder is the Z axis
+ // the origin of the cylinder is the center of its bottom cap face
+ GLModel::Geometry smooth_cylinder(unsigned int resolution, float radius, float height);
+ // create a torus with smooth normals
+ // the axis of the torus is the Z axis
+ // the origin of the torus is in its center
+ GLModel::Geometry smooth_torus(unsigned int primary_resolution, unsigned int secondary_resolution, float radius, float thickness);
+
} // namespace GUI
} // namespace Slic3r
diff --git a/src/slic3r/GUI/GUI_Geometry.cpp b/src/slic3r/GUI/GUI_Geometry.cpp
new file mode 100644
index 0000000000..3cee023e12
--- /dev/null
+++ b/src/slic3r/GUI/GUI_Geometry.cpp
@@ -0,0 +1,13 @@
+///|/ Copyright (c) Prusa Research 2021 Enrico Turri @enricoturri1966
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
+#include "libslic3r/libslic3r.h"
+#include "GUI_Geometry.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+
+} // namespace Slic3r
+} // namespace GUI
diff --git a/src/slic3r/GUI/GUI_Geometry.hpp b/src/slic3r/GUI/GUI_Geometry.hpp
new file mode 100644
index 0000000000..ed89af649c
--- /dev/null
+++ b/src/slic3r/GUI/GUI_Geometry.hpp
@@ -0,0 +1,82 @@
+///|/ Copyright (c) Prusa Research 2021 - 2023 Enrico Turri @enricoturri1966
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
+#ifndef slic3r_GUI_Geometry_hpp_
+#define slic3r_GUI_Geometry_hpp_
+
+namespace Slic3r {
+namespace GUI {
+
+enum class ECoordinatesType : unsigned char
+{
+ World,
+ Instance,
+ Local
+};
+
+class TransformationType
+{
+public:
+ enum Enum {
+ // Transforming in a world coordinate system
+ World = 0,
+ // Transforming in a instance coordinate system
+ Instance = 1,
+ // Transforming in a local coordinate system
+ Local = 2,
+ // Absolute transformations, allowed in local coordinate system only.
+ Absolute = 0,
+ // Relative transformations, allowed in both local and world coordinate system.
+ Relative = 4,
+ // For group selection, the transformation is performed as if the group made a single solid body.
+ Joint = 0,
+ // For group selection, the transformation is performed on each object independently.
+ Independent = 8,
+
+ World_Relative_Joint = World | Relative | Joint,
+ World_Relative_Independent = World | Relative | Independent,
+ Instance_Absolute_Joint = Instance | Absolute | Joint,
+ Instance_Absolute_Independent = Instance | Absolute | Independent,
+ Instance_Relative_Joint = Instance | Relative | Joint,
+ Instance_Relative_Independent = Instance | Relative | Independent,
+ Local_Absolute_Joint = Local | Absolute | Joint,
+ Local_Absolute_Independent = Local | Absolute | Independent,
+ Local_Relative_Joint = Local | Relative | Joint,
+ Local_Relative_Independent = Local | Relative | Independent,
+ };
+
+ TransformationType() : m_value(World) {}
+ TransformationType(Enum value) : m_value(value) {}
+ TransformationType& operator=(Enum value) { m_value = value; return *this; }
+
+ Enum operator()() const { return m_value; }
+ bool has(Enum v) const { return ((unsigned int)m_value & (unsigned int)v) != 0; }
+
+ void set_world() { this->remove(Instance); this->remove(Local); }
+ void set_instance() { this->remove(Local); this->add(Instance); }
+ void set_local() { this->remove(Instance); this->add(Local); }
+ void set_absolute() { this->remove(Relative); }
+ void set_relative() { this->add(Relative); }
+ void set_joint() { this->remove(Independent); }
+ void set_independent() { this->add(Independent); }
+
+ bool world() const { return !this->has(Instance) && !this->has(Local); }
+ bool instance() const { return this->has(Instance); }
+ bool local() const { return this->has(Local); }
+ bool absolute() const { return !this->has(Relative); }
+ bool relative() const { return this->has(Relative); }
+ bool joint() const { return !this->has(Independent); }
+ bool independent() const { return this->has(Independent); }
+
+private:
+ void add(Enum v) { m_value = Enum((unsigned int)m_value | (unsigned int)v); }
+ void remove(Enum v) { m_value = Enum((unsigned int)m_value & (~(unsigned int)v)); }
+
+ Enum m_value;
+};
+
+} // namespace Slic3r
+} // namespace GUI
+
+#endif // slic3r_GUI_Geometry_hpp_
diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp
index ab3d2fd794..6fbec69ba7 100644
--- a/src/slic3r/GUI/GUI_Utils.hpp
+++ b/src/slic3r/GUI/GUI_Utils.hpp
@@ -1,3 +1,7 @@
+///|/ Copyright (c) Prusa Research 2018 - 2023 Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Vojtěch Král @vojtechkral
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
#ifndef slic3r_GUI_Utils_hpp_
#define slic3r_GUI_Utils_hpp_
@@ -463,6 +467,16 @@ public:
~TaskTimer();
};
+class KeyAutoRepeatFilter
+{
+ size_t m_count{ 0 };
+
+public:
+ void increase_count() { ++m_count; }
+ void reset_count() { m_count = 0; }
+ bool is_first() const { return m_count == 0; }
+};
+
/* Image Generator */
#define _3MF_COVER_SIZE wxSize(240, 240)
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp
new file mode 100644
index 0000000000..5b4d3bd932
--- /dev/null
+++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp
@@ -0,0 +1,2154 @@
+///|/ Copyright (c) Prusa Research 2019 - 2023 Lukáš Matěna @lukasmatena, Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, Vojtěch Bubník @bubnikv, Filip Sykala @Jony01
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
+#include "GLGizmoMeasure.hpp"
+#include "slic3r/GUI/GLCanvas3D.hpp"
+#include "slic3r/GUI/GUI_App.hpp"
+#include "slic3r/GUI/Plater.hpp"
+#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
+
+
+#include "libslic3r/PresetBundle.hpp"
+#include "libslic3r/MeasureUtils.hpp"
+
+#include
+
+#include
+
+#include
+
+#include
+
+#include
+
+namespace Slic3r {
+namespace GUI {
+
+static const Slic3r::ColorRGBA SELECTED_1ST_COLOR = { 0.25f, 0.75f, 0.75f, 1.0f };
+static const Slic3r::ColorRGBA SELECTED_2ND_COLOR = { 0.75f, 0.25f, 0.75f, 1.0f };
+static const Slic3r::ColorRGBA NEUTRAL_COLOR = {0.5f, 0.5f, 0.5f, 1.0f};
+static const Slic3r::ColorRGBA HOVER_COLOR = ColorRGBA::GREEN();
+
+static const int POINT_ID = 100;
+static const int EDGE_ID = 200;
+static const int CIRCLE_ID = 300;
+static const int PLANE_ID = 400;
+static const int SEL_SPHERE_1_ID = 501;
+static const int SEL_SPHERE_2_ID = 502;
+
+static const float TRIANGLE_BASE = 10.0f;
+static const float TRIANGLE_HEIGHT = TRIANGLE_BASE * 1.618033f;
+
+static const std::string CTRL_STR =
+#ifdef __APPLE__
+"⌘"
+#else
+"Ctrl"
+#endif //__APPLE__
+;
+
+static std::string format_double(double value)
+{
+ char buf[1024];
+ sprintf(buf, "%.3f", value);
+ return std::string(buf);
+}
+
+static std::string format_vec3(const Vec3d& v)
+{
+ char buf[1024];
+ sprintf(buf, "X: %.3f, Y: %.3f, Z: %.3f", v.x(), v.y(), v.z());
+ return std::string(buf);
+}
+
+static std::string surface_feature_type_as_string(Measure::SurfaceFeatureType type)
+{
+ switch (type)
+ {
+ default:
+ case Measure::SurfaceFeatureType::Undef: { return ("No feature"); }
+ case Measure::SurfaceFeatureType::Point: { return _u8L("Vertex"); }
+ case Measure::SurfaceFeatureType::Edge: { return _u8L("Edge"); }
+ case Measure::SurfaceFeatureType::Circle: { return _u8L("Circle"); }
+ case Measure::SurfaceFeatureType::Plane: { return _u8L("Plane"); }
+ }
+}
+
+static std::string point_on_feature_type_as_string(Measure::SurfaceFeatureType type, int hover_id)
+{
+ std::string ret;
+ switch (type) {
+ case Measure::SurfaceFeatureType::Point: { ret = _u8L("Vertex"); break; }
+ case Measure::SurfaceFeatureType::Edge: { ret = _u8L("Point on edge"); break; }
+ case Measure::SurfaceFeatureType::Circle: { ret = _u8L("Point on circle"); break; }
+ case Measure::SurfaceFeatureType::Plane: { ret = _u8L("Point on plane"); break; }
+ default: { assert(false); break; }
+ }
+ return ret;
+}
+
+static std::string center_on_feature_type_as_string(Measure::SurfaceFeatureType type)
+{
+ std::string ret;
+ switch (type) {
+ case Measure::SurfaceFeatureType::Edge: { ret = _u8L("Center of edge"); break; }
+ case Measure::SurfaceFeatureType::Circle: { ret = _u8L("Center of circle"); break; }
+ default: { assert(false); break; }
+ }
+ return ret;
+}
+
+static GLModel::Geometry init_plane_data(const indexed_triangle_set& its, const std::vector& triangle_indices)
+{
+ GLModel::Geometry init_data;
+ init_data.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
+ init_data.reserve_indices(3 * triangle_indices.size());
+ init_data.reserve_vertices(3 * triangle_indices.size());
+ unsigned int i = 0;
+ for (int idx : triangle_indices) {
+ const Vec3f& v0 = its.vertices[its.indices[idx][0]];
+ const Vec3f& v1 = its.vertices[its.indices[idx][1]];
+ const Vec3f& v2 = its.vertices[its.indices[idx][2]];
+
+ const Vec3f n = (v1 - v0).cross(v2 - v0).normalized();
+ init_data.add_vertex(v0, n);
+ init_data.add_vertex(v1, n);
+ init_data.add_vertex(v2, n);
+ init_data.add_triangle(i, i + 1, i + 2);
+ i += 3;
+ }
+
+ return init_data;
+}
+
+static GLModel::Geometry init_torus_data(unsigned int primary_resolution, unsigned int secondary_resolution, const Vec3f& center,
+ float radius, float thickness, const Vec3f& model_axis, const Transform3f& world_trafo)
+{
+ const unsigned int torus_sector_count = std::max(4, primary_resolution);
+ const unsigned int section_sector_count = std::max(4, secondary_resolution);
+ const float torus_sector_step = 2.0f * float(M_PI) / float(torus_sector_count);
+ const float section_sector_step = 2.0f * float(M_PI) / float(section_sector_count);
+
+ GLModel::Geometry data;
+ data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
+ data.reserve_vertices(torus_sector_count * section_sector_count);
+ data.reserve_indices(torus_sector_count * section_sector_count * 2 * 3);
+
+ // vertices
+ const Transform3f local_to_world_matrix = world_trafo * Geometry::translation_transform(center.cast()).cast() *
+ Eigen::Quaternion::FromTwoVectors(Vec3f::UnitZ(), model_axis);
+ for (unsigned int i = 0; i < torus_sector_count; ++i) {
+ const float section_angle = torus_sector_step * i;
+ const Vec3f radius_dir(std::cos(section_angle), std::sin(section_angle), 0.0f);
+ const Vec3f local_section_center = radius * radius_dir;
+ const Vec3f world_section_center = local_to_world_matrix * local_section_center;
+ const Vec3f local_section_normal = local_section_center.normalized().cross(Vec3f::UnitZ()).normalized();
+ const Vec3f world_section_normal = (Vec3f)(local_to_world_matrix.matrix().block(0, 0, 3, 3) * local_section_normal).normalized();
+ const Vec3f base_v = thickness * radius_dir;
+ for (unsigned int j = 0; j < section_sector_count; ++j) {
+ const Vec3f v = Eigen::AngleAxisf(section_sector_step * j, world_section_normal) * base_v;
+ data.add_vertex(world_section_center + v, (Vec3f)v.normalized());
+ }
+ }
+
+ // triangles
+ for (unsigned int i = 0; i < torus_sector_count; ++i) {
+ const unsigned int ii = i * section_sector_count;
+ const unsigned int ii_next = ((i + 1) % torus_sector_count) * section_sector_count;
+ for (unsigned int j = 0; j < section_sector_count; ++j) {
+ const unsigned int j_next = (j + 1) % section_sector_count;
+ const unsigned int i0 = ii + j;
+ const unsigned int i1 = ii_next + j;
+ const unsigned int i2 = ii_next + j_next;
+ const unsigned int i3 = ii + j_next;
+ data.add_triangle(i0, i1, i2);
+ data.add_triangle(i0, i2, i3);
+ }
+ }
+
+ return data;
+}
+
+static bool is_feature_with_center(const Measure::SurfaceFeature& feature)
+{
+ const Measure::SurfaceFeatureType type = feature.get_type();
+ return (type == Measure::SurfaceFeatureType::Circle || (type == Measure::SurfaceFeatureType::Edge && feature.get_extra_point().has_value()));
+}
+
+static Vec3d get_feature_offset(const Measure::SurfaceFeature& feature)
+{
+ Vec3d ret;
+ switch (feature.get_type())
+ {
+ case Measure::SurfaceFeatureType::Circle:
+ {
+ const auto [center, radius, normal] = feature.get_circle();
+ ret = center;
+ break;
+ }
+ case Measure::SurfaceFeatureType::Edge:
+ {
+ std::optional p = feature.get_extra_point();
+ assert(p.has_value());
+ ret = *p;
+ break;
+ }
+ case Measure::SurfaceFeatureType::Point:
+ {
+ ret = feature.get_point();
+ break;
+ }
+ default: { assert(false); }
+ }
+
+ return ret;
+}
+
+class TransformHelper
+{
+ struct Cache
+ {
+ std::array viewport;
+ Matrix4d ndc_to_ss_matrix;
+ Transform3d ndc_to_ss_matrix_inverse;
+ };
+
+ static Cache s_cache;
+
+public:
+ static Vec3d model_to_world(const Vec3d& model, const Transform3d& world_matrix) {
+ return world_matrix * model;
+ }
+
+ static Vec4d world_to_clip(const Vec3d& world, const Matrix4d& projection_view_matrix) {
+ return projection_view_matrix * Vec4d(world.x(), world.y(), world.z(), 1.0);
+ }
+
+ static Vec3d clip_to_ndc(const Vec4d& clip) {
+ return Vec3d(clip.x(), clip.y(), clip.z()) / clip.w();
+ }
+
+ static Vec2d ndc_to_ss(const Vec3d& ndc, const std::array& viewport) {
+ const double half_w = 0.5 * double(viewport[2]);
+ const double half_h = 0.5 * double(viewport[3]);
+ return { half_w * ndc.x() + double(viewport[0]) + half_w, half_h * ndc.y() + double(viewport[1]) + half_h };
+ };
+
+ static Vec4d model_to_clip(const Vec3d& model, const Transform3d& world_matrix, const Matrix4d& projection_view_matrix) {
+ return world_to_clip(model_to_world(model, world_matrix), projection_view_matrix);
+ }
+
+ static Vec3d model_to_ndc(const Vec3d& model, const Transform3d& world_matrix, const Matrix4d& projection_view_matrix) {
+ return clip_to_ndc(world_to_clip(model_to_world(model, world_matrix), projection_view_matrix));
+ }
+
+ static Vec2d model_to_ss(const Vec3d& model, const Transform3d& world_matrix, const Matrix4d& projection_view_matrix, const std::array& viewport) {
+ return ndc_to_ss(clip_to_ndc(world_to_clip(model_to_world(model, world_matrix), projection_view_matrix)), viewport);
+ }
+
+ static Vec2d world_to_ss(const Vec3d& world, const Matrix4d& projection_view_matrix, const std::array& viewport) {
+ return ndc_to_ss(clip_to_ndc(world_to_clip(world, projection_view_matrix)), viewport);
+ }
+
+ static const Matrix4d& ndc_to_ss_matrix(const std::array& viewport) {
+ update(viewport);
+ return s_cache.ndc_to_ss_matrix;
+ }
+
+ static const Transform3d ndc_to_ss_matrix_inverse(const std::array& viewport) {
+ update(viewport);
+ return s_cache.ndc_to_ss_matrix_inverse;
+ }
+
+private:
+ static void update(const std::array& viewport) {
+ if (s_cache.viewport == viewport)
+ return;
+
+ const double half_w = 0.5 * double(viewport[2]);
+ const double half_h = 0.5 * double(viewport[3]);
+ s_cache.ndc_to_ss_matrix << half_w, 0.0, 0.0, double(viewport[0]) + half_w,
+ 0.0, half_h, 0.0, double(viewport[1]) + half_h,
+ 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0;
+
+ s_cache.ndc_to_ss_matrix_inverse = s_cache.ndc_to_ss_matrix.inverse();
+ s_cache.viewport = viewport;
+ }
+};
+
+TransformHelper::Cache TransformHelper::s_cache = { { 0, 0, 0, 0 }, Matrix4d::Identity(), Transform3d::Identity() };
+
+GLGizmoMeasure::GLGizmoMeasure(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
+: GLGizmoBase(parent, icon_filename, sprite_id)
+{
+ GLModel::Geometry sphere_geometry = smooth_sphere(16, 7.5f);
+ m_sphere.mesh_raycaster = std::make_unique(std::make_shared(sphere_geometry.get_as_indexed_triangle_set()));
+ m_sphere.model.init_from(std::move(sphere_geometry));
+
+ GLModel::Geometry cylinder_geometry = smooth_cylinder(16, 5.0f, 1.0f);
+ m_cylinder.mesh_raycaster = std::make_unique(std::make_shared(cylinder_geometry.get_as_indexed_triangle_set()));
+ m_cylinder.model.init_from(std::move(cylinder_geometry));
+}
+
+bool GLGizmoMeasure::on_mouse(const wxMouseEvent &mouse_event)
+{
+ m_mouse_pos = { double(mouse_event.GetX()), double(mouse_event.GetY()) };
+
+ if (mouse_event.Moving()) {
+ // only for sure
+ m_mouse_left_down = false;
+ return false;
+ }
+ else if (mouse_event.Dragging()) {
+ // Enable/Disable panning/rotating the 3D scene
+ // Ctrl is pressed or the mouse is not hovering a selected volume
+ bool unlock_dragging = mouse_event.CmdDown() || (m_hover_id == -1 && !m_parent.get_selection().contains_volume(m_parent.get_first_hover_volume_idx()));
+ // mode is not center selection or mouse is not hovering a center
+ unlock_dragging &= !mouse_event.ShiftDown() || (m_hover_id != SEL_SPHERE_1_ID && m_hover_id != SEL_SPHERE_2_ID && m_hover_id != POINT_ID);
+ return !unlock_dragging;
+ }
+ else if (mouse_event.LeftDown()) {
+ // let the event pass through to allow panning/rotating the 3D scene
+ if (mouse_event.CmdDown())
+ return false;
+
+ if (m_hover_id != -1) {
+ m_mouse_left_down = true;
+
+ auto detect_current_item = [this]() {
+ SelectedFeatures::Item item;
+ if (m_hover_id == SEL_SPHERE_1_ID) {
+ if (m_selected_features.first.is_center)
+ // mouse is hovering over a selected center
+ item = { true, m_selected_features.first.source, { Measure::SurfaceFeature(get_feature_offset(*m_selected_features.first.source)) } };
+ else if (is_feature_with_center(*m_selected_features.first.feature))
+ // mouse is hovering over a unselected center
+ item = { true, m_selected_features.first.feature, { Measure::SurfaceFeature(get_feature_offset(*m_selected_features.first.feature)) } };
+ else
+ // mouse is hovering over a point
+ item = m_selected_features.first;
+ }
+ else if (m_hover_id == SEL_SPHERE_2_ID) {
+ if (m_selected_features.second.is_center)
+ // mouse is hovering over a selected center
+ item = { true, m_selected_features.second.source, { Measure::SurfaceFeature(get_feature_offset(*m_selected_features.second.source)) } };
+ else if (is_feature_with_center(*m_selected_features.second.feature))
+ // mouse is hovering over a center
+ item = { true, m_selected_features.second.feature, { Measure::SurfaceFeature(get_feature_offset(*m_selected_features.second.feature)) } };
+ else
+ // mouse is hovering over a point
+ item = m_selected_features.second;
+ }
+ else {
+ switch (m_mode)
+ {
+ case EMode::FeatureSelection: { item = { false, m_curr_feature, m_curr_feature }; break; }
+ case EMode::PointSelection: { item = { false, m_curr_feature, Measure::SurfaceFeature(*m_curr_point_on_feature_position) }; break; }
+ }
+ }
+ return item;
+ };
+
+ auto requires_sphere_raycaster_for_picking = [this](const SelectedFeatures::Item& item) {
+ if (m_mode == EMode::PointSelection || item.feature->get_type() == Measure::SurfaceFeatureType::Point)
+ return true;
+ else if (m_mode == EMode::FeatureSelection) {
+ if (is_feature_with_center(*item.feature))
+ return true;
+ }
+ return false;
+ };
+
+ if (m_selected_features.first.feature.has_value()) {
+ const SelectedFeatures::Item item = detect_current_item();
+ if (m_selected_features.first != item) {
+ bool processed = false;
+ if (item.is_center) {
+ if (item.source == m_selected_features.first.feature) {
+ // switch 1st selection from feature to its center
+ m_selected_features.first = item;
+ processed = true;
+ }
+ else if (item.source == m_selected_features.second.feature) {
+ // switch 2nd selection from feature to its center
+ m_selected_features.second = item;
+ processed = true;
+ }
+ }
+ else if (is_feature_with_center(*item.feature)) {
+ if (m_selected_features.first.is_center && m_selected_features.first.source == item.feature) {
+ // switch 1st selection from center to its feature
+ m_selected_features.first = item;
+ processed = true;
+ }
+ else if (m_selected_features.second.is_center && m_selected_features.second.source == item.feature) {
+ // switch 2nd selection from center to its feature
+ m_selected_features.second = item;
+ processed = true;
+ }
+ }
+
+ if (!processed) {
+ remove_selected_sphere_raycaster(SEL_SPHERE_2_ID);
+ if (m_selected_features.second == item)
+ // 2nd feature deselection
+ m_selected_features.second.reset();
+ else {
+ // 2nd feature selection
+ m_selected_features.second = item;
+ if (requires_sphere_raycaster_for_picking(item))
+ m_selected_sphere_raycasters.push_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, SEL_SPHERE_2_ID, *m_sphere.mesh_raycaster));
+ }
+ }
+ }
+ else {
+ remove_selected_sphere_raycaster(SEL_SPHERE_1_ID);
+ if (m_selected_features.second.feature.has_value()) {
+ // promote 2nd feature to 1st feature
+ remove_selected_sphere_raycaster(SEL_SPHERE_2_ID);
+ m_selected_features.first = m_selected_features.second;
+ if (requires_sphere_raycaster_for_picking(m_selected_features.first))
+ m_selected_sphere_raycasters.push_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, SEL_SPHERE_1_ID, *m_sphere.mesh_raycaster));
+ m_selected_features.second.reset();
+ }
+ else
+ // 1st feature deselection
+ m_selected_features.first.reset();
+ }
+ }
+ else {
+ // 1st feature selection
+ const SelectedFeatures::Item item = detect_current_item();
+ m_selected_features.first = item;
+ if (requires_sphere_raycaster_for_picking(item))
+ m_selected_sphere_raycasters.push_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, SEL_SPHERE_1_ID, *m_sphere.mesh_raycaster));
+ }
+
+ update_measurement_result();
+
+ m_imgui->set_requires_extra_frame();
+
+ return true;
+ }
+ else
+ // if the mouse pointer is on any volume, filter out the event to prevent the user to move it
+ // equivalent tp: return (m_parent.get_first_hover_volume_idx() != -1);
+ return m_curr_feature.has_value();
+
+ // 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;
+ }
+ if (m_hover_id == -1 && !m_parent.is_mouse_dragging())
+ // avoid closing the gizmo if the user clicks outside of any volume
+ return true;
+ }
+ else if (mouse_event.RightDown()) {
+ // let the event pass through to allow panning/rotating the 3D scene
+ if (mouse_event.CmdDown())
+ return false;
+ }
+ else if (mouse_event.Leaving())
+ m_mouse_left_down = false;
+
+ return false;
+}
+
+void GLGizmoMeasure::data_changed(bool is_serializing)
+{
+ m_parent.toggle_sla_auxiliaries_visibility(false, nullptr, -1);
+
+ update_if_needed();
+
+ m_last_inv_zoom = 0.0f;
+ m_last_plane_idx = -1;
+ if (m_pending_scale) {
+ update_measurement_result();
+ m_pending_scale = false;
+ }
+ else
+ m_selected_features.reset();
+ m_selected_sphere_raycasters.clear();
+ m_editing_distance = false;
+ m_is_editing_distance_first_frame = true;
+}
+
+bool GLGizmoMeasure::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down)
+{
+ if (action == SLAGizmoEventType::ShiftDown) {
+ if (m_shift_kar_filter.is_first()) {
+ m_mode = EMode::PointSelection;
+ disable_scene_raycasters();
+ }
+ m_shift_kar_filter.increase_count();
+ }
+ else if (action == SLAGizmoEventType::ShiftUp) {
+ m_shift_kar_filter.reset_count();
+ m_mode = EMode::FeatureSelection;
+ restore_scene_raycasters_state();
+ }
+ else if (action == SLAGizmoEventType::Delete) {
+ m_selected_features.reset();
+ m_selected_sphere_raycasters.clear();
+ m_parent.request_extra_frame();
+ }
+ else if (action == SLAGizmoEventType::Escape) {
+ if (!m_selected_features.first.feature.has_value()) {
+ update_measurement_result();
+ return false;
+ }
+ else {
+ if (m_selected_features.second.feature.has_value()) {
+ remove_selected_sphere_raycaster(SEL_SPHERE_2_ID);
+ m_selected_features.second.feature.reset();
+ }
+ else {
+ remove_selected_sphere_raycaster(SEL_SPHERE_1_ID);
+ m_selected_features.first.feature.reset();
+ }
+
+ update_measurement_result();
+ }
+ }
+
+ return true;
+}
+
+bool GLGizmoMeasure::on_init()
+{
+ m_shortcut_key = WXK_CONTROL_U;
+ return true;
+}
+
+void GLGizmoMeasure::on_set_state()
+{
+ if (m_state == Off) {
+ m_parent.toggle_sla_auxiliaries_visibility(true, nullptr, -1);
+ m_shift_kar_filter.reset_count();
+ m_curr_feature.reset();
+ m_curr_point_on_feature_position.reset();
+ restore_scene_raycasters_state();
+ m_editing_distance = false;
+ m_is_editing_distance_first_frame = true;
+ m_measuring.reset();
+ m_raycaster.reset();
+ }
+ else {
+ m_mode = EMode::FeatureSelection;
+ // store current state of scene raycaster for later use
+ m_scene_raycasters.clear();
+ auto scene_raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume);
+ if (scene_raycasters != nullptr) {
+ m_scene_raycasters.reserve(scene_raycasters->size());
+ for (auto r : *scene_raycasters) {
+ SceneRaycasterState state = { r, r->is_active() };
+ m_scene_raycasters.emplace_back(state);
+ }
+ }
+ }
+}
+
+std::string GLGizmoMeasure::on_get_name() const
+{
+ return _u8L("Measure");
+}
+
+bool GLGizmoMeasure::on_is_activable() const
+{
+ const Selection& selection = m_parent.get_selection();
+ bool res = (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) ?
+ selection.is_single_full_instance() :
+ selection.is_single_full_instance() || selection.is_single_volume() || selection.is_single_modifier();
+ if (res)
+ res &= !selection.contains_sinking_volumes();
+
+ return res;
+}
+
+void GLGizmoMeasure::on_render()
+{
+#if ENABLE_MEASURE_GIZMO_DEBUG
+ render_debug_dialog();
+#endif // ENABLE_MEASURE_GIZMO_DEBUG
+
+// // do not render if the user is panning/rotating the 3d scene
+// if (m_parent.is_mouse_dragging())
+// return;
+
+ update_if_needed();
+
+ const Camera& camera = wxGetApp().plater()->get_camera();
+ const float inv_zoom = (float)camera.get_inv_zoom();
+
+ Vec3f position_on_model;
+ Vec3f normal_on_model;
+ size_t model_facet_idx;
+ const bool mouse_on_object = m_raycaster->unproject_on_mesh(m_mouse_pos, Transform3d::Identity(), camera, position_on_model, normal_on_model, nullptr, &model_facet_idx);
+ const bool is_hovering_on_feature = m_mode == EMode::PointSelection && m_hover_id != -1;
+
+ auto update_circle = [this, inv_zoom]() {
+ if (m_last_inv_zoom != inv_zoom || m_last_circle != m_curr_feature) {
+ m_last_inv_zoom = inv_zoom;
+ m_last_circle = m_curr_feature;
+ m_circle.reset();
+ const auto [center, radius, normal] = m_curr_feature->get_circle();
+ GLModel::Geometry circle_geometry = init_torus_data(64, 16, center.cast(), float(radius), 5.0f * inv_zoom, normal.cast(), Transform3f::Identity());
+ m_circle.mesh_raycaster = std::make_unique(std::make_shared(circle_geometry.get_as_indexed_triangle_set()));
+ m_circle.model.init_from(std::move(circle_geometry));
+ return true;
+ }
+ return false;
+ };
+
+ if (m_mode == EMode::FeatureSelection || m_mode == EMode::PointSelection) {
+ if (m_hover_id == SEL_SPHERE_1_ID || m_hover_id == SEL_SPHERE_2_ID) {
+ // Skip feature detection if hovering on a selected point/center
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, POINT_ID);
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, EDGE_ID);
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, PLANE_ID);
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID);
+ m_curr_feature.reset();
+ m_curr_point_on_feature_position.reset();
+ }
+ else {
+ std::optional curr_feature = wxGetMouseState().LeftIsDown() ? m_curr_feature :
+ mouse_on_object ? m_measuring->get_feature(model_facet_idx, position_on_model.cast()) : std::nullopt;
+
+ if (m_curr_feature != curr_feature ||
+ (curr_feature.has_value() && curr_feature->get_type() == Measure::SurfaceFeatureType::Circle && (m_curr_feature != curr_feature || m_last_inv_zoom != inv_zoom))) {
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, POINT_ID);
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, EDGE_ID);
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, PLANE_ID);
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID);
+ m_raycasters.clear();
+ m_curr_feature = curr_feature;
+ if (!m_curr_feature.has_value())
+ return;
+
+ switch (m_curr_feature->get_type()) {
+ default: { assert(false); break; }
+ case Measure::SurfaceFeatureType::Point:
+ {
+ m_raycasters.insert({ POINT_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, POINT_ID, *m_sphere.mesh_raycaster) });
+ break;
+ }
+ case Measure::SurfaceFeatureType::Edge:
+ {
+ m_raycasters.insert({ EDGE_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, EDGE_ID, *m_cylinder.mesh_raycaster) });
+ break;
+ }
+ case Measure::SurfaceFeatureType::Circle:
+ {
+ update_circle();
+ m_raycasters.insert({ CIRCLE_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID, *m_circle.mesh_raycaster) });
+ break;
+ }
+ case Measure::SurfaceFeatureType::Plane:
+ {
+ const auto [idx, normal, point] = m_curr_feature->get_plane();
+ if (m_last_plane_idx != idx) {
+ m_last_plane_idx = idx;
+ const indexed_triangle_set& its = m_measuring->get_its();
+ const std::vector& plane_triangles = m_measuring->get_plane_triangle_indices(idx);
+ GLModel::Geometry init_data = init_plane_data(its, plane_triangles);
+ m_plane.reset();
+ m_plane.mesh_raycaster = std::make_unique(std::make_shared(init_data.get_as_indexed_triangle_set()));
+ }
+
+ m_raycasters.insert({ PLANE_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, PLANE_ID, *m_plane.mesh_raycaster) });
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (m_mode != EMode::PointSelection)
+ m_curr_point_on_feature_position.reset();
+ else if (is_hovering_on_feature) {
+ auto position_on_feature = [this](int feature_type_id, const Camera& camera, std::function callback = nullptr) -> Vec3d {
+ auto it = m_raycasters.find(feature_type_id);
+ if (it != m_raycasters.end() && it->second != nullptr) {
+ Vec3f p;
+ Vec3f n;
+ const Transform3d& trafo = it->second->get_transform();
+ bool res = it->second->get_raycaster()->closest_hit(m_mouse_pos, trafo, camera, p, n);
+ if (res) {
+ if (callback)
+ p = callback(p);
+ return trafo * p.cast();
+ }
+ }
+ return Vec3d(DBL_MAX, DBL_MAX, DBL_MAX);
+ };
+
+ if (m_curr_feature.has_value()) {
+ switch (m_curr_feature->get_type())
+ {
+ default: { assert(false); break; }
+ case Measure::SurfaceFeatureType::Point:
+ {
+ m_curr_point_on_feature_position = m_curr_feature->get_point();
+ break;
+ }
+ case Measure::SurfaceFeatureType::Edge:
+ {
+ const std::optional extra = m_curr_feature->get_extra_point();
+ if (extra.has_value() && m_hover_id == POINT_ID)
+ m_curr_point_on_feature_position = *extra;
+ else {
+ const Vec3d pos = position_on_feature(EDGE_ID, camera, [](const Vec3f& v) { return Vec3f(0.0f, 0.0f, v.z()); });
+ if (!pos.isApprox(Vec3d(DBL_MAX, DBL_MAX, DBL_MAX)))
+ m_curr_point_on_feature_position = pos;
+ }
+ break;
+ }
+ case Measure::SurfaceFeatureType::Plane:
+ {
+ m_curr_point_on_feature_position = position_on_feature(PLANE_ID, camera);
+ break;
+ }
+ case Measure::SurfaceFeatureType::Circle:
+ {
+ const auto [center, radius, normal] = m_curr_feature->get_circle();
+ if (m_hover_id == POINT_ID)
+ m_curr_point_on_feature_position = center;
+ else {
+ const Vec3d world_pof = position_on_feature(CIRCLE_ID, camera, [](const Vec3f& v) { return v; });
+ const Eigen::Hyperplane plane(normal, center);
+ const Transform3d local_to_model_matrix = Geometry::translation_transform(center) * Eigen::Quaternion::FromTwoVectors(Vec3d::UnitZ(), normal);
+ const Vec3d local_proj = local_to_model_matrix.inverse() * plane.projection(world_pof);
+ double angle = std::atan2(local_proj.y(), local_proj.x());
+ if (angle < 0.0)
+ angle += 2.0 * double(M_PI);
+
+ const Vec3d local_pos = radius * Vec3d(std::cos(angle), std::sin(angle), 0.0);
+ m_curr_point_on_feature_position = local_to_model_matrix * local_pos;
+ }
+ break;
+ }
+ }
+ }
+ }
+ else {
+ m_curr_point_on_feature_position.reset();
+ if (m_curr_feature.has_value() && m_curr_feature->get_type() == Measure::SurfaceFeatureType::Circle) {
+ if (update_circle()) {
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID);
+ auto it = m_raycasters.find(CIRCLE_ID);
+ if (it != m_raycasters.end())
+ m_raycasters.erase(it);
+ m_raycasters.insert({ CIRCLE_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID, *m_circle.mesh_raycaster) });
+ }
+ }
+ }
+
+ if (!m_curr_feature.has_value() && !m_selected_features.first.feature.has_value())
+ return;
+
+ GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
+ if (shader == nullptr)
+ return;
+
+ shader->start_using();
+ shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+
+ glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
+ glsafe(::glEnable(GL_DEPTH_TEST));
+ const bool old_cullface = ::glIsEnabled(GL_CULL_FACE);
+ glsafe(::glDisable(GL_CULL_FACE));
+
+ const Transform3d& view_matrix = camera.get_view_matrix();
+
+ auto set_matrix_uniforms = [shader, &view_matrix](const Transform3d& model_matrix) {
+ const Transform3d view_model_matrix = view_matrix * model_matrix;
+ shader->set_uniform("view_model_matrix", view_model_matrix);
+ const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
+ shader->set_uniform("view_normal_matrix", view_normal_matrix);
+ };
+
+ auto set_emission_uniform = [shader](const ColorRGBA& color, bool hover) {
+ shader->set_uniform("emission_factor", /*(color == GLVolume::SELECTED_COLOR) ? 0.0f :*/
+ hover ? 0.5f : 0.25f);
+ };
+
+ auto render_feature = [this, set_matrix_uniforms, set_emission_uniform](const Measure::SurfaceFeature& feature, const std::vector& colors,
+ float inv_zoom, bool hover, bool update_raycasters_transform) {
+ switch (feature.get_type())
+ {
+ default: { assert(false); break; }
+ case Measure::SurfaceFeatureType::Point:
+ {
+ const Transform3d feature_matrix = Geometry::translation_transform(feature.get_point()) * Geometry::scale_transform(inv_zoom);
+ set_matrix_uniforms(feature_matrix);
+ set_emission_uniform(colors.front(), hover);
+ m_sphere.model.set_color(colors.front());
+ m_sphere.model.render();
+ if (update_raycasters_transform) {
+ auto it = m_raycasters.find(POINT_ID);
+ if (it != m_raycasters.end() && it->second != nullptr)
+ it->second->set_transform(feature_matrix);
+ }
+ break;
+ }
+ case Measure::SurfaceFeatureType::Circle:
+ {
+ const auto& [center, radius, normal] = feature.get_circle();
+ // render circle
+ const Transform3d circle_matrix = Transform3d::Identity();
+ set_matrix_uniforms(circle_matrix);
+ if (update_raycasters_transform) {
+ set_emission_uniform(colors.front(), hover);
+ m_circle.model.set_color(colors.front());
+ m_circle.model.render();
+ auto it = m_raycasters.find(CIRCLE_ID);
+ if (it != m_raycasters.end() && it->second != nullptr)
+ it->second->set_transform(circle_matrix);
+ }
+ else {
+ GLModel circle;
+ GLModel::Geometry circle_geometry = init_torus_data(64, 16, center.cast(), float(radius), 5.0f * inv_zoom, normal.cast(), Transform3f::Identity());
+ circle.init_from(std::move(circle_geometry));
+ set_emission_uniform(colors.front(), hover);
+ circle.set_color(colors.front());
+ circle.render();
+ }
+ // render center
+ if (colors.size() > 1) {
+ const Transform3d center_matrix = Geometry::translation_transform(center) * Geometry::scale_transform(inv_zoom);
+ set_matrix_uniforms(center_matrix);
+ set_emission_uniform(colors.back(), hover);
+ m_sphere.model.set_color(colors.back());
+ m_sphere.model.render();
+ auto it = m_raycasters.find(POINT_ID);
+ if (it != m_raycasters.end() && it->second != nullptr)
+ it->second->set_transform(center_matrix);
+ }
+ break;
+ }
+ case Measure::SurfaceFeatureType::Edge:
+ {
+ const auto& [from, to] = feature.get_edge();
+ // render edge
+ const Transform3d edge_matrix = Geometry::translation_transform(from) *
+ Eigen::Quaternion::FromTwoVectors(Vec3d::UnitZ(), to - from) *
+ Geometry::scale_transform({ (double)inv_zoom, (double)inv_zoom, (to - from).norm() });
+ set_matrix_uniforms(edge_matrix);
+ set_emission_uniform(colors.front(), hover);
+ m_cylinder.model.set_color(colors.front());
+ m_cylinder.model.render();
+ if (update_raycasters_transform) {
+ auto it = m_raycasters.find(EDGE_ID);
+ if (it != m_raycasters.end() && it->second != nullptr)
+ it->second->set_transform(edge_matrix);
+ }
+
+ // render extra point
+ if (colors.size() > 1) {
+ const std::optional extra = feature.get_extra_point();
+ if (extra.has_value()) {
+ const Transform3d point_matrix = Geometry::translation_transform(*extra) * Geometry::scale_transform(inv_zoom);
+ set_matrix_uniforms(point_matrix);
+ set_emission_uniform(colors.back(), hover);
+ m_sphere.model.set_color(colors.back());
+ m_sphere.model.render();
+ auto it = m_raycasters.find(POINT_ID);
+ if (it != m_raycasters.end() && it->second != nullptr)
+ it->second->set_transform(point_matrix);
+ }
+ }
+ break;
+ }
+ case Measure::SurfaceFeatureType::Plane:
+ {
+ const auto& [idx, normal, pt] = feature.get_plane();
+ assert(idx < m_plane_models_cache.size());
+ set_matrix_uniforms(Transform3d::Identity());
+ set_emission_uniform(colors.front(), hover);
+ m_plane_models_cache[idx].set_color(colors.front());
+ m_plane_models_cache[idx].render();
+ if (update_raycasters_transform) {
+ auto it = m_raycasters.find(PLANE_ID);
+ if (it != m_raycasters.end() && it->second != nullptr)
+ it->second->set_transform(Transform3d::Identity());
+ }
+ break;
+ }
+ }
+ };
+
+ auto hover_selection_color = [this]() {
+ return ((m_mode == EMode::PointSelection && !m_selected_features.first.feature.has_value()) ||
+ (m_mode != EMode::PointSelection && (!m_selected_features.first.feature.has_value() || *m_curr_feature == *m_selected_features.first.feature))) ?
+ SELECTED_1ST_COLOR : SELECTED_2ND_COLOR;
+ };
+
+ auto hovering_color = [this, hover_selection_color]() {
+ return (m_mode == EMode::PointSelection) ? HOVER_COLOR : hover_selection_color();
+ };
+
+ if (m_curr_feature.has_value()) {
+ // render hovered feature
+
+ std::vector colors;
+ if (m_selected_features.first.feature.has_value() && *m_curr_feature == *m_selected_features.first.feature) {
+ // hovering over the 1st selected feature
+ if (m_selected_features.first.is_center)
+ // hovering over a center
+ colors = { NEUTRAL_COLOR, hovering_color() };
+ else if (is_feature_with_center(*m_selected_features.first.feature))
+ // hovering over a feature with center
+ colors = { hovering_color(), NEUTRAL_COLOR };
+ else
+ colors = { hovering_color() };
+ }
+ else if (m_selected_features.second.feature.has_value() && *m_curr_feature == *m_selected_features.second.feature) {
+ // hovering over the 2nd selected feature
+ if (m_selected_features.second.is_center)
+ // hovering over a center
+ colors = { NEUTRAL_COLOR, hovering_color() };
+ else if (is_feature_with_center(*m_selected_features.second.feature))
+ // hovering over a feature with center
+ colors = { hovering_color(), NEUTRAL_COLOR };
+ else
+ colors = { hovering_color() };
+ }
+ else {
+ switch (m_curr_feature->get_type())
+ {
+ default: { assert(false); break; }
+ case Measure::SurfaceFeatureType::Point:
+ {
+ colors.emplace_back(hover_selection_color());
+ break;
+ }
+ case Measure::SurfaceFeatureType::Edge:
+ case Measure::SurfaceFeatureType::Circle:
+ {
+ if (m_selected_features.first.is_center && m_curr_feature == m_selected_features.first.source)
+ colors = { SELECTED_1ST_COLOR, NEUTRAL_COLOR };
+ else if (m_selected_features.second.is_center && m_curr_feature == m_selected_features.second.source)
+ colors = { SELECTED_2ND_COLOR, NEUTRAL_COLOR };
+ else
+ colors = { hovering_color(), hovering_color() };
+ break;
+ }
+ case Measure::SurfaceFeatureType::Plane:
+ {
+ colors.emplace_back(hovering_color());
+ break;
+ }
+ }
+ }
+
+ render_feature(*m_curr_feature, colors, inv_zoom, true, true);
+ }
+
+ if (m_selected_features.first.feature.has_value() && (!m_curr_feature.has_value() || *m_curr_feature != *m_selected_features.first.feature)) {
+ // render 1st selected feature
+
+ std::optional feature_to_render;
+ std::vector colors;
+ bool requires_raycaster_update = false;
+ if (m_hover_id == SEL_SPHERE_1_ID && (m_selected_features.first.is_center || is_feature_with_center(*m_selected_features.first.feature))) {
+ // hovering over a center
+ feature_to_render = m_selected_features.first.source;
+ colors = { NEUTRAL_COLOR, SELECTED_1ST_COLOR };
+ requires_raycaster_update = true;
+ }
+ else if (is_feature_with_center(*m_selected_features.first.feature)) {
+ // hovering over a feature with center
+ feature_to_render = m_selected_features.first.feature;
+ colors = { SELECTED_1ST_COLOR, NEUTRAL_COLOR };
+ requires_raycaster_update = true;
+ }
+ else {
+ feature_to_render = m_selected_features.first.feature;
+ colors = { SELECTED_1ST_COLOR };
+ requires_raycaster_update = m_selected_features.first.feature->get_type() == Measure::SurfaceFeatureType::Point;
+ }
+
+ render_feature(*feature_to_render, colors, inv_zoom, m_hover_id == SEL_SPHERE_1_ID, false);
+
+ if (requires_raycaster_update) {
+ auto it = std::find_if(m_selected_sphere_raycasters.begin(), m_selected_sphere_raycasters.end(),
+ [](std::shared_ptr item) { return SceneRaycaster::decode_id(SceneRaycaster::EType::Gizmo, item->get_id()) == SEL_SPHERE_1_ID; });
+ if (it != m_selected_sphere_raycasters.end())
+ (*it)->set_transform(Geometry::translation_transform(get_feature_offset(*m_selected_features.first.feature)) * Geometry::scale_transform(inv_zoom));
+ }
+ }
+
+ if (m_selected_features.second.feature.has_value() && (!m_curr_feature.has_value() || *m_curr_feature != *m_selected_features.second.feature)) {
+ // render 2nd selected feature
+
+ std::optional feature_to_render;
+ std::vector colors;
+ bool requires_raycaster_update = false;
+ if (m_hover_id == SEL_SPHERE_2_ID && (m_selected_features.second.is_center || is_feature_with_center(*m_selected_features.second.feature))) {
+ // hovering over a center
+ feature_to_render = m_selected_features.second.source;
+ colors = { NEUTRAL_COLOR, SELECTED_2ND_COLOR };
+ requires_raycaster_update = true;
+ }
+ else if (is_feature_with_center(*m_selected_features.second.feature)) {
+ // hovering over a feature with center
+ feature_to_render = m_selected_features.second.feature;
+ colors = { SELECTED_2ND_COLOR, NEUTRAL_COLOR };
+ requires_raycaster_update = true;
+ }
+ else {
+ feature_to_render = m_selected_features.second.feature;
+ colors = { SELECTED_2ND_COLOR };
+ requires_raycaster_update = m_selected_features.second.feature->get_type() == Measure::SurfaceFeatureType::Point;
+ }
+
+ render_feature(*feature_to_render, colors, inv_zoom, m_hover_id == SEL_SPHERE_2_ID, false);
+
+ if (requires_raycaster_update) {
+ auto it = std::find_if(m_selected_sphere_raycasters.begin(), m_selected_sphere_raycasters.end(),
+ [](std::shared_ptr item) { return SceneRaycaster::decode_id(SceneRaycaster::EType::Gizmo, item->get_id()) == SEL_SPHERE_2_ID; });
+ if (it != m_selected_sphere_raycasters.end())
+ (*it)->set_transform(Geometry::translation_transform(get_feature_offset(*m_selected_features.second.feature)) * Geometry::scale_transform(inv_zoom));
+ }
+ }
+
+ if (is_hovering_on_feature && m_curr_point_on_feature_position.has_value()) {
+ if (m_hover_id != POINT_ID) {
+ // render point on feature while SHIFT is pressed
+ const Transform3d matrix = Geometry::translation_transform(*m_curr_point_on_feature_position) * Geometry::scale_transform(inv_zoom);
+ set_matrix_uniforms(matrix);
+ const ColorRGBA color = hover_selection_color();
+ set_emission_uniform(color, true);
+ m_sphere.model.set_color(color);
+ m_sphere.model.render();
+ }
+ }
+
+ shader->stop_using();
+
+ if (old_cullface)
+ glsafe(::glEnable(GL_CULL_FACE));
+
+ render_dimensioning();
+}
+
+void GLGizmoMeasure::update_if_needed()
+{
+ auto update_plane_models_cache = [this](const indexed_triangle_set& its) {
+ m_plane_models_cache.clear();
+ m_plane_models_cache.resize(m_measuring->get_num_of_planes(), GLModel());
+
+ auto& plane_models_cache = m_plane_models_cache;
+ const auto& measuring = m_measuring;
+
+ //for (int idx = 0; idx < m_measuring->get_num_of_planes(); ++idx) {
+ tbb::parallel_for(tbb::blocked_range(0, m_measuring->get_num_of_planes()),
+ [&plane_models_cache, &measuring, &its](const tbb::blocked_range& range) {
+ for (size_t idx = range.begin(); idx != range.end(); ++idx) {
+ GLModel::Geometry init_data = init_plane_data(its, measuring->get_plane_triangle_indices(idx));
+ plane_models_cache[idx].init_from(std::move(init_data));
+ }
+ });
+ };
+
+ auto do_update = [this, update_plane_models_cache](const std::vector& volumes_cache, const Selection& selection) {
+ TriangleMesh composite_mesh;
+ for (const auto& vol : volumes_cache) {
+// if (selection.is_single_full_instance() && vol.volume->is_modifier())
+// continue;
+
+ TriangleMesh volume_mesh = vol.volume->mesh();
+ volume_mesh.transform(vol.world_trafo);
+
+ if (vol.world_trafo.matrix().determinant() < 0.0)
+ volume_mesh.flip_triangles();
+
+ composite_mesh.merge(volume_mesh);
+ }
+
+ m_measuring.reset(new Measure::Measuring(composite_mesh.its));
+ update_plane_models_cache(m_measuring->get_its());
+ m_raycaster.reset(new MeshRaycaster(std::make_shared(composite_mesh)));
+ m_volumes_cache = volumes_cache;
+ };
+
+ const Selection& selection = m_parent.get_selection();
+ if (selection.is_empty())
+ return;
+
+ const Selection::IndicesList& idxs = selection.get_volume_idxs();
+ std::vector volumes_cache;
+ volumes_cache.reserve(idxs.size());
+ for (unsigned int idx : idxs) {
+ const GLVolume* v = selection.get_volume(idx);
+ const int volume_idx = v->volume_idx();
+ if (volume_idx < 0)
+ continue;
+
+ const ModelObject* obj = selection.get_model()->objects[v->object_idx()];
+ const ModelInstance* inst = obj->instances[v->instance_idx()];
+ const ModelVolume* vol = obj->volumes[volume_idx];
+ const VolumeCacheItem item = {
+ obj, inst, vol,
+ Geometry::translation_transform(selection.get_first_volume()->get_sla_shift_z() * Vec3d::UnitZ()) * inst->get_matrix() * vol->get_matrix()
+ };
+ volumes_cache.emplace_back(item);
+ }
+
+ if (m_state != On || volumes_cache.empty())
+ return;
+
+ if (m_measuring == nullptr || m_volumes_cache != volumes_cache)
+ do_update(volumes_cache, selection);
+}
+
+void GLGizmoMeasure::disable_scene_raycasters()
+{
+ for (auto r : m_scene_raycasters) {
+ r.raycaster->set_active(false);
+ }
+}
+
+void GLGizmoMeasure::restore_scene_raycasters_state()
+{
+ for (auto r : m_scene_raycasters) {
+ r.raycaster->set_active(r.state);
+ }
+}
+
+void GLGizmoMeasure::render_dimensioning()
+{
+ static SelectedFeatures last_selected_features;
+
+ if (!m_selected_features.first.feature.has_value())
+ return;
+
+ if (!m_selected_features.second.feature.has_value() && m_selected_features.first.feature->get_type() != Measure::SurfaceFeatureType::Circle)
+ return;
+
+ GLShaderProgram* shader = wxGetApp().get_shader("flat");
+ if (shader == nullptr)
+ return;
+
+ auto point_point = [this, &shader](const Vec3d& v1, const Vec3d& v2, float distance) {
+ if ((v2 - v1).squaredNorm() < 0.000001 || distance < 0.001f)
+ return;
+
+ const Camera& camera = wxGetApp().plater()->get_camera();
+ const Matrix4d projection_view_matrix = camera.get_projection_matrix().matrix() * camera.get_view_matrix().matrix();
+ const std::array& viewport = camera.get_viewport();
+
+ // screen coordinates
+ const Vec2d v1ss = TransformHelper::world_to_ss(v1, projection_view_matrix, viewport);
+ const Vec2d v2ss = TransformHelper::world_to_ss(v2, projection_view_matrix, viewport);
+
+ if (v1ss.isApprox(v2ss))
+ return;
+
+ const Vec2d v12ss = v2ss - v1ss;
+ const double v12ss_len = v12ss.norm();
+
+ const bool overlap = v12ss_len - 2.0 * TRIANGLE_HEIGHT < 0.0;
+
+ const auto q12ss = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Vec3d(v12ss.x(), v12ss.y(), 0.0));
+ const auto q21ss = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Vec3d(-v12ss.x(), -v12ss.y(), 0.0));
+
+ shader->set_uniform("projection_matrix", Transform3d::Identity());
+
+ const Vec3d v1ss_3 = { v1ss.x(), v1ss.y(), 0.0 };
+ const Vec3d v2ss_3 = { v2ss.x(), v2ss.y(), 0.0 };
+
+ const Transform3d ss_to_ndc_matrix = TransformHelper::ndc_to_ss_matrix_inverse(viewport);
+
+#if ENABLE_GL_CORE_PROFILE
+ if (OpenGLManager::get_gl_info().is_core_profile()) {
+ shader->stop_using();
+
+ shader = wxGetApp().get_shader("dashed_thick_lines");
+ if (shader == nullptr)
+ return;
+
+ shader->start_using();
+ shader->set_uniform("projection_matrix", Transform3d::Identity());
+ const std::array& viewport = camera.get_viewport();
+ shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3])));
+ shader->set_uniform("width", 1.0f);
+ shader->set_uniform("gap_size", 0.0f);
+ }
+ else
+#endif // ENABLE_GL_CORE_PROFILE
+ glsafe(::glLineWidth(2.0f));
+
+ // stem
+ shader->set_uniform("view_model_matrix", overlap ?
+ ss_to_ndc_matrix * Geometry::translation_transform(v1ss_3) * q12ss * Geometry::translation_transform(-2.0 * TRIANGLE_HEIGHT * Vec3d::UnitX()) * Geometry::scale_transform({ v12ss_len + 4.0 * TRIANGLE_HEIGHT, 1.0f, 1.0f }) :
+ ss_to_ndc_matrix * Geometry::translation_transform(v1ss_3) * q12ss * Geometry::scale_transform({ v12ss_len, 1.0f, 1.0f }));
+ m_dimensioning.line.set_color(ColorRGBA::WHITE());
+ m_dimensioning.line.render();
+
+#if ENABLE_GL_CORE_PROFILE
+ if (OpenGLManager::get_gl_info().is_core_profile()) {
+ shader->stop_using();
+
+ shader = wxGetApp().get_shader("flat");
+ if (shader == nullptr)
+ return;
+
+ shader->start_using();
+ }
+ else
+#endif // ENABLE_GL_CORE_PROFILE
+ glsafe(::glLineWidth(1.0f));
+
+ // arrow 1
+ shader->set_uniform("view_model_matrix", overlap ?
+ ss_to_ndc_matrix * Geometry::translation_transform(v1ss_3) * q12ss :
+ ss_to_ndc_matrix * Geometry::translation_transform(v1ss_3) * q21ss);
+ m_dimensioning.triangle.render();
+
+ // arrow 2
+ shader->set_uniform("view_model_matrix", overlap ?
+ ss_to_ndc_matrix * Geometry::translation_transform(v2ss_3) * q21ss :
+ ss_to_ndc_matrix * Geometry::translation_transform(v2ss_3) * q12ss);
+ m_dimensioning.triangle.render();
+
+ const bool use_inches = wxGetApp().app_config->get_bool("use_inches");
+ const double curr_value = use_inches ? GizmoObjectManipulation::mm_to_in * distance : distance;
+ const std::string curr_value_str = format_double(curr_value);
+ const std::string units = use_inches ? _u8L("in") : _u8L("mm");
+ const float value_str_width = 20.0f + ImGui::CalcTextSize(curr_value_str.c_str()).x;
+ static double edit_value = 0.0;
+
+ const Vec2d label_position = 0.5 * (v1ss + v2ss);
+ m_imgui->set_next_window_pos(label_position.x(), viewport[3] - label_position.y(), ImGuiCond_Always, 0.0f, 1.0f);
+ m_imgui->set_next_window_bg_alpha(0.0f);
+
+ if (!m_editing_distance) {
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 1.0f, 1.0f });
+ m_imgui->begin(std::string("distance"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration);
+ ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
+ ImGui::AlignTextToFramePadding();
+ ImDrawList* draw_list = ImGui::GetWindowDrawList();
+ const ImVec2 pos = ImGui::GetCursorScreenPos();
+ const std::string txt = curr_value_str + " " + units;
+ ImVec2 txt_size = ImGui::CalcTextSize(txt.c_str());
+ const ImGuiStyle& style = ImGui::GetStyle();
+ draw_list->AddRectFilled({ pos.x - style.FramePadding.x, pos.y + style.FramePadding.y }, { pos.x + txt_size.x + 2.0f * style.FramePadding.x , pos.y + txt_size.y + 2.0f * style.FramePadding.y },
+ ImGuiWrapper::to_ImU32(ColorRGBA(0.5f, 0.5f, 0.5f, 0.5f)));
+ ImGui::SetCursorScreenPos({ pos.x + style.FramePadding.x, pos.y });
+ m_imgui->text(txt);
+ ImGui::SameLine();
+ if (m_imgui->image_button(ImGui::SliderFloatEditBtnIcon, _L("Edit to scale"))) {
+ m_editing_distance = true;
+ edit_value = curr_value;
+ m_imgui->requires_extra_frame();
+ }
+ m_imgui->end();
+ ImGui::PopStyleVar(3);
+ }
+
+ if (m_editing_distance && !ImGui::IsPopupOpen("distance_popup"))
+ ImGui::OpenPopup("distance_popup");
+
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 1.0f, 1.0f });
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 4.0f, 0.0f });
+ if (ImGui::BeginPopupModal("distance_popup", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration)) {
+ auto perform_scale = [this](double new_value, double old_value) {
+ if (new_value == old_value || new_value <= 0.0)
+ return;
+
+ const double ratio = new_value / old_value;
+ wxGetApp().plater()->take_snapshot(_u8L("Scale"));
+
+ struct TrafoData
+ {
+ double ratio;
+ Vec3d old_pivot;
+ Vec3d new_pivot;
+ Transform3d scale_matrix;
+
+ TrafoData(double ratio, const Vec3d& old_pivot, const Vec3d& new_pivot) {
+ this->ratio = ratio;
+ this->scale_matrix = Geometry::scale_transform(ratio);
+ this->old_pivot = old_pivot;
+ this->new_pivot = new_pivot;
+ }
+
+ Vec3d transform(const Vec3d& point) const { return this->scale_matrix * (point - this->old_pivot) + this->new_pivot; }
+ };
+
+ auto scale_feature = [](Measure::SurfaceFeature& feature, const TrafoData& trafo_data) {
+ switch (feature.get_type())
+ {
+ case Measure::SurfaceFeatureType::Point:
+ {
+ feature = Measure::SurfaceFeature(trafo_data.transform(feature.get_point()));
+ break;
+ }
+ case Measure::SurfaceFeatureType::Edge:
+ {
+ const auto [from, to] = feature.get_edge();
+ const std::optional extra = feature.get_extra_point();
+ const std::optional new_extra = extra.has_value() ? trafo_data.transform(*extra) : extra;
+ feature = Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, trafo_data.transform(from), trafo_data.transform(to), new_extra);
+ break;
+ }
+ case Measure::SurfaceFeatureType::Circle:
+ {
+ const auto [center, radius, normal] = feature.get_circle();
+ feature = Measure::SurfaceFeature(Measure::SurfaceFeatureType::Circle, trafo_data.transform(center), normal, std::nullopt, trafo_data.ratio * radius);
+ break;
+ }
+ case Measure::SurfaceFeatureType::Plane:
+ {
+ const auto [idx, normal, origin] = feature.get_plane();
+ feature = Measure::SurfaceFeature(Measure::SurfaceFeatureType::Plane, normal, trafo_data.transform(origin), std::nullopt, idx);
+ break;
+ }
+ default: { break; }
+ }
+ };
+
+ // apply scale
+ TransformationType type;
+ type.set_world();
+ type.set_relative();
+ type.set_joint();
+
+ // scale selection
+ Selection& selection = m_parent.get_selection();
+ const Vec3d old_center = selection.get_bounding_box().center();
+ selection.setup_cache();
+ selection.scale(ratio * Vec3d::Ones(), type);
+ wxGetApp().plater()->canvas3D()->do_scale(""); // avoid storing another snapshot
+ wxGetApp().obj_manipul()->set_dirty();
+
+ // scale dimensioning
+ const Vec3d new_center = selection.get_bounding_box().center();
+ const TrafoData trafo_data(ratio, old_center, new_center);
+ scale_feature(*m_selected_features.first.feature, trafo_data);
+ if (m_selected_features.second.feature.has_value())
+ scale_feature(*m_selected_features.second.feature, trafo_data);
+
+ // update measure on next call to data_changed()
+ m_pending_scale = true;
+ };
+ auto action_exit = [this]() {
+ m_editing_distance = false;
+ m_is_editing_distance_first_frame = true;
+ ImGui::CloseCurrentPopup();
+ };
+ auto action_scale = [perform_scale, action_exit](double new_value, double old_value) {
+ perform_scale(new_value, old_value);
+ action_exit();
+ };
+
+ m_imgui->disable_background_fadeout_animation();
+ ImGui::PushItemWidth(value_str_width);
+ if (ImGui::InputDouble("##distance", &edit_value, 0.0f, 0.0f, "%.3f")) {
+ }
+
+ // trick to auto-select text in the input widgets on 1st frame
+ if (m_is_editing_distance_first_frame) {
+ ImGui::SetKeyboardFocusHere(0);
+ m_is_editing_distance_first_frame = false;
+ m_imgui->set_requires_extra_frame();
+ }
+
+ // handle keys input
+ if (ImGui::IsKeyPressedMap(ImGuiKey_Enter) || ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter))
+ action_scale(edit_value, curr_value);
+ else if (ImGui::IsKeyPressedMap(ImGuiKey_Escape))
+ action_exit();
+
+ ImGui::SameLine();
+ if (m_imgui->button(_CTX(L_CONTEXT("Scale", "Verb"), "Verb")))
+ action_scale(edit_value, curr_value);
+ ImGui::SameLine();
+ if (m_imgui->button(_L("Cancel")))
+ action_exit();
+ ImGui::EndPopup();
+ }
+ ImGui::PopStyleVar(4);
+ };
+
+ auto point_edge = [this, shader](const Measure::SurfaceFeature& f1, const Measure::SurfaceFeature& f2) {
+ assert(f1.get_type() == Measure::SurfaceFeatureType::Point && f2.get_type() == Measure::SurfaceFeatureType::Edge);
+ std::pair e = f2.get_edge();
+ const Vec3d v_proj = m_measurement_result.distance_infinite->to;
+ const Vec3d e1e2 = e.second - e.first;
+ const Vec3d v_proje1 = v_proj - e.first;
+ const bool on_e1_side = v_proje1.dot(e1e2) < -EPSILON;
+ const bool on_e2_side = !on_e1_side && v_proje1.norm() > e1e2.norm();
+ if (on_e1_side || on_e2_side) {
+ const Camera& camera = wxGetApp().plater()->get_camera();
+ const Matrix4d projection_view_matrix = camera.get_projection_matrix().matrix() * camera.get_view_matrix().matrix();
+ const std::array& viewport = camera.get_viewport();
+ const Transform3d ss_to_ndc_matrix = TransformHelper::ndc_to_ss_matrix_inverse(viewport);
+
+ const Vec2d v_projss = TransformHelper::world_to_ss(v_proj, projection_view_matrix, viewport);
+ auto render_extension = [this, &v_projss, &projection_view_matrix, &viewport, &ss_to_ndc_matrix, shader](const Vec3d& p) {
+ const Vec2d pss = TransformHelper::world_to_ss(p, projection_view_matrix, viewport);
+ if (!pss.isApprox(v_projss)) {
+ const Vec2d pv_projss = v_projss - pss;
+ const double pv_projss_len = pv_projss.norm();
+
+ const auto q = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Vec3d(pv_projss.x(), pv_projss.y(), 0.0));
+
+ shader->set_uniform("projection_matrix", Transform3d::Identity());
+ shader->set_uniform("view_model_matrix", ss_to_ndc_matrix * Geometry::translation_transform({ pss.x(), pss.y(), 0.0 }) * q *
+ Geometry::scale_transform({ pv_projss_len, 1.0f, 1.0f }));
+ m_dimensioning.line.set_color(ColorRGBA::LIGHT_GRAY());
+ m_dimensioning.line.render();
+ }
+ };
+
+ render_extension(on_e1_side ? e.first : e.second);
+ }
+ };
+
+ auto arc_edge_edge = [this, &shader](const Measure::SurfaceFeature& f1, const Measure::SurfaceFeature& f2, double radius = 0.0) {
+ assert(f1.get_type() == Measure::SurfaceFeatureType::Edge && f2.get_type() == Measure::SurfaceFeatureType::Edge);
+ if (!m_measurement_result.angle.has_value())
+ return;
+
+ const double angle = m_measurement_result.angle->angle;
+ const Vec3d center = m_measurement_result.angle->center;
+ const std::pair e1 = m_measurement_result.angle->e1;
+ const std::pair e2 = m_measurement_result.angle->e2;
+ const double calc_radius = m_measurement_result.angle->radius;
+ const bool coplanar = m_measurement_result.angle->coplanar;
+
+ if (std::abs(angle) < EPSILON || std::abs(calc_radius) < EPSILON)
+ return;
+
+ const double draw_radius = (radius > 0.0) ? radius : calc_radius;
+
+ const Vec3d e1_unit = Measure::edge_direction(e1);
+ const Vec3d e2_unit = Measure::edge_direction(e2);
+
+ const unsigned int resolution = std::max(2, 64 * angle / double(PI));
+ const double step = angle / double(resolution);
+ const Vec3d normal = e1_unit.cross(e2_unit).normalized();
+
+ if (!m_dimensioning.arc.is_initialized()) {
+ GLModel::Geometry init_data;
+ init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 };
+ init_data.color = ColorRGBA::WHITE();
+ init_data.reserve_vertices(resolution + 1);
+ init_data.reserve_indices(resolution + 1);
+
+ // vertices + indices
+ for (unsigned int i = 0; i <= resolution; ++i) {
+ const double a = step * double(i);
+ const Vec3d v = draw_radius * (Eigen::Quaternion(Eigen::AngleAxisd(a, normal)) * e1_unit);
+ init_data.add_vertex((Vec3f)v.cast());
+ init_data.add_index(i);
+ }
+
+ m_dimensioning.arc.init_from(std::move(init_data));
+ }
+
+ const Camera& camera = wxGetApp().plater()->get_camera();
+#if ENABLE_GL_CORE_PROFILE
+ if (OpenGLManager::get_gl_info().is_core_profile()) {
+ shader->stop_using();
+
+ shader = wxGetApp().get_shader("dashed_thick_lines");
+ if (shader == nullptr)
+ return;
+
+ shader->start_using();
+ shader->set_uniform("projection_matrix", Transform3d::Identity());
+ const std::array& viewport = camera.get_viewport();
+ shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3])));
+ shader->set_uniform("width", 1.0f);
+ shader->set_uniform("gap_size", 0.0f);
+ }
+ else
+#endif // ENABLE_GL_CORE_PROFILE
+ glsafe(::glLineWidth(2.0f));
+
+ // arc
+ shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+ shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::translation_transform(center));
+ m_dimensioning.arc.render();
+
+#if ENABLE_GL_CORE_PROFILE
+ if (OpenGLManager::get_gl_info().is_core_profile()) {
+ shader->stop_using();
+
+ shader = wxGetApp().get_shader("flat");
+ if (shader == nullptr)
+ return;
+
+ shader->start_using();
+ }
+ else
+#endif // ENABLE_GL_CORE_PROFILE
+ glsafe(::glLineWidth(1.0f));
+
+ // arrows
+ auto render_arrow = [this, shader, &camera, &normal, ¢er, &e1_unit, draw_radius, step, resolution](unsigned int endpoint_id) {
+ const double angle = (endpoint_id == 1) ? 0.0 : step * double(resolution);
+ const Vec3d position_model = Geometry::translation_transform(center) * (draw_radius * (Eigen::Quaternion(Eigen::AngleAxisd(angle, normal)) * e1_unit));
+ const Vec3d direction_model = (endpoint_id == 1) ? -normal.cross(position_model - center).normalized() : normal.cross(position_model - center).normalized();
+ const auto qz = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitZ(), (endpoint_id == 1) ? normal : -normal);
+ const auto qx = Eigen::Quaternion::FromTwoVectors(qz * Vec3d::UnitX(), direction_model);
+ const Transform3d view_model_matrix = camera.get_view_matrix() * Geometry::translation_transform(position_model) *
+ qx * qz * Geometry::scale_transform(camera.get_inv_zoom());
+ shader->set_uniform("view_model_matrix", view_model_matrix);
+ m_dimensioning.triangle.render();
+ };
+
+ glsafe(::glDisable(GL_CULL_FACE));
+ render_arrow(1);
+ render_arrow(2);
+ glsafe(::glEnable(GL_CULL_FACE));
+
+ // edge 1 extension
+ const Vec3d e11e12 = e1.second - e1.first;
+ const Vec3d e11center = center - e1.first;
+ const double e11center_len = e11center.norm();
+ if (e11center_len > EPSILON && e11center.dot(e11e12) < 0.0) {
+ shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::translation_transform(center) *
+ Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Measure::edge_direction(e1.first, e1.second)) *
+ Geometry::scale_transform({ e11center_len, 1.0f, 1.0f }));
+ m_dimensioning.line.set_color(ColorRGBA::LIGHT_GRAY());
+ m_dimensioning.line.render();
+ }
+
+ // edge 2 extension
+ const Vec3d e21center = center - e2.first;
+ const double e21center_len = e21center.norm();
+ if (e21center_len > EPSILON) {
+ shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::translation_transform(center) *
+ Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Measure::edge_direction(e2.first, e2.second)) *
+ Geometry::scale_transform({ (coplanar && radius > 0.0) ? e21center_len : draw_radius, 1.0f, 1.0f }));
+ m_dimensioning.line.set_color(ColorRGBA::LIGHT_GRAY());
+ m_dimensioning.line.render();
+ }
+
+ // label
+ // label world coordinates
+ const Vec3d label_position_world = Geometry::translation_transform(center) * (draw_radius * (Eigen::Quaternion(Eigen::AngleAxisd(step * 0.5 * double(resolution), normal)) * e1_unit));
+
+ // label screen coordinates
+ const std::array& viewport = camera.get_viewport();
+ const Vec2d label_position_ss = TransformHelper::world_to_ss(label_position_world,
+ camera.get_projection_matrix().matrix() * camera.get_view_matrix().matrix(), viewport);
+
+ m_imgui->set_next_window_pos(label_position_ss.x(), viewport[3] - label_position_ss.y(), ImGuiCond_Always, 0.0f, 1.0f);
+ m_imgui->set_next_window_bg_alpha(0.0f);
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
+ m_imgui->begin(wxString("##angle"), ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove);
+ ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
+ ImGui::AlignTextToFramePadding();
+ ImDrawList* draw_list = ImGui::GetWindowDrawList();
+ const ImVec2 pos = ImGui::GetCursorScreenPos();
+ const std::string txt = format_double(Geometry::rad2deg(angle)) + "°";
+ ImVec2 txt_size = ImGui::CalcTextSize(txt.c_str());
+ const ImGuiStyle& style = ImGui::GetStyle();
+ draw_list->AddRectFilled({ pos.x - style.FramePadding.x, pos.y + style.FramePadding.y }, { pos.x + txt_size.x + 2.0f * style.FramePadding.x , pos.y + txt_size.y + 2.0f * style.FramePadding.y },
+ ImGuiWrapper::to_ImU32(ColorRGBA(0.5f, 0.5f, 0.5f, 0.5f)));
+ ImGui::SetCursorScreenPos({ pos.x + style.FramePadding.x, pos.y });
+ m_imgui->text(txt);
+ m_imgui->end();
+ ImGui::PopStyleVar();
+ };
+
+ auto arc_edge_plane = [this, arc_edge_edge](const Measure::SurfaceFeature& f1, const Measure::SurfaceFeature& f2) {
+ assert(f1.get_type() == Measure::SurfaceFeatureType::Edge && f2.get_type() == Measure::SurfaceFeatureType::Plane);
+ if (!m_measurement_result.angle.has_value())
+ return;
+
+ const std::pair e1 = m_measurement_result.angle->e1;
+ const std::pair e2 = m_measurement_result.angle->e2;
+ const double calc_radius = m_measurement_result.angle->radius;
+
+ if (calc_radius == 0.0)
+ return;
+
+ arc_edge_edge(Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, e1.first, e1.second),
+ Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, e2.first, e2.second), calc_radius);
+ };
+
+ auto arc_plane_plane = [this, arc_edge_edge](const Measure::SurfaceFeature& f1, const Measure::SurfaceFeature& f2) {
+ assert(f1.get_type() == Measure::SurfaceFeatureType::Plane && f2.get_type() == Measure::SurfaceFeatureType::Plane);
+ if (!m_measurement_result.angle.has_value())
+ return;
+
+ const std::pair e1 = m_measurement_result.angle->e1;
+ const std::pair e2 = m_measurement_result.angle->e2;
+ const double calc_radius = m_measurement_result.angle->radius;
+
+ if (calc_radius == 0.0)
+ return;
+
+ arc_edge_edge(Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, e1.first, e1.second),
+ Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, e2.first, e2.second), calc_radius);
+ };
+
+ shader->start_using();
+
+ if (!m_dimensioning.line.is_initialized()) {
+ GLModel::Geometry init_data;
+ init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 };
+ init_data.color = ColorRGBA::WHITE();
+ init_data.reserve_vertices(2);
+ init_data.reserve_indices(2);
+
+ // vertices
+ init_data.add_vertex(Vec3f(0.0f, 0.0f, 0.0f));
+ init_data.add_vertex(Vec3f(1.0f, 0.0f, 0.0f));
+
+ // indices
+ init_data.add_line(0, 1);
+
+ m_dimensioning.line.init_from(std::move(init_data));
+ }
+
+ if (!m_dimensioning.triangle.is_initialized()) {
+ GLModel::Geometry init_data;
+ init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
+ init_data.color = ColorRGBA::WHITE();
+ init_data.reserve_vertices(3);
+ init_data.reserve_indices(3);
+
+ // vertices
+ init_data.add_vertex(Vec3f(0.0f, 0.0f, 0.0f));
+ init_data.add_vertex(Vec3f(-TRIANGLE_HEIGHT, 0.5f * TRIANGLE_BASE, 0.0f));
+ init_data.add_vertex(Vec3f(-TRIANGLE_HEIGHT, -0.5f * TRIANGLE_BASE, 0.0f));
+
+ // indices
+ init_data.add_triangle(0, 1, 2);
+
+ m_dimensioning.triangle.init_from(std::move(init_data));
+ }
+
+ if (last_selected_features != m_selected_features)
+ m_dimensioning.arc.reset();
+
+ glsafe(::glDisable(GL_DEPTH_TEST));
+
+ const bool has_distance = m_measurement_result.has_distance_data();
+
+ const Measure::SurfaceFeature* f1 = &(*m_selected_features.first.feature);
+ const Measure::SurfaceFeature* f2 = nullptr;
+ std::unique_ptr temp_feature;
+ if (m_selected_features.second.feature.has_value())
+ f2 = &(*m_selected_features.second.feature);
+ else {
+ assert(m_selected_features.first.feature->get_type() == Measure::SurfaceFeatureType::Circle);
+ temp_feature = std::make_unique(std::get<0>(m_selected_features.first.feature->get_circle()));
+ f2 = temp_feature.get();
+ }
+
+ if (!m_selected_features.second.feature.has_value() && m_selected_features.first.feature->get_type() != Measure::SurfaceFeatureType::Circle)
+ return;
+
+ Measure::SurfaceFeatureType ft1 = f1->get_type();
+ Measure::SurfaceFeatureType ft2 = f2->get_type();
+
+ // Order features by type so following conditions are simple.
+ if (ft1 > ft2) {
+ std::swap(ft1, ft2);
+ std::swap(f1, f2);
+ }
+
+ // If there is an angle to show, draw the arc:
+ if (ft1 == Measure::SurfaceFeatureType::Edge && ft2 == Measure::SurfaceFeatureType::Edge)
+ arc_edge_edge(*f1, *f2);
+ else if (ft1 == Measure::SurfaceFeatureType::Edge && ft2 == Measure::SurfaceFeatureType::Plane)
+ arc_edge_plane(*f1, *f2);
+ else if (ft1 == Measure::SurfaceFeatureType::Plane && ft2 == Measure::SurfaceFeatureType::Plane)
+ arc_plane_plane(*f1, *f2);
+
+ if (has_distance){
+ // Where needed, draw the extension of the edge to where the dist is measured:
+ if (ft1 == Measure::SurfaceFeatureType::Point && ft2 == Measure::SurfaceFeatureType::Edge)
+ point_edge(*f1, *f2);
+
+ // Render the arrow between the points that the backend passed:
+ const Measure::DistAndPoints& dap = m_measurement_result.distance_infinite.has_value()
+ ? *m_measurement_result.distance_infinite
+ : *m_measurement_result.distance_strict;
+ point_point(dap.from, dap.to, dap.dist);
+ }
+
+ glsafe(::glEnable(GL_DEPTH_TEST));
+
+ shader->stop_using();
+}
+
+static void add_row_to_table(std::function col_1 = nullptr, std::function col_2 = nullptr)
+{
+ assert(col_1 != nullptr && col_2 != nullptr);
+ ImGui::TableNextRow();
+ ImGui::TableSetColumnIndex(0);
+ col_1();
+ ImGui::TableSetColumnIndex(1);
+ col_2();
+}
+
+static void add_strings_row_to_table(ImGuiWrapper& imgui, const std::string& col_1, const ImVec4& col_1_color, const std::string& col_2, const ImVec4& col_2_color)
+{
+ add_row_to_table([&]() { imgui.text_colored(col_1_color, col_1); }, [&]() { imgui.text_colored(col_2_color, col_2); });
+};
+
+#if ENABLE_MEASURE_GIZMO_DEBUG
+void GLGizmoMeasure::render_debug_dialog()
+{
+ auto add_feature_data = [this](const SelectedFeatures::Item& item) {
+ const std::string text = (item.source == item.feature) ? surface_feature_type_as_string(item.feature->get_type()) : point_on_feature_type_as_string(item.source->get_type(), m_hover_id);
+ add_strings_row_to_table(*m_imgui, "Type", ImGuiWrapper::COL_ORANGE_LIGHT, text, ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ switch (item.feature->get_type())
+ {
+ case Measure::SurfaceFeatureType::Point:
+ {
+ add_strings_row_to_table(*m_imgui, "m_pt1", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(item.feature->get_point()), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ break;
+ }
+ case Measure::SurfaceFeatureType::Edge:
+ {
+ auto [from, to] = item.feature->get_edge();
+ add_strings_row_to_table(*m_imgui, "m_pt1", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(from), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ add_strings_row_to_table(*m_imgui, "m_pt2", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(to), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ break;
+ }
+ case Measure::SurfaceFeatureType::Plane:
+ {
+ auto [idx, normal, origin] = item.feature->get_plane();
+ add_strings_row_to_table(*m_imgui, "m_pt1", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(normal), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ add_strings_row_to_table(*m_imgui, "m_pt2", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(origin), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ add_strings_row_to_table(*m_imgui, "m_value", ImGuiWrapper::COL_ORANGE_LIGHT, format_double(idx), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ break;
+ }
+ case Measure::SurfaceFeatureType::Circle:
+ {
+ auto [center, radius, normal] = item.feature->get_circle();
+ const Vec3d on_circle = center + radius * Measure::get_orthogonal(normal, true);
+ radius = (on_circle - center).norm();
+ add_strings_row_to_table(*m_imgui, "m_pt1", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(center), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ add_strings_row_to_table(*m_imgui, "m_pt2", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(normal), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ add_strings_row_to_table(*m_imgui, "m_value", ImGuiWrapper::COL_ORANGE_LIGHT, format_double(radius), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ break;
+ }
+ }
+ std::optional extra_point = item.feature->get_extra_point();
+ if (extra_point.has_value())
+ add_strings_row_to_table(*m_imgui, "m_pt3", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(*extra_point), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ };
+
+ m_imgui->begin("Measure tool debug", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
+ if (ImGui::BeginTable("Mode", 2)) {
+ std::string txt;
+ switch (m_mode)
+ {
+ case EMode::FeatureSelection: { txt = "Feature selection"; break; }
+ case EMode::PointSelection: { txt = "Point selection"; break; }
+ default: { assert(false); break; }
+ }
+ add_strings_row_to_table(*m_imgui, "Mode", ImGuiWrapper::COL_ORANGE_LIGHT, txt, ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ImGui::EndTable();
+ }
+
+ ImGui::Separator();
+ if (ImGui::BeginTable("Hover", 2)) {
+ add_strings_row_to_table(*m_imgui, "Hover id", ImGuiWrapper::COL_ORANGE_LIGHT, std::to_string(m_hover_id), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ const std::string txt = m_curr_feature.has_value() ? surface_feature_type_as_string(m_curr_feature->get_type()) : "None";
+ add_strings_row_to_table(*m_imgui, "Current feature", ImGuiWrapper::COL_ORANGE_LIGHT, txt, ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ImGui::EndTable();
+ }
+
+ ImGui::Separator();
+ if (!m_selected_features.first.feature.has_value() && !m_selected_features.second.feature.has_value())
+ m_imgui->text("Empty selection");
+ else {
+ const ImGuiTableFlags flags = ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersH;
+ if (m_selected_features.first.feature.has_value()) {
+ m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Selection 1");
+ if (ImGui::BeginTable("Selection 1", 2, flags)) {
+ add_feature_data(m_selected_features.first);
+ ImGui::EndTable();
+ }
+ }
+ if (m_selected_features.second.feature.has_value()) {
+ m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Selection 2");
+ if (ImGui::BeginTable("Selection 2", 2, flags)) {
+ add_feature_data(m_selected_features.second);
+ ImGui::EndTable();
+ }
+ }
+ }
+ m_imgui->end();
+}
+#endif // ENABLE_MEASURE_GIZMO_DEBUG
+
+void GLGizmoMeasure::on_render_input_window(float x, float y, float bottom_limit)
+{
+ static std::optional last_feature;
+ static EMode last_mode = EMode::FeatureSelection;
+ static SelectedFeatures last_selected_features;
+
+ static float last_y = 0.0f;
+ static float last_h = 0.0f;
+
+ if (m_editing_distance)
+ return;
+
+ m_imgui->begin(get_name(), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
+
+ // adjust window position to avoid overlap the view toolbar
+ const float win_h = ImGui::GetWindowHeight();
+ y = std::min(y, bottom_limit - win_h);
+ ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always);
+ if (last_h != win_h || last_y != y) {
+ // ask canvas for another frame to render the window in the correct position
+ m_imgui->set_requires_extra_frame();
+ if (last_h != win_h)
+ last_h = win_h;
+ if (last_y != y)
+ last_y = y;
+ }
+
+ if (ImGui::BeginTable("Commands", 2)) {
+ unsigned int row_count = 1;
+ add_row_to_table(
+ [this]() {
+ m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _u8L("Left mouse button"));
+ },
+ [this]() {
+ std::string text;
+ ColorRGBA color;
+ if (m_selected_features.second.feature.has_value()) {
+ if (m_selected_features.first.feature == m_curr_feature && m_mode == EMode::FeatureSelection) {
+ // hovering over 1st selected feature
+ text = _u8L("Unselect feature");
+ color = SELECTED_1ST_COLOR;
+ }
+ else if (m_hover_id == SEL_SPHERE_1_ID) {
+ if (m_selected_features.first.is_center) {
+ // hovering over center selected as 1st feature
+ text = _u8L("Unselect center");
+ color = SELECTED_1ST_COLOR;
+ }
+ else if (is_feature_with_center(*m_selected_features.first.feature)) {
+ // hovering over center of 1st selected feature
+ text = _u8L("Select center");
+ color = SELECTED_1ST_COLOR;
+ }
+ else {
+ // hovering over point selected as 1st feature
+ text = _u8L("Unselect point");
+ color = SELECTED_1ST_COLOR;
+ }
+ }
+ else if (m_selected_features.first.is_center && m_selected_features.first.source == m_curr_feature) {
+ // hovering over feature whose center is selected as 1st feature
+ text = _u8L("Select feature");
+ color = SELECTED_1ST_COLOR;
+ }
+ else if (m_selected_features.second.feature == m_curr_feature && m_mode == EMode::FeatureSelection) {
+ // hovering over 2nd selected feature
+ text = _u8L("Unselect feature");
+ color = SELECTED_2ND_COLOR;
+ }
+ else if (m_hover_id == SEL_SPHERE_2_ID) {
+ if (m_selected_features.second.is_center) {
+ // hovering over center selected as 2nd feature
+ text = _u8L("Unselect feature");
+ color = SELECTED_2ND_COLOR;
+ }
+ else if (is_feature_with_center(*m_selected_features.second.feature)) {
+ // hovering over center of 2nd selected feature
+ text = _u8L("Select center");
+ color = SELECTED_2ND_COLOR;
+ }
+ else {
+ // hovering over point selected as 2nd feature
+ text = _u8L("Unselect point");
+ color = SELECTED_2ND_COLOR;
+ }
+ }
+ else if (m_selected_features.second.is_center && m_selected_features.second.source == m_curr_feature) {
+ // hovering over feature whose center is selected as 2nd feature
+ text = _u8L("Select feature");
+ color = SELECTED_2ND_COLOR;
+ }
+ else {
+ // 1st feature selected
+ text = (m_mode == EMode::PointSelection) ? _u8L("Select point") : _u8L("Select feature");
+ color = SELECTED_2ND_COLOR;
+ }
+ }
+ else {
+ if (m_selected_features.first.feature.has_value()) {
+ if (m_selected_features.first.feature == m_curr_feature && m_mode == EMode::FeatureSelection) {
+ // hovering over 1st selected feature
+ text = _u8L("Unselect feature");
+ color = SELECTED_1ST_COLOR;
+ }
+ else {
+ if (m_hover_id == SEL_SPHERE_1_ID) {
+ if (m_selected_features.first.is_center) {
+ // hovering over center selected as 1st feature
+ text = _u8L("Unselect feature");
+ color = SELECTED_1ST_COLOR;
+ }
+ else if (is_feature_with_center(*m_selected_features.first.feature)) {
+ // hovering over center of 1st selected feature
+ text = _u8L("Select center");
+ color = SELECTED_1ST_COLOR;
+ }
+ else {
+ // hovering over point selected as 1st feature
+ text = _u8L("Unselect point");
+ color = SELECTED_1ST_COLOR;
+ }
+ }
+ else {
+ if (m_selected_features.first.is_center && m_selected_features.first.source == m_curr_feature) {
+ // hovering over feature whose center is selected as 1st feature
+ text = _u8L("Select feature");
+ color = SELECTED_1ST_COLOR;
+ }
+ else {
+ // 1st feature selected
+ text = (m_mode == EMode::PointSelection) ? _u8L("Select point") : _u8L("Select feature");
+ color = SELECTED_2ND_COLOR;
+ }
+ }
+ }
+ }
+ else {
+ // nothing is selected
+ text = (m_mode == EMode::PointSelection) ? _u8L("Select point") : _u8L("Select feature");
+ color = SELECTED_1ST_COLOR;
+ }
+ }
+
+ assert(!text.empty());
+
+ m_imgui->text_colored(ImGui::GetStyleColorVec4(ImGuiCol_Text), text);
+ ImGui::SameLine();
+ const ImVec2 pos = ImGui::GetCursorScreenPos();
+ const float rect_size = ImGui::GetTextLineHeight();
+ ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(pos.x + 1.0f, pos.y + 1.0f), ImVec2(pos.x + rect_size, pos.y + rect_size), ImGuiWrapper::to_ImU32(color));
+ ImGui::Dummy(ImVec2(rect_size, rect_size));
+ }
+ );
+
+ if (m_mode == EMode::FeatureSelection && m_hover_id != -1) {
+ add_strings_row_to_table(*m_imgui, "Shift", ImGuiWrapper::COL_ORANGE_LIGHT, _u8L("Enable point selection"), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ++row_count;
+ }
+
+ if (m_selected_features.first.feature.has_value()) {
+ add_strings_row_to_table(*m_imgui, "Delete", ImGuiWrapper::COL_ORANGE_LIGHT, _u8L("Restart selection"), ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ++row_count;
+ }
+
+ if (m_selected_features.first.feature.has_value() || m_selected_features.second.feature.has_value()) {
+ add_row_to_table(
+ [this]() {
+ m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Esc");
+ },
+ [this]() {
+ m_imgui->text_colored(ImGui::GetStyleColorVec4(ImGuiCol_Text), _u8L("Unselect"));
+ ImGui::SameLine();
+ const ImVec2 pos = ImGui::GetCursorScreenPos();
+ const float rect_size = ImGui::GetTextLineHeight();
+ const ColorRGBA color = m_selected_features.second.feature.has_value() ? SELECTED_2ND_COLOR : SELECTED_1ST_COLOR;
+ ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(pos.x + 1.0f, pos.y + 1.0f), ImVec2(pos.x + rect_size, pos.y + rect_size), ImGuiWrapper::to_ImU32(color));
+ ImGui::Dummy(ImVec2(rect_size, rect_size));
+ }
+ );
+
+ ++row_count;
+ }
+
+ // add dummy rows to keep dialog size fixed
+ for (unsigned int i = row_count; i < 4; ++i) {
+ add_strings_row_to_table(*m_imgui, " ", ImGuiWrapper::COL_ORANGE_LIGHT, " ", ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ }
+
+ ImGui::EndTable();
+ }
+
+ const bool use_inches = wxGetApp().app_config->get_bool("use_inches");
+ const std::string units = use_inches ? " " + _u8L("in") : " " + _u8L("mm");
+
+ ImGui::Separator();
+ const ImGuiTableFlags flags = ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersH;
+ if (ImGui::BeginTable("Selection", 2, flags)) {
+ auto format_item_text = [this, use_inches, &units](const SelectedFeatures::Item& item) {
+ if (!item.feature.has_value())
+ return _u8L("None");
+
+ std::string text = (item.source == item.feature) ? surface_feature_type_as_string(item.feature->get_type()) :
+ item.is_center ? center_on_feature_type_as_string(item.source->get_type()) : point_on_feature_type_as_string(item.source->get_type(), m_hover_id);
+ if (item.feature.has_value() && item.feature->get_type() == Measure::SurfaceFeatureType::Circle) {
+ auto [center, radius, normal] = item.feature->get_circle();
+ const Vec3d on_circle = center + radius * Measure::get_orthogonal(normal, true);
+ radius = (on_circle - center).norm();
+ if (use_inches)
+ radius = GizmoObjectManipulation::mm_to_in * radius;
+ text += " (" + _u8L("Diameter") + ": " + format_double(2.0 * radius) + units + ")";
+ }
+ else if (item.feature.has_value() && item.feature->get_type() == Measure::SurfaceFeatureType::Edge) {
+ auto [start, end] = item.feature->get_edge();
+ double length = (end - start).norm();
+ if (use_inches)
+ length = GizmoObjectManipulation::mm_to_in * length;
+ text += " (" + _u8L("Length") + ": " + format_double(length) + units + ")";
+ }
+ return text;
+ };
+
+ add_strings_row_to_table(*m_imgui, _u8L("Selection") + " 1:", ImGuiWrapper::to_ImVec4(SELECTED_1ST_COLOR), format_item_text(m_selected_features.first),
+ ImGuiWrapper::to_ImVec4(SELECTED_1ST_COLOR));
+ add_strings_row_to_table(*m_imgui, _u8L("Selection") + " 2:", ImGuiWrapper::to_ImVec4(SELECTED_2ND_COLOR), format_item_text(m_selected_features.second),
+ ImGuiWrapper::to_ImVec4(SELECTED_2ND_COLOR));
+ ImGui::EndTable();
+ }
+
+ m_imgui->disabled_begin(!m_selected_features.first.feature.has_value());
+ if (m_imgui->button(_L("Restart selection"))) {
+ m_selected_features.reset();
+ m_selected_sphere_raycasters.clear();
+ m_imgui->set_requires_extra_frame();
+ }
+ m_imgui->disabled_end();
+
+ auto add_measure_row_to_table = [this](const std::string& col_1, const ImVec4& col_1_color, const std::string& col_2, const ImVec4& col_2_color) {
+ ImGui::TableNextRow();
+ ImGui::TableSetColumnIndex(0);
+ m_imgui->text_colored(col_1_color, col_1);
+ ImGui::TableSetColumnIndex(1);
+ m_imgui->text_colored(col_2_color, col_2);
+ ImGui::TableSetColumnIndex(2);
+ if (m_imgui->image_button(ImGui::ClipboardBtnIcon, _L("Copy to clipboard"))) {
+ wxTheClipboard->Open();
+ wxTheClipboard->SetData(new wxTextDataObject(col_1 + ": " + col_2));
+ wxTheClipboard->Close();
+ }
+ };
+
+ ImGui::Separator();
+ m_imgui->text(_u8L("Measure"));
+
+ const unsigned int max_measure_row_count = 2;
+ unsigned int measure_row_count = 0;
+ if (ImGui::BeginTable("Measure", 4)) {
+ if (m_selected_features.second.feature.has_value()) {
+ const Measure::MeasurementResult& measure = m_measurement_result;
+ if (measure.angle.has_value()) {
+ ImGui::PushID("ClipboardAngle");
+ add_measure_row_to_table(_u8L("Angle"), ImGuiWrapper::COL_ORANGE_LIGHT, format_double(Geometry::rad2deg(measure.angle->angle)) + "°",
+ ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ++measure_row_count;
+ ImGui::PopID();
+ }
+
+ const bool show_strict = measure.distance_strict.has_value() &&
+ (!measure.distance_infinite.has_value() || std::abs(measure.distance_strict->dist - measure.distance_infinite->dist) > EPSILON);
+
+ if (measure.distance_infinite.has_value()) {
+ double distance = measure.distance_infinite->dist;
+ if (use_inches)
+ distance = GizmoObjectManipulation::mm_to_in * distance;
+ ImGui::PushID("ClipboardDistanceInfinite");
+ add_measure_row_to_table(show_strict ? _u8L("Perpendicular distance") : _u8L("Distance"), ImGuiWrapper::COL_ORANGE_LIGHT, format_double(distance) + units,
+ ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ++measure_row_count;
+ ImGui::PopID();
+ }
+ if (show_strict) {
+ double distance = measure.distance_strict->dist;
+ if (use_inches)
+ distance = GizmoObjectManipulation::mm_to_in * distance;
+ ImGui::PushID("ClipboardDistanceStrict");
+ add_measure_row_to_table(_u8L("Direct distance"), ImGuiWrapper::COL_ORANGE_LIGHT, format_double(distance) + units,
+ ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ++measure_row_count;
+ ImGui::PopID();
+ }
+ if (measure.distance_xyz.has_value() && measure.distance_xyz->norm() > EPSILON) {
+ Vec3d distance = *measure.distance_xyz;
+ if (use_inches)
+ distance = GizmoObjectManipulation::mm_to_in * distance;
+ ImGui::PushID("ClipboardDistanceXYZ");
+ add_measure_row_to_table(_u8L("Distance XYZ"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(distance),
+ ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ ++measure_row_count;
+ ImGui::PopID();
+ }
+ }
+
+ // add dummy rows to keep dialog size fixed
+ for (unsigned int i = measure_row_count; i < max_measure_row_count; ++i) {
+ add_strings_row_to_table(*m_imgui, " ", ImGuiWrapper::COL_ORANGE_LIGHT, " ", ImGui::GetStyleColorVec4(ImGuiCol_Text));
+ }
+ ImGui::EndTable();
+ }
+
+ if (last_feature != m_curr_feature || last_mode != m_mode || last_selected_features != m_selected_features) {
+ // the dialog may have changed its size, ask for an extra frame to render it properly
+ last_feature = m_curr_feature;
+ last_mode = m_mode;
+ last_selected_features = m_selected_features;
+ m_imgui->set_requires_extra_frame();
+ }
+
+ m_imgui->end();
+}
+
+void GLGizmoMeasure::on_register_raycasters_for_picking()
+{
+ // the features are rendered on top of the scene, so the raytraced picker should take it into account
+ m_parent.set_raycaster_gizmos_on_top(true);
+}
+
+void GLGizmoMeasure::on_unregister_raycasters_for_picking()
+{
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo);
+ m_parent.set_raycaster_gizmos_on_top(false);
+ m_raycasters.clear();
+ m_selected_sphere_raycasters.clear();
+}
+
+void GLGizmoMeasure::remove_selected_sphere_raycaster(int id)
+{
+ auto it = std::find_if(m_selected_sphere_raycasters.begin(), m_selected_sphere_raycasters.end(),
+ [id](std::shared_ptr item) { return SceneRaycaster::decode_id(SceneRaycaster::EType::Gizmo, item->get_id()) == id; });
+ if (it != m_selected_sphere_raycasters.end())
+ m_selected_sphere_raycasters.erase(it);
+ m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, id);
+}
+
+void GLGizmoMeasure::update_measurement_result()
+{
+ if (!m_selected_features.first.feature.has_value())
+ m_measurement_result = Measure::MeasurementResult();
+ else if (m_selected_features.second.feature.has_value())
+ m_measurement_result = Measure::get_measurement(*m_selected_features.first.feature, *m_selected_features.second.feature, m_measuring.get());
+ else if (!m_selected_features.second.feature.has_value() && m_selected_features.first.feature->get_type() == Measure::SurfaceFeatureType::Circle)
+ m_measurement_result = Measure::get_measurement(*m_selected_features.first.feature, Measure::SurfaceFeature(std::get<0>(m_selected_features.first.feature->get_circle())), m_measuring.get());
+}
+
+} // 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..4ab67b8d00
--- /dev/null
+++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp
@@ -0,0 +1,190 @@
+///|/ Copyright (c) Prusa Research 2019 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Vojtěch Bubník @bubnikv, Filip Sykala @Jony01
+///|/
+///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
+///|/
+#ifndef slic3r_GLGizmoMeasure_hpp_
+#define slic3r_GLGizmoMeasure_hpp_
+
+#include "GLGizmoBase.hpp"
+#include "slic3r/GUI/GLModel.hpp"
+#include "slic3r/GUI/GUI_Utils.hpp"
+#include "slic3r/GUI/MeshUtils.hpp"
+#include "slic3r/GUI/I18N.hpp"
+#include "libslic3r/Measure.hpp"
+#include "libslic3r/Model.hpp"
+
+namespace Slic3r {
+
+enum class ModelVolumeType : int;
+
+namespace Measure { class Measuring; }
+
+
+namespace GUI {
+
+enum class SLAGizmoEventType : unsigned char;
+
+class GLGizmoMeasure : public GLGizmoBase
+{
+ enum class EMode : unsigned char
+ {
+ FeatureSelection,
+ PointSelection
+ };
+
+ struct SelectedFeatures
+ {
+ struct Item
+ {
+ bool is_center{ false };
+ std::optional source;
+ std::optional feature;
+
+ bool operator == (const Item& other) const {
+ return this->is_center == other.is_center && this->source == other.source && this->feature == other.feature;
+ }
+
+ bool operator != (const Item& other) const {
+ return !operator == (other);
+ }
+
+ void reset() {
+ is_center = false;
+ source.reset();
+ feature.reset();
+ }
+ };
+
+ Item first;
+ Item second;
+
+ void reset() {
+ first.reset();
+ second.reset();
+ }
+
+ bool operator == (const SelectedFeatures & other) const {
+ if (this->first != other.first) return false;
+ return this->second == other.second;
+ }
+
+ bool operator != (const SelectedFeatures & other) const {
+ return !operator == (other);
+ }
+ };
+
+ struct VolumeCacheItem
+ {
+ const ModelObject* object{ nullptr };
+ const ModelInstance* instance{ nullptr };
+ const ModelVolume* volume{ nullptr };
+ Transform3d world_trafo;
+
+ bool operator == (const VolumeCacheItem& other) const {
+ return this->object == other.object && this->instance == other.instance && this->volume == other.volume &&
+ this->world_trafo.isApprox(other.world_trafo);
+ }
+ };
+
+ std::vector m_volumes_cache;
+
+ EMode m_mode{ EMode::FeatureSelection };
+ Measure::MeasurementResult m_measurement_result;
+
+ std::unique_ptr m_measuring; // PIMPL
+
+ PickingModel m_sphere;
+ PickingModel m_cylinder;
+ PickingModel m_circle;
+ PickingModel m_plane;
+ struct Dimensioning
+ {
+ GLModel line;
+ GLModel triangle;
+ GLModel arc;
+ };
+ Dimensioning m_dimensioning;
+
+ // Uses a standalone raycaster and not the shared one because of the
+ // difference in how the mesh is updated
+ std::unique_ptr m_raycaster;
+
+ std::vector m_plane_models_cache;
+ std::map> m_raycasters;
+ // used to keep the raycasters for point/center spheres
+ std::vector> m_selected_sphere_raycasters;
+ std::optional m_curr_feature;
+ std::optional m_curr_point_on_feature_position;
+ struct SceneRaycasterState
+ {
+ std::shared_ptr raycaster{ nullptr };
+ bool state{true};
+
+ };
+ std::vector m_scene_raycasters;
+
+ // These hold information to decide whether recalculation is necessary:
+ float m_last_inv_zoom{ 0.0f };
+ std::optional m_last_circle;
+ int m_last_plane_idx{ -1 };
+
+ bool m_mouse_left_down{ false }; // for detection left_up of this gizmo
+
+ Vec2d m_mouse_pos{ Vec2d::Zero() };
+
+ KeyAutoRepeatFilter m_shift_kar_filter;
+
+ SelectedFeatures m_selected_features;
+ bool m_pending_scale{ false };
+ bool m_editing_distance{ false };
+ bool m_is_editing_distance_first_frame{ true };
+
+ void update_if_needed();
+
+ void disable_scene_raycasters();
+ void restore_scene_raycasters_state();
+
+ void render_dimensioning();
+
+#if ENABLE_MEASURE_GIZMO_DEBUG
+ void render_debug_dialog();
+#endif // ENABLE_MEASURE_GIZMO_DEBUG
+
+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(bool is_serializing) override;
+
+ bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down);
+
+ bool wants_enter_leave_snapshots() const override { return true; }
+ std::string get_gizmo_entering_text() const override { return _u8L("Entering Measure gizmo"); }
+ std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Measure gizmo"); }
+ std::string get_action_snapshot_name() const override { return _u8L("Measure gizmo editing"); }
+
+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;
+
+ virtual void on_render_input_window(float x, float y, float bottom_limit) override;
+ virtual void on_register_raycasters_for_picking() override;
+ virtual void on_unregister_raycasters_for_picking() override;
+
+ void remove_selected_sphere_raycaster(int id);
+ void update_measurement_result();
+};
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // slic3r_GLGizmoMeasure_hpp_
diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp
index a063167125..6f2138346e 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp
@@ -30,8 +30,12 @@ enum class SLAGizmoEventType : unsigned char {
Dragging,
Delete,
SelectAll,
+ CtrlDown,
+ CtrlUp,
+ ShiftDown,
ShiftUp,
AltUp,
+ Escape,
ApplyChanges,
DiscardChanges,
AutomaticGeneration,
diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
index 7b5735d803..25169aca8b 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
@@ -27,6 +27,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 "slic3r/GUI/Gizmos/GLGizmoText.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp"
@@ -195,6 +196,7 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam));
m_gizmos.emplace_back(new GLGizmoText(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Text));
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmuSegmentation));
+ m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", EType::Measure));
m_gizmos.emplace_back(new GLGizmoSimplify(m_parent, "reduce_triangles.svg", EType::Simplify));
//m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++));
//m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++));
@@ -438,6 +440,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
return dynamic_cast(m_gizmos[MmuSegmentation].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == Text)
return dynamic_cast(m_gizmos[Text].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
+ else if (m_current == Measure)
+ return dynamic_cast(m_gizmos[Measure].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == Cut)
return dynamic_cast