icplot 0.2.3__py3-none-any.whl → 0.3.0__py3-none-any.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 (70) hide show
  1. icplot/{gantt → charts}/gantt.py +2 -3
  2. icplot/{tree.py → charts/tree.py} +2 -3
  3. icplot/converter/__init__.py +3 -0
  4. icplot/converter/cli.py +27 -0
  5. icplot/converter/convert.py +32 -0
  6. icplot/{image_utils.py → converter/image.py} +5 -9
  7. icplot/geometry/base.py +6 -0
  8. icplot/{mesh → geometry/mesh}/cell.py +21 -0
  9. icplot/{mesh → geometry/mesh}/edge.py +1 -1
  10. icplot/{mesh → geometry/mesh}/mesh.py +50 -4
  11. icplot/{mesh → geometry/mesh}/openfoam/blockmesh.py +2 -3
  12. icplot/{mesh → geometry/mesh}/openfoam/foamfile.py +10 -2
  13. icplot/geometry/mesh/openfoam/polymesh.py +313 -0
  14. icplot/geometry/mesh/shapes.py +403 -0
  15. icplot/{mesh → geometry/mesh}/vertex.py +39 -1
  16. icplot/geometry/shapes.py +29 -0
  17. icplot/geometry/transform.py +2 -2
  18. icplot/graph/__init__.py +3 -4
  19. icplot/graph/plot.py +2 -53
  20. icplot/graph/plot_group.py +1 -1
  21. icplot/graph/series.py +3 -4
  22. icplot/main_cli.py +13 -62
  23. icplot/renderer/__init__.py +1 -0
  24. icplot/renderer/charts/pgfgantt.py +71 -0
  25. icplot/{gantt/gantt_renderer.py → renderer/charts/svggantt.py} +8 -6
  26. icplot/renderer/cli.py +58 -0
  27. icplot/renderer/config.py +41 -0
  28. icplot/renderer/formats/__init__.py +1 -0
  29. icplot/renderer/formats/mermaid.py +41 -0
  30. icplot/renderer/formats/plantuml.py +37 -0
  31. icplot/renderer/formats/tex.py +117 -0
  32. icplot/renderer/graph/__init__.py +3 -0
  33. icplot/renderer/graph/color.py +17 -0
  34. icplot/{graph → renderer/graph}/matplotlib.py +15 -7
  35. icplot/{graph → renderer/graph}/mpl.py +8 -8
  36. icplot/{graph/generator.py → renderer/graph/renderer.py} +43 -8
  37. icplot/{graph → renderer/graph}/vtk.py +2 -3
  38. icplot/renderer/render.py +140 -0
  39. icplot/{cairo_interface.py → renderer/scene/cairo.py} +5 -5
  40. icplot/{graph → renderer}/video.py +2 -3
  41. icplot/scene.py +1 -1
  42. icplot/theme/__init__.py +0 -0
  43. icplot/theme/cli.py +50 -0
  44. icplot/theme/color.py +97 -0
  45. icplot/theme/templates/puml.j2 +91 -0
  46. icplot/theme/templates/tikz.j2 +100 -0
  47. icplot/theme/theme.json +81 -0
  48. icplot/theme/theme.py +117 -0
  49. icplot/theme/theme_generator.py +119 -0
  50. {icplot-0.2.3.dist-info → icplot-0.3.0.dist-info}/METADATA +66 -18
  51. icplot-0.3.0.dist-info/RECORD +65 -0
  52. {icplot-0.2.3.dist-info → icplot-0.3.0.dist-info}/WHEEL +1 -1
  53. icplot/color.py +0 -50
  54. icplot/convert.py +0 -51
  55. icplot/gantt/__init__.py +0 -1
  56. icplot/gantt/pgfgantt.py +0 -15
  57. icplot/mermaid.py +0 -39
  58. icplot/mesh/openfoam/polymesh.py +0 -46
  59. icplot/mesh/shapes.py +0 -205
  60. icplot/tex.py +0 -78
  61. icplot/trace/__init__.py +0 -1
  62. icplot-0.2.3.dist-info/RECORD +0 -50
  63. icplot/{trace/trace.py → charts/__init__.py} +0 -0
  64. icplot/{mesh → geometry/mesh}/__init__.py +1 -1
  65. icplot/{mesh → geometry/mesh}/openfoam/__init__.py +1 -1
  66. icplot/{mesh → geometry/mesh}/operations.py +1 -1
  67. icplot/{mesh → geometry/mesh}/vtk.py +2 -2
  68. {icplot-0.2.3.dist-info → icplot-0.3.0.dist-info}/entry_points.txt +0 -0
  69. {icplot-0.2.3.dist-info → icplot-0.3.0.dist-info}/licenses/LICENSE +0 -0
  70. {icplot-0.2.3.dist-info → icplot-0.3.0.dist-info}/top_level.txt +0 -0
