Skip to content

Commit 206cf2f

Browse files
authored
Merge pull request #20 from Loop3D/fix/speed-up
fix: add direct calls to geometry attributes e.g. area, points
2 parents 70f95fa + a5f3440 commit 206cf2f

7 files changed

Lines changed: 810 additions & 47 deletions

File tree

loop_cgal/__init__.py

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import copy
34
from typing import Tuple
45

56
import numpy as np
@@ -20,12 +21,20 @@ class TriMesh(_TriMesh):
2021
Inherits from the base TriMesh class and provides additional functionality.
2122
"""
2223

23-
def __init__(self, surface: pv.PolyData):
24+
def __init__(self, surface):
25+
if isinstance(surface, _TriMesh):
26+
# Copy-construct directly from another TriMesh at the C++ level.
27+
# This is used by read_from_file and anywhere a TriMesh-to-TriMesh
28+
# copy is needed without a pyvista round-trip.
29+
super().__init__(surface)
30+
return
31+
2432
# Validate input surface
2533
validate_pyvista_polydata(surface, "input surface")
2634

2735
# Triangulate to ensure we have triangular faces
28-
surface = surface.triangulate()
36+
if not surface.is_all_triangles:
37+
surface = surface.triangulate()
2938

3039
# Extract vertices and triangles
3140
verts = np.array(surface.points, dtype=np.float64).copy()
@@ -34,6 +43,7 @@ def __init__(self, surface: pv.PolyData):
3443
raise ValueError("Invalid surface geometry")
3544

3645
super().__init__(verts, faces)
46+
3747
@classmethod
3848
def from_vertices_and_triangles(
3949
cls, vertices: np.ndarray, triangles: np.ndarray
@@ -56,7 +66,10 @@ def from_vertices_and_triangles(
5666
# Create a temporary PyVista PolyData object for validation
5767
if not validate_vertices_and_faces(vertices, triangles):
5868
raise ValueError("Invalid vertices or triangles")
59-
surface = pv.PolyData(vertices, np.hstack((np.full((triangles.shape[0], 1), 3), triangles)).flatten())
69+
surface = pv.PolyData(
70+
vertices,
71+
np.hstack((np.full((triangles.shape[0], 1), 3), triangles)).flatten(),
72+
)
6073
return cls(surface)
6174

6275
def get_vertices_and_triangles(
@@ -107,13 +120,62 @@ def vtk(
107120
"""
108121
return self.to_pyvista(area_threshold, duplicate_vertex_threshold)
109122

110-
def copy(self) -> TriMesh:
123+
@property
124+
def area(self) -> float:
125+
"""Surface area computed directly from the CGAL mesh (no pyvista conversion)."""
126+
return self._cgal_area()
127+
128+
@property
129+
def points(self) -> np.ndarray:
130+
"""Vertex coordinates as (n, 3) numpy array (no pyvista conversion)."""
131+
return self._cgal_points()
132+
133+
@property
134+
def n_cells(self) -> int:
135+
"""Number of faces in the CGAL mesh (no pyvista conversion)."""
136+
return self._cgal_n_faces()
137+
138+
@property
139+
def n_points(self) -> int:
140+
"""Number of vertices in the CGAL mesh (no pyvista conversion)."""
141+
return self._cgal_n_vertices()
142+
143+
@classmethod
144+
def read_from_file(cls, path: str) -> "TriMesh":
145+
"""Read a mesh from a binary file written by :meth:`write_to_file`.
146+
147+
Bypasses the pyvista/VTK stack entirely — much faster for temp files
148+
passed between CGAL operations.
111149
"""
112-
Create a copy of the TriMesh.
150+
return cls(_TriMesh._read_from_file(path))
151+
152+
def clone(self) -> TriMesh:
153+
"""
154+
Return a deep copy of this TriMesh as a Python ``TriMesh`` instance.
155+
156+
Uses the C++ copy-constructor binding to clone the CGAL mesh directly,
157+
with no numpy array roundtrip.
158+
"""
159+
return TriMesh(self)
160+
161+
def copy(self, deep: bool = True) -> TriMesh:
162+
"""
163+
Return a deep copy of this TriMesh.
164+
165+
Uses the C++-level ``clone()`` to copy the CGAL mesh directly without
166+
a pyvista round-trip, preserving all vertices, faces, and fixed edges.
113167
114168
Returns
115169
-------
116170
TriMesh
117-
A copy of the TriMesh object.
171+
A deep copy of this TriMesh.
118172
"""
119-
return TriMesh(self.to_pyvista())
173+
return self.clone()
174+
175+
def __copy__(self) -> TriMesh:
176+
return self.clone()
177+
178+
def __deepcopy__(self, memo: dict) -> TriMesh:
179+
result = self.clone()
180+
memo[id(self)] = result
181+
return result

loop_cgal/bindings.cpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ PYBIND11_MODULE(_loop_cgal, m)
2222
py::class_<TriMesh>(m, "TriMesh")
2323
.def(py::init<const pybind11::array_t<double> &, const pybind11::array_t<int> &>(),
2424
py::arg("vertices"), py::arg("triangles"))
25+
.def(py::init([](const TriMesh& other) { return other.clone(); }),
26+
py::arg("other"),
27+
"Copy-construct a TriMesh as a deep clone of another (no array roundtrip).")
28+
.def("clip_with_plane", &TriMesh::clipWithPlane,
29+
py::arg("a"), py::arg("b"), py::arg("c"), py::arg("d"),
30+
py::arg("use_exact_kernel") = true,
31+
"Clip the mesh with the halfspace ax+by+cz+d <= 0. "
32+
"Uses PMP::clip(mesh, Plane_3) directly — no corefinement, no skirt construction. "
33+
"Returns the number of faces removed (0 = no-op).")
2534
.def("cut_with_surface", &TriMesh::cutWithSurface, py::arg("surface"),
2635
py::arg("preserve_intersection") = false,
2736
py::arg("preserve_intersection_clipper") = false,
@@ -42,6 +51,21 @@ PYBIND11_MODULE(_loop_cgal, m)
4251
"Vertex index pairs defining edges to be fixed in mesh when remeshing.")
4352
.def("cut_with_implicit_function", &TriMesh::cut_with_implicit_function,
4453
py::arg("property"), py::arg("value"),py::arg("cutmode") = ImplicitCutMode::KEEP_POSITIVE_SIDE,
45-
"Cut the mesh with an implicit function defined by vertex properties.");
54+
"Cut the mesh with an implicit function defined by vertex properties.")
55+
.def("_cgal_area", &TriMesh::area, "Surface area computed directly from CGAL mesh.")
56+
.def("_cgal_n_faces", &TriMesh::n_faces, "Number of faces in the CGAL mesh.")
57+
.def("_cgal_n_vertices", &TriMesh::n_vertices, "Number of vertices in the CGAL mesh.")
58+
.def("_cgal_points", &TriMesh::get_points, "Vertex coordinates as (n, 3) numpy array.")
59+
.def("overlaps", &TriMesh::overlaps, py::arg("other"), py::arg("bbox_tol") = 1e-6,
60+
"Return True if this mesh intersects another TriMesh (AABB fast-reject + triangle-level check).")
61+
.def("clone", &TriMesh::clone,
62+
"Return a deep copy of this TriMesh at the C++ level (no pyvista round-trip). "
63+
"Preserves the full fixed-edge set including any user-added edges.")
64+
.def("write_to_file", &TriMesh::write_to_file, py::arg("path"),
65+
"Write the mesh to a compact binary file (LCMESH format). "
66+
"Much faster than converting to pyvista and saving as VTK.")
67+
.def_static("_read_from_file", &TriMesh::read_from_file, py::arg("path"),
68+
"Read a mesh from a binary file written by write_to_file. "
69+
"Returns a _TriMesh — use TriMesh.read_from_file() from Python instead.");
4670

4771
} // End of PYBIND11_MODULE

loop_cgal/utils.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pyvista as pv
22
import numpy as np
33
import scipy.sparse as sp
4+
5+
46
def validate_pyvista_polydata(
57
surface: pv.PolyData, surface_name: str = "surface"
68
) -> None:
@@ -80,19 +82,10 @@ def validate_vertices_and_faces(verts, faces):
8082
# build a ntris x nverts matrix
8183
# populate with true for vertex in each triangle
8284
# sum rows and if not equal to 3 then it is degenerate
83-
face_idx = np.arange(faces.shape[0])
84-
face_idx = np.tile(face_idx, (3, 1)).T.flatten()
85-
faces_flat = faces.flatten()
86-
m = sp.coo_matrix(
87-
(np.ones(faces_flat.shape[0]), (faces_flat, face_idx)),
88-
shape=(verts.shape[0], faces.shape[0]),
89-
dtype=bool,
90-
)
91-
# coo duplicates entries so just make sure its boolean
92-
m = m > 0
93-
if not np.all(m.sum(axis=0) == 3):
94-
degen_idx = np.where(m.sum(axis=0) != 3)[1]
85+
a, b, c = faces[:, 0], faces[:, 1], faces[:, 2]
86+
if np.any((a == b) | (b == c) | (a == c)):
87+
degen_idx = np.where((a == b) | (b == c) | (a == c))
9588
raise ValueError(
9689
f"Surface contains degenerate triangles: {degen_idx} (each triangle must have exactly 3 vertices)"
9790
)
98-
return True
91+
return True

0 commit comments

Comments
 (0)