chilmesh-cpp 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.15)
|
|
2
|
+
project(chilmesh_cpp CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
|
|
7
|
+
find_package(pybind11 REQUIRED)
|
|
8
|
+
|
|
9
|
+
pybind11_add_module(chilmesh_cpp
|
|
10
|
+
src/halfedge.cpp
|
|
11
|
+
src/bindings.cpp
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# -march=native bakes the build machine's CPU instruction set into the wheel,
|
|
15
|
+
# which SIGILLs on older consumer CPUs — never use it for distributable wheels.
|
|
16
|
+
# Default to portable -O3; opt in to native tuning only for local builds.
|
|
17
|
+
option(CHILMESH_NATIVE_ARCH "Tune for the build machine's CPU (non-portable; do NOT use for distributable wheels)" OFF)
|
|
18
|
+
target_compile_options(chilmesh_cpp PRIVATE -O3)
|
|
19
|
+
if(CHILMESH_NATIVE_ARCH)
|
|
20
|
+
target_compile_options(chilmesh_cpp PRIVATE -march=native)
|
|
21
|
+
endif()
|
|
22
|
+
target_include_directories(chilmesh_cpp PRIVATE src)
|
|
23
|
+
|
|
24
|
+
# scikit-build-core only packages what CMake installs. Without this rule the
|
|
25
|
+
# compiled module never lands in the wheel and `import chilmesh_cpp` resolves
|
|
26
|
+
# to an empty namespace stub (CHILmesh #163).
|
|
27
|
+
install(TARGETS chilmesh_cpp LIBRARY DESTINATION . RUNTIME DESTINATION .)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["scikit-build-core>=0.3", "pybind11>=2.10"]
|
|
3
|
+
build-backend = "scikit_build_core.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chilmesh-cpp"
|
|
7
|
+
version = "0.6.0"
|
|
8
|
+
description = "C++ half-edge backend for CHILmesh"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
|
|
11
|
+
[tool.scikit-build]
|
|
12
|
+
cmake.build-type = "Release"
|
|
13
|
+
wheel.packages = []
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Functional smoke test for a built chilmesh_cpp wheel (CIBW_TEST_COMMAND).
|
|
2
|
+
|
|
3
|
+
Lives in a file, not an inline ``python -c`` string, because the Windows leg
|
|
4
|
+
runs the test command through cmd.exe, where nested quoting of a multi-statement
|
|
5
|
+
one-liner is fragile (#256).
|
|
6
|
+
|
|
7
|
+
Runs ``full_init`` on a 2-triangle unit square and asserts the topology
|
|
8
|
+
invariants: 4 verts, 2 elems, 5 unique edges (4 boundary + 1 shared diagonal),
|
|
9
|
+
adjacency built, at least one peel layer, edge2vert shape (5, 2).
|
|
10
|
+
"""
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
import chilmesh_cpp
|
|
14
|
+
|
|
15
|
+
pts = np.array([[0., 0.], [1., 0.], [1., 1.], [0., 1.]], dtype=np.float64)
|
|
16
|
+
conn = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int32)
|
|
17
|
+
m = chilmesh_cpp.full_init(pts, conn)
|
|
18
|
+
assert m.n_verts == 4, m.n_verts
|
|
19
|
+
assert m.n_elems == 2, m.n_elems
|
|
20
|
+
assert m.n_edges == 5, m.n_edges
|
|
21
|
+
assert m.adjacency_built, "adjacency not built"
|
|
22
|
+
assert m.n_layers >= 1, m.n_layers
|
|
23
|
+
assert np.asarray(m.edge2vert).shape == (5, 2), np.asarray(m.edge2vert).shape
|
|
24
|
+
print("chilmesh_cpp full_init smoke OK:", m.n_verts, "v", m.n_elems, "e",
|
|
25
|
+
m.n_edges, "edges", m.n_layers, "layers")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#include <pybind11/pybind11.h>
|
|
2
|
+
#include <pybind11/numpy.h>
|
|
3
|
+
#include <pybind11/stl.h>
|
|
4
|
+
#include "halfedge.hpp"
|
|
5
|
+
|
|
6
|
+
namespace py = pybind11;
|
|
7
|
+
using namespace chilmesh;
|
|
8
|
+
|
|
9
|
+
// Helper: build mesh from numpy arrays
|
|
10
|
+
static HalfEdgeMesh* _build(
|
|
11
|
+
py::array_t<double, py::array::c_style | py::array::forcecast> points,
|
|
12
|
+
py::array_t<int32_t, py::array::c_style | py::array::forcecast> connectivity) {
|
|
13
|
+
|
|
14
|
+
auto pts = points.unchecked<2>();
|
|
15
|
+
auto conn = connectivity.unchecked<2>();
|
|
16
|
+
|
|
17
|
+
int n_pts = (int)pts.shape(0);
|
|
18
|
+
int n_el = (int)conn.shape(0);
|
|
19
|
+
int vpe = (int)conn.shape(1);
|
|
20
|
+
|
|
21
|
+
// Pack points as x0..xN y0..yN for build()
|
|
22
|
+
std::vector<double> flat_pts(n_pts * 2);
|
|
23
|
+
for (int i = 0; i < n_pts; ++i) {
|
|
24
|
+
flat_pts[i] = pts(i, 0);
|
|
25
|
+
flat_pts[n_pts + i] = pts(i, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Flatten connectivity
|
|
29
|
+
std::vector<int32_t> flat_conn(n_el * vpe);
|
|
30
|
+
for (int e = 0; e < n_el; ++e)
|
|
31
|
+
for (int j = 0; j < vpe; ++j)
|
|
32
|
+
flat_conn[e * vpe + j] = conn(e, j);
|
|
33
|
+
|
|
34
|
+
auto* mesh = new HalfEdgeMesh();
|
|
35
|
+
mesh->build(flat_pts.data(), n_pts, flat_conn.data(), n_el, vpe);
|
|
36
|
+
return mesh;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
PYBIND11_MODULE(chilmesh_cpp, m) {
|
|
40
|
+
m.doc() = "CHILmesh C++ half-edge backend";
|
|
41
|
+
|
|
42
|
+
py::class_<HalfEdgeMesh>(m, "CppMesh")
|
|
43
|
+
.def_readonly("n_verts", &HalfEdgeMesh::n_verts)
|
|
44
|
+
.def_readonly("n_elems", &HalfEdgeMesh::n_elems)
|
|
45
|
+
.def_readonly("n_edges", &HalfEdgeMesh::n_edges)
|
|
46
|
+
.def_readonly("adjacency_built", &HalfEdgeMesh::adjacency_built)
|
|
47
|
+
.def_property_readonly("n_layers", [](const HalfEdgeMesh& m) {
|
|
48
|
+
return (int)m.layers.size();
|
|
49
|
+
})
|
|
50
|
+
.def_property_readonly("edge2vert", [](const HalfEdgeMesh& m) {
|
|
51
|
+
if (m.edge2vert.empty()) {
|
|
52
|
+
std::vector<py::ssize_t> sh = {0, 2};
|
|
53
|
+
return py::array_t<int32_t>(sh);
|
|
54
|
+
}
|
|
55
|
+
return py::array_t<int32_t>(
|
|
56
|
+
{(py::ssize_t)m.n_edges, (py::ssize_t)2},
|
|
57
|
+
m.edge2vert.data());
|
|
58
|
+
})
|
|
59
|
+
.def_property_readonly("elem2edge", [](const HalfEdgeMesh& m) {
|
|
60
|
+
if (m.elem2edge.empty()) {
|
|
61
|
+
std::vector<py::ssize_t> sh = {0, (py::ssize_t)m.max_vpe};
|
|
62
|
+
return py::array_t<int32_t>(sh);
|
|
63
|
+
}
|
|
64
|
+
return py::array_t<int32_t>(
|
|
65
|
+
{(py::ssize_t)m.n_elems, (py::ssize_t)m.max_vpe},
|
|
66
|
+
m.elem2edge.data());
|
|
67
|
+
})
|
|
68
|
+
.def_property_readonly("vert2edge", [](const HalfEdgeMesh& m) {
|
|
69
|
+
py::list result;
|
|
70
|
+
for (const auto& ve : m.vert2edge) {
|
|
71
|
+
result.append(py::array_t<int32_t>(ve.size(), ve.data()));
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
})
|
|
75
|
+
.def_property_readonly("layers", [](const HalfEdgeMesh& m) {
|
|
76
|
+
py::list result;
|
|
77
|
+
for (const auto& ld : m.layers) {
|
|
78
|
+
py::dict layer;
|
|
79
|
+
layer["OE"] = py::array_t<int32_t>(ld.OE.size(), ld.OE.data());
|
|
80
|
+
layer["IE"] = py::array_t<int32_t>(ld.IE.size(), ld.IE.data());
|
|
81
|
+
layer["OV"] = py::array_t<int32_t>(ld.OV.size(), ld.OV.data());
|
|
82
|
+
layer["IV"] = py::array_t<int32_t>(ld.IV.size(), ld.IV.data());
|
|
83
|
+
layer["bEdgeIDs"] = py::array_t<int32_t>(ld.bEdgeIDs.size(), ld.bEdgeIDs.data());
|
|
84
|
+
result.append(layer);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// fast_init: build half-edge + CCW check, no adjacency
|
|
90
|
+
m.def("fast_init", [](
|
|
91
|
+
py::array_t<double, py::array::c_style | py::array::forcecast> points,
|
|
92
|
+
py::array_t<int32_t, py::array::c_style | py::array::forcecast> connectivity) {
|
|
93
|
+
return _build(points, connectivity);
|
|
94
|
+
}, py::return_value_policy::take_ownership,
|
|
95
|
+
"Build half-edge mesh (no adjacency). Returns CppMesh.");
|
|
96
|
+
|
|
97
|
+
// full_init: build + adjacency + skeletonize
|
|
98
|
+
m.def("full_init", [](
|
|
99
|
+
py::array_t<double, py::array::c_style | py::array::forcecast> points,
|
|
100
|
+
py::array_t<int32_t, py::array::c_style | py::array::forcecast> connectivity) {
|
|
101
|
+
auto* mesh = _build(points, connectivity);
|
|
102
|
+
mesh->build_adjacency();
|
|
103
|
+
mesh->skeletonize();
|
|
104
|
+
return mesh;
|
|
105
|
+
}, py::return_value_policy::take_ownership,
|
|
106
|
+
"Build half-edge mesh with adjacency and skeletonization. Returns CppMesh.");
|
|
107
|
+
|
|
108
|
+
// quality_analysis: signed areas
|
|
109
|
+
m.def("quality_analysis", [](const HalfEdgeMesh& mesh) {
|
|
110
|
+
auto areas = mesh.signed_areas();
|
|
111
|
+
return py::array_t<double>(areas.size(), areas.data());
|
|
112
|
+
}, "Compute signed area for each element. Returns numpy float64 array.");
|
|
113
|
+
|
|
114
|
+
// get_vertex_edges
|
|
115
|
+
m.def("get_vertex_edges", [](const HalfEdgeMesh& mesh, int vertex_id) {
|
|
116
|
+
if (!mesh.adjacency_built)
|
|
117
|
+
throw std::runtime_error("Adjacency not built. Use full_init.");
|
|
118
|
+
if (vertex_id < 0 || vertex_id >= mesh.n_verts)
|
|
119
|
+
throw std::out_of_range("vertex_id out of range");
|
|
120
|
+
const auto& ve = mesh.vert2edge[vertex_id];
|
|
121
|
+
return py::array_t<int32_t>(ve.size(), ve.data());
|
|
122
|
+
}, "Return edge IDs incident to vertex.");
|
|
123
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#include "halfedge.hpp"
|
|
2
|
+
#include <unordered_map>
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cmath>
|
|
5
|
+
#include <cassert>
|
|
6
|
+
|
|
7
|
+
namespace chilmesh {
|
|
8
|
+
|
|
9
|
+
// Pack two 32-bit ints into one 64-bit key (canonical: lo < hi)
|
|
10
|
+
static inline uint64_t edge_key(int32_t a, int32_t b) {
|
|
11
|
+
if (a > b) std::swap(a, b);
|
|
12
|
+
return (static_cast<uint64_t>(a) << 32) | static_cast<uint64_t>(b);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
void HalfEdgeMesh::build(const double* pts, int n_pts,
|
|
16
|
+
const int32_t* conn, int n_el, int vpe) {
|
|
17
|
+
n_verts = n_pts;
|
|
18
|
+
n_elems = n_el;
|
|
19
|
+
max_vpe = vpe;
|
|
20
|
+
|
|
21
|
+
px.assign(pts, pts + n_pts);
|
|
22
|
+
py.assign(pts + n_pts, pts + 2 * n_pts); // caller packs x0..xN y0..yN
|
|
23
|
+
// Actually caller passes interleaved [n,2] — adjust in bindings
|
|
24
|
+
|
|
25
|
+
connectivity.assign(conn, conn + n_el * vpe);
|
|
26
|
+
|
|
27
|
+
// Allocate half-edges: each elem edge = 1 half-edge
|
|
28
|
+
// Each element with k real vertices has k half-edges
|
|
29
|
+
// triangles in quad mesh have 3 real edges, quads have 4
|
|
30
|
+
|
|
31
|
+
// Count real verts per elem
|
|
32
|
+
std::vector<int32_t> vpe_arr(n_el);
|
|
33
|
+
for (int e = 0; e < n_el; ++e) {
|
|
34
|
+
const int32_t* row = conn + e * vpe;
|
|
35
|
+
int k = vpe;
|
|
36
|
+
// -1 padding means triangle
|
|
37
|
+
while (k > 3 && row[k-1] == -1) --k;
|
|
38
|
+
vpe_arr[e] = k;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Total half-edges
|
|
42
|
+
int n_he = 0;
|
|
43
|
+
std::vector<int32_t> face_he_start(n_el);
|
|
44
|
+
for (int e = 0; e < n_el; ++e) {
|
|
45
|
+
face_he_start[e] = n_he;
|
|
46
|
+
n_he += vpe_arr[e];
|
|
47
|
+
}
|
|
48
|
+
half_edges.resize(n_he);
|
|
49
|
+
face_he = face_he_start;
|
|
50
|
+
|
|
51
|
+
// Fill in vert, face, next, prev for each face loop
|
|
52
|
+
for (int e = 0; e < n_el; ++e) {
|
|
53
|
+
const int32_t* row = conn + e * vpe;
|
|
54
|
+
int k = vpe_arr[e];
|
|
55
|
+
int base = face_he_start[e];
|
|
56
|
+
for (int i = 0; i < k; ++i) {
|
|
57
|
+
HalfEdge& he = half_edges[base + i];
|
|
58
|
+
he.vert = row[i];
|
|
59
|
+
he.face = e;
|
|
60
|
+
he.next = base + (i + 1) % k;
|
|
61
|
+
he.prev = base + (i + k - 1) % k;
|
|
62
|
+
he.twin = -1; // to be filled
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build twin map: directed edge (a→b) matches twin (b→a)
|
|
67
|
+
// Key: edge_key(a,b) with canonical order; store both directed HE ids
|
|
68
|
+
// Use map from directed edge to HE id
|
|
69
|
+
std::unordered_map<uint64_t, int32_t> dir_map;
|
|
70
|
+
dir_map.reserve(n_he * 2);
|
|
71
|
+
// directed key: a*N + b (but N can be large, use 64-bit pack differently)
|
|
72
|
+
auto dir_key = [&](int32_t a, int32_t b) -> uint64_t {
|
|
73
|
+
return (static_cast<uint64_t>(a) << 32) | static_cast<uint64_t>(b);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (int h = 0; h < n_he; ++h) {
|
|
77
|
+
int32_t a = half_edges[h].vert;
|
|
78
|
+
int32_t b = half_edges[half_edges[h].next].vert;
|
|
79
|
+
dir_map[dir_key(a, b)] = h;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Match twins
|
|
83
|
+
for (int h = 0; h < n_he; ++h) {
|
|
84
|
+
if (half_edges[h].twin != -1) continue;
|
|
85
|
+
int32_t a = half_edges[h].vert;
|
|
86
|
+
int32_t b = half_edges[half_edges[h].next].vert;
|
|
87
|
+
auto it = dir_map.find(dir_key(b, a));
|
|
88
|
+
if (it != dir_map.end()) {
|
|
89
|
+
int32_t t = it->second;
|
|
90
|
+
half_edges[h].twin = t;
|
|
91
|
+
half_edges[t].twin = h;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build vert_he: first HE with vert[h] == v
|
|
96
|
+
vert_he.assign(n_verts, -1);
|
|
97
|
+
for (int h = 0; h < n_he; ++h) {
|
|
98
|
+
int32_t v = half_edges[h].vert;
|
|
99
|
+
if (vert_he[v] == -1) vert_he[v] = h;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
void HalfEdgeMesh::build_adjacency() {
|
|
104
|
+
int n_he = (int)half_edges.size();
|
|
105
|
+
|
|
106
|
+
// Assign edge IDs: canonical undirected edges
|
|
107
|
+
// edge = min(h, twin(h)) for interior; boundary has twin=-1
|
|
108
|
+
std::vector<int32_t> he_to_edge(n_he, -1);
|
|
109
|
+
int edge_count = 0;
|
|
110
|
+
for (int h = 0; h < n_he; ++h) {
|
|
111
|
+
int32_t t = half_edges[h].twin;
|
|
112
|
+
if (t == -1 || h < t) {
|
|
113
|
+
he_to_edge[h] = edge_count++;
|
|
114
|
+
if (t != -1) he_to_edge[t] = he_to_edge[h];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
n_edges = edge_count;
|
|
118
|
+
|
|
119
|
+
// edge2vert: [n_edges, 2]
|
|
120
|
+
edge2vert.assign(n_edges * 2, -1);
|
|
121
|
+
for (int h = 0; h < n_he; ++h) {
|
|
122
|
+
int32_t t = half_edges[h].twin;
|
|
123
|
+
if (t == -1 || h < t) {
|
|
124
|
+
int32_t eid = he_to_edge[h];
|
|
125
|
+
int32_t a = half_edges[h].vert;
|
|
126
|
+
int32_t b = half_edges[half_edges[h].next].vert;
|
|
127
|
+
edge2vert[eid * 2] = std::min(a, b);
|
|
128
|
+
edge2vert[eid * 2 + 1] = std::max(a, b);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// elem2edge: [n_elems, max_vpe]
|
|
133
|
+
elem2edge.assign(static_cast<size_t>(n_elems) * max_vpe, -1);
|
|
134
|
+
for (int e = 0; e < n_elems; ++e) {
|
|
135
|
+
int base_he = face_he[e];
|
|
136
|
+
int k = max_vpe;
|
|
137
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
138
|
+
for (int i = 0; i < k; ++i) {
|
|
139
|
+
int h = base_he + i;
|
|
140
|
+
elem2edge[e * max_vpe + i] = he_to_edge[h];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// vert2edge: jagged
|
|
145
|
+
vert2edge.assign(n_verts, {});
|
|
146
|
+
for (int eid = 0; eid < n_edges; ++eid) {
|
|
147
|
+
int32_t v0 = edge2vert[eid * 2];
|
|
148
|
+
int32_t v1 = edge2vert[eid * 2 + 1];
|
|
149
|
+
vert2edge[v0].push_back(eid);
|
|
150
|
+
vert2edge[v1].push_back(eid);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
adjacency_built = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
void HalfEdgeMesh::skeletonize() {
|
|
157
|
+
if (!adjacency_built) build_adjacency();
|
|
158
|
+
|
|
159
|
+
// elem2edge is [n_elems * max_vpe]
|
|
160
|
+
// edge2elem: build from half-edges
|
|
161
|
+
std::vector<int32_t> edge2elem(n_edges * 2, -1);
|
|
162
|
+
for (int h = 0; h < (int)half_edges.size(); ++h) {
|
|
163
|
+
int32_t t = half_edges[h].twin;
|
|
164
|
+
int32_t eid = -1;
|
|
165
|
+
if (t == -1 || h < t) {
|
|
166
|
+
eid = -1; // canonical
|
|
167
|
+
}
|
|
168
|
+
// simpler: directly from elem2edge
|
|
169
|
+
}
|
|
170
|
+
// Build edge2elem from elem2edge
|
|
171
|
+
for (int e = 0; e < n_elems; ++e) {
|
|
172
|
+
int k = max_vpe;
|
|
173
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
174
|
+
for (int i = 0; i < k; ++i) {
|
|
175
|
+
int32_t eid = elem2edge[e * max_vpe + i];
|
|
176
|
+
if (eid < 0) continue;
|
|
177
|
+
if (edge2elem[eid * 2] == -1) edge2elem[eid * 2] = e;
|
|
178
|
+
else edge2elem[eid * 2 + 1] = e;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Identify boundary edges (twin == -1 → only one elem)
|
|
183
|
+
// active set of elements
|
|
184
|
+
std::vector<bool> active_elem(n_elems, true);
|
|
185
|
+
std::vector<int32_t> elem_layer(n_elems, -1);
|
|
186
|
+
|
|
187
|
+
// active edge boundary count per element
|
|
188
|
+
std::vector<int32_t> boundary_edge_count(n_elems, 0);
|
|
189
|
+
for (int e = 0; e < n_elems; ++e) {
|
|
190
|
+
int k = max_vpe;
|
|
191
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
192
|
+
for (int i = 0; i < k; ++i) {
|
|
193
|
+
int32_t eid = elem2edge[e * max_vpe + i];
|
|
194
|
+
if (eid < 0) continue;
|
|
195
|
+
bool is_boundary = (edge2elem[eid * 2] == -1 || edge2elem[eid * 2 + 1] == -1);
|
|
196
|
+
if (is_boundary) boundary_edge_count[e]++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
layers.clear();
|
|
201
|
+
int layer_idx = 0;
|
|
202
|
+
int remaining = n_elems;
|
|
203
|
+
|
|
204
|
+
// For vert active count
|
|
205
|
+
std::vector<int32_t> vert_active_elem_count(n_verts, 0);
|
|
206
|
+
for (int e = 0; e < n_elems; ++e) {
|
|
207
|
+
int k = max_vpe;
|
|
208
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
209
|
+
for (int i = 0; i < k; ++i) {
|
|
210
|
+
int32_t v = connectivity[e * max_vpe + i];
|
|
211
|
+
if (v >= 0) vert_active_elem_count[v]++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
while (remaining > 0) {
|
|
216
|
+
// Find outer elements: boundary_edge_count > 0
|
|
217
|
+
std::vector<int32_t> to_peel;
|
|
218
|
+
for (int e = 0; e < n_elems; ++e) {
|
|
219
|
+
if (active_elem[e] && boundary_edge_count[e] > 0) {
|
|
220
|
+
to_peel.push_back(e);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (to_peel.empty()) {
|
|
224
|
+
// no boundary elements — remaining are fully interior, force peel all
|
|
225
|
+
for (int e = 0; e < n_elems; ++e) {
|
|
226
|
+
if (active_elem[e]) to_peel.push_back(e);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
LayerData ld;
|
|
231
|
+
// OE = outer elements (those being peeled)
|
|
232
|
+
ld.OE = to_peel;
|
|
233
|
+
|
|
234
|
+
// Identify OV: vertices on the outer boundary of this layer
|
|
235
|
+
// = vertices that belong to peeled elems AND have an active boundary edge
|
|
236
|
+
std::vector<bool> is_peeled_vert(n_verts, false);
|
|
237
|
+
for (int e : to_peel) {
|
|
238
|
+
int k = max_vpe;
|
|
239
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
240
|
+
for (int i = 0; i < k; ++i) {
|
|
241
|
+
int32_t v = connectivity[e * max_vpe + i];
|
|
242
|
+
if (v >= 0) is_peeled_vert[v] = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// OV: vertices on the boundary frontier of this layer (boundary edges).
|
|
247
|
+
// bEdgeIDs: the boundary edges themselves (Python parity).
|
|
248
|
+
std::unordered_map<int32_t, bool> ov_set;
|
|
249
|
+
std::unordered_map<int32_t, bool> bedge_set;
|
|
250
|
+
for (int e : to_peel) {
|
|
251
|
+
int k = max_vpe;
|
|
252
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
253
|
+
for (int i = 0; i < k; ++i) {
|
|
254
|
+
int32_t eid = elem2edge[e * max_vpe + i];
|
|
255
|
+
if (eid < 0) continue;
|
|
256
|
+
bool is_boundary = (edge2elem[eid * 2] == -1 || edge2elem[eid * 2 + 1] == -1);
|
|
257
|
+
if (is_boundary) {
|
|
258
|
+
int32_t v0 = edge2vert[eid * 2];
|
|
259
|
+
int32_t v1 = edge2vert[eid * 2 + 1];
|
|
260
|
+
ov_set[v0] = true;
|
|
261
|
+
ov_set[v1] = true;
|
|
262
|
+
bedge_set[eid] = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (auto& [v, _] : ov_set) ld.OV.push_back(v);
|
|
267
|
+
for (auto& [eid, _] : bedge_set) ld.bEdgeIDs.push_back(eid);
|
|
268
|
+
|
|
269
|
+
// IE: inner elements = active, not-peeled elements that share an OV vertex
|
|
270
|
+
// (Python parity: IE is adjacent to OV only, NOT to every OE vertex)
|
|
271
|
+
std::vector<bool> is_peel_elem(n_elems, false);
|
|
272
|
+
for (int e : to_peel) is_peel_elem[e] = true;
|
|
273
|
+
std::vector<bool> is_ov_vert(n_verts, false);
|
|
274
|
+
for (int32_t v : ld.OV) is_ov_vert[v] = true;
|
|
275
|
+
for (int e = 0; e < n_elems; ++e) {
|
|
276
|
+
if (!active_elem[e] || is_peel_elem[e]) continue;
|
|
277
|
+
int k = max_vpe;
|
|
278
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
279
|
+
bool adjacent = false;
|
|
280
|
+
for (int i = 0; i < k && !adjacent; ++i) {
|
|
281
|
+
int32_t v = connectivity[e * max_vpe + i];
|
|
282
|
+
if (v >= 0 && is_ov_vert[v]) adjacent = true;
|
|
283
|
+
}
|
|
284
|
+
if (adjacent) ld.IE.push_back(e);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// IV: vertices of (OE ∪ IE) not in OV (Python parity — IE vertices count).
|
|
288
|
+
// Must run AFTER IE so vertices from inner elements are included.
|
|
289
|
+
for (int e : ld.IE) {
|
|
290
|
+
int k = max_vpe;
|
|
291
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
292
|
+
for (int i = 0; i < k; ++i) {
|
|
293
|
+
int32_t v = connectivity[e * max_vpe + i];
|
|
294
|
+
if (v >= 0) is_peeled_vert[v] = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (int v = 0; v < n_verts; ++v) {
|
|
298
|
+
if (is_peeled_vert[v] && ov_set.find(v) == ov_set.end()) {
|
|
299
|
+
ld.IV.push_back(v);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Python parity: each skeletonization iteration consumes BOTH the outer
|
|
304
|
+
// ring (OE) and the inner vertex-adjacent ring (IE). Build a single
|
|
305
|
+
// peel list combining both, then update inactive state once.
|
|
306
|
+
std::vector<int32_t> to_consume;
|
|
307
|
+
to_consume.reserve(ld.OE.size() + ld.IE.size());
|
|
308
|
+
for (int32_t e : ld.OE) to_consume.push_back(e);
|
|
309
|
+
for (int32_t e : ld.IE) to_consume.push_back(e);
|
|
310
|
+
|
|
311
|
+
layers.push_back(std::move(ld));
|
|
312
|
+
|
|
313
|
+
// Mark all consumed elements inactive, update boundary counts
|
|
314
|
+
for (int e : to_consume) {
|
|
315
|
+
active_elem[e] = false;
|
|
316
|
+
elem_layer[e] = layer_idx;
|
|
317
|
+
remaining--;
|
|
318
|
+
|
|
319
|
+
// Update vertex active counts
|
|
320
|
+
int k = max_vpe;
|
|
321
|
+
while (k > 3 && connectivity[e * max_vpe + k - 1] == -1) --k;
|
|
322
|
+
for (int i = 0; i < k; ++i) {
|
|
323
|
+
int32_t v = connectivity[e * max_vpe + i];
|
|
324
|
+
if (v >= 0) vert_active_elem_count[v]--;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// For each edge of e, find the neighbor and update its boundary count
|
|
328
|
+
for (int i = 0; i < k; ++i) {
|
|
329
|
+
int32_t eid = elem2edge[e * max_vpe + i];
|
|
330
|
+
if (eid < 0) continue;
|
|
331
|
+
int32_t nb = -1;
|
|
332
|
+
if (edge2elem[eid * 2] == e) nb = edge2elem[eid * 2 + 1];
|
|
333
|
+
else if (edge2elem[eid * 2 + 1] == e) nb = edge2elem[eid * 2];
|
|
334
|
+
if (nb >= 0 && active_elem[nb]) {
|
|
335
|
+
boundary_edge_count[nb]++;
|
|
336
|
+
// Mark this edge as now boundary
|
|
337
|
+
edge2elem[eid * 2] = -1;
|
|
338
|
+
edge2elem[eid * 2 + 1] = -1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
layer_idx++;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
std::vector<double> HalfEdgeMesh::signed_areas() const {
|
|
348
|
+
std::vector<double> areas(n_elems, 0.0);
|
|
349
|
+
for (int e = 0; e < n_elems; ++e) {
|
|
350
|
+
int k = max_vpe;
|
|
351
|
+
const int32_t* row = connectivity.data() + e * max_vpe;
|
|
352
|
+
while (k > 3 && row[k-1] == -1) --k;
|
|
353
|
+
// Shoelace formula for polygon
|
|
354
|
+
double area = 0.0;
|
|
355
|
+
for (int i = 0; i < k; ++i) {
|
|
356
|
+
int32_t v0 = row[i];
|
|
357
|
+
int32_t v1 = row[(i + 1) % k];
|
|
358
|
+
area += px[v0] * py[v1] - px[v1] * py[v0];
|
|
359
|
+
}
|
|
360
|
+
areas[e] = area * 0.5;
|
|
361
|
+
}
|
|
362
|
+
return areas;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
} // namespace chilmesh
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <vector>
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <stdexcept>
|
|
6
|
+
|
|
7
|
+
namespace chilmesh {
|
|
8
|
+
|
|
9
|
+
// Half-edge structure — flat array storage for cache efficiency
|
|
10
|
+
struct HalfEdge {
|
|
11
|
+
int32_t twin; // opposite half-edge (-1 if boundary)
|
|
12
|
+
int32_t next; // next half-edge in face loop
|
|
13
|
+
int32_t prev; // prev half-edge in face loop
|
|
14
|
+
int32_t vert; // origin vertex
|
|
15
|
+
int32_t face; // owning face (-1 if boundary)
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
struct LayerData {
|
|
19
|
+
std::vector<int32_t> OE; // outer elements
|
|
20
|
+
std::vector<int32_t> IE; // inner elements
|
|
21
|
+
std::vector<int32_t> OV; // outer vertices
|
|
22
|
+
std::vector<int32_t> IV; // inner vertices
|
|
23
|
+
std::vector<int32_t> bEdgeIDs; // boundary edges defining this layer's outer frontier
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class HalfEdgeMesh {
|
|
27
|
+
public:
|
|
28
|
+
// Geometry
|
|
29
|
+
std::vector<double> px, py; // vertex coords (flat, cache-friendly)
|
|
30
|
+
int32_t n_verts = 0;
|
|
31
|
+
int32_t n_elems = 0;
|
|
32
|
+
int32_t n_edges = 0;
|
|
33
|
+
|
|
34
|
+
// Half-edge topology
|
|
35
|
+
std::vector<HalfEdge> half_edges;
|
|
36
|
+
|
|
37
|
+
// CSR-style: first half-edge out of each vertex
|
|
38
|
+
std::vector<int32_t> vert_he; // size n_verts, -1 if none
|
|
39
|
+
|
|
40
|
+
// first half-edge of each face
|
|
41
|
+
std::vector<int32_t> face_he; // size n_elems
|
|
42
|
+
|
|
43
|
+
// elem2vert (connectivity): n_elems x max_vpe (-1 for triangles in 4-col)
|
|
44
|
+
std::vector<int32_t> connectivity; // row-major [n_elems * max_vpe]
|
|
45
|
+
int32_t max_vpe = 3; // vertices per element (3 or 4)
|
|
46
|
+
|
|
47
|
+
// Adjacency (populated in full_init)
|
|
48
|
+
std::vector<int32_t> edge2vert; // [n_edges * 2]
|
|
49
|
+
std::vector<int32_t> elem2edge; // [n_elems * max_vpe]
|
|
50
|
+
std::vector<std::vector<int32_t>> vert2edge; // jagged
|
|
51
|
+
|
|
52
|
+
// Skeletonization layers
|
|
53
|
+
std::vector<LayerData> layers;
|
|
54
|
+
|
|
55
|
+
bool adjacency_built = false;
|
|
56
|
+
|
|
57
|
+
// Build from points+connectivity arrays
|
|
58
|
+
void build(const double* pts, int n_pts,
|
|
59
|
+
const int32_t* conn, int n_el, int vpe);
|
|
60
|
+
|
|
61
|
+
// Build adjacency structures
|
|
62
|
+
void build_adjacency();
|
|
63
|
+
|
|
64
|
+
// Skeletonize (layer peeling)
|
|
65
|
+
void skeletonize();
|
|
66
|
+
|
|
67
|
+
// Quality analysis: returns signed area for each element
|
|
68
|
+
std::vector<double> signed_areas() const;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
} // namespace chilmesh
|