cadgmsh 0.1.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.
- cadgmsh-0.1.0/LICENSE +21 -0
- cadgmsh-0.1.0/PKG-INFO +43 -0
- cadgmsh-0.1.0/README.md +68 -0
- cadgmsh-0.1.0/pyproject.toml +44 -0
- cadgmsh-0.1.0/setup.cfg +4 -0
- cadgmsh-0.1.0/src/cadgmsh/__init__.py +4 -0
- cadgmsh-0.1.0/src/cadgmsh/_extract.py +52 -0
- cadgmsh-0.1.0/src/cadgmsh/_mesh.py +106 -0
- cadgmsh-0.1.0/src/cadgmsh/_occ.py +25 -0
- cadgmsh-0.1.0/src/cadgmsh/_resolve.py +19 -0
- cadgmsh-0.1.0/src/cadgmsh/_types.py +31 -0
- cadgmsh-0.1.0/src/cadgmsh.egg-info/PKG-INFO +43 -0
- cadgmsh-0.1.0/src/cadgmsh.egg-info/SOURCES.txt +17 -0
- cadgmsh-0.1.0/src/cadgmsh.egg-info/dependency_links.txt +1 -0
- cadgmsh-0.1.0/src/cadgmsh.egg-info/requires.txt +14 -0
- cadgmsh-0.1.0/src/cadgmsh.egg-info/top_level.txt +1 -0
- cadgmsh-0.1.0/tests/test_extract.py +78 -0
- cadgmsh-0.1.0/tests/test_mesh.py +81 -0
- cadgmsh-0.1.0/tests/test_occ.py +61 -0
cadgmsh-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 David Straub
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cadgmsh-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cadgmsh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mesh CadQuery/build123d geometry with gmsh
|
|
5
|
+
Author-email: David Straub <straub@protonmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 David Straub
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/DavidMStraub/cadgmsh
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: gmsh
|
|
32
|
+
Requires-Dist: meshio
|
|
33
|
+
Requires-Dist: numpy
|
|
34
|
+
Provides-Extra: test
|
|
35
|
+
Requires-Dist: pytest; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
37
|
+
Requires-Dist: build123d; extra == "test"
|
|
38
|
+
Requires-Dist: cadquery; extra == "test"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: cadgmsh[test]; extra == "dev"
|
|
41
|
+
Requires-Dist: pyright; extra == "dev"
|
|
42
|
+
Requires-Dist: ruff; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
cadgmsh-0.1.0/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# cadgmsh
|
|
2
|
+
|
|
3
|
+
Mesh [CadQuery](https://github.com/CadQuery/cadquery) / [build123d](https://github.com/gumyr/build123d) geometry with [gmsh](https://gmsh.info). No temp files, no exposed `initialize`/`finalize`.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import cadgmsh
|
|
7
|
+
|
|
8
|
+
mesh = cadgmsh.mesh(shape, lc=0.3)
|
|
9
|
+
|
|
10
|
+
mesh = cadgmsh.mesh(
|
|
11
|
+
[box, sphere],
|
|
12
|
+
physical={
|
|
13
|
+
"steel": box,
|
|
14
|
+
"rubber": sphere,
|
|
15
|
+
"fixed": box.faces().sort_by(Axis.Z)[0],
|
|
16
|
+
},
|
|
17
|
+
imprint=True,
|
|
18
|
+
lc=0.3,
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Returns a [`meshio.Mesh`](https://github.com/nschloe/meshio). Works with [CadQuery](https://github.com/CadQuery/cadquery) and [build123d](https://github.com/gumyr/build123d) shapes interchangeably.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install cadgmsh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires [CadQuery](https://github.com/CadQuery/cadquery) or [build123d](https://github.com/gumyr/build123d) in your environment — neither is a hard dependency of cadgmsh itself.
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
cadgmsh.mesh(
|
|
36
|
+
shapes, # single shape or list of shapes
|
|
37
|
+
*,
|
|
38
|
+
physical=None, # dict[str, shape | list[shape]]
|
|
39
|
+
dim=3, # max mesh dimension (1 / 2 / 3)
|
|
40
|
+
order=1, # element order (1 = linear, 2 = quadratic, …)
|
|
41
|
+
algorithm=None, # gmsh algorithm number; None = default
|
|
42
|
+
lc=None, # characteristic length (max element size)
|
|
43
|
+
lc_min=None, # min characteristic length
|
|
44
|
+
imprint=False, # boolean-fragment for conforming multi-body meshes
|
|
45
|
+
verbose=False, # show gmsh output
|
|
46
|
+
) -> meshio.Mesh
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**`physical`** maps string labels to shapes (solids, faces, edges, or vertices). Labels become named cell sets in the returned mesh, usable for boundary conditions and material assignment.
|
|
50
|
+
|
|
51
|
+
**`imprint`** runs `BooleanFragments` on all input shapes so that shared interfaces are meshed conformally. Required for multi-domain simulations. Note: interface faces tagged in `physical` will have their OCC pointer invalidated by the fragment operation — tag volumes instead.
|
|
52
|
+
|
|
53
|
+
## Custom shapes
|
|
54
|
+
|
|
55
|
+
cadgmsh accepts any shape that exposes `.wrapped._address()` (OCP pybind11 pointer):
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from cadgmsh import OccShape
|
|
59
|
+
|
|
60
|
+
class MyShape:
|
|
61
|
+
@property
|
|
62
|
+
def wrapped(self) -> object: # must have ._address()
|
|
63
|
+
return self._occ_shape
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cadgmsh"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Mesh CadQuery/build123d geometry with gmsh"
|
|
9
|
+
authors = [{name = "David Straub", email = "straub@protonmail.com"}]
|
|
10
|
+
license = {file = "LICENSE"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"gmsh",
|
|
14
|
+
"meshio",
|
|
15
|
+
"numpy",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Repository = "https://github.com/DavidMStraub/cadgmsh"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
test = ["pytest", "pytest-cov", "build123d", "cadquery"]
|
|
23
|
+
dev = ["cadgmsh[test]", "pyright", "ruff"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = ["tests"]
|
|
30
|
+
pythonpath = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.pyright]
|
|
33
|
+
include = ["src"]
|
|
34
|
+
pythonVersion = "3.10"
|
|
35
|
+
typeCheckingMode = "standard"
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
line-length = 88
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint.per-file-ignores]
|
|
44
|
+
"tests/*" = ["B011"] # allow assert False in tests
|
cadgmsh-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gmsh
|
|
4
|
+
import meshio
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import ArrayLike, NDArray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _to_meshio() -> meshio.Mesh:
|
|
10
|
+
"""
|
|
11
|
+
Extract the current gmsh model into a :class:`meshio.Mesh`.
|
|
12
|
+
|
|
13
|
+
Reads nodes, elements, and any named physical groups from the active gmsh
|
|
14
|
+
session and returns them as a meshio-compatible object.
|
|
15
|
+
Must be called while a gmsh session is active (between ``initialize`` /
|
|
16
|
+
``finalize``) and after ``gmsh.model.mesh.generate``.
|
|
17
|
+
"""
|
|
18
|
+
idx, raw_pts, _ = gmsh.model.mesh.getNodes()
|
|
19
|
+
pts: NDArray[np.float64] = np.asarray(raw_pts).reshape(-1, 3)
|
|
20
|
+
node_idx: NDArray[np.int64] = np.asarray(idx, dtype=np.int64) - 1
|
|
21
|
+
srt = np.argsort(node_idx)
|
|
22
|
+
if not np.all(node_idx[srt] == np.arange(len(node_idx))):
|
|
23
|
+
raise ValueError("gmsh returned non-consecutive node indices")
|
|
24
|
+
pts = pts[srt]
|
|
25
|
+
|
|
26
|
+
elem_types, elem_tags, node_tags = gmsh.model.mesh.getElements()
|
|
27
|
+
|
|
28
|
+
cells: list[tuple[str, ArrayLike] | meshio.CellBlock] = []
|
|
29
|
+
for etype, etags, ntags in zip(elem_types, elem_tags, node_tags, strict=True):
|
|
30
|
+
n: int = gmsh.model.mesh.getElementProperties(etype)[3]
|
|
31
|
+
conn: NDArray[np.int64] = np.asarray(ntags, dtype=np.int64).reshape(-1, n) - 1
|
|
32
|
+
conn = conn[np.argsort(np.asarray(etags, dtype=np.int64))]
|
|
33
|
+
cells.append(meshio.CellBlock(meshio.gmsh.gmsh_to_meshio_type[etype], conn))
|
|
34
|
+
|
|
35
|
+
cell_sets: dict[str, list[ArrayLike]] = {}
|
|
36
|
+
for dim, tag in gmsh.model.getPhysicalGroups():
|
|
37
|
+
name = gmsh.model.getPhysicalName(dim, tag)
|
|
38
|
+
groups: list[list[NDArray[np.int64]]] = [[] for _ in range(len(cells))]
|
|
39
|
+
for etag in gmsh.model.getEntitiesForPhysicalGroup(dim, tag):
|
|
40
|
+
etypes_e, etags_e, _ = gmsh.model.mesh.getElements(dim, etag)
|
|
41
|
+
if not etypes_e:
|
|
42
|
+
continue
|
|
43
|
+
mtype = meshio.gmsh.gmsh_to_meshio_type[etypes_e[0]]
|
|
44
|
+
for k, cb in enumerate(cells):
|
|
45
|
+
if isinstance(cb, meshio.CellBlock) and cb.type == mtype:
|
|
46
|
+
groups[k].append(np.asarray(etags_e[0], dtype=np.int64) - 1)
|
|
47
|
+
cell_sets[name] = [
|
|
48
|
+
np.concatenate(ids) if ids else np.empty(0, dtype=np.int64)
|
|
49
|
+
for ids in groups
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
return meshio.Mesh(pts, cells, cell_sets=cell_sets)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gmsh
|
|
4
|
+
import meshio
|
|
5
|
+
|
|
6
|
+
from cadgmsh._extract import _to_meshio
|
|
7
|
+
from cadgmsh._occ import _pointer
|
|
8
|
+
from cadgmsh._resolve import _resolve_tags
|
|
9
|
+
from cadgmsh._types import Shape
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mesh(
|
|
13
|
+
shapes: Shape | list[Shape],
|
|
14
|
+
*,
|
|
15
|
+
lc: float | None = None,
|
|
16
|
+
lc_min: float | None = None,
|
|
17
|
+
imprint: bool = False,
|
|
18
|
+
dim: int = 3,
|
|
19
|
+
order: int = 1,
|
|
20
|
+
algorithm: int | None = None,
|
|
21
|
+
physical: dict[str, Shape | list[Shape]] | None = None,
|
|
22
|
+
verbose: bool = False,
|
|
23
|
+
) -> meshio.Mesh:
|
|
24
|
+
"""
|
|
25
|
+
Mesh one or more CAD shapes and return a :class:`meshio.Mesh`.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
shapes:
|
|
30
|
+
A single cadquery/build123d shape or a list of them.
|
|
31
|
+
lc:
|
|
32
|
+
Characteristic length (maximum element size).
|
|
33
|
+
lc_min:
|
|
34
|
+
Minimum characteristic length.
|
|
35
|
+
imprint:
|
|
36
|
+
If ``True``, boolean-fragment all input shapes so shared faces are
|
|
37
|
+
conformally meshed. Required for conforming multi-domain assemblies.
|
|
38
|
+
|
|
39
|
+
.. warning::
|
|
40
|
+
OCC Boolean operations on coincident or touching faces can produce
|
|
41
|
+
a hard segfault inside the OCC kernel that cannot be caught as a
|
|
42
|
+
Python exception. Verify that input shapes have no coincident
|
|
43
|
+
boundary faces before enabling this option.
|
|
44
|
+
dim:
|
|
45
|
+
Maximum mesh dimension (1, 2, or 3).
|
|
46
|
+
order:
|
|
47
|
+
Element order (1 = linear, 2 = quadratic, …).
|
|
48
|
+
algorithm:
|
|
49
|
+
gmsh meshing algorithm number (see gmsh docs). ``None`` = gmsh default.
|
|
50
|
+
physical:
|
|
51
|
+
Dict mapping string labels to shapes (or lists of shapes).
|
|
52
|
+
Each value may be a solid, face, edge, or vertex from cadquery or
|
|
53
|
+
build123d. Labels become named cell sets in the returned mesh.
|
|
54
|
+
verbose:
|
|
55
|
+
Show gmsh terminal output.
|
|
56
|
+
"""
|
|
57
|
+
shapes_list: list[Shape] = shapes if isinstance(shapes, list) else [shapes]
|
|
58
|
+
|
|
59
|
+
gmsh.initialize()
|
|
60
|
+
try:
|
|
61
|
+
gmsh.option.setNumber("General.Terminal", 1 if verbose else 0)
|
|
62
|
+
gmsh.model.add("model")
|
|
63
|
+
|
|
64
|
+
if lc is not None:
|
|
65
|
+
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", lc)
|
|
66
|
+
if lc_min is not None:
|
|
67
|
+
gmsh.option.setNumber("Mesh.CharacteristicLengthMin", lc_min)
|
|
68
|
+
|
|
69
|
+
all_dim_tags: list[list[tuple[int, int]]] = []
|
|
70
|
+
for s in shapes_list:
|
|
71
|
+
dt = gmsh.model.occ.importShapesNativePointer(
|
|
72
|
+
_pointer(s), highestDimOnly=False
|
|
73
|
+
)
|
|
74
|
+
all_dim_tags.append(list(dt))
|
|
75
|
+
|
|
76
|
+
if imprint and len(shapes_list) > 1:
|
|
77
|
+
flat: list[tuple[int, int]] = [
|
|
78
|
+
dt for group in all_dim_tags for dt in group
|
|
79
|
+
]
|
|
80
|
+
gmsh.model.occ.fragment(flat, [], removeObject=True, removeTool=True)
|
|
81
|
+
|
|
82
|
+
gmsh.model.occ.synchronize()
|
|
83
|
+
|
|
84
|
+
if physical:
|
|
85
|
+
for label, value in physical.items():
|
|
86
|
+
entries: list[Shape] = value if isinstance(value, list) else [value]
|
|
87
|
+
tags_by_dim: dict[int, list[int]] = {}
|
|
88
|
+
for entry in entries:
|
|
89
|
+
for d, t in _resolve_tags(entry):
|
|
90
|
+
tags_by_dim.setdefault(d, []).append(t)
|
|
91
|
+
for d, tags in tags_by_dim.items():
|
|
92
|
+
pg = gmsh.model.addPhysicalGroup(d, tags)
|
|
93
|
+
gmsh.model.setPhysicalName(d, pg, label)
|
|
94
|
+
|
|
95
|
+
if algorithm is not None:
|
|
96
|
+
gmsh.option.setNumber("Mesh.Algorithm", algorithm)
|
|
97
|
+
|
|
98
|
+
gmsh.model.mesh.generate(dim)
|
|
99
|
+
|
|
100
|
+
if order != 1:
|
|
101
|
+
gmsh.model.mesh.setOrder(order)
|
|
102
|
+
|
|
103
|
+
return _to_meshio()
|
|
104
|
+
|
|
105
|
+
finally:
|
|
106
|
+
gmsh.finalize()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from cadgmsh._types import Shape
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _unwrap(shape: Shape) -> Any:
|
|
9
|
+
"""Return the raw OCP ``TopoDS_*`` object, or pass through if already unwrapped."""
|
|
10
|
+
return shape.wrapped if hasattr(shape, "wrapped") else shape
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _pointer(shape: Shape) -> int:
|
|
14
|
+
"""
|
|
15
|
+
Return the integer memory address of a ``TopoDS_Shape``.
|
|
16
|
+
|
|
17
|
+
Compatible with ``gmsh.model.occ.importShapesNativePointer``.
|
|
18
|
+
Supports OCP (pybind11) via ``._address()`` and PythonOCC (SWIG) via ``.this``.
|
|
19
|
+
"""
|
|
20
|
+
occ: Any = _unwrap(shape)
|
|
21
|
+
if hasattr(occ, "_address"):
|
|
22
|
+
return int(occ._address())
|
|
23
|
+
if hasattr(occ, "this"):
|
|
24
|
+
return int(occ.this)
|
|
25
|
+
raise TypeError(f"Cannot extract OCC pointer from {type(shape)}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gmsh
|
|
4
|
+
|
|
5
|
+
from cadgmsh._occ import _pointer
|
|
6
|
+
from cadgmsh._types import Shape
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _resolve_tags(shape: Shape) -> list[tuple[int, int]]:
|
|
10
|
+
"""
|
|
11
|
+
Return the gmsh ``(dim, tag)`` pairs for *shape* by re-importing its OCC pointer.
|
|
12
|
+
|
|
13
|
+
gmsh tracks topology by TShape pointer identity, so importing a sub-shape that
|
|
14
|
+
is already part of the model returns existing tags rather than creating new ones.
|
|
15
|
+
Must be called after ``gmsh.model.occ.synchronize()``.
|
|
16
|
+
"""
|
|
17
|
+
return list(
|
|
18
|
+
gmsh.model.occ.importShapesNativePointer(_pointer(shape), highestDimOnly=True)
|
|
19
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Centralized type definitions for cadgmsh."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from build123d import Shape as _B3DShape
|
|
9
|
+
from cadquery.occ_impl.shapes import (
|
|
10
|
+
Shape as _CQShape, # type: ignore[import-untyped]
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class OccShape(Protocol):
|
|
16
|
+
"""
|
|
17
|
+
Structural protocol for OCC-backed shapes accepted by cadgmsh.
|
|
18
|
+
|
|
19
|
+
Satisfied by cadquery and build123d shapes.
|
|
20
|
+
Implement ``.wrapped`` pointing to an OCC ``TopoDS_Shape`` to integrate
|
|
21
|
+
custom shape types without depending on either library.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def wrapped(self) -> object: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
Shape = _B3DShape | _CQShape | OccShape
|
|
30
|
+
else:
|
|
31
|
+
Shape = OccShape
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cadgmsh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mesh CadQuery/build123d geometry with gmsh
|
|
5
|
+
Author-email: David Straub <straub@protonmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 David Straub
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/DavidMStraub/cadgmsh
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: gmsh
|
|
32
|
+
Requires-Dist: meshio
|
|
33
|
+
Requires-Dist: numpy
|
|
34
|
+
Provides-Extra: test
|
|
35
|
+
Requires-Dist: pytest; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
37
|
+
Requires-Dist: build123d; extra == "test"
|
|
38
|
+
Requires-Dist: cadquery; extra == "test"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: cadgmsh[test]; extra == "dev"
|
|
41
|
+
Requires-Dist: pyright; extra == "dev"
|
|
42
|
+
Requires-Dist: ruff; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/cadgmsh/__init__.py
|
|
5
|
+
src/cadgmsh/_extract.py
|
|
6
|
+
src/cadgmsh/_mesh.py
|
|
7
|
+
src/cadgmsh/_occ.py
|
|
8
|
+
src/cadgmsh/_resolve.py
|
|
9
|
+
src/cadgmsh/_types.py
|
|
10
|
+
src/cadgmsh.egg-info/PKG-INFO
|
|
11
|
+
src/cadgmsh.egg-info/SOURCES.txt
|
|
12
|
+
src/cadgmsh.egg-info/dependency_links.txt
|
|
13
|
+
src/cadgmsh.egg-info/requires.txt
|
|
14
|
+
src/cadgmsh.egg-info/top_level.txt
|
|
15
|
+
tests/test_extract.py
|
|
16
|
+
tests/test_mesh.py
|
|
17
|
+
tests/test_occ.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cadgmsh
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import gmsh
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from cadgmsh._extract import _to_meshio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def box_mesh():
|
|
10
|
+
gmsh.initialize()
|
|
11
|
+
gmsh.option.setNumber("General.Terminal", 0)
|
|
12
|
+
gmsh.model.add("test")
|
|
13
|
+
gmsh.model.occ.addBox(0, 0, 0, 1, 1, 1)
|
|
14
|
+
gmsh.model.occ.synchronize()
|
|
15
|
+
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 0.5)
|
|
16
|
+
gmsh.model.mesh.generate(3)
|
|
17
|
+
yield
|
|
18
|
+
gmsh.finalize()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def box_mesh_with_physical():
|
|
23
|
+
gmsh.initialize()
|
|
24
|
+
gmsh.option.setNumber("General.Terminal", 0)
|
|
25
|
+
gmsh.model.add("test")
|
|
26
|
+
vol_tag = gmsh.model.occ.addBox(0, 0, 0, 1, 1, 1)
|
|
27
|
+
gmsh.model.occ.synchronize()
|
|
28
|
+
pg_vol = gmsh.model.addPhysicalGroup(3, [vol_tag])
|
|
29
|
+
gmsh.model.setPhysicalName(3, pg_vol, "volume")
|
|
30
|
+
face_tags = [t for _, t in gmsh.model.getEntities(2)[:2]]
|
|
31
|
+
pg_face = gmsh.model.addPhysicalGroup(2, face_tags)
|
|
32
|
+
gmsh.model.setPhysicalName(2, pg_face, "surface")
|
|
33
|
+
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 0.5)
|
|
34
|
+
gmsh.model.mesh.generate(3)
|
|
35
|
+
yield
|
|
36
|
+
gmsh.finalize()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_points_shape(box_mesh):
|
|
40
|
+
m = _to_meshio()
|
|
41
|
+
assert m.points.ndim == 2
|
|
42
|
+
assert m.points.shape[1] == 3
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_points_nonempty(box_mesh):
|
|
46
|
+
m = _to_meshio()
|
|
47
|
+
assert len(m.points) > 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_cells_nonempty(box_mesh):
|
|
51
|
+
m = _to_meshio()
|
|
52
|
+
assert len(m.cells) > 0
|
|
53
|
+
assert sum(len(cb.data) for cb in m.cells) > 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_no_physical_groups(box_mesh):
|
|
57
|
+
m = _to_meshio()
|
|
58
|
+
assert m.cell_sets == {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_physical_group_names(box_mesh_with_physical):
|
|
62
|
+
m = _to_meshio()
|
|
63
|
+
assert "volume" in m.cell_sets
|
|
64
|
+
assert "surface" in m.cell_sets
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_physical_group_nonempty(box_mesh_with_physical):
|
|
68
|
+
m = _to_meshio()
|
|
69
|
+
volume_arrays = [a for a in m.cell_sets["volume"] if a is not None]
|
|
70
|
+
assert len(volume_arrays) > 0
|
|
71
|
+
assert sum(len(a) for a in volume_arrays) > 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_node_indices_are_consecutive(box_mesh):
|
|
75
|
+
idx, _, _ = gmsh.model.mesh.getNodes()
|
|
76
|
+
idx -= 1
|
|
77
|
+
srt = np.argsort(idx)
|
|
78
|
+
assert np.all(idx[srt] == np.arange(len(idx)))
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import meshio
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
bd = pytest.importorskip("build123d")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def box():
|
|
8
|
+
return bd.Box(1, 1, 1)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def sphere():
|
|
12
|
+
return bd.Sphere(0.4)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_returns_meshio_mesh():
|
|
16
|
+
import cadgmsh
|
|
17
|
+
result = cadgmsh.mesh(box(), lc=0.5)
|
|
18
|
+
assert isinstance(result, meshio.Mesh)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_points_and_cells():
|
|
22
|
+
import cadgmsh
|
|
23
|
+
result = cadgmsh.mesh(box(), lc=0.5)
|
|
24
|
+
assert result.points.shape[1] == 3
|
|
25
|
+
assert len(result.points) > 0
|
|
26
|
+
assert len(result.cells) > 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_list_input():
|
|
30
|
+
import cadgmsh
|
|
31
|
+
result = cadgmsh.mesh([box()], lc=0.5)
|
|
32
|
+
assert isinstance(result, meshio.Mesh)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_physical_volume():
|
|
36
|
+
import cadgmsh
|
|
37
|
+
b = box()
|
|
38
|
+
result = cadgmsh.mesh(b, physical={"steel": b}, lc=0.5)
|
|
39
|
+
assert "steel" in result.cell_sets
|
|
40
|
+
volume_arrays = [a for a in result.cell_sets["steel"] if a is not None]
|
|
41
|
+
assert len(volume_arrays) > 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_physical_face():
|
|
45
|
+
import cadgmsh
|
|
46
|
+
b = box()
|
|
47
|
+
bottom = b.faces().sort_by(bd.Axis.Z)[0]
|
|
48
|
+
result = cadgmsh.mesh(b, physical={"bottom": bottom}, lc=0.5)
|
|
49
|
+
assert "bottom" in result.cell_sets
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_dim2():
|
|
53
|
+
import cadgmsh
|
|
54
|
+
result = cadgmsh.mesh(box(), dim=2, lc=0.5)
|
|
55
|
+
cell_types = {cb.type for cb in result.cells}
|
|
56
|
+
assert not any("tetra" in t for t in cell_types)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_order2():
|
|
60
|
+
import cadgmsh
|
|
61
|
+
result = cadgmsh.mesh(box(), order=2, lc=0.5)
|
|
62
|
+
cell_types = {cb.type for cb in result.cells}
|
|
63
|
+
assert "tetra10" in cell_types
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_imprint_two_separated_boxes():
|
|
67
|
+
import cadgmsh
|
|
68
|
+
# imprint=True with non-touching shapes: exercises the code path without
|
|
69
|
+
# triggering OCC Boolean failures on coincident faces
|
|
70
|
+
b1 = bd.Box(1, 1, 1)
|
|
71
|
+
b2 = bd.Location((2, 0, 0)) * bd.Box(1, 1, 1)
|
|
72
|
+
result = cadgmsh.mesh([b1, b2], imprint=True, lc=0.5)
|
|
73
|
+
assert isinstance(result, meshio.Mesh)
|
|
74
|
+
assert len(result.points) > 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_lc_affects_mesh_density():
|
|
78
|
+
import cadgmsh
|
|
79
|
+
coarse = cadgmsh.mesh(box(), lc=0.5)
|
|
80
|
+
fine = cadgmsh.mesh(box(), lc=0.12)
|
|
81
|
+
assert len(fine.points) > len(coarse.points)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from cadgmsh._occ import _pointer, _unwrap
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _WithAddress:
|
|
7
|
+
def _address(self):
|
|
8
|
+
return 42
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _WithThis:
|
|
12
|
+
this = 99
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_unwrap_passthrough():
|
|
16
|
+
obj = object()
|
|
17
|
+
assert _unwrap(obj) is obj
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_unwrap_unwraps():
|
|
21
|
+
inner = object()
|
|
22
|
+
|
|
23
|
+
class Wrapped:
|
|
24
|
+
wrapped = inner
|
|
25
|
+
|
|
26
|
+
assert _unwrap(Wrapped()) is inner
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pointer_address():
|
|
30
|
+
assert _pointer(_WithAddress()) == 42
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_pointer_this():
|
|
34
|
+
assert _pointer(_WithThis()) == 99
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_pointer_wrapped_address():
|
|
38
|
+
class Shape:
|
|
39
|
+
wrapped = _WithAddress()
|
|
40
|
+
|
|
41
|
+
assert _pointer(Shape()) == 42
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_pointer_wrapped_this():
|
|
45
|
+
class Shape:
|
|
46
|
+
wrapped = _WithThis()
|
|
47
|
+
|
|
48
|
+
assert _pointer(Shape()) == 99
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_pointer_raises_on_unknown():
|
|
52
|
+
with pytest.raises(TypeError):
|
|
53
|
+
_pointer(object())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_pointer_raises_on_wrapped_unknown():
|
|
57
|
+
class Shape:
|
|
58
|
+
wrapped = object()
|
|
59
|
+
|
|
60
|
+
with pytest.raises(TypeError):
|
|
61
|
+
_pointer(Shape())
|