Skip to content

Commit 8b4ee00

Browse files
WIP simplify boundaries
1 parent 1d25db9 commit 8b4ee00

File tree

6 files changed

+142
-52
lines changed

6 files changed

+142
-52
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020

2121
### Changed
2222

23+
- Fixed point welding bug in map_mesh function.
24+
2325
### Removed
2426

2527

CMakeLists.txt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ project(compas_libigl LANGUAGES CXX)
44
set(CMAKE_CXX_STANDARD 20)
55
set(CMAKE_CXX_EXTENSIONS OFF)
66

7-
option(ENABLE_PRECOMPILED_HEADERS "Enable precompiled headers" OFF)
7+
option(ENABLE_PRECOMPILED_HEADERS "Enable precompiled headers" ON)
88
option(MULTITHREADED_COMPILATION "Enable multi-threaded compilation (Ninja only)" ON)
99

1010
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
@@ -152,13 +152,13 @@ endfunction()
152152
# === Add your modules ===
153153
add_nanobind_module(_nanobind src/nanobind.cpp)
154154
add_nanobind_module(_types_std src/types_std.cpp)
155-
add_nanobind_module(_boundaries src/boundaries.cpp)
156-
add_nanobind_module(_curvature src/curvature.cpp)
157-
add_nanobind_module(_geodistance src/geodistance.cpp)
158-
add_nanobind_module(_intersections src/intersections.cpp)
159-
add_nanobind_module(_isolines src/isolines.cpp)
160-
add_nanobind_module(_massmatrix src/massmatrix.cpp)
161-
add_nanobind_module(_meshing src/meshing.cpp)
162-
add_nanobind_module(_parametrisation src/parametrisation.cpp)
163-
add_nanobind_module(_planarize src/planarize.cpp)
155+
# add_nanobind_module(_boundaries src/boundaries.cpp)
156+
# add_nanobind_module(_curvature src/curvature.cpp)
157+
# add_nanobind_module(_geodistance src/geodistance.cpp)
158+
# add_nanobind_module(_intersections src/intersections.cpp)
159+
# add_nanobind_module(_isolines src/isolines.cpp)
160+
# add_nanobind_module(_massmatrix src/massmatrix.cpp)
161+
# add_nanobind_module(_meshing src/meshing.cpp)
162+
# add_nanobind_module(_parametrisation src/parametrisation.cpp)
163+
# add_nanobind_module(_planarize src/planarize.cpp)
164164
add_nanobind_module(_mapping src/mapping.cpp)

docs/examples/example_mapping_patterns.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
# Mapping: 3D Mesh, 2D Pattern, UV
2424
# ==============================================================================
2525

26-
mesh_mapped = map_pattern_to_mesh("ZigZag", mesh, clip_boundaries=True, tolerance=1e-6, pattern_u=16, pattern_v=16)
27-
26+
mesh_mapped0 = map_pattern_to_mesh("ZigZag", mesh, clip_boundaries=True, tolerance=1e-6, pattern_u=16, pattern_v=16, simplify_borders=True)
27+
mesh_mapped1 = map_pattern_to_mesh("ZigZag", mesh, clip_boundaries=True, tolerance=1e-6, pattern_u=16, pattern_v=16, simplify_borders=False)
2828
# ==============================================================================
2929
# Viewer
3030
# ==============================================================================
@@ -36,6 +36,8 @@
3636
viewer = Viewer(config=config)
3737

3838
# viewer.scene.add(mesh, name="mesh", show_faces=False, linecolor=Color.grey(), opacity=0.2)
39-
viewer.scene.add(mesh_mapped, name="mesh_mapped", facecolor=Color.red())
39+
viewer.scene.add(mesh_mapped0, name="mesh_mapped0", facecolor=Color.red(), show_points=True)
40+
viewer.scene.add(mesh_mapped1, name="mesh_mapped1", facecolor=Color.blue(), show_points=True, show_faces=False)
41+
4042

4143
viewer.show()

