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
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
"""Interactive sizing editor using PyVista.
|
|
2
|
+
|
|
3
|
+
This module provides a visual interface for defining mesh sizing constraints
|
|
4
|
+
by clicking on the mesh to place refinement regions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pyvista as pv
|
|
15
|
+
|
|
16
|
+
from mmgpy.sizing import BoxSize, CylinderSize, PointSize, SizingConstraint, SphereSize
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from numpy.typing import NDArray
|
|
20
|
+
|
|
21
|
+
from mmgpy import Mesh
|
|
22
|
+
from mmgpy._mmgpy import MmgMesh2D, MmgMesh3D, MmgMeshS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConstraintMode(Enum):
|
|
26
|
+
"""Interaction mode for constraint placement."""
|
|
27
|
+
|
|
28
|
+
NONE = auto()
|
|
29
|
+
SPHERE = auto()
|
|
30
|
+
BOX = auto()
|
|
31
|
+
CYLINDER = auto()
|
|
32
|
+
POINT = auto()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ConstraintVisual:
|
|
37
|
+
"""Visual representation of a sizing constraint."""
|
|
38
|
+
|
|
39
|
+
constraint: SizingConstraint
|
|
40
|
+
actor: pv.Actor
|
|
41
|
+
label_actor: pv.Actor | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class EditorState:
|
|
46
|
+
"""State for multi-click constraint creation."""
|
|
47
|
+
|
|
48
|
+
mode: ConstraintMode = ConstraintMode.NONE
|
|
49
|
+
first_point: NDArray[np.float64] | None = None
|
|
50
|
+
preview_actor: pv.Actor | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_DEFAULT_SIZE = 0.01
|
|
54
|
+
_DEFAULT_RADIUS = 0.1
|
|
55
|
+
_DEFAULT_NEAR_SIZE = 0.01
|
|
56
|
+
_DEFAULT_FAR_SIZE = 0.1
|
|
57
|
+
_DEFAULT_INFLUENCE_RADIUS = 0.5
|
|
58
|
+
|
|
59
|
+
_SPHERE_COLOR = "red"
|
|
60
|
+
_BOX_COLOR = "blue"
|
|
61
|
+
_CYLINDER_COLOR = "green"
|
|
62
|
+
_POINT_COLOR = "orange"
|
|
63
|
+
_CONSTRAINT_OPACITY = 0.3
|
|
64
|
+
_PREVIEW_OPACITY = 0.5
|
|
65
|
+
|
|
66
|
+
_MIN_SIZE = 0.001
|
|
67
|
+
_MAX_SIZE = 1.0
|
|
68
|
+
_MIN_RADIUS = 0.01
|
|
69
|
+
_MAX_RADIUS = 10.0
|
|
70
|
+
|
|
71
|
+
_DIMS_2D = 2
|
|
72
|
+
_DIMS_3D = 3
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SizingEditor:
|
|
76
|
+
"""Interactive editor for mesh sizing constraints.
|
|
77
|
+
|
|
78
|
+
This class provides a PyVista-based GUI for visually defining local
|
|
79
|
+
mesh sizing constraints by clicking on the mesh.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
mesh : Mesh | MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
84
|
+
The mesh to edit sizing for.
|
|
85
|
+
|
|
86
|
+
Attributes
|
|
87
|
+
----------
|
|
88
|
+
constraints : list[SizingConstraint]
|
|
89
|
+
List of sizing constraints defined through interaction.
|
|
90
|
+
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
>>> from mmgpy import Mesh
|
|
94
|
+
>>> from mmgpy.interactive import SizingEditor
|
|
95
|
+
>>>
|
|
96
|
+
>>> mesh = Mesh("model.mesh")
|
|
97
|
+
>>> editor = SizingEditor(mesh)
|
|
98
|
+
>>> editor.add_sphere_tool()
|
|
99
|
+
>>> editor.run()
|
|
100
|
+
>>>
|
|
101
|
+
>>> # Get and apply constraints
|
|
102
|
+
>>> for c in editor.get_constraints():
|
|
103
|
+
... print(c)
|
|
104
|
+
>>> editor.apply_to_mesh()
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
mesh: Mesh | MmgMesh3D | MmgMesh2D | MmgMeshS,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Initialize the sizing editor.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
mesh : Mesh | MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
117
|
+
The mesh to edit sizing for.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
# Wrap raw MmgMesh* objects in a Mesh wrapper
|
|
121
|
+
from mmgpy import Mesh as MeshClass
|
|
122
|
+
from mmgpy import MeshKind
|
|
123
|
+
from mmgpy._mmgpy import MmgMesh2D, MmgMesh3D, MmgMeshS
|
|
124
|
+
|
|
125
|
+
if isinstance(mesh, MmgMesh3D):
|
|
126
|
+
self._mesh: Mesh = MeshClass._from_impl(mesh, MeshKind.TETRAHEDRAL) # noqa: SLF001
|
|
127
|
+
elif isinstance(mesh, MmgMesh2D):
|
|
128
|
+
self._mesh = MeshClass._from_impl(mesh, MeshKind.TRIANGULAR_2D) # noqa: SLF001
|
|
129
|
+
elif isinstance(mesh, MmgMeshS):
|
|
130
|
+
self._mesh = MeshClass._from_impl(mesh, MeshKind.TRIANGULAR_SURFACE) # noqa: SLF001
|
|
131
|
+
else:
|
|
132
|
+
self._mesh = mesh
|
|
133
|
+
self._pv_mesh: pv.PolyData | pv.UnstructuredGrid | None = None
|
|
134
|
+
self._plotter: pv.Plotter | None = None
|
|
135
|
+
|
|
136
|
+
self._constraints: list[SizingConstraint] = []
|
|
137
|
+
self._visuals: list[ConstraintVisual] = []
|
|
138
|
+
self._state = EditorState()
|
|
139
|
+
|
|
140
|
+
self._current_size = _DEFAULT_SIZE
|
|
141
|
+
self._current_radius = _DEFAULT_RADIUS
|
|
142
|
+
self._current_near_size = _DEFAULT_NEAR_SIZE
|
|
143
|
+
self._current_far_size = _DEFAULT_FAR_SIZE
|
|
144
|
+
self._current_influence_radius = _DEFAULT_INFLUENCE_RADIUS
|
|
145
|
+
|
|
146
|
+
self._is_3d = self._detect_mesh_dimension()
|
|
147
|
+
|
|
148
|
+
def _detect_mesh_dimension(self) -> bool:
|
|
149
|
+
"""Detect if mesh is 3D (vs 2D planar)."""
|
|
150
|
+
from mmgpy import MeshKind
|
|
151
|
+
|
|
152
|
+
return self._mesh.kind != MeshKind.TRIANGULAR_2D
|
|
153
|
+
|
|
154
|
+
def _get_pyvista_mesh(self) -> pv.PolyData | pv.UnstructuredGrid:
|
|
155
|
+
"""Get PyVista representation of the mesh."""
|
|
156
|
+
if self._pv_mesh is None:
|
|
157
|
+
self._pv_mesh = self._mesh.to_pyvista()
|
|
158
|
+
return self._pv_mesh
|
|
159
|
+
|
|
160
|
+
def add_sphere_tool(self) -> SizingEditor:
|
|
161
|
+
"""Enable sphere constraint placement mode.
|
|
162
|
+
|
|
163
|
+
In sphere mode, click on the mesh to place the center of a
|
|
164
|
+
spherical refinement region. Use the radius slider to adjust
|
|
165
|
+
the sphere size.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
SizingEditor
|
|
170
|
+
Self for method chaining.
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
self._state.mode = ConstraintMode.SPHERE
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def add_box_tool(self) -> SizingEditor:
|
|
177
|
+
"""Enable box constraint placement mode.
|
|
178
|
+
|
|
179
|
+
In box mode, click twice on the mesh to define two opposite
|
|
180
|
+
corners of a box refinement region.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
SizingEditor
|
|
185
|
+
Self for method chaining.
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
self._state.mode = ConstraintMode.BOX
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def add_cylinder_tool(self) -> SizingEditor:
|
|
192
|
+
"""Enable cylinder constraint placement mode.
|
|
193
|
+
|
|
194
|
+
In cylinder mode, click twice on the mesh to define the axis
|
|
195
|
+
of a cylindrical refinement region. Use the radius slider to
|
|
196
|
+
adjust the cylinder radius.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
SizingEditor
|
|
201
|
+
Self for method chaining.
|
|
202
|
+
|
|
203
|
+
Raises
|
|
204
|
+
------
|
|
205
|
+
TypeError
|
|
206
|
+
If the mesh is 2D (cylinder not supported for 2D meshes).
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
if not self._is_3d:
|
|
210
|
+
msg = "Cylinder tool is not available for 2D meshes"
|
|
211
|
+
raise TypeError(msg)
|
|
212
|
+
self._state.mode = ConstraintMode.CYLINDER
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def add_point_tool(self) -> SizingEditor:
|
|
216
|
+
"""Enable point-based sizing mode.
|
|
217
|
+
|
|
218
|
+
In point mode, click on the mesh to place a reference point
|
|
219
|
+
for distance-based sizing. Size varies linearly from near_size
|
|
220
|
+
at the point to far_size at influence_radius distance.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
SizingEditor
|
|
225
|
+
Self for method chaining.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
self._state.mode = ConstraintMode.POINT
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
def run(self) -> None:
|
|
232
|
+
"""Launch the interactive editor window.
|
|
233
|
+
|
|
234
|
+
Opens a PyVista plotter window with the mesh and interactive
|
|
235
|
+
controls for placing sizing constraints.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
self._plotter = pv.Plotter(title="mmgpy Sizing Editor")
|
|
239
|
+
|
|
240
|
+
pv_mesh = self._get_pyvista_mesh()
|
|
241
|
+
self._plotter.add_mesh(
|
|
242
|
+
pv_mesh,
|
|
243
|
+
show_edges=True,
|
|
244
|
+
pickable=True,
|
|
245
|
+
name="mesh",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self._setup_picking()
|
|
249
|
+
self._setup_sliders()
|
|
250
|
+
self._setup_buttons()
|
|
251
|
+
self._setup_help_panel()
|
|
252
|
+
self._setup_instructions()
|
|
253
|
+
|
|
254
|
+
self._plotter.show()
|
|
255
|
+
|
|
256
|
+
def _setup_help_panel(self) -> None:
|
|
257
|
+
"""Set up help text panel."""
|
|
258
|
+
if self._plotter is None:
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
help_text = (
|
|
262
|
+
"HOW TO USE:\n"
|
|
263
|
+
"1. Select a tool (colored buttons, bottom-left)\n"
|
|
264
|
+
"2. Adjust sliders (bottom) for size/radius\n"
|
|
265
|
+
"3. Click on mesh to place constraints\n"
|
|
266
|
+
"4. Close window when done\n"
|
|
267
|
+
"\n"
|
|
268
|
+
"TOOLS:\n"
|
|
269
|
+
" Sphere: Click once for center\n"
|
|
270
|
+
" Box: Click twice for corners\n"
|
|
271
|
+
" Cylinder: Click twice for axis\n"
|
|
272
|
+
" Point: Click for gradual refinement"
|
|
273
|
+
)
|
|
274
|
+
self._plotter.add_text(
|
|
275
|
+
help_text,
|
|
276
|
+
position="upper_left",
|
|
277
|
+
font_size=9,
|
|
278
|
+
name="help_panel",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _setup_picking(self) -> None:
|
|
282
|
+
"""Set up point picking callback."""
|
|
283
|
+
if self._plotter is None:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
self._plotter.enable_point_picking(
|
|
287
|
+
callback=self._on_point_picked,
|
|
288
|
+
show_message=False,
|
|
289
|
+
pickable_window=False,
|
|
290
|
+
left_clicking=True,
|
|
291
|
+
show_point=True,
|
|
292
|
+
point_size=10,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _setup_sliders(self) -> None:
|
|
296
|
+
"""Set up slider widgets for parameters."""
|
|
297
|
+
if self._plotter is None:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
self._plotter.add_slider_widget(
|
|
301
|
+
callback=self._on_size_changed,
|
|
302
|
+
rng=[_MIN_SIZE, _MAX_SIZE],
|
|
303
|
+
value=self._current_size,
|
|
304
|
+
title="Target Size",
|
|
305
|
+
pointa=(0.025, 0.1),
|
|
306
|
+
pointb=(0.31, 0.1),
|
|
307
|
+
style="modern",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
self._plotter.add_slider_widget(
|
|
311
|
+
callback=self._on_radius_changed,
|
|
312
|
+
rng=[_MIN_RADIUS, _MAX_RADIUS],
|
|
313
|
+
value=self._current_radius,
|
|
314
|
+
title="Radius",
|
|
315
|
+
pointa=(0.35, 0.1),
|
|
316
|
+
pointb=(0.64, 0.1),
|
|
317
|
+
style="modern",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
self._plotter.add_slider_widget(
|
|
321
|
+
callback=self._on_influence_changed,
|
|
322
|
+
rng=[_MIN_RADIUS, _MAX_RADIUS],
|
|
323
|
+
value=self._current_influence_radius,
|
|
324
|
+
title="Influence Radius",
|
|
325
|
+
pointa=(0.67, 0.1),
|
|
326
|
+
pointb=(0.98, 0.1),
|
|
327
|
+
style="modern",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def _setup_buttons(self) -> None:
|
|
331
|
+
"""Set up mode selection buttons."""
|
|
332
|
+
if self._plotter is None:
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
button_size = 30
|
|
336
|
+
x_start = 10
|
|
337
|
+
y_start = 10
|
|
338
|
+
spacing = 40
|
|
339
|
+
|
|
340
|
+
self._plotter.add_checkbox_button_widget(
|
|
341
|
+
callback=lambda state: self._set_mode(
|
|
342
|
+
ConstraintMode.SPHERE if state else ConstraintMode.NONE,
|
|
343
|
+
),
|
|
344
|
+
value=self._state.mode == ConstraintMode.SPHERE,
|
|
345
|
+
position=(x_start, y_start + 3 * spacing),
|
|
346
|
+
size=button_size,
|
|
347
|
+
color_on=_SPHERE_COLOR,
|
|
348
|
+
color_off="white",
|
|
349
|
+
)
|
|
350
|
+
self._plotter.add_text(
|
|
351
|
+
"Sphere",
|
|
352
|
+
position=(x_start + button_size + 5, y_start + 3 * spacing + 5),
|
|
353
|
+
font_size=10,
|
|
354
|
+
name="sphere_label",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
self._plotter.add_checkbox_button_widget(
|
|
358
|
+
callback=lambda state: self._set_mode(
|
|
359
|
+
ConstraintMode.BOX if state else ConstraintMode.NONE,
|
|
360
|
+
),
|
|
361
|
+
value=self._state.mode == ConstraintMode.BOX,
|
|
362
|
+
position=(x_start, y_start + 2 * spacing),
|
|
363
|
+
size=button_size,
|
|
364
|
+
color_on=_BOX_COLOR,
|
|
365
|
+
color_off="white",
|
|
366
|
+
)
|
|
367
|
+
self._plotter.add_text(
|
|
368
|
+
"Box",
|
|
369
|
+
position=(x_start + button_size + 5, y_start + 2 * spacing + 5),
|
|
370
|
+
font_size=10,
|
|
371
|
+
name="box_label",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if self._is_3d:
|
|
375
|
+
self._plotter.add_checkbox_button_widget(
|
|
376
|
+
callback=lambda state: self._set_mode(
|
|
377
|
+
ConstraintMode.CYLINDER if state else ConstraintMode.NONE,
|
|
378
|
+
),
|
|
379
|
+
value=self._state.mode == ConstraintMode.CYLINDER,
|
|
380
|
+
position=(x_start, y_start + spacing),
|
|
381
|
+
size=button_size,
|
|
382
|
+
color_on=_CYLINDER_COLOR,
|
|
383
|
+
color_off="white",
|
|
384
|
+
)
|
|
385
|
+
self._plotter.add_text(
|
|
386
|
+
"Cylinder",
|
|
387
|
+
position=(x_start + button_size + 5, y_start + spacing + 5),
|
|
388
|
+
font_size=10,
|
|
389
|
+
name="cylinder_label",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
self._plotter.add_checkbox_button_widget(
|
|
393
|
+
callback=lambda state: self._set_mode(
|
|
394
|
+
ConstraintMode.POINT if state else ConstraintMode.NONE,
|
|
395
|
+
),
|
|
396
|
+
value=self._state.mode == ConstraintMode.POINT,
|
|
397
|
+
position=(x_start, y_start),
|
|
398
|
+
size=button_size,
|
|
399
|
+
color_on=_POINT_COLOR,
|
|
400
|
+
color_off="white",
|
|
401
|
+
)
|
|
402
|
+
self._plotter.add_text(
|
|
403
|
+
"Point",
|
|
404
|
+
position=(x_start + button_size + 5, y_start + 5),
|
|
405
|
+
font_size=10,
|
|
406
|
+
name="point_label",
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def _setup_instructions(self) -> None:
|
|
410
|
+
"""Set up instruction text."""
|
|
411
|
+
if self._plotter is None:
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
self._update_instructions()
|
|
415
|
+
|
|
416
|
+
def _get_status_text(self) -> str:
|
|
417
|
+
"""Get current status text including mode and constraint count."""
|
|
418
|
+
mode_names = {
|
|
419
|
+
ConstraintMode.NONE: "None",
|
|
420
|
+
ConstraintMode.SPHERE: "SPHERE",
|
|
421
|
+
ConstraintMode.BOX: "BOX",
|
|
422
|
+
ConstraintMode.CYLINDER: "CYLINDER",
|
|
423
|
+
ConstraintMode.POINT: "POINT",
|
|
424
|
+
}
|
|
425
|
+
mode_instructions = {
|
|
426
|
+
ConstraintMode.NONE: "Select a tool below",
|
|
427
|
+
ConstraintMode.SPHERE: "Click mesh to place sphere",
|
|
428
|
+
ConstraintMode.BOX: (
|
|
429
|
+
"Click mesh for second corner"
|
|
430
|
+
if self._state.first_point is not None
|
|
431
|
+
else "Click mesh for first corner"
|
|
432
|
+
),
|
|
433
|
+
ConstraintMode.CYLINDER: (
|
|
434
|
+
"Click mesh for second axis point"
|
|
435
|
+
if self._state.first_point is not None
|
|
436
|
+
else "Click mesh for first axis point"
|
|
437
|
+
),
|
|
438
|
+
ConstraintMode.POINT: "Click mesh to place point",
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
mode = mode_names.get(self._state.mode, "")
|
|
442
|
+
instruction = mode_instructions.get(self._state.mode, "")
|
|
443
|
+
count = len(self._constraints)
|
|
444
|
+
|
|
445
|
+
return f"Mode: {mode}\n{instruction}\n\nConstraints: {count}"
|
|
446
|
+
|
|
447
|
+
def _set_mode(self, mode: ConstraintMode) -> None:
|
|
448
|
+
"""Set the current interaction mode."""
|
|
449
|
+
self._state.mode = mode
|
|
450
|
+
self._state.first_point = None
|
|
451
|
+
self._clear_preview()
|
|
452
|
+
self._update_instructions()
|
|
453
|
+
|
|
454
|
+
def _update_instructions(self) -> None:
|
|
455
|
+
"""Update instruction text based on current mode and state."""
|
|
456
|
+
if self._plotter is None:
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
self._plotter.add_text(
|
|
460
|
+
self._get_status_text(),
|
|
461
|
+
position="upper_right",
|
|
462
|
+
font_size=11,
|
|
463
|
+
name="instructions",
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
def _on_size_changed(self, value: float) -> None:
|
|
467
|
+
"""Handle size slider change."""
|
|
468
|
+
self._current_size = value
|
|
469
|
+
self._current_near_size = value
|
|
470
|
+
self._current_far_size = value * 10
|
|
471
|
+
|
|
472
|
+
def _on_radius_changed(self, value: float) -> None:
|
|
473
|
+
"""Handle radius slider change."""
|
|
474
|
+
self._current_radius = value
|
|
475
|
+
self._update_preview()
|
|
476
|
+
|
|
477
|
+
def _on_influence_changed(self, value: float) -> None:
|
|
478
|
+
"""Handle influence radius slider change."""
|
|
479
|
+
self._current_influence_radius = value
|
|
480
|
+
|
|
481
|
+
def _on_point_picked(self, point: NDArray[np.float64]) -> None:
|
|
482
|
+
"""Handle point picking callback."""
|
|
483
|
+
if self._state.mode == ConstraintMode.NONE:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
point = np.asarray(point, dtype=np.float64)
|
|
487
|
+
|
|
488
|
+
if self._state.mode == ConstraintMode.SPHERE:
|
|
489
|
+
self._add_sphere_constraint(point)
|
|
490
|
+
elif self._state.mode == ConstraintMode.BOX:
|
|
491
|
+
self._handle_box_click(point)
|
|
492
|
+
elif self._state.mode == ConstraintMode.CYLINDER:
|
|
493
|
+
self._handle_cylinder_click(point)
|
|
494
|
+
elif self._state.mode == ConstraintMode.POINT:
|
|
495
|
+
self._add_point_constraint(point)
|
|
496
|
+
|
|
497
|
+
def _add_sphere_constraint(self, center: NDArray[np.float64]) -> None:
|
|
498
|
+
"""Add a sphere constraint at the given center."""
|
|
499
|
+
constraint = SphereSize(
|
|
500
|
+
center=center,
|
|
501
|
+
radius=self._current_radius,
|
|
502
|
+
size=self._current_size,
|
|
503
|
+
)
|
|
504
|
+
self._constraints.append(constraint)
|
|
505
|
+
self._visualize_constraint(constraint)
|
|
506
|
+
self._update_instructions()
|
|
507
|
+
|
|
508
|
+
def _handle_box_click(self, point: NDArray[np.float64]) -> None:
|
|
509
|
+
"""Handle click in box mode."""
|
|
510
|
+
if self._state.first_point is None:
|
|
511
|
+
self._state.first_point = point
|
|
512
|
+
self._show_preview_point(point, _BOX_COLOR)
|
|
513
|
+
self._update_instructions()
|
|
514
|
+
else:
|
|
515
|
+
self._add_box_constraint(self._state.first_point, point)
|
|
516
|
+
self._state.first_point = None
|
|
517
|
+
self._clear_preview()
|
|
518
|
+
self._update_instructions()
|
|
519
|
+
|
|
520
|
+
def _add_box_constraint(
|
|
521
|
+
self,
|
|
522
|
+
point1: NDArray[np.float64],
|
|
523
|
+
point2: NDArray[np.float64],
|
|
524
|
+
) -> None:
|
|
525
|
+
"""Add a box constraint between two points."""
|
|
526
|
+
min_corner = np.minimum(point1, point2)
|
|
527
|
+
max_corner = np.maximum(point1, point2)
|
|
528
|
+
|
|
529
|
+
if not self._is_3d:
|
|
530
|
+
min_corner = min_corner[:2]
|
|
531
|
+
max_corner = max_corner[:2]
|
|
532
|
+
|
|
533
|
+
constraint = BoxSize(
|
|
534
|
+
bounds=np.array([min_corner, max_corner]),
|
|
535
|
+
size=self._current_size,
|
|
536
|
+
)
|
|
537
|
+
self._constraints.append(constraint)
|
|
538
|
+
self._visualize_constraint(constraint)
|
|
539
|
+
|
|
540
|
+
def _handle_cylinder_click(self, point: NDArray[np.float64]) -> None:
|
|
541
|
+
"""Handle click in cylinder mode."""
|
|
542
|
+
if self._state.first_point is None:
|
|
543
|
+
self._state.first_point = point
|
|
544
|
+
self._show_preview_point(point, _CYLINDER_COLOR)
|
|
545
|
+
self._update_instructions()
|
|
546
|
+
else:
|
|
547
|
+
self._add_cylinder_constraint(self._state.first_point, point)
|
|
548
|
+
self._state.first_point = None
|
|
549
|
+
self._clear_preview()
|
|
550
|
+
self._update_instructions()
|
|
551
|
+
|
|
552
|
+
def _add_cylinder_constraint(
|
|
553
|
+
self,
|
|
554
|
+
point1: NDArray[np.float64],
|
|
555
|
+
point2: NDArray[np.float64],
|
|
556
|
+
) -> None:
|
|
557
|
+
"""Add a cylinder constraint between two axis points."""
|
|
558
|
+
constraint = CylinderSize(
|
|
559
|
+
point1=point1,
|
|
560
|
+
point2=point2,
|
|
561
|
+
radius=self._current_radius,
|
|
562
|
+
size=self._current_size,
|
|
563
|
+
)
|
|
564
|
+
self._constraints.append(constraint)
|
|
565
|
+
self._visualize_constraint(constraint)
|
|
566
|
+
|
|
567
|
+
def _add_point_constraint(self, point: NDArray[np.float64]) -> None:
|
|
568
|
+
"""Add a point-based sizing constraint."""
|
|
569
|
+
if not self._is_3d:
|
|
570
|
+
point = point[:2]
|
|
571
|
+
|
|
572
|
+
constraint = PointSize(
|
|
573
|
+
point=point,
|
|
574
|
+
near_size=self._current_near_size,
|
|
575
|
+
far_size=self._current_far_size,
|
|
576
|
+
influence_radius=self._current_influence_radius,
|
|
577
|
+
)
|
|
578
|
+
self._constraints.append(constraint)
|
|
579
|
+
self._visualize_constraint(constraint)
|
|
580
|
+
self._update_instructions()
|
|
581
|
+
|
|
582
|
+
def _visualize_constraint(self, constraint: SizingConstraint) -> None:
|
|
583
|
+
"""Add visual representation of a constraint."""
|
|
584
|
+
if self._plotter is None:
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
actor: pv.Actor | None = None
|
|
588
|
+
|
|
589
|
+
if isinstance(constraint, SphereSize):
|
|
590
|
+
sphere = pv.Sphere(
|
|
591
|
+
center=constraint.center,
|
|
592
|
+
radius=constraint.radius,
|
|
593
|
+
)
|
|
594
|
+
actor = self._plotter.add_mesh(
|
|
595
|
+
sphere,
|
|
596
|
+
color=_SPHERE_COLOR,
|
|
597
|
+
opacity=_CONSTRAINT_OPACITY,
|
|
598
|
+
name=f"constraint_{len(self._visuals)}",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
elif isinstance(constraint, BoxSize):
|
|
602
|
+
bounds = constraint.bounds
|
|
603
|
+
if len(bounds[0]) == _DIMS_2D:
|
|
604
|
+
box = pv.Rectangle(
|
|
605
|
+
[
|
|
606
|
+
[bounds[0][0], bounds[0][1], 0],
|
|
607
|
+
[bounds[1][0], bounds[0][1], 0],
|
|
608
|
+
[bounds[1][0], bounds[1][1], 0],
|
|
609
|
+
[bounds[0][0], bounds[1][1], 0],
|
|
610
|
+
],
|
|
611
|
+
)
|
|
612
|
+
else:
|
|
613
|
+
box = pv.Box(
|
|
614
|
+
bounds=[
|
|
615
|
+
bounds[0][0],
|
|
616
|
+
bounds[1][0],
|
|
617
|
+
bounds[0][1],
|
|
618
|
+
bounds[1][1],
|
|
619
|
+
bounds[0][2],
|
|
620
|
+
bounds[1][2],
|
|
621
|
+
],
|
|
622
|
+
)
|
|
623
|
+
actor = self._plotter.add_mesh(
|
|
624
|
+
box,
|
|
625
|
+
color=_BOX_COLOR,
|
|
626
|
+
opacity=_CONSTRAINT_OPACITY,
|
|
627
|
+
name=f"constraint_{len(self._visuals)}",
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
elif isinstance(constraint, CylinderSize):
|
|
631
|
+
direction = constraint.point2 - constraint.point1
|
|
632
|
+
height = np.linalg.norm(direction)
|
|
633
|
+
center = (constraint.point1 + constraint.point2) / 2
|
|
634
|
+
cylinder = pv.Cylinder(
|
|
635
|
+
center=center,
|
|
636
|
+
direction=direction,
|
|
637
|
+
radius=constraint.radius,
|
|
638
|
+
height=height,
|
|
639
|
+
)
|
|
640
|
+
actor = self._plotter.add_mesh(
|
|
641
|
+
cylinder,
|
|
642
|
+
color=_CYLINDER_COLOR,
|
|
643
|
+
opacity=_CONSTRAINT_OPACITY,
|
|
644
|
+
name=f"constraint_{len(self._visuals)}",
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
elif isinstance(constraint, PointSize):
|
|
648
|
+
point_3d = (
|
|
649
|
+
constraint.point
|
|
650
|
+
if len(constraint.point) == _DIMS_3D
|
|
651
|
+
else np.array([constraint.point[0], constraint.point[1], 0.0])
|
|
652
|
+
)
|
|
653
|
+
sphere = pv.Sphere(
|
|
654
|
+
center=point_3d,
|
|
655
|
+
radius=constraint.influence_radius,
|
|
656
|
+
)
|
|
657
|
+
actor = self._plotter.add_mesh(
|
|
658
|
+
sphere,
|
|
659
|
+
color=_POINT_COLOR,
|
|
660
|
+
opacity=_CONSTRAINT_OPACITY * 0.5,
|
|
661
|
+
name=f"constraint_{len(self._visuals)}",
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
inner_sphere = pv.Sphere(
|
|
665
|
+
center=point_3d,
|
|
666
|
+
radius=constraint.influence_radius * 0.1,
|
|
667
|
+
)
|
|
668
|
+
self._plotter.add_mesh(
|
|
669
|
+
inner_sphere,
|
|
670
|
+
color=_POINT_COLOR,
|
|
671
|
+
opacity=_CONSTRAINT_OPACITY,
|
|
672
|
+
name=f"constraint_{len(self._visuals)}_inner",
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
if actor is not None:
|
|
676
|
+
self._visuals.append(ConstraintVisual(constraint=constraint, actor=actor))
|
|
677
|
+
|
|
678
|
+
def _show_preview_point(
|
|
679
|
+
self,
|
|
680
|
+
point: NDArray[np.float64],
|
|
681
|
+
color: str,
|
|
682
|
+
) -> None:
|
|
683
|
+
"""Show preview marker at a point."""
|
|
684
|
+
if self._plotter is None:
|
|
685
|
+
return
|
|
686
|
+
|
|
687
|
+
self._clear_preview()
|
|
688
|
+
sphere = pv.Sphere(center=point, radius=self._current_radius * 0.2)
|
|
689
|
+
self._state.preview_actor = self._plotter.add_mesh(
|
|
690
|
+
sphere,
|
|
691
|
+
color=color,
|
|
692
|
+
opacity=_PREVIEW_OPACITY,
|
|
693
|
+
name="preview",
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
def _update_preview(self) -> None:
|
|
697
|
+
"""Update preview based on current state."""
|
|
698
|
+
if self._state.first_point is None or self._plotter is None:
|
|
699
|
+
return
|
|
700
|
+
|
|
701
|
+
color = (
|
|
702
|
+
_BOX_COLOR if self._state.mode == ConstraintMode.BOX else _CYLINDER_COLOR
|
|
703
|
+
)
|
|
704
|
+
self._show_preview_point(self._state.first_point, color)
|
|
705
|
+
|
|
706
|
+
def _clear_preview(self) -> None:
|
|
707
|
+
"""Clear any preview visualization."""
|
|
708
|
+
if self._plotter is not None and self._state.preview_actor is not None:
|
|
709
|
+
self._plotter.remove_actor(self._state.preview_actor)
|
|
710
|
+
self._state.preview_actor = None
|
|
711
|
+
|
|
712
|
+
def get_constraints(self) -> list[SizingConstraint]:
|
|
713
|
+
"""Get the list of defined constraints.
|
|
714
|
+
|
|
715
|
+
Returns
|
|
716
|
+
-------
|
|
717
|
+
list[SizingConstraint]
|
|
718
|
+
List of sizing constraints defined through interaction.
|
|
719
|
+
|
|
720
|
+
"""
|
|
721
|
+
return list(self._constraints)
|
|
722
|
+
|
|
723
|
+
def apply_to_mesh(self) -> None:
|
|
724
|
+
"""Apply the defined constraints to the mesh.
|
|
725
|
+
|
|
726
|
+
This adds all constraints to the mesh's sizing system, ready
|
|
727
|
+
for the next remesh operation.
|
|
728
|
+
|
|
729
|
+
"""
|
|
730
|
+
for constraint in self._constraints:
|
|
731
|
+
if isinstance(constraint, SphereSize):
|
|
732
|
+
self._mesh.set_size_sphere(
|
|
733
|
+
center=constraint.center,
|
|
734
|
+
radius=constraint.radius,
|
|
735
|
+
size=constraint.size,
|
|
736
|
+
)
|
|
737
|
+
elif isinstance(constraint, BoxSize):
|
|
738
|
+
self._mesh.set_size_box(
|
|
739
|
+
bounds=constraint.bounds,
|
|
740
|
+
size=constraint.size,
|
|
741
|
+
)
|
|
742
|
+
elif isinstance(constraint, CylinderSize):
|
|
743
|
+
self._mesh.set_size_cylinder(
|
|
744
|
+
point1=constraint.point1,
|
|
745
|
+
point2=constraint.point2,
|
|
746
|
+
radius=constraint.radius,
|
|
747
|
+
size=constraint.size,
|
|
748
|
+
)
|
|
749
|
+
elif isinstance(constraint, PointSize):
|
|
750
|
+
self._mesh.set_size_from_point(
|
|
751
|
+
point=constraint.point,
|
|
752
|
+
near_size=constraint.near_size,
|
|
753
|
+
far_size=constraint.far_size,
|
|
754
|
+
influence_radius=constraint.influence_radius,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def clear_constraints(self) -> None:
|
|
758
|
+
"""Clear all defined constraints."""
|
|
759
|
+
self._constraints.clear()
|
|
760
|
+
if self._plotter is not None:
|
|
761
|
+
for visual in self._visuals:
|
|
762
|
+
self._plotter.remove_actor(visual.actor)
|
|
763
|
+
if visual.label_actor is not None:
|
|
764
|
+
self._plotter.remove_actor(visual.label_actor)
|
|
765
|
+
self._visuals.clear()
|
|
766
|
+
|
|
767
|
+
def remove_constraint(self, index: int) -> None:
|
|
768
|
+
"""Remove a constraint by index.
|
|
769
|
+
|
|
770
|
+
Parameters
|
|
771
|
+
----------
|
|
772
|
+
index : int
|
|
773
|
+
Index of the constraint to remove.
|
|
774
|
+
|
|
775
|
+
Raises
|
|
776
|
+
------
|
|
777
|
+
IndexError
|
|
778
|
+
If index is out of range.
|
|
779
|
+
|
|
780
|
+
"""
|
|
781
|
+
if index < 0 or index >= len(self._constraints):
|
|
782
|
+
msg = f"Constraint index {index} out of range"
|
|
783
|
+
raise IndexError(msg)
|
|
784
|
+
|
|
785
|
+
self._constraints.pop(index)
|
|
786
|
+
if self._plotter is not None:
|
|
787
|
+
visual = self._visuals.pop(index)
|
|
788
|
+
self._plotter.remove_actor(visual.actor)
|
|
789
|
+
if visual.label_actor is not None:
|
|
790
|
+
self._plotter.remove_actor(visual.label_actor)
|