mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-02 23:06:49 +00:00
NEW:add groove cut function
Jira:STUDIO-4227 Approximately 70% of the code comes from Prusa,thanks for PrusaSlcer and YuSanka commit 492e356a21734b3503caae115fbb280da5fbaa22 Author: YuSanka <yusanka@gmail.com> Date: Thu Aug 3 16:09:28 2023 +0200 CutGizmo: Fixed and improved Undo/Redo. ... Change-Id: I63abb69180aec2ab0ce9bc8f30477d9e9a25a5fb
This commit is contained in:
@@ -6,7 +6,10 @@
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/CameraUtils.hpp"
|
||||
|
||||
#include <GL/glew.h>
|
||||
|
||||
@@ -14,34 +17,35 @@
|
||||
#include "CameraUtils.hpp"
|
||||
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
namespace Slic3r { namespace GUI {
|
||||
void MeshClipper::set_behaviour(bool fill_cut, double contour_width)
|
||||
{
|
||||
if (fill_cut != m_fill_cut || !is_approx(contour_width, m_contour_width)) m_result.reset();
|
||||
m_fill_cut = fill_cut;
|
||||
m_contour_width = contour_width;
|
||||
}
|
||||
|
||||
void MeshClipper::set_plane(const ClippingPlane& plane)
|
||||
void MeshClipper::set_plane(const ClippingPlane &plane)
|
||||
{
|
||||
if (m_plane != plane) {
|
||||
m_plane = plane;
|
||||
m_triangles_valid = false;
|
||||
m_result.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MeshClipper::set_limiting_plane(const ClippingPlane& plane)
|
||||
{
|
||||
if (m_limiting_plane != plane) {
|
||||
m_limiting_plane = plane;
|
||||
m_triangles_valid = false;
|
||||
m_result.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void MeshClipper::set_mesh(const TriangleMesh& mesh)
|
||||
{
|
||||
if (m_mesh != &mesh) {
|
||||
m_mesh = &mesh;
|
||||
m_triangles_valid = false;
|
||||
m_triangles2d.resize(0);
|
||||
m_result.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +53,7 @@ void MeshClipper::set_negative_mesh(const TriangleMesh& mesh)
|
||||
{
|
||||
if (m_negative_mesh != &mesh) {
|
||||
m_negative_mesh = &mesh;
|
||||
m_triangles_valid = false;
|
||||
m_triangles2d.resize(0);
|
||||
m_result.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,34 +63,75 @@ void MeshClipper::set_transformation(const Geometry::Transformation& trafo)
|
||||
{
|
||||
if (! m_trafo.get_matrix().isApprox(trafo.get_matrix())) {
|
||||
m_trafo = trafo;
|
||||
m_triangles_valid = false;
|
||||
m_triangles2d.resize(0);
|
||||
m_result.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void MeshClipper::render_cut()
|
||||
void MeshClipper::render_cut(const ColorRGBA &color, const std::vector<size_t> *ignore_idxs)
|
||||
{
|
||||
if (! m_triangles_valid)
|
||||
recalculate_triangles();
|
||||
if (!m_result) recalculate_triangles();
|
||||
GLShaderProgram *curr_shader = wxGetApp().get_current_shader();
|
||||
if (curr_shader != nullptr) curr_shader->stop_using();
|
||||
|
||||
if (m_vertex_array.has_VBOs())
|
||||
m_vertex_array.render();
|
||||
GLShaderProgram *shader = wxGetApp().get_shader("flat");
|
||||
if (shader != nullptr) {
|
||||
shader->start_using();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
shader->set_uniform("view_model_matrix", camera.get_view_matrix());
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
for (size_t i = 0; i < m_result->cut_islands.size(); ++i) {
|
||||
if (ignore_idxs && std::binary_search(ignore_idxs->begin(), ignore_idxs->end(), i)) continue;
|
||||
CutIsland &isl = m_result->cut_islands[i];
|
||||
ColorRGBA gray{0.5f, 0.5f, 0.5f, 1.f};
|
||||
isl.model.set_color(-1, isl.disabled ? gray : color);
|
||||
isl.model.render();
|
||||
}
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
if (curr_shader != nullptr) curr_shader->start_using();
|
||||
}
|
||||
|
||||
bool MeshClipper::is_projection_inside_cut(const Vec3d &point_in) const
|
||||
void MeshClipper::render_contour(const ColorRGBA &color, const std::vector<size_t> *ignore_idxs)
|
||||
{
|
||||
if (!m_result) recalculate_triangles();
|
||||
|
||||
GLShaderProgram *curr_shader = wxGetApp().get_current_shader();
|
||||
if (curr_shader != nullptr) curr_shader->stop_using();
|
||||
|
||||
GLShaderProgram *shader = wxGetApp().get_shader("flat");
|
||||
if (shader != nullptr) {
|
||||
shader->start_using();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
shader->set_uniform("view_model_matrix", camera.get_view_matrix());
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
for (size_t i = 0; i < m_result->cut_islands.size(); ++i) {
|
||||
if (ignore_idxs && std::binary_search(ignore_idxs->begin(), ignore_idxs->end(), i)) continue;
|
||||
CutIsland &isl = m_result->cut_islands[i];
|
||||
ColorRGBA red{1.0f, 0.f, 0.f, 1.f};
|
||||
isl.model_expanded.set_color(-1, isl.disabled ? red : color);
|
||||
isl.model_expanded.render();
|
||||
}
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
if (curr_shader != nullptr)
|
||||
curr_shader->start_using();
|
||||
}
|
||||
|
||||
int MeshClipper::is_projection_inside_cut(const Vec3d &point_in) const
|
||||
{
|
||||
if (!m_result || m_result->cut_islands.empty())
|
||||
return false;
|
||||
return -1;
|
||||
Vec3d point = m_result->trafo.inverse() * point_in;
|
||||
Point pt_2d = Point::new_scale(Vec2d(point.x(), point.y()));
|
||||
|
||||
for (const CutIsland &isl : m_result->cut_islands) {
|
||||
for (int i = 0; i < int(m_result->cut_islands.size()); ++i) {
|
||||
const CutIsland &isl = m_result->cut_islands[i];
|
||||
if (isl.expoly_bb.contains(pt_2d) && isl.expoly.contains(pt_2d))
|
||||
return true;
|
||||
return i; // TODO: handle intersecting contours
|
||||
}
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool MeshClipper::has_valid_contour() const
|
||||
@@ -138,22 +182,25 @@ std::vector<Vec3d> MeshClipper::point_per_contour() const {
|
||||
|
||||
void MeshClipper::recalculate_triangles()
|
||||
{
|
||||
const Transform3f& instance_matrix_no_translation_no_scaling = m_trafo.get_matrix(true,false,true).cast<float>();
|
||||
// Calculate clipping plane normal in mesh coordinates.
|
||||
const Vec3f up_noscale = instance_matrix_no_translation_no_scaling.inverse() * m_plane.get_normal().cast<float>();
|
||||
const Vec3d up = up_noscale.cast<double>().cwiseProduct(m_trafo.get_scaling_factor());
|
||||
// Calculate distance from mesh origin to the clipping plane (in mesh coordinates).
|
||||
const float height_mesh = m_plane.distance(m_trafo.get_offset()) * (up_noscale.norm()/up.norm());
|
||||
m_result = ClipResult();
|
||||
|
||||
auto plane_mesh = Eigen::Hyperplane<double, 3>(m_plane.get_normal(), -m_plane.distance(Vec3d::Zero())).transform(m_trafo.get_matrix().inverse());
|
||||
const Vec3d up = plane_mesh.normal();
|
||||
const float height_mesh = -plane_mesh.offset();
|
||||
|
||||
// Now do the cutting
|
||||
MeshSlicingParams slicing_params;
|
||||
slicing_params.trafo.rotate(Eigen::Quaternion<double, Eigen::DontAlign>::FromTwoVectors(up, Vec3d::UnitZ()));
|
||||
|
||||
ExPolygons expolys = union_ex(slice_mesh(m_mesh->its, height_mesh, slicing_params));
|
||||
ExPolygons expolys;
|
||||
|
||||
// if (m_csgmesh.empty()) {
|
||||
if (m_mesh) {
|
||||
expolys = union_ex(slice_mesh(m_mesh->its, height_mesh, slicing_params));
|
||||
}
|
||||
if (m_negative_mesh && !m_negative_mesh->empty()) {
|
||||
const ExPolygons neg_expolys = union_ex(slice_mesh(m_negative_mesh->its, height_mesh, slicing_params));
|
||||
expolys = diff_ex(expolys, neg_expolys);
|
||||
expolys = diff_ex(expolys, neg_expolys);
|
||||
}
|
||||
|
||||
// Triangulate and rotate the cut into world coords:
|
||||
@@ -163,41 +210,37 @@ void MeshClipper::recalculate_triangles()
|
||||
tr.rotate(q);
|
||||
tr = m_trafo.get_matrix() * tr;
|
||||
|
||||
m_result = ClipResult();
|
||||
m_result->trafo = tr;
|
||||
|
||||
if (m_limiting_plane != ClippingPlane::ClipsNothing())
|
||||
{
|
||||
if (m_limiting_plane != ClippingPlane::ClipsNothing()) {
|
||||
// Now remove whatever ended up below the limiting plane (e.g. sinking objects).
|
||||
// First transform the limiting plane from world to mesh coords.
|
||||
// Note that inverse of tr transforms the plane from world to horizontal.
|
||||
const Vec3d normal_old = m_limiting_plane.get_normal().normalized();
|
||||
const Vec3d normal_new = (tr.matrix().block<3,3>(0,0).transpose() * normal_old).normalized();
|
||||
const Vec3d normal_new = (tr.matrix().block<3, 3>(0, 0).transpose() * normal_old).normalized();
|
||||
|
||||
// normal_new should now be the plane normal in mesh coords. To find the offset,
|
||||
// transform a point and set offset so it belongs to the transformed plane.
|
||||
Vec3d pt = Vec3d::Zero();
|
||||
Vec3d pt = Vec3d::Zero();
|
||||
const double plane_offset = m_limiting_plane.get_data()[3];
|
||||
if (std::abs(normal_old.z()) > 0.5) // normal is normalized, at least one of the coords if larger than sqrt(3)/3 = 0.57
|
||||
pt.z() = - plane_offset / normal_old.z();
|
||||
pt.z() = -plane_offset / normal_old.z();
|
||||
else if (std::abs(normal_old.y()) > 0.5)
|
||||
pt.y() = - plane_offset / normal_old.y();
|
||||
pt.y() = -plane_offset / normal_old.y();
|
||||
else
|
||||
pt.x() = - plane_offset / normal_old.x();
|
||||
pt = tr.inverse() * pt;
|
||||
pt.x() = -plane_offset / normal_old.x();
|
||||
pt = tr.inverse() * pt;
|
||||
const double offset = -(normal_new.dot(pt));
|
||||
|
||||
if (std::abs(normal_old.dot(m_plane.get_normal().normalized())) > 0.99) {
|
||||
// The cuts are parallel, show all or nothing.
|
||||
if (normal_old.dot(m_plane.get_normal().normalized()) < 0.0 && offset < height_mesh)
|
||||
expolys.clear();
|
||||
if (normal_old.dot(m_plane.get_normal().normalized()) < 0.0 && offset < height_mesh) expolys.clear();
|
||||
} else {
|
||||
// The cut is a horizontal plane defined by z=height_mesh.
|
||||
// ax+by+e=0 is the line of intersection with the limiting plane.
|
||||
// Normalized so a^2 + b^2 = 1.
|
||||
const double len = std::hypot(normal_new.x(), normal_new.y());
|
||||
if (len == 0.)
|
||||
return;
|
||||
if (len == 0.) return;
|
||||
const double a = normal_new.x() / len;
|
||||
const double b = normal_new.y() / len;
|
||||
const double e = (normal_new.z() * height_mesh + offset) / len;
|
||||
@@ -211,36 +254,140 @@ void MeshClipper::recalculate_triangles()
|
||||
// it so it lies on our line. This will be the figure to subtract
|
||||
// from the cut. The coordinates must not overflow after the transform,
|
||||
// make the rectangle a bit smaller.
|
||||
const coord_t size = (std::numeric_limits<coord_t>::max() - scale_(std::max(std::abs(e*a), std::abs(e*b)))) / 4;
|
||||
Polygons ep {Polygon({Point(-size, 0), Point(size, 0), Point(size, 2*size), Point(-size, 2*size)})};
|
||||
const coord_t size = (std::numeric_limits<coord_t>::max() / 2 - scale_(std::max(std::abs(e * a), std::abs(e * b)))) / 4;
|
||||
Polygons ep{Polygon({Point(-size, 0), Point(size, 0), Point(size, 2 * size), Point(-size, 2 * size)})};
|
||||
ep.front().rotate(angle);
|
||||
ep.front().translate(scale_(-e * a), scale_(-e * b));
|
||||
expolys = diff_ex(expolys, ep);
|
||||
}
|
||||
}
|
||||
|
||||
for (const ExPolygon &exp : expolys) {
|
||||
tr.pretranslate(0.001 * m_plane.get_normal().normalized()); // to avoid z-fighting
|
||||
Transform3d tr2 = tr;
|
||||
tr2.pretranslate(0.002 * m_plane.get_normal().normalized());
|
||||
|
||||
std::vector<Vec2f> triangles2d;
|
||||
|
||||
for (const ExPolygon &exp : expolys) {
|
||||
triangles2d.clear();
|
||||
|
||||
m_result->cut_islands.push_back(CutIsland());
|
||||
CutIsland &isl = m_result->cut_islands.back();
|
||||
isl.expoly = std::move(exp);
|
||||
isl.expoly_bb = get_extents(exp);
|
||||
|
||||
if (m_fill_cut) {
|
||||
triangles2d = triangulate_expolygon_2f(exp, m_trafo.get_matrix().matrix().determinant() < 0.);
|
||||
GLModel::InitializationData init_data; // GLModel::Geometry init_data;
|
||||
init_data.entities.push_back(GLModel::InitializationData::Entity());
|
||||
init_data.entities.back().type = GLModel::PrimitiveType::Triangles;
|
||||
init_data.entities.back().positions.reserve(triangles2d.size() * (init_data.entities.back().type == GLModel::PrimitiveType::Triangles ? 3 : 2));
|
||||
init_data.entities.back().normals.reserve(triangles2d.size() * (init_data.entities.back().type == GLModel::PrimitiveType::Triangles ? 3 : 2));
|
||||
init_data.entities.back().indices.reserve(triangles2d.size());
|
||||
/*init_data.format = {GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3};
|
||||
init_data.reserve_vertices(triangles2d.size());
|
||||
init_data.reserve_indices(triangles2d.size());*/
|
||||
|
||||
// vertices + indices
|
||||
for (auto it = triangles2d.cbegin(); it != triangles2d.cend(); it = it + 3) {
|
||||
/*init_data.add_vertex((Vec3f) (tr * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast<float>(), (Vec3f) up.cast<float>());
|
||||
init_data.add_vertex((Vec3f) (tr * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast<float>(), (Vec3f) up.cast<float>());
|
||||
init_data.add_vertex((Vec3f) (tr * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast<float>(), (Vec3f) up.cast<float>());
|
||||
const size_t idx = it - triangles2d.cbegin();
|
||||
init_data.add_triangle((unsigned int) idx, (unsigned int) idx + 1, (unsigned int) idx + 2);*/
|
||||
|
||||
init_data.entities.back().positions.push_back((Vec3f) (tr * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast<float>());
|
||||
init_data.entities.back().normals.push_back((Vec3f) up.cast<float>());
|
||||
|
||||
init_data.entities.back().positions.push_back((Vec3f) (tr * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast<float>());
|
||||
init_data.entities.back().normals.push_back((Vec3f) up.cast<float>());
|
||||
|
||||
init_data.entities.back().positions.push_back((Vec3f) (tr * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast<float>());
|
||||
init_data.entities.back().normals.push_back((Vec3f) up.cast<float>());
|
||||
|
||||
const size_t idx = it - triangles2d.cbegin();
|
||||
init_data.entities.back().indices.push_back((unsigned int) idx);
|
||||
init_data.entities.back().indices.push_back((unsigned int) idx + 1);
|
||||
init_data.entities.back().indices.push_back((unsigned int) idx + 2);
|
||||
}
|
||||
|
||||
if (init_data.entities.back().indices.size() != 0) isl.model.init_from(std::move(init_data));
|
||||
}
|
||||
|
||||
if (m_contour_width != 0. && !exp.contour.empty()) {
|
||||
triangles2d.clear();
|
||||
|
||||
// The contours must not scale with the object. Check the scale factor
|
||||
// in the respective directions, create a scaled copy of the ExPolygon
|
||||
// offset it and then unscale the result again.
|
||||
|
||||
Transform3d t = tr;
|
||||
t.translation() = Vec3d::Zero();
|
||||
double scale_x = (t * Vec3d::UnitX()).norm();
|
||||
double scale_y = (t * Vec3d::UnitY()).norm();
|
||||
|
||||
// To prevent overflow after scaling, downscale the input if needed:
|
||||
double extra_scale = 1.;
|
||||
int32_t limit = int32_t(
|
||||
std::min(std::numeric_limits<coord_t>::max() / (2. * std::max(1., scale_x)), std::numeric_limits<coord_t>::max() / (2. * std::max(1., scale_y))));
|
||||
int32_t max_coord = 0;
|
||||
for (const Point &pt : exp.contour) max_coord = std::max(max_coord, std::max(std::abs(pt.x()), std::abs(pt.y())));
|
||||
if (max_coord + m_contour_width >= limit) extra_scale = 0.9 * double(limit) / max_coord;
|
||||
|
||||
ExPolygon exp_copy = exp;
|
||||
if (extra_scale != 1.) exp_copy.scale(extra_scale);
|
||||
exp_copy.scale(scale_x, scale_y);
|
||||
|
||||
ExPolygons expolys_exp = offset_ex(exp_copy, scale_(m_contour_width));
|
||||
expolys_exp = diff_ex(expolys_exp, ExPolygons({exp_copy}));
|
||||
|
||||
for (ExPolygon &e : expolys_exp) {
|
||||
e.scale(1. / scale_x, 1. / scale_y);
|
||||
if (extra_scale != 1.) e.scale(1. / extra_scale);
|
||||
}
|
||||
|
||||
triangles2d = triangulate_expolygons_2f(expolys_exp, m_trafo.get_matrix().matrix().determinant() < 0.);
|
||||
GLModel::InitializationData init_data; // GLModel::Geometry init_data;
|
||||
init_data.entities.push_back(GLModel::InitializationData::Entity());
|
||||
init_data.entities.back().type = GLModel::PrimitiveType::Triangles;
|
||||
init_data.entities.back().positions.reserve(triangles2d.size() * (init_data.entities.back().type == GLModel::PrimitiveType::Triangles ? 3 : 2));
|
||||
init_data.entities.back().normals.reserve(triangles2d.size() * (init_data.entities.back().type == GLModel::PrimitiveType::Triangles ? 3 : 2));
|
||||
init_data.entities.back().indices.reserve(triangles2d.size());
|
||||
/*GLModel::Geometry init_data = GLModel::Geometry();
|
||||
init_data.format = {GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3};
|
||||
init_data.reserve_vertices(triangles2d.size());
|
||||
init_data.reserve_indices(triangles2d.size());*/
|
||||
|
||||
// vertices + indices
|
||||
for (auto it = triangles2d.cbegin(); it != triangles2d.cend(); it = it + 3) {
|
||||
init_data.entities.back().positions.push_back((Vec3f) (tr * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast<float>());
|
||||
init_data.entities.back().normals.push_back((Vec3f) up.cast<float>());
|
||||
|
||||
init_data.entities.back().positions.push_back((Vec3f) (tr * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast<float>());
|
||||
init_data.entities.back().normals.push_back((Vec3f) up.cast<float>());
|
||||
|
||||
init_data.entities.back().positions.push_back((Vec3f) (tr * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast<float>());
|
||||
init_data.entities.back().normals.push_back((Vec3f) up.cast<float>());
|
||||
|
||||
const size_t idx = it - triangles2d.cbegin();
|
||||
init_data.entities.back().indices.push_back((unsigned int) idx);
|
||||
init_data.entities.back().indices.push_back((unsigned int) idx + 1);
|
||||
init_data.entities.back().indices.push_back((unsigned int) idx + 2);
|
||||
}
|
||||
|
||||
if (init_data.entities.back().indices.size() != 0) isl.model_expanded.init_from(std::move(init_data));
|
||||
}
|
||||
|
||||
isl.expoly = std::move(exp);
|
||||
isl.expoly_bb = get_extents(isl.expoly);
|
||||
|
||||
Point centroid_scaled = isl.expoly.contour.centroid();
|
||||
Vec3d centroid_world = m_result->trafo * Vec3d(unscale(centroid_scaled).x(), unscale(centroid_scaled).y(), 0.);
|
||||
isl.hash = isl.expoly.contour.size() + size_t(std::abs(100. * centroid_world.x())) + size_t(std::abs(100. * centroid_world.y())) +
|
||||
size_t(std::abs(100. * centroid_world.z()));
|
||||
}
|
||||
|
||||
m_triangles2d = triangulate_expolygons_2f(expolys, m_trafo.get_matrix().matrix().determinant() < 0.);
|
||||
|
||||
tr.pretranslate(0.001 * m_plane.get_normal().normalized()); // to avoid z-fighting
|
||||
|
||||
m_vertex_array.release_geometry();
|
||||
for (auto it=m_triangles2d.cbegin(); it != m_triangles2d.cend(); it=it+3) {
|
||||
m_vertex_array.push_geometry(tr * Vec3d((*(it+0))(0), (*(it+0))(1), height_mesh), up);
|
||||
m_vertex_array.push_geometry(tr * Vec3d((*(it+1))(0), (*(it+1))(1), height_mesh), up);
|
||||
m_vertex_array.push_geometry(tr * Vec3d((*(it+2))(0), (*(it+2))(1), height_mesh), up);
|
||||
const size_t idx = it - m_triangles2d.cbegin();
|
||||
m_vertex_array.push_triangle(idx, idx+1, idx+2);
|
||||
}
|
||||
m_vertex_array.finalize_geometry(true);
|
||||
|
||||
m_triangles_valid = true;
|
||||
// Now sort the islands so they are in defined order. This is a hack needed by cut gizmo, which sometimes
|
||||
// flips the normal of the cut, in which case the contours stay the same but their order may change.
|
||||
std::sort(m_result->cut_islands.begin(), m_result->cut_islands.end(), [](const CutIsland &a, const CutIsland &b) { return a.hash < b.hash; });
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user