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,6 @@
1
+ Metadata-Version: 2.2
2
+ Name: chilmesh-cpp
3
+ Version: 0.6.0
4
+ Summary: C++ half-edge backend for CHILmesh
5
+ Requires-Python: >=3.10
6
+
@@ -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