src/compas_libigl/mapping.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from compas_libigl._types_std import VectorVectorInt # noqa: F401
2727

2828

29-
def map_mesh(target_mesh, pattern_mesh, clip_boundaries=True, tolerance=1e-6):
29+
def map_mesh(target_mesh, pattern_mesh, clip_boundaries=True, simplify_borders=True, tolerance=1e-6):
3030
"""
3131
Map a 2D pattern mesh onto a 3D target.
3232
@@ -38,6 +38,8 @@ def map_mesh(target_mesh, pattern_mesh, clip_boundaries=True, tolerance=1e-6):
3838
A tuple of (vertices, faces) representing the pattern mesh.
3939
clip_boundaries : bool
4040
Whether to clip the pattern mesh to the boundaries of the target mesh.
41+
simplify_borders : bool
42+
Whether to simplify the border of the pattern mesh.
4143
tolerance : float
4244
The tolerance for point comparison, to remove duplicates.
4345
@@ -63,14 +65,14 @@ def map_mesh(target_mesh, pattern_mesh, clip_boundaries=True, tolerance=1e-6):
6365

6466
# Perform the mapping
6567
pv_numpy_copy, pf_numpy_cleaned, p_normals, pattern_is_boundary, pattern_groups = _mapping.map_mesh_with_automatic_parameterization(
66-
v_numpy, f_numpy, pattern_v_numpy, pf, clip_boundaries, tolerance
68+
v_numpy, f_numpy, pattern_v_numpy, pf, clip_boundaries, simplify_borders, tolerance
6769
)
6870

6971
# Return the result as a tuple
7072
return pv_numpy_copy, pf_numpy_cleaned, p_normals, pattern_is_boundary, pattern_groups
7173

7274

73-
def map_pattern_to_mesh(name, mesh, clip_boundaries=True, tolerance=1e-6, pattern_u=16, pattern_v=16):
75+
def map_pattern_to_mesh(name, mesh, clip_boundaries=True, tolerance=1e-6, pattern_u=16, pattern_v=16, simplify_borders=True):
7476
"""
7577
Map a 2D pattern mesh onto a 3D target.
7678
@@ -108,6 +110,8 @@ def map_pattern_to_mesh(name, mesh, clip_boundaries=True, tolerance=1e-6, patter
108110
The number of pattern vertices in the u direction.
109111
pattern_v : int
110112
The number of pattern vertices in the v direction.
113+
simplify_borders : bool
114+
Whether to simplify the border of the pattern mesh.
111115
112116
Returns
113117
-------
@@ -165,6 +169,6 @@ def map_pattern_to_mesh(name, mesh, clip_boundaries=True, tolerance=1e-6, patter
165169
pf = tessagon_mesh["face_list"]
166170

167171
v, f = mesh.to_vertices_and_faces()
168-
mapped_vertices, mapped_faces, mapped_normals, mapped_is_boundary, mapped_groups = map_mesh((v, f), (pv, pf), clip_boundaries=clip_boundaries, tolerance=tolerance)
172+
mapped_vertices, mapped_faces, mapped_normals, mapped_is_boundary, mapped_groups = map_mesh((v, f), (pv, pf), clip_boundaries=clip_boundaries, simplify_borders=simplify_borders, tolerance=tolerance)
169173

170174
return Mesh.from_vertices_and_faces(mapped_vertices, mapped_faces)

src/mapping.cpp

Lines changed: 111 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#include "mapping.hpp"
2-
2+
#include <algorithm>
3+
#include <iostream>
4+
#include <iomanip>
5+
#include <cmath>
6+
#include <unordered_set>
37

48
// Custom hash function for tuple
59
struct TupleHash {
@@ -18,13 +22,10 @@ std::tuple<int64_t, int64_t> grid_key(double x, double y, double tolerance) {
1822
};
1923
}
2024

21-
bool is_same_point(double x1, double y1, double z1,
22-
double x2, double y2, double z2,
23-
double tol) {
24-
double dx = x1 - x2;
25-
double dy = y1 - y2;
26-
double dz = z1 - z2;
27-
return (dx*dx + dy*dy + dz*dz) < (tol*tol);
25+
bool is_same_point(double x1, double y1, double x2, double y2, double tol) {
26+
double dx = x1 - x2;
27+
double dy = y1 - y2;
28+
return (dx*dx + dy*dy) < (tol*tol);
2829
}
2930

3031
std::vector<std::vector<int>> map_mesh_cropped(
@@ -98,7 +99,7 @@ std::vector<std::vector<int>> map_mesh_cropped(
9899
}
99100

100101
// Update pattern vertex position through barycentric interpolation, comment this out if you keep 2D pattern
101-
pattern_v.row(id) = A * L(0, 0) + B * L(0, 1) + C * L(0, 2);
102+
// pattern_v.row(id) = A * L(0, 0) + B * L(0, 1) + C * L(0, 2);
102103

103104
// If normals are requested, interpolate them using the same barycentric coordinates
104105
if (compute_normals) {
@@ -166,13 +167,16 @@ bool paths_intersect(const Clipper2Lib::PathsD& paths1, const Clipper2Lib::Paths
166167
}
167168

168169

170+
171+
169172
std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, std::vector<bool>, std::vector<int>> eigen_to_clipper (
170173
Eigen::Ref<const compas::RowMatrixXd> flattned_target_uv,
171174
Eigen::Ref<const compas::RowMatrixXi> target_f,
172175

173176
Eigen::Ref<const compas::RowMatrixXd> pattern_v,
174177
const std::vector<std::vector<int>>& pattern_f,
175178
bool clip_boundaries,
179+
bool simplify_borders,
176180
double tolerance
177181
)
178182
{
@@ -217,21 +221,18 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, std::vector<bool>
217221
path.emplace_back(pattern_v(point_id, 0), pattern_v(point_id, 1));
218222
paths.emplace_back(path);
219223

224+
if (!clip_boundaries){
225+
patterns_to_keep.push_back(paths);
226+
continue;
227+
}
228+
220229
// Get bounds of current pattern polygon
221230
Clipper2Lib::RectD pattern_bounds = Clipper2Lib::GetBounds(paths);
222231

223232
// Check if the pattern bounds intersect with boundary bounds
224233
if (!pattern_bounds.Intersects(boundary_bounds))
225234
continue;
226-
227-
// Check if Elements are inside the boundary
228-
// Clipper2Lib::PointD corners[] = {
229-
// {pattern_bounds.left, pattern_bounds.top}, // Top-left
230-
// {pattern_bounds.right, pattern_bounds.top}, // Top-right
231-
// {pattern_bounds.right, pattern_bounds.bottom}, // Bottom-right
232-
// {pattern_bounds.left, pattern_bounds.bottom} // Bottom-left
233-
// };
234-
235+
235236
bool is_rect_inside_polygon = false;
236237
size_t num_corners_in_polygon = 0;
237238
for (const auto& corner : paths[0]) {
@@ -241,8 +242,6 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, std::vector<bool>
241242
is_rect_inside_polygon = true;
242243
bool has_collision = paths_intersect(paths, boundary);
243244

244-
245-
246245
// check if the point is not in a hole
247246
bool is_rect_outside_polygon_hole = true;
248247
for (size_t i = 1; i < boundary.size(); i++) {
@@ -274,12 +273,10 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, std::vector<bool>
274273
// Fully enclosed polygons are added to the keep list
275274
// Other polygons edges are intersected with the boundary
276275

277-
if (num_corners_in_polygon == paths[0].size() && !is_hole_in_polygon){
276+
if (num_corners_in_polygon == paths[0].size() && !is_hole_in_polygon){ //
278277
patterns_to_keep.push_back(paths);
279-
}else if (clip_boundaries){
280-
if (paths_intersect(paths, boundary) || is_hole_in_polygon){
281-
patterns_to_cut.push_back(paths);
282-
}
278+
}else if(num_corners_in_polygon > 0 || paths_intersect(paths, boundary) || is_hole_in_polygon){
279+
patterns_to_cut.push_back(paths);
283280
}
284281

285282

@@ -290,10 +287,56 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, std::vector<bool>
290287
std::vector<Clipper2Lib::PathsD> solutions;
291288
for (const auto &subject : patterns_to_cut){
292289
Clipper2Lib::PathsD solution = Clipper2Lib::Intersect(subject, boundary, Clipper2Lib::FillRule::NonZero, 8);
293-
if (solution.size() == 0)
290+
291+
if (solution.size() == 0){
292+
// If no intersection is found, skip this polygon
294293
continue;
294+
}else if (solution.size() > 1 || !simplify_borders){
295+
// Highly likely we do not consider polygons with holes, we do not simplify them.
296+
solutions.push_back(solution);
297+
}else{
298+
// After boolean intersection the pattern is merged with boundary polygons. This is often not wanted.
299+
// Simplification is made by comparing polygon after boolean intersection and before. If the points lie on the initial polygon edges we keep them.
300+
// Additionally we add an option to keep arbitrary points from a user given points or boundary valence points.
301+
Clipper2Lib::PathsD simplified_paths{Clipper2Lib::PathD()};
302+
simplified_paths[0].reserve(subject[0].size());
303+
304+
for (const auto &p : solution[0]){ // iterate the points of the intersection polygon
305+
// First check if point is close to any vertex (corner)
306+
bool point_added = false;
307+
for(size_t i = 0; i < subject[0].size(); i++){
308+
double dx = p.x - subject[0][i].x;
309+
double dy = p.y - subject[0][i].y;
310+
if (dx*dx + dy*dy < tolerance*tolerance){
311+
simplified_paths[0].push_back(p);
312+
point_added = true;
313+
break;
314+
}
315+
}
316+
317+
// If not close to a vertex, check if close to any edge
318+
if (!point_added) {
319+
for(size_t i = 0; i < subject[0].size()-1; i++){
320+
if (Clipper2Lib::PerpendicDistFromLineSqrd(p, subject[0][i], subject[0][i+1]) < tolerance*tolerance){
321+
simplified_paths[0].push_back(p);
322+
break;
323+
}
324+
}
325+
// Also check the closing edge between last and first point
326+
if (subject[0].size() > 1){
327+
size_t last = subject[0].size()-1;
328+
if (Clipper2Lib::PerpendicDistFromLineSqrd(p, subject[0][last], subject[0][0]) < tolerance*tolerance){
329+
simplified_paths[0].push_back(p);
330+
}
331+
}
332+
}
333+
}
334+
335+
if (simplified_paths[0].size() > 2)
336+
solutions.push_back(simplified_paths);
337+
}
295338

296-
solutions.push_back(solution);
339+
297340
}
298341

299342
std::vector<std::array<double, 3>> unique_points;
@@ -305,6 +348,7 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, std::vector<bool>
305348
groups.reserve(solutions.size() + patterns_to_keep.size());
306349
is_boundary.reserve(solutions.size() + patterns_to_keep.size());
307350

351+
// 9 bucket search to avoid rounding wrong rounded grid key, when the distance is close to the tolerance.
308352
auto find_or_add_point = [&](double x, double y) -> int {
309353
double z = 0.0;
310354

@@ -323,7 +367,7 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, std::vector<bool>
323367
// Check all points in this cell
324368
for (int idx : grid_it->second) {
325369
const auto& pt = unique_points[idx];
326-
if (is_same_point(pt[0], pt[1], pt[2], x, y, z, tolerance))
370+
if (is_same_point(pt[0], pt[1], x, y, tolerance))
327371
return idx;
328372
}
329373
}
@@ -378,12 +422,15 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, std::vector<bool>
378422

379423
}
380424

425+
426+
381427
std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, compas::RowMatrixXd, std::vector<bool>, std::vector<int>> map_mesh_with_automatic_parameterization(
382428
Eigen::Ref<const compas::RowMatrixXd> target_v,
383429
Eigen::Ref<const compas::RowMatrixXi> target_f,
384430
Eigen::Ref<compas::RowMatrixXd> pattern_v,
385431
const std::vector<std::vector<int>>& pattern_f,
386432
bool clip_boundaries,
433+
bool simplify_borders,
387434
double tolerance)
388435
{
389436

@@ -395,6 +442,35 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, compas::RowMatrix
395442
Eigen::VectorXi B;
396443
igl::boundary_loop(target_f, B);
397444

445+
// Get Boundary vertices
446+
std::vector<Eigen::Vector3d> corner_vertex_coords;
447+
448+
std::vector<std::vector<int>> VV;
449+
igl::adjacency_list(target_f, VV);
450+
451+
// Create a set of boundary vertices for faster lookup
452+
std::unordered_set<int> boundary_set;
453+
for (int i = 0; i < B.size(); i++) {
454+
boundary_set.insert(B(i));
455+
}
456+
457+
for (int i = 0; i < B.size(); i++) {
458+
int v = B(i);
459+
int boundary_valence = 0;
460+
for (int neighbor : VV[v])
461+
if (boundary_set.count(neighbor) > 0)
462+
boundary_valence++;
463+
464+
if (boundary_valence < 2) {
465+
Eigen::Vector3d vertex_pos(target_v(v, 0), target_v(v, 1), target_v(v, 2));
466+
corner_vertex_coords.push_back(vertex_pos);
467+
std::cout << "Corner vertex " << v << " at position ("
468+
<< target_v(v, 0) << ", "
469+
<< target_v(v, 1) << ", "
470+
<< target_v(v, 2) << ")" << std::endl;
471+
}
472+
}
473+
398474
// Fix two points on the boundary
399475
Eigen::VectorXi fixed(2, 1);
400476
fixed(0) = B(0);
@@ -415,10 +491,10 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, compas::RowMatrix
415491
double scale_factor = 1.0 / std::max(size(0), size(1)); // Scale to fit in a 0-1 box while maintaining aspect ratio
416492
target_uv *= scale_factor;
417493

418-
// Clip the pattern
419-
auto [clipped_pattern_v, clipped_pattern_f, clipped_pattern_is_boundary, clipped_pattern_groups] = eigen_to_clipper(target_uv, target_f, pattern_v, pattern_f, clip_boundaries, tolerance);
494+
420495

421-
// return std::make_tuple(clipped_pattern_v, clipped_pattern_f); // Comment this out to see 2d cropped pattern
496+
// Clip the pattern
497+
auto [clipped_pattern_v, clipped_pattern_f, clipped_pattern_is_boundary, clipped_pattern_groups] = eigen_to_clipper(target_uv, target_f, pattern_v, pattern_f, clip_boundaries, simplify_borders, tolerance);
422498

423499
Eigen::MatrixXd clipped_pattern_uv;
424500
clipped_pattern_uv.setZero();
@@ -438,15 +514,13 @@ std::tuple<compas::RowMatrixXd, std::vector<std::vector<int>>, compas::RowMatrix
438514
clipped_pattern_uv,
439515
pattern_normals);
440516

441-
442517
return std::make_tuple(clipped_pattern_v, result, pattern_normals, clipped_pattern_is_boundary, clipped_pattern_groups);
443518

444519
}
445520

446-
521+
// Define the nanobind module at global scope
447522
NB_MODULE(_mapping, m)
448523
{
449-
450524
m.def(
451525
"map_mesh_with_automatic_parameterization",
452526
&map_mesh_with_automatic_parameterization,
@@ -456,5 +530,7 @@ NB_MODULE(_mapping, m)
456530
"pattern_v"_a,
457531
"pattern_f"_a,
458532
"clip_boundaries"_a,
459-
"tolerance"_a);
533+
"simplify_borders"_a,
534+
"tolerance"_a
535+
);
460536
}

0 commit comments

Comments
 (0)