@@ -4,11 +4,10 @@ This module represents a gantt chard
4
4
 
5
5
  from datetime import datetime
6
6
 
7
- from pydantic import BaseModel
8
-
9
7
  from iccore.project import Milestone
8
+ from pydantic import BaseModel
10
9
 
11
- from icplot.color import Color
10
+ from icplot.theme.color import Color
12
11
 
13
12
 
14
13
  class GanttChart(BaseModel, frozen=True):
@@ -1,13 +1,12 @@
1
+ import logging
1
2
  import os
2
3
  import shutil
3
- import logging
4
4
  from dataclasses import dataclass
5
- from typing import Callable
6
5
  from pathlib import Path
6
+ from typing import Callable
7
7
 
8
8
  import graphviz
9
9
 
10
-
11
10
  logger = logging.getLogger(__name__)
12
11
 
13
12
 
@@ -0,0 +1,3 @@
1
+ from .image import pdf_to_png, svg_to_pdf, svg_to_png
2
+
3
+ __all__ = ["pdf_to_png", "svg_to_pdf", "svg_to_png"]
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+
3
+ from .convert import convert
4
+
5
+
6
+ def convert_cli(args):
7
+
8
+ source = args.source.resolve()
9
+ target = Path(args.target).resolve() if args.target else None
10
+ convert(source, target)
11
+
12
+
13
+ def register_parser(subparsers):
14
+
15
+ convert_parser = subparsers.add_parser("convert")
16
+ convert_parser.add_argument(
17
+ "source",
18
+ type=Path,
19
+ help="Path to file to be converted from",
20
+ )
21
+ convert_parser.add_argument(
22
+ "--target",
23
+ type=str,
24
+ default="",
25
+ help="Path to file to be converted to",
26
+ )
27
+ convert_parser.set_defaults(func=convert_cli)
@@ -0,0 +1,32 @@
1
+ """
2
+ Module supporting conversion between image formats
3
+ """
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ from .image import pdf_to_png, svg_to_pdf, svg_to_png
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def convert(source: Path, target: Path | None = None):
14
+ """
15
+ Convert between image formats using the source and target
16
+ file extensions as format hints.
17
+ """
18
+
19
+ logger.info("Converting %s", source)
20
+
21
+ if source.suffix == ".pdf":
22
+ pdf_to_png(source, target)
23
+ elif source.suffix == ".svg":
24
+ if target:
25
+ if target.suffix == ".png":
26
+ svg_to_png(source, target)
27
+ elif target.suffix == ".pdf":
28
+ svg_to_pdf(source, target)
29
+ else:
30
+ svg_to_png(source)
31
+
32
+ logger.info("Finished conversion")
@@ -7,27 +7,23 @@ import logging
7
7
  import os
8
8
  from pathlib import Path
9
9
 
10
+ logger = logging.getLogger(__name__)
11
+
10
12
  _HAS_WAND = True
11
13
  try:
12
- from wand.image import Image
13
14
  from wand.color import Color
15
+ from wand.image import Image
14
16
  except ImportError as e:
15
- logging.getLogger(__name__).warning(
16
- "Disabling Wand stupport. Failed to load with: %s", e
17
- )
17
+ logger.debug("wand not available: %s", e)
18
18
  _HAS_WAND = False
19
19
 
20
20
  _HAS_CAIRO_SVG = True
21
21
  try:
22
22
  import cairosvg
23
23
  except ImportError as e:
24
- logging.getLogger(__name__).warning(
25
- "Disabling CairoSVG stupport. Failed to load with: %s", e
26
- )
24
+ logger.debug("cairosvg not available: %s", e)
27
25
  _HAS_CAIRO_SVG = False
28
26
 
29
- logger = logging.getLogger(__name__)
30
-
31
27
 
32
28
  def has_wand() -> bool:
33
29
  return _HAS_WAND
icplot/geometry/base.py CHANGED
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
4
 
4
5
  import numpy as np
5
6
 
7
+ from .operations import get_point_distance
8
+
6
9
 
7
10
  @dataclass(frozen=True)
8
11
  class Vector:
@@ -47,6 +50,9 @@ class Point:
47
50
  def subtract(self, v: Vector) -> Vector:
48
51
  return Vector(self.x - v.x, self.y - v.y, self.z - v.z)
49
52
 
53
+ def get_distance(self, p: Point) -> float:
54
+ return get_point_distance(p.as_array(), self.as_array())
55
+
50
56
 
51
57
  @dataclass(frozen=True)
52
58
  class Bounds:
@@ -14,6 +14,27 @@ class Cell:
14
14
  raise NotImplementedError()
15
15
 
16
16
 
17
+ def flip_normals(cell: Cell) -> Cell:
18
+ if cell.type == "quad":
19
+ return Cell(type=cell.type, vertices=tuple(reversed(cell.vertices)))
20
+ else:
21
+ raise RuntimeError("Not implemented for this cell type")
22
+
23
+
24
+ def get_hex_faces(cell: Cell) -> tuple[tuple[int, ...], ...]:
25
+ if cell.type != "hex":
26
+ raise RuntimeError("Can only get hex faces for hex cells")
27
+
28
+ return (
29
+ (cell.vertices[0], cell.vertices[1], cell.vertices[5], cell.vertices[4]),
30
+ (cell.vertices[1], cell.vertices[2], cell.vertices[6], cell.vertices[5]),
31
+ (cell.vertices[2], cell.vertices[3], cell.vertices[7], cell.vertices[6]),
32
+ (cell.vertices[3], cell.vertices[0], cell.vertices[4], cell.vertices[7]),
33
+ (cell.vertices[0], cell.vertices[3], cell.vertices[2], cell.vertices[1]),
34
+ (cell.vertices[4], cell.vertices[5], cell.vertices[6], cell.vertices[7]),
35
+ )
36
+
37
+
17
38
  @dataclass(frozen=True)
18
39
  class HexCell(Cell):
19
40
  """
@@ -1,6 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
 
3
- from icplot.geometry import Point, segment_contains_point, get_segment_distance
3
+ from icplot.geometry import Point, get_segment_distance, segment_contains_point
4
4
 
5
5
  from .vertex import Vertex
6
6
 
@@ -4,13 +4,14 @@ over performance. Consider something else for high performance meshing.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
7
+
7
8
  from dataclasses import dataclass
8
9
 
9
- from icplot.geometry import Point, Vector, Transform, Bounds
10
+ from icplot.geometry import Bounds, Point, Shape, Transform, Vector
10
11
 
11
- from .vertex import Vertex
12
- from .cell import Cell
12
+ from .cell import Cell, flip_normals
13
13
  from .edge import Edge, contains_point, get_distance
14
+ from .vertex import Vertex, find_closest_vertex
14
15
 
15
16
 
16
17
  @dataclass(frozen=True)
@@ -49,9 +50,14 @@ class Mesh:
49
50
  ymin = min(ymin, v.y)
50
51
  ymax = max(ymax, v.y)
51
52
  zmin = min(zmin, v.z)
52
- zmax = min(zmax, v.z)
53
+ zmax = max(zmax, v.z)
53
54
  return Bounds(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, zmin=zmin, zmax=zmax)
54
55
 
56
+ def flip_normals(self) -> Mesh:
57
+ return Mesh(
58
+ vertices=self.vertices, cells=tuple(flip_normals(c) for c in self.cells)
59
+ )
60
+
55
61
  def get_location(self) -> Vector:
56
62
  bounds = self.get_bounds()
57
63
  return Vector(bounds.xmin, bounds.ymin, bounds.zmin)
@@ -62,6 +68,32 @@ class Mesh:
62
68
  vertices=tuple(v.translate(delta) for v in self.vertices), cells=self.cells
63
69
  )
64
70
 
71
+ def scale_by(self, factor: float) -> Mesh:
72
+ return Mesh(
73
+ vertices=tuple(v.scale(factor) for v in self.vertices), cells=self.cells
74
+ )
75
+
76
+ def rotate_to(self, vec: Vector) -> Mesh:
77
+ return Mesh(
78
+ vertices=tuple(v.rotate_to(vec) for v in self.vertices), cells=self.cells
79
+ )
80
+
81
+ def move_by(
82
+ self, x: float, y: float, z: float, selection: list[int] | None = None
83
+ ) -> Mesh:
84
+ delta = Vector(x, y, z)
85
+ if selection:
86
+ return Mesh(
87
+ vertices=tuple(
88
+ v.translate(delta) if v.id in selection else v.copy()
89
+ for v in self.vertices
90
+ ),
91
+ cells=self.cells,
92
+ )
93
+ return Mesh(
94
+ vertices=tuple(v.translate(delta) for v in self.vertices), cells=self.cells
95
+ )
96
+
65
97
  def select_edge(self, p: Point) -> tuple[int, int]:
66
98
 
67
99
  for c in self.cells:
@@ -70,6 +102,9 @@ class Mesh:
70
102
  return (e[0], e[1])
71
103
  raise RuntimeError("No edge found at selection point")
72
104
 
105
+ def get_closest_vertex(self, v: Vertex) -> Vertex:
106
+ return find_closest_vertex(self.vertices, v)
107
+
73
108
  def get_closest_edge(self, p: Point) -> tuple[int, int]:
74
109
 
75
110
  min_dist = -1.0
@@ -85,3 +120,14 @@ class Mesh:
85
120
  min_dist = dist
86
121
  closest_edges = (e[0], e[1])
87
122
  return closest_edges
123
+
124
+ def copy(self) -> Mesh:
125
+ return Mesh(
126
+ vertices=tuple(Vertex(v.x, v.y, v.z, v.id) for v in self.vertices),
127
+ cells=tuple(Cell(c.vertices, c.type) for c in self.cells),
128
+ )
129
+
130
+ def select_region(self, region: Shape) -> list[int]:
131
+ return [
132
+ v.id for v in self.vertices if region.contains_point(Point(v.x, v.y, v.z))
133
+ ]
@@ -2,14 +2,13 @@
2
2
  Meshing utilities for use in openfoam
3
3
  """
4
4
 
5
+ import os
5
6
  import shutil
6
7
  import subprocess
7
- import os
8
8
  from pathlib import Path
9
9
  from typing import cast
10
10
 
11
-
12
- from icplot.mesh import Edge, HexCell, Mesh, Patch
11
+ from icplot.geometry.mesh import Edge, HexCell, Mesh, Patch
13
12
 
14
13
  from .foamfile import FoamFile, write_header
15
14
 
@@ -1,4 +1,4 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from pathlib import Path
3
3
 
4
4
 
@@ -14,8 +14,9 @@ class FoamFile:
14
14
  body: tuple[str, ...] = ()
15
15
  version: float = 2.0
16
16
  format: str = "ascii"
17
- arch: str = '"LSB;label=32;scalar=64"'
17
+ arch: str = "LSB;label=32;scalar=64"
18
18
  foam_class: str = "vectorField"
19
+ meta: dict[str, tuple] = field(default_factory=dict)
19
20
 
20
21
 
21
22
  def write_header(spec: FoamFile) -> str:
@@ -27,6 +28,13 @@ def write_header(spec: FoamFile) -> str:
27
28
  output += f"\tclass\t{spec.foam_class};\n"
28
29
  output += f'\tlocation\t"{spec.location}";\n'
29
30
  output += f"\tobject\t{spec.foam_object};\n"
31
+ if spec.meta:
32
+ output += "\tmeta\n{\n"
33
+ for key, value in spec.meta.items():
34
+ values = list(value)
35
+ value_items = " ".join(str(item) for item in values)
36
+ output += f"\t{key} {len(values)}({value_items});\n"
37
+ output += "}\n"
30
38
  output += "}\n\n"
31
39
  return output
32
40
 
@@ -0,0 +1,313 @@
1
+ from dataclasses import dataclass
2
+ from functools import cmp_to_key
3
+ from pathlib import Path
4
+
5
+ from icplot.geometry import Point
6
+ from icplot.geometry.mesh import Mesh, Vertex, get_hex_faces
7
+
8
+ from .foamfile import FoamFile, read_foamfile, write_header
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Face:
13
+ """
14
+ A mesh face, which is a sequence of vertices
15
+ """
16
+
17
+ vertices: tuple[int, ...]
18
+ id: int = -1
19
+ owner: int = -1
20
+ neighbour: int = -1
21
+
22
+ def get_centre(self, mesh: Mesh) -> Point:
23
+ x = sum(mesh.vertices[idx].x for idx in self.vertices)
24
+ y = sum(mesh.vertices[idx].y for idx in self.vertices)
25
+ z = sum(mesh.vertices[idx].z for idx in self.vertices)
26
+ return Point(
27
+ x / len(self.vertices), y / len(self.vertices), z / len(self.vertices)
28
+ )
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class Cell:
33
+ """
34
+ A mesh cell, which is a squence of faces
35
+ """
36
+
37
+ faces: tuple[int, ...]
38
+ id: int = -1
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Polymesh:
43
+ """
44
+ The polymesh
45
+ """
46
+
47
+ vertices: tuple[Vertex, ...]
48
+ faces: tuple[Face, ...]
49
+ cells: tuple[Cell, ...]
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class PolymeshPatch:
54
+ name: str
55
+ faces: tuple[Face, ...]
56
+ startFace: int
57
+ type: str = "patch"
58
+
59
+ def has_face(self, face: Face) -> bool:
60
+ for f in self.faces:
61
+ if f.id == face.id:
62
+ return True
63
+ return False
64
+
65
+
66
+ def read_polymesh(path: Path) -> Polymesh:
67
+
68
+ _ = read_foamfile(path / "points")
69
+ # faces_file = read_foamfile(path / "faces")
70
+ return Polymesh((), (), ())
71
+
72
+
73
+ def _write_points(vertices: tuple[Vertex, ...]) -> str:
74
+
75
+ # Write file header
76
+ header = FoamFile(
77
+ location="constant/polyMesh", foam_object="points", foam_class="vectorField"
78
+ )
79
+ ret = write_header(header)
80
+
81
+ ret += f"{len(vertices)}\n(\n"
82
+ for v in vertices:
83
+ ret += f"({v.x} {v.y} {v.z})\n"
84
+ ret += ")\n"
85
+ return ret
86
+
87
+
88
+ def _write_faces(internal_faces: tuple[Face, ...], patches: list[PolymeshPatch]) -> str:
89
+ header = FoamFile(
90
+ location="constant/polyMesh", foam_object="faces", foam_class="faceList"
91
+ )
92
+ ret = write_header(header)
93
+
94
+ num_patch_faces = sum(len(p.faces) for p in patches)
95
+ num_faces = len(internal_faces) + num_patch_faces
96
+
97
+ ret += f"{num_faces}\n(\n"
98
+ for f in internal_faces:
99
+ verts = f.vertices
100
+ ret += f"{len(verts)}({verts[0]} {verts[1]} {verts[2]} {verts[3]})\n"
101
+
102
+ for p in patches:
103
+ for f in p.faces:
104
+ verts = f.vertices
105
+ ret += f"{len(verts)}({verts[0]} {verts[1]} {verts[2]} {verts[3]})\n"
106
+ ret += ")\n"
107
+ return ret
108
+
109
+
110
+ def _write_owners(
111
+ internal_faces: tuple[Face, ...], patches: list[PolymeshPatch]
112
+ ) -> str:
113
+ header = FoamFile(
114
+ location="constant/polyMesh", foam_object="owner", foam_class="labelList"
115
+ )
116
+ ret = write_header(header)
117
+
118
+ num_patch_faces = sum(len(p.faces) for p in patches)
119
+ num_faces = len(internal_faces) + num_patch_faces
120
+
121
+ ret += f"{num_faces}\n(\n"
122
+ for f in internal_faces:
123
+ ret += f"{f.owner}\n"
124
+ for p in patches:
125
+ for f in p.faces:
126
+ ret += f"{f.owner}\n"
127
+ ret += ")\n"
128
+ return ret
129
+
130
+
131
+ def _write_neighbours(
132
+ internal_faces: tuple[Face, ...], patches: list[PolymeshPatch]
133
+ ) -> str:
134
+ header = FoamFile(
135
+ location="constant/polyMesh", foam_object="neighbour", foam_class="labelList"
136
+ )
137
+ ret = write_header(header)
138
+
139
+ num_patch_faces = sum(len(p.faces) for p in patches)
140
+ num_faces = len(internal_faces) + num_patch_faces
141
+
142
+ ret += f"{num_faces}\n(\n"
143
+ for f in internal_faces:
144
+ ret += f"{f.neighbour}\n"
145
+ for p in patches:
146
+ for f in p.faces:
147
+ ret += f"{f.neighbour}\n"
148
+ ret += ")\n"
149
+ return ret
150
+
151
+
152
+ """
153
+ def _write_face_zones(zones: list[FaceZone]) -> str:
154
+
155
+ header = FoamFile(
156
+ location="constant/polyMesh",
157
+ foam_object="faceZones",
158
+ foam_class="regIOobject",
159
+ meta={"names": tuple(zone.name for zone in zones)},
160
+ )
161
+ ret = write_header(header)
162
+
163
+ ret += f"{len(zones)}\n( "
164
+ for zone in zones:
165
+ ret += f"{zone.name}\n{{"
166
+ ret += "\ttype\tfaceZone;\n\tfaceLabels\tList<label>\n"
167
+ ret += f"{len(zone.faces)}\n("
168
+ for face in zone.faces:
169
+ ret += f"{face}\n"
170
+ ret += ");\n"
171
+ ret += "\tflipMap\tList<bool>\n"
172
+ ret += f"{len(zone.faces)}\n("
173
+ for face in zone.faces:
174
+ ret += "0\n"
175
+ ret += ");\n"
176
+ ret += "}\n\n"
177
+ ret += ")"
178
+ return ret
179
+ """
180
+
181
+
182
+ def _write_boundaries(patches: list[PolymeshPatch]):
183
+
184
+ header = FoamFile(
185
+ location="constant/polyMesh",
186
+ foam_object="boundary",
187
+ foam_class="polyBoundaryMesh",
188
+ )
189
+ ret = write_header(header)
190
+
191
+ ret += f"{len(patches)}(\n"
192
+ for p in patches:
193
+ ret += f"{p.name}\n{{"
194
+ ret += f"\ttype\t{p.type};\n"
195
+ ret += f"\tnFaces\t{len(p.faces)};\n"
196
+ ret += f"\tstartFace\t{p.startFace};\n"
197
+ ret += "}\n\n"
198
+ ret += ")"
199
+ return ret
200
+
201
+
202
+ def _faces_equal(f0, f1):
203
+ return set(f0) == set(f1)
204
+
205
+
206
+ def _compare_faces(f0: Face, f1: Face):
207
+ if f0.owner == f1.owner:
208
+ if f0.neighbour == f1.neighbour:
209
+ return 0
210
+ elif f0.neighbour > f1.neighbour:
211
+ return 1
212
+ else:
213
+ return -1
214
+ else:
215
+ if f0.owner > f1.owner:
216
+ return 1
217
+ else:
218
+ return -1
219
+
220
+
221
+ def _build_faces(mesh: Mesh) -> list[Face]:
222
+ faces: list[Face] = []
223
+
224
+ count = 0
225
+ for idx, cell in enumerate(mesh.cells):
226
+ hex_faces = get_hex_faces(cell)
227
+ for hex_face in hex_faces:
228
+ found = False
229
+ for face in faces:
230
+ if _faces_equal(hex_face, face.vertices):
231
+ if idx < face.owner:
232
+ faces[face.id] = Face(
233
+ vertices=tuple(list(reversed(face.vertices))),
234
+ owner=idx,
235
+ id=face.id,
236
+ neighbour=face.owner,
237
+ )
238
+ else:
239
+ faces[face.id] = Face(
240
+ vertices=face.vertices,
241
+ owner=face.owner,
242
+ id=face.id,
243
+ neighbour=idx,
244
+ )
245
+ found = True
246
+ break
247
+ if not found:
248
+ faces.append(Face(id=count, vertices=hex_face, owner=idx, neighbour=-1))
249
+ count += 1
250
+
251
+ faces.sort(key=cmp_to_key(_compare_faces))
252
+ return faces
253
+
254
+
255
+ def _get_external_faces(faces: tuple[Face, ...]) -> tuple[Face, ...]:
256
+ return tuple(f for f in faces if f.neighbour == -1)
257
+
258
+
259
+ def _get_internal_faces(faces: tuple[Face, ...]) -> tuple[Face, ...]:
260
+ return tuple(f for f in faces if f.neighbour != -1)
261
+
262
+
263
+ def write_polymesh(path: Path, mesh: Mesh, patch_funcs: dict):
264
+
265
+ # Write points
266
+ with open(path / "points", "w", encoding="utf-8") as f:
267
+ f.write(_write_points(mesh.vertices))
268
+
269
+ # Prepare faces, owners and neighbours
270
+ faces = _build_faces(mesh)
271
+
272
+ internal_faces = _get_internal_faces(tuple(faces))
273
+ external_faces = _get_external_faces(tuple(faces))
274
+
275
+ # Get patches
276
+ count = len(internal_faces)
277
+ patches = []
278
+ for name, func in patch_funcs.items():
279
+ patch_faces = tuple(f for f in external_faces if func(f.get_centre(mesh)))
280
+ patches.append(PolymeshPatch(name=name, faces=patch_faces, startFace=count))
281
+ count += len(patch_faces)
282
+
283
+ # Add a default wall patch
284
+ wall_faces = []
285
+ for face in external_faces:
286
+ found = False
287
+ for patch in patches:
288
+ if patch.has_face(face):
289
+ found = True
290
+ break
291
+ if not found:
292
+ wall_faces.append(face)
293
+ patches.append(
294
+ PolymeshPatch(
295
+ name="wall",
296
+ type="wall",
297
+ faces=tuple(f for f in wall_faces),
298
+ startFace=count,
299
+ )
300
+ )
301
+
302
+ with open(path / "faces", "w", encoding="utf-8") as f:
303
+ f.write(_write_faces(internal_faces, patches))
304
+
305
+ with open(path / "owner", "w", encoding="utf-8") as f:
306
+ f.write(_write_owners(internal_faces, patches))
307
+
308
+ with open(path / "neighbour", "w", encoding="utf-8") as f:
309
+ f.write(_write_neighbours(internal_faces, patches))
310
+
311
+ # Write boundaries
312
+ with open(path / "boundary", "w", encoding="utf-8") as f:
313
+ f.write(_write_boundaries(patches))