mmgpy 0.5.0__cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
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.
- mmgpy/__init__.py +296 -0
- mmgpy/__main__.py +13 -0
- mmgpy/_io.py +535 -0
- mmgpy/_logging.py +290 -0
- mmgpy/_mesh.py +2286 -0
- mmgpy/_mmgpy.cpython-311-x86_64-linux-gnu.so +0 -0
- mmgpy/_mmgpy.pyi +2140 -0
- mmgpy/_options.py +304 -0
- mmgpy/_progress.py +850 -0
- mmgpy/_pyvista.py +410 -0
- mmgpy/_result.py +143 -0
- mmgpy/_transfer.py +273 -0
- mmgpy/_validation.py +669 -0
- mmgpy/_version.py +3 -0
- mmgpy/_version.py.in +3 -0
- mmgpy/bin/mmg2d_O3 +0 -0
- mmgpy/bin/mmg3d_O3 +0 -0
- mmgpy/bin/mmgs_O3 +0 -0
- mmgpy/interactive/__init__.py +24 -0
- mmgpy/interactive/sizing_editor.py +790 -0
- mmgpy/lagrangian.py +394 -0
- mmgpy/lib/libmmg2d.so +0 -0
- mmgpy/lib/libmmg2d.so.5 +0 -0
- mmgpy/lib/libmmg2d.so.5.8.0 +0 -0
- mmgpy/lib/libmmg3d.so +0 -0
- mmgpy/lib/libmmg3d.so.5 +0 -0
- mmgpy/lib/libmmg3d.so.5.8.0 +0 -0
- mmgpy/lib/libmmgs.so +0 -0
- mmgpy/lib/libmmgs.so.5 +0 -0
- mmgpy/lib/libmmgs.so.5.8.0 +0 -0
- mmgpy/lib/libvtkCommonColor-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonComputationalGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonDataModel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonExecutionModel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonMath-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonMisc-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonSystem-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonTransforms-9.5.so.1 +0 -0
- mmgpy/lib/libvtkDICOMParser-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersCellGrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersExtraction-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersGeneral-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersHybrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersHyperTree-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersModeling-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersParallel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersReduction-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersSources-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersStatistics-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersTexture-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersVerdict-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOCellGrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOImage-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOLegacy-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOParallel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOParallelXML-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOXML-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOXMLParser-9.5.so.1 +0 -0
- mmgpy/lib/libvtkImagingCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkImagingSources-9.5.so.1 +0 -0
- mmgpy/lib/libvtkParallelCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkParallelDIY-9.5.so.1 +0 -0
- mmgpy/lib/libvtkRenderingCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkdoubleconversion-9.5.so.1 +0 -0
- mmgpy/lib/libvtkexpat-9.5.so.1 +0 -0
- mmgpy/lib/libvtkfmt-9.5.so.1 +0 -0
- mmgpy/lib/libvtkjpeg-9.5.so.1 +0 -0
- mmgpy/lib/libvtkjsoncpp-9.5.so.1 +0 -0
- mmgpy/lib/libvtkkissfft-9.5.so.1 +0 -0
- mmgpy/lib/libvtkloguru-9.5.so.1 +0 -0
- mmgpy/lib/libvtklz4-9.5.so.1 +0 -0
- mmgpy/lib/libvtklzma-9.5.so.1 +0 -0
- mmgpy/lib/libvtkmetaio-9.5.so.1 +0 -0
- mmgpy/lib/libvtkpng-9.5.so.1 +0 -0
- mmgpy/lib/libvtkpugixml-9.5.so.1 +0 -0
- mmgpy/lib/libvtksys-9.5.so.1 +0 -0
- mmgpy/lib/libvtktiff-9.5.so.1 +0 -0
- mmgpy/lib/libvtktoken-9.5.so.1 +0 -0
- mmgpy/lib/libvtkverdict-9.5.so.1 +0 -0
- mmgpy/lib/libvtkzlib-9.5.so.1 +0 -0
- mmgpy/metrics.py +596 -0
- mmgpy/progress.py +69 -0
- mmgpy/py.typed +0 -0
- mmgpy/repair/__init__.py +37 -0
- mmgpy/repair/_core.py +226 -0
- mmgpy/repair/_elements.py +241 -0
- mmgpy/repair/_vertices.py +219 -0
- mmgpy/sizing.py +370 -0
- mmgpy/ui/__init__.py +97 -0
- mmgpy/ui/__main__.py +87 -0
- mmgpy/ui/app.py +1837 -0
- mmgpy/ui/parsers.py +501 -0
- mmgpy/ui/remeshing.py +448 -0
- mmgpy/ui/samples.py +249 -0
- mmgpy/ui/utils.py +280 -0
- mmgpy/ui/viewer.py +587 -0
- mmgpy-0.5.0.dist-info/METADATA +186 -0
- mmgpy-0.5.0.dist-info/RECORD +109 -0
- mmgpy-0.5.0.dist-info/WHEEL +6 -0
- mmgpy-0.5.0.dist-info/entry_points.txt +13 -0
- mmgpy-0.5.0.dist-info/licenses/LICENSE +38 -0
- share/man/man1/mmg2d.1.gz +0 -0
- share/man/man1/mmg3d.1.gz +0 -0
- share/man/man1/mmgs.1.gz +0 -0
mmgpy/sizing.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Local sizing parameters for per-region mesh control.
|
|
2
|
+
|
|
3
|
+
This module provides convenient APIs for specifying local mesh sizing parameters,
|
|
4
|
+
enabling different mesh densities in different regions without manually constructing
|
|
5
|
+
metric fields.
|
|
6
|
+
|
|
7
|
+
Sizing constraints are stored on mesh objects and combined (minimum size wins)
|
|
8
|
+
to produce per-vertex metric fields before remeshing.
|
|
9
|
+
|
|
10
|
+
Examples
|
|
11
|
+
--------
|
|
12
|
+
Fine mesh in a spherical region:
|
|
13
|
+
|
|
14
|
+
>>> from mmgpy import MmgMesh3D
|
|
15
|
+
>>> mesh = MmgMesh3D.from_file("model.mesh")
|
|
16
|
+
>>> mesh.set_size_sphere(center=[0.5, 0.5, 0.5], radius=0.2, size=0.01)
|
|
17
|
+
>>> mesh.remesh(hmax=0.1, verbose=-1)
|
|
18
|
+
|
|
19
|
+
Multiple sizing constraints (minimum size wins):
|
|
20
|
+
|
|
21
|
+
>>> mesh.set_size_sphere(center=[0, 0, 0], radius=0.3, size=0.01)
|
|
22
|
+
>>> mesh.set_size_sphere(center=[1, 1, 1], radius=0.3, size=0.01)
|
|
23
|
+
>>> mesh.set_size_box(bounds=[[0.4, 0.4, 0.4], [0.6, 0.6, 0.6]], size=0.005)
|
|
24
|
+
>>> mesh.remesh(hmax=0.1, verbose=-1)
|
|
25
|
+
|
|
26
|
+
Distance-based sizing from a point:
|
|
27
|
+
|
|
28
|
+
>>> mesh.set_size_from_point(
|
|
29
|
+
... point=[0.5, 0.5, 0.5],
|
|
30
|
+
... near_size=0.01,
|
|
31
|
+
... far_size=0.1,
|
|
32
|
+
... influence_radius=0.5,
|
|
33
|
+
... )
|
|
34
|
+
>>> mesh.remesh(verbose=-1)
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from abc import ABC, abstractmethod
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
from typing import TYPE_CHECKING
|
|
43
|
+
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from numpy.typing import NDArray
|
|
48
|
+
|
|
49
|
+
from mmgpy import MmgMesh2D, MmgMesh3D, MmgMeshS
|
|
50
|
+
|
|
51
|
+
_BOUNDS_DIM_COUNT = 2
|
|
52
|
+
_ZERO_LENGTH_THRESHOLD = 1e-12
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class SizingConstraint(ABC):
|
|
57
|
+
"""Base class for sizing constraints."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def compute_sizes(
|
|
61
|
+
self,
|
|
62
|
+
vertices: NDArray[np.float64],
|
|
63
|
+
) -> NDArray[np.float64]:
|
|
64
|
+
"""Compute target size at each vertex.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
vertices : NDArray[np.float64]
|
|
69
|
+
Vertex coordinates, shape (n_vertices, dim).
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
NDArray[np.float64]
|
|
74
|
+
Target size at each vertex, shape (n_vertices,).
|
|
75
|
+
Use np.inf for vertices where this constraint doesn't apply.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class SphereSize(SizingConstraint):
|
|
82
|
+
"""Uniform size within a spherical region.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
center : array_like
|
|
87
|
+
Center of the sphere, shape (dim,).
|
|
88
|
+
radius : float
|
|
89
|
+
Radius of the sphere. Must be positive.
|
|
90
|
+
size : float
|
|
91
|
+
Target edge size within the sphere. Must be positive.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
center: NDArray[np.float64]
|
|
96
|
+
radius: float
|
|
97
|
+
size: float
|
|
98
|
+
|
|
99
|
+
def __post_init__(self) -> None: # noqa: D105
|
|
100
|
+
self.center = np.asarray(self.center, dtype=np.float64)
|
|
101
|
+
if self.radius <= 0:
|
|
102
|
+
msg = f"radius must be positive, got {self.radius}"
|
|
103
|
+
raise ValueError(msg)
|
|
104
|
+
if self.size <= 0:
|
|
105
|
+
msg = f"size must be positive, got {self.size}"
|
|
106
|
+
raise ValueError(msg)
|
|
107
|
+
|
|
108
|
+
def compute_sizes( # noqa: D102
|
|
109
|
+
self,
|
|
110
|
+
vertices: NDArray[np.float64],
|
|
111
|
+
) -> NDArray[np.float64]:
|
|
112
|
+
distances = np.linalg.norm(vertices - self.center, axis=1)
|
|
113
|
+
|
|
114
|
+
sizes = np.full(len(vertices), np.inf, dtype=np.float64)
|
|
115
|
+
|
|
116
|
+
inside_mask = distances <= self.radius
|
|
117
|
+
sizes[inside_mask] = self.size
|
|
118
|
+
|
|
119
|
+
return sizes
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class BoxSize(SizingConstraint):
|
|
124
|
+
"""Uniform size within a box region.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
bounds : array_like
|
|
129
|
+
Box bounds as [[xmin, ymin, zmin], [xmax, ymax, zmax]] for 3D
|
|
130
|
+
or [[xmin, ymin], [xmax, ymax]] for 2D.
|
|
131
|
+
size : float
|
|
132
|
+
Target edge size within the box. Must be positive.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
bounds: NDArray[np.float64]
|
|
137
|
+
size: float
|
|
138
|
+
|
|
139
|
+
def __post_init__(self) -> None: # noqa: D105
|
|
140
|
+
self.bounds = np.asarray(self.bounds, dtype=np.float64)
|
|
141
|
+
if self.bounds.shape[0] != _BOUNDS_DIM_COUNT:
|
|
142
|
+
msg = f"bounds must have shape (2, dim), got {self.bounds.shape}"
|
|
143
|
+
raise ValueError(msg)
|
|
144
|
+
if self.size <= 0:
|
|
145
|
+
msg = f"size must be positive, got {self.size}"
|
|
146
|
+
raise ValueError(msg)
|
|
147
|
+
|
|
148
|
+
def compute_sizes( # noqa: D102
|
|
149
|
+
self,
|
|
150
|
+
vertices: NDArray[np.float64],
|
|
151
|
+
) -> NDArray[np.float64]:
|
|
152
|
+
min_corner = self.bounds[0]
|
|
153
|
+
max_corner = self.bounds[1]
|
|
154
|
+
|
|
155
|
+
inside_mask = np.all(
|
|
156
|
+
(vertices >= min_corner) & (vertices <= max_corner),
|
|
157
|
+
axis=1,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
sizes = np.full(len(vertices), np.inf, dtype=np.float64)
|
|
161
|
+
sizes[inside_mask] = self.size
|
|
162
|
+
|
|
163
|
+
return sizes
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class CylinderSize(SizingConstraint):
|
|
168
|
+
"""Uniform size within a cylindrical region.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
point1 : array_like
|
|
173
|
+
First endpoint of cylinder axis, shape (3,).
|
|
174
|
+
point2 : array_like
|
|
175
|
+
Second endpoint of cylinder axis, shape (3,).
|
|
176
|
+
radius : float
|
|
177
|
+
Radius of the cylinder. Must be positive.
|
|
178
|
+
size : float
|
|
179
|
+
Target edge size within the cylinder. Must be positive.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
point1: NDArray[np.float64]
|
|
184
|
+
point2: NDArray[np.float64]
|
|
185
|
+
radius: float
|
|
186
|
+
size: float
|
|
187
|
+
|
|
188
|
+
def __post_init__(self) -> None: # noqa: D105
|
|
189
|
+
self.point1 = np.asarray(self.point1, dtype=np.float64)
|
|
190
|
+
self.point2 = np.asarray(self.point2, dtype=np.float64)
|
|
191
|
+
if self.radius <= 0:
|
|
192
|
+
msg = f"radius must be positive, got {self.radius}"
|
|
193
|
+
raise ValueError(msg)
|
|
194
|
+
if self.size <= 0:
|
|
195
|
+
msg = f"size must be positive, got {self.size}"
|
|
196
|
+
raise ValueError(msg)
|
|
197
|
+
|
|
198
|
+
def compute_sizes( # noqa: D102
|
|
199
|
+
self,
|
|
200
|
+
vertices: NDArray[np.float64],
|
|
201
|
+
) -> NDArray[np.float64]:
|
|
202
|
+
axis = self.point2 - self.point1
|
|
203
|
+
axis_length = np.linalg.norm(axis)
|
|
204
|
+
if axis_length < _ZERO_LENGTH_THRESHOLD:
|
|
205
|
+
msg = "Cylinder axis has zero length"
|
|
206
|
+
raise ValueError(msg)
|
|
207
|
+
axis_unit = axis / axis_length
|
|
208
|
+
|
|
209
|
+
v = vertices - self.point1
|
|
210
|
+
proj_length = np.dot(v, axis_unit)
|
|
211
|
+
proj_point = self.point1 + np.outer(proj_length, axis_unit)
|
|
212
|
+
radial_dist = np.linalg.norm(vertices - proj_point, axis=1)
|
|
213
|
+
|
|
214
|
+
in_height = (proj_length >= 0) & (proj_length <= axis_length)
|
|
215
|
+
in_radius = radial_dist <= self.radius
|
|
216
|
+
inside_mask = in_height & in_radius
|
|
217
|
+
|
|
218
|
+
sizes = np.full(len(vertices), np.inf, dtype=np.float64)
|
|
219
|
+
sizes[inside_mask] = self.size
|
|
220
|
+
|
|
221
|
+
return sizes
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class PointSize(SizingConstraint):
|
|
226
|
+
"""Distance-based sizing from a point.
|
|
227
|
+
|
|
228
|
+
Size varies linearly from near_size at the point to far_size at
|
|
229
|
+
influence_radius distance.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
point : array_like
|
|
234
|
+
Reference point, shape (dim,).
|
|
235
|
+
near_size : float
|
|
236
|
+
Target size at the reference point. Must be positive.
|
|
237
|
+
far_size : float
|
|
238
|
+
Target size at influence_radius distance and beyond. Must be positive.
|
|
239
|
+
influence_radius : float
|
|
240
|
+
Distance over which size transitions from near_size to far_size.
|
|
241
|
+
Must be positive.
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
point: NDArray[np.float64]
|
|
246
|
+
near_size: float
|
|
247
|
+
far_size: float
|
|
248
|
+
influence_radius: float
|
|
249
|
+
|
|
250
|
+
def __post_init__(self) -> None: # noqa: D105
|
|
251
|
+
self.point = np.asarray(self.point, dtype=np.float64)
|
|
252
|
+
if self.near_size <= 0:
|
|
253
|
+
msg = f"near_size must be positive, got {self.near_size}"
|
|
254
|
+
raise ValueError(msg)
|
|
255
|
+
if self.far_size <= 0:
|
|
256
|
+
msg = f"far_size must be positive, got {self.far_size}"
|
|
257
|
+
raise ValueError(msg)
|
|
258
|
+
if self.influence_radius <= 0:
|
|
259
|
+
msg = f"influence_radius must be positive, got {self.influence_radius}"
|
|
260
|
+
raise ValueError(msg)
|
|
261
|
+
|
|
262
|
+
def compute_sizes( # noqa: D102
|
|
263
|
+
self,
|
|
264
|
+
vertices: NDArray[np.float64],
|
|
265
|
+
) -> NDArray[np.float64]:
|
|
266
|
+
distances = np.linalg.norm(vertices - self.point, axis=1)
|
|
267
|
+
t = np.clip(distances / self.influence_radius, 0.0, 1.0)
|
|
268
|
+
return self.near_size + t * (self.far_size - self.near_size)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def compute_sizes_from_constraints(
|
|
272
|
+
vertices: NDArray[np.float64],
|
|
273
|
+
constraints: list[SizingConstraint],
|
|
274
|
+
) -> NDArray[np.float64]:
|
|
275
|
+
"""Compute combined sizing from multiple constraints.
|
|
276
|
+
|
|
277
|
+
Multiple constraints are combined by taking the minimum size at each vertex
|
|
278
|
+
(finest mesh wins).
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
vertices : NDArray[np.float64]
|
|
283
|
+
Vertex coordinates, shape (n_vertices, dim).
|
|
284
|
+
constraints : list[SizingConstraint]
|
|
285
|
+
List of sizing constraints.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
NDArray[np.float64]
|
|
290
|
+
Combined target size at each vertex, shape (n_vertices,).
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
if not constraints:
|
|
294
|
+
msg = "No sizing constraints provided"
|
|
295
|
+
raise ValueError(msg)
|
|
296
|
+
|
|
297
|
+
n_vertices = len(vertices)
|
|
298
|
+
combined = np.full(n_vertices, np.inf, dtype=np.float64)
|
|
299
|
+
|
|
300
|
+
for constraint in constraints:
|
|
301
|
+
sizes = constraint.compute_sizes(vertices)
|
|
302
|
+
combined = np.minimum(combined, sizes)
|
|
303
|
+
|
|
304
|
+
return combined
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def sizes_to_metric(
|
|
308
|
+
sizes: NDArray[np.float64],
|
|
309
|
+
) -> NDArray[np.float64]:
|
|
310
|
+
"""Convert scalar sizes to metric tensor format.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
sizes : NDArray[np.float64]
|
|
315
|
+
Target sizes at each vertex, shape (n_vertices,).
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
NDArray[np.float64]
|
|
320
|
+
Metric field suitable for mesh["metric"], shape (n_vertices, 1).
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
return sizes.reshape(-1, 1)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def apply_sizing_constraints(
|
|
327
|
+
mesh: MmgMesh3D | MmgMesh2D | MmgMeshS,
|
|
328
|
+
constraints: list[SizingConstraint],
|
|
329
|
+
existing_metric: NDArray[np.float64] | None = None,
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Apply sizing constraints to a mesh by setting its metric field.
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
mesh : MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
336
|
+
Mesh to apply sizing to.
|
|
337
|
+
constraints : list[SizingConstraint]
|
|
338
|
+
List of sizing constraints.
|
|
339
|
+
existing_metric : NDArray[np.float64] | None
|
|
340
|
+
Existing metric field to combine with. If provided, minimum size wins.
|
|
341
|
+
|
|
342
|
+
"""
|
|
343
|
+
if not constraints:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
vertices = mesh.get_vertices()
|
|
347
|
+
sizes = compute_sizes_from_constraints(vertices, constraints)
|
|
348
|
+
|
|
349
|
+
if existing_metric is not None and existing_metric.shape[1] == 1:
|
|
350
|
+
existing_sizes = existing_metric.ravel()
|
|
351
|
+
sizes = np.minimum(sizes, existing_sizes)
|
|
352
|
+
|
|
353
|
+
finite_mask = np.isfinite(sizes)
|
|
354
|
+
if not np.any(finite_mask):
|
|
355
|
+
# No constraints applied to any vertex (all sizes are inf).
|
|
356
|
+
# This can happen if all region-based constraints are placed outside
|
|
357
|
+
# the mesh bounds. Silently return without modifying the metric field,
|
|
358
|
+
# allowing remeshing to proceed with global parameters only.
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
metric = sizes_to_metric(sizes)
|
|
362
|
+
|
|
363
|
+
inf_mask = ~finite_mask
|
|
364
|
+
if np.any(inf_mask):
|
|
365
|
+
finite_sizes = sizes[finite_mask]
|
|
366
|
+
if len(finite_sizes) > 0:
|
|
367
|
+
max_size = np.max(finite_sizes) * 10
|
|
368
|
+
metric[inf_mask] = max_size
|
|
369
|
+
|
|
370
|
+
mesh["metric"] = metric
|
mmgpy/ui/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Web interface for mmgpy using trame.
|
|
2
|
+
|
|
3
|
+
This module provides a web-based GUI for mesh remeshing operations.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> from mmgpy.ui import run_ui
|
|
7
|
+
>>> run_ui() # Opens browser with the UI
|
|
8
|
+
|
|
9
|
+
>>> # Or with a pre-loaded mesh
|
|
10
|
+
>>> from mmgpy import Mesh
|
|
11
|
+
>>> mesh = Mesh("model.vtk")
|
|
12
|
+
>>> run_ui(mesh=mesh)
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from mmgpy import Mesh
|
|
22
|
+
from mmgpy.ui.app import MmgpyApp
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_ui( # noqa: PLR0913
|
|
26
|
+
*,
|
|
27
|
+
mesh: Mesh | None = None,
|
|
28
|
+
port: int = 0,
|
|
29
|
+
open_browser: bool = True,
|
|
30
|
+
debug: bool = False,
|
|
31
|
+
exec_mode: str = "desktop",
|
|
32
|
+
maximized: bool = True,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Launch the mmgpy interface.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
mesh : Mesh, optional
|
|
39
|
+
Pre-loaded mesh to display. If None, starts with empty viewer.
|
|
40
|
+
port : int, default=0
|
|
41
|
+
Port to run the server on. 0 means auto-select.
|
|
42
|
+
open_browser : bool, default=True
|
|
43
|
+
Whether to automatically open a browser window (only used in browser mode).
|
|
44
|
+
debug : bool, default=False
|
|
45
|
+
Enable debug mode with HTML structure printing.
|
|
46
|
+
exec_mode : str, default="desktop"
|
|
47
|
+
Execution mode: "desktop" for standalone app, "main" for browser mode.
|
|
48
|
+
maximized : bool, default=True
|
|
49
|
+
Start the desktop window maximized (only used in desktop mode).
|
|
50
|
+
|
|
51
|
+
Examples
|
|
52
|
+
--------
|
|
53
|
+
>>> from mmgpy.ui import run_ui
|
|
54
|
+
>>> run_ui() # Opens as desktop app (default, maximized)
|
|
55
|
+
|
|
56
|
+
>>> run_ui(exec_mode="main") # Opens in browser
|
|
57
|
+
|
|
58
|
+
>>> run_ui(exec_mode="main", open_browser=False) # Server only
|
|
59
|
+
|
|
60
|
+
>>> # From command line:
|
|
61
|
+
>>> # mmgpy-ui # Desktop app (default)
|
|
62
|
+
>>> # mmgpy-ui --browser # Browser mode
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
from mmgpy.ui.app import MmgpyApp
|
|
66
|
+
|
|
67
|
+
app = MmgpyApp(mesh=mesh, debug=debug)
|
|
68
|
+
app.server.start(
|
|
69
|
+
port=port,
|
|
70
|
+
open_browser=open_browser,
|
|
71
|
+
exec_mode=exec_mode,
|
|
72
|
+
maximized=maximized,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def print_html(app: MmgpyApp | None = None) -> str:
|
|
77
|
+
"""Print the HTML structure for debugging.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
app : MmgpyApp, optional
|
|
82
|
+
The application instance. If None, creates a temporary one.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
str
|
|
87
|
+
The HTML structure as a string.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
from mmgpy.ui.app import MmgpyApp
|
|
91
|
+
|
|
92
|
+
if app is None:
|
|
93
|
+
app = MmgpyApp()
|
|
94
|
+
return app.ui.html
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["print_html", "run_ui"]
|
mmgpy/ui/__main__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Allow running the UI with `python -m mmgpy.ui` or `mmgpy-ui` command.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
mmgpy-ui # Desktop app (default)
|
|
5
|
+
mmgpy-ui --browser # Run in browser instead
|
|
6
|
+
mmgpy-ui --port 8080 # Specify port (browser mode)
|
|
7
|
+
mmgpy-ui --server # Server only, don't open browser
|
|
8
|
+
|
|
9
|
+
# With uvx (no install needed)
|
|
10
|
+
uvx --from "mmgpy[ui]" mmgpy-ui
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
"""Run the mmgpy UI."""
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="mmgpy-ui",
|
|
22
|
+
description="mmgpy interface for mesh remeshing (runs as desktop app by default)",
|
|
23
|
+
epilog="Run with uvx: uvx --from 'mmgpy[ui]' mmgpy-ui",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--browser",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="Run in browser instead of desktop app",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--server",
|
|
32
|
+
action="store_true",
|
|
33
|
+
help="Server mode: don't open browser automatically (implies --browser)",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--port",
|
|
37
|
+
type=int,
|
|
38
|
+
default=0,
|
|
39
|
+
help="Port to run on (0 = auto-select available port)",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--debug",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="Enable debug mode with HTML structure printing",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--version",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help="Show mmgpy version and exit",
|
|
50
|
+
)
|
|
51
|
+
args = parser.parse_args()
|
|
52
|
+
|
|
53
|
+
if args.version:
|
|
54
|
+
try:
|
|
55
|
+
from importlib.metadata import version
|
|
56
|
+
|
|
57
|
+
print(f"mmgpy-ui (mmgpy {version('mmgpy')})")
|
|
58
|
+
except Exception:
|
|
59
|
+
print("mmgpy-ui (version unknown)")
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
|
|
62
|
+
# Check if trame is available
|
|
63
|
+
try:
|
|
64
|
+
import trame # noqa: F401
|
|
65
|
+
except ImportError:
|
|
66
|
+
print("Error: trame is not installed.")
|
|
67
|
+
print("Install with: pip install 'mmgpy[ui]'")
|
|
68
|
+
print("Or run with: uvx --from 'mmgpy[ui]' mmgpy-ui")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
from mmgpy.ui import run_ui
|
|
72
|
+
|
|
73
|
+
# --server implies --browser mode
|
|
74
|
+
use_browser_mode = args.browser or args.server
|
|
75
|
+
exec_mode = "main" if use_browser_mode else "desktop"
|
|
76
|
+
|
|
77
|
+
run_ui(
|
|
78
|
+
port=args.port,
|
|
79
|
+
open_browser=not args.server,
|
|
80
|
+
debug=args.debug,
|
|
81
|
+
exec_mode=exec_mode,
|
|
82
|
+
maximized=not use_browser_mode, # Maximized only for desktop mode
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|