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.
Files changed (109) hide show
  1. mmgpy/__init__.py +296 -0
  2. mmgpy/__main__.py +13 -0
  3. mmgpy/_io.py +535 -0
  4. mmgpy/_logging.py +290 -0
  5. mmgpy/_mesh.py +2286 -0
  6. mmgpy/_mmgpy.cpython-311-x86_64-linux-gnu.so +0 -0
  7. mmgpy/_mmgpy.pyi +2140 -0
  8. mmgpy/_options.py +304 -0
  9. mmgpy/_progress.py +850 -0
  10. mmgpy/_pyvista.py +410 -0
  11. mmgpy/_result.py +143 -0
  12. mmgpy/_transfer.py +273 -0
  13. mmgpy/_validation.py +669 -0
  14. mmgpy/_version.py +3 -0
  15. mmgpy/_version.py.in +3 -0
  16. mmgpy/bin/mmg2d_O3 +0 -0
  17. mmgpy/bin/mmg3d_O3 +0 -0
  18. mmgpy/bin/mmgs_O3 +0 -0
  19. mmgpy/interactive/__init__.py +24 -0
  20. mmgpy/interactive/sizing_editor.py +790 -0
  21. mmgpy/lagrangian.py +394 -0
  22. mmgpy/lib/libmmg2d.so +0 -0
  23. mmgpy/lib/libmmg2d.so.5 +0 -0
  24. mmgpy/lib/libmmg2d.so.5.8.0 +0 -0
  25. mmgpy/lib/libmmg3d.so +0 -0
  26. mmgpy/lib/libmmg3d.so.5 +0 -0
  27. mmgpy/lib/libmmg3d.so.5.8.0 +0 -0
  28. mmgpy/lib/libmmgs.so +0 -0
  29. mmgpy/lib/libmmgs.so.5 +0 -0
  30. mmgpy/lib/libmmgs.so.5.8.0 +0 -0
  31. mmgpy/lib/libvtkCommonColor-9.5.so.1 +0 -0
  32. mmgpy/lib/libvtkCommonComputationalGeometry-9.5.so.1 +0 -0
  33. mmgpy/lib/libvtkCommonCore-9.5.so.1 +0 -0
  34. mmgpy/lib/libvtkCommonDataModel-9.5.so.1 +0 -0
  35. mmgpy/lib/libvtkCommonExecutionModel-9.5.so.1 +0 -0
  36. mmgpy/lib/libvtkCommonMath-9.5.so.1 +0 -0
  37. mmgpy/lib/libvtkCommonMisc-9.5.so.1 +0 -0
  38. mmgpy/lib/libvtkCommonSystem-9.5.so.1 +0 -0
  39. mmgpy/lib/libvtkCommonTransforms-9.5.so.1 +0 -0
  40. mmgpy/lib/libvtkDICOMParser-9.5.so.1 +0 -0
  41. mmgpy/lib/libvtkFiltersCellGrid-9.5.so.1 +0 -0
  42. mmgpy/lib/libvtkFiltersCore-9.5.so.1 +0 -0
  43. mmgpy/lib/libvtkFiltersExtraction-9.5.so.1 +0 -0
  44. mmgpy/lib/libvtkFiltersGeneral-9.5.so.1 +0 -0
  45. mmgpy/lib/libvtkFiltersGeometry-9.5.so.1 +0 -0
  46. mmgpy/lib/libvtkFiltersHybrid-9.5.so.1 +0 -0
  47. mmgpy/lib/libvtkFiltersHyperTree-9.5.so.1 +0 -0
  48. mmgpy/lib/libvtkFiltersModeling-9.5.so.1 +0 -0
  49. mmgpy/lib/libvtkFiltersParallel-9.5.so.1 +0 -0
  50. mmgpy/lib/libvtkFiltersReduction-9.5.so.1 +0 -0
  51. mmgpy/lib/libvtkFiltersSources-9.5.so.1 +0 -0
  52. mmgpy/lib/libvtkFiltersStatistics-9.5.so.1 +0 -0
  53. mmgpy/lib/libvtkFiltersTexture-9.5.so.1 +0 -0
  54. mmgpy/lib/libvtkFiltersVerdict-9.5.so.1 +0 -0
  55. mmgpy/lib/libvtkIOCellGrid-9.5.so.1 +0 -0
  56. mmgpy/lib/libvtkIOCore-9.5.so.1 +0 -0
  57. mmgpy/lib/libvtkIOGeometry-9.5.so.1 +0 -0
  58. mmgpy/lib/libvtkIOImage-9.5.so.1 +0 -0
  59. mmgpy/lib/libvtkIOLegacy-9.5.so.1 +0 -0
  60. mmgpy/lib/libvtkIOParallel-9.5.so.1 +0 -0
  61. mmgpy/lib/libvtkIOParallelXML-9.5.so.1 +0 -0
  62. mmgpy/lib/libvtkIOXML-9.5.so.1 +0 -0
  63. mmgpy/lib/libvtkIOXMLParser-9.5.so.1 +0 -0
  64. mmgpy/lib/libvtkImagingCore-9.5.so.1 +0 -0
  65. mmgpy/lib/libvtkImagingSources-9.5.so.1 +0 -0
  66. mmgpy/lib/libvtkParallelCore-9.5.so.1 +0 -0
  67. mmgpy/lib/libvtkParallelDIY-9.5.so.1 +0 -0
  68. mmgpy/lib/libvtkRenderingCore-9.5.so.1 +0 -0
  69. mmgpy/lib/libvtkdoubleconversion-9.5.so.1 +0 -0
  70. mmgpy/lib/libvtkexpat-9.5.so.1 +0 -0
  71. mmgpy/lib/libvtkfmt-9.5.so.1 +0 -0
  72. mmgpy/lib/libvtkjpeg-9.5.so.1 +0 -0
  73. mmgpy/lib/libvtkjsoncpp-9.5.so.1 +0 -0
  74. mmgpy/lib/libvtkkissfft-9.5.so.1 +0 -0
  75. mmgpy/lib/libvtkloguru-9.5.so.1 +0 -0
  76. mmgpy/lib/libvtklz4-9.5.so.1 +0 -0
  77. mmgpy/lib/libvtklzma-9.5.so.1 +0 -0
  78. mmgpy/lib/libvtkmetaio-9.5.so.1 +0 -0
  79. mmgpy/lib/libvtkpng-9.5.so.1 +0 -0
  80. mmgpy/lib/libvtkpugixml-9.5.so.1 +0 -0
  81. mmgpy/lib/libvtksys-9.5.so.1 +0 -0
  82. mmgpy/lib/libvtktiff-9.5.so.1 +0 -0
  83. mmgpy/lib/libvtktoken-9.5.so.1 +0 -0
  84. mmgpy/lib/libvtkverdict-9.5.so.1 +0 -0
  85. mmgpy/lib/libvtkzlib-9.5.so.1 +0 -0
  86. mmgpy/metrics.py +596 -0
  87. mmgpy/progress.py +69 -0
  88. mmgpy/py.typed +0 -0
  89. mmgpy/repair/__init__.py +37 -0
  90. mmgpy/repair/_core.py +226 -0
  91. mmgpy/repair/_elements.py +241 -0
  92. mmgpy/repair/_vertices.py +219 -0
  93. mmgpy/sizing.py +370 -0
  94. mmgpy/ui/__init__.py +97 -0
  95. mmgpy/ui/__main__.py +87 -0
  96. mmgpy/ui/app.py +1837 -0
  97. mmgpy/ui/parsers.py +501 -0
  98. mmgpy/ui/remeshing.py +448 -0
  99. mmgpy/ui/samples.py +249 -0
  100. mmgpy/ui/utils.py +280 -0
  101. mmgpy/ui/viewer.py +587 -0
  102. mmgpy-0.5.0.dist-info/METADATA +186 -0
  103. mmgpy-0.5.0.dist-info/RECORD +109 -0
  104. mmgpy-0.5.0.dist-info/WHEEL +6 -0
  105. mmgpy-0.5.0.dist-info/entry_points.txt +13 -0
  106. mmgpy-0.5.0.dist-info/licenses/LICENSE +38 -0
  107. share/man/man1/mmg2d.1.gz +0 -0
  108. share/man/man1/mmg3d.1.gz +0 -0
  109. 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()