deformable 0.1.0__py3-none-win_amd64.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.
deformable/license.py ADDED
@@ -0,0 +1,154 @@
1
+ """The license of this device and user: its state, and starting, renewing, activating and deactivating it with our
2
+ licensing service.
3
+
4
+ The library keeps one license state per device and user, shared by every program that loads it: the Blender plugin,
5
+ this package and the other SDKs. Engine calls check it themselves and raise LicenseError when the trial or license
6
+ does not allow them: LicenseRequiredError once the trial ended or the license was removed, TrialLimitExceededError for
7
+ a trial simulation past its tetrahedron limit, and LicenseError itself when one online check is needed. The error's
8
+ message is written for the user, such as "Trial ended — Buy license" or "Trial limit: 2500 tetrahedrons".
9
+
10
+ status() never contacts the service and is cheap enough for every redraw. refresh(), activate() and deactivate() wait
11
+ on the network for at most their timeout, with the global interpreter lock released, so a host with an interface
12
+ runs them off its main thread. set_online_access() keeps a process off the network when its user asked for that.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import ctypes
18
+ import dataclasses
19
+ import enum
20
+ import math
21
+
22
+ from . import errors, library, native
23
+
24
+ __all__ = [
25
+ "DEFAULT_TIMEOUT",
26
+ "LicenseState",
27
+ "LicenseStatus",
28
+ "activate",
29
+ "deactivate",
30
+ "refresh",
31
+ "set_online_access",
32
+ "status",
33
+ ]
34
+
35
+ DEFAULT_TIMEOUT = 15.0
36
+ """How long refresh(), activate() and deactivate() wait for the licensing service by default, in seconds."""
37
+
38
+ _LARGEST_TIMEOUT_MILLISECONDS = 0xFFFFFFFF
39
+
40
+
41
+ class LicenseState(enum.IntEnum):
42
+ """The DEFORMABLE_LICENSE_STATE_* values of deformable.h."""
43
+
44
+ NOT_STARTED = 0
45
+ """No trial or license yet: the next refresh(), or the first engine call, starts the trial online."""
46
+ TRIAL = 1
47
+ """The trial runs: every feature, at most tetrahedron_limit tetrahedrons per simulation."""
48
+ TRIAL_ENDED = 2
49
+ """The trial ended, or this device cannot have one; engine calls fail until a license is activated."""
50
+ LICENSED = 3
51
+ """A license is active on this device; no limit."""
52
+ NEEDS_ONLINE = 4
53
+ """A license (or the trial) needs one online check before engine calls work again."""
54
+ DEVELOPMENT = 5
55
+ """A development build of the library: licensing is off and nothing contacts the licensing service."""
56
+
57
+
58
+ @dataclasses.dataclass(frozen=True)
59
+ class LicenseStatus:
60
+ """The license state as a status line shows it."""
61
+
62
+ state: LicenseState | int
63
+ """The state; a plain int for a state a newer library reports that this package does not know yet."""
64
+ days_left: int
65
+ """In TRIAL, the whole days left, 7 down to 1, counting the current day; 0 in any other state."""
66
+ tetrahedron_limit: int | None
67
+ """The most tetrahedrons one simulation may hold in TRIAL; None when there is no limit."""
68
+ usable: bool
69
+ """Engine calls work (TRIAL, LICENSED, DEVELOPMENT)."""
70
+ needs_online: bool
71
+ """The state changes only after an online check (NOT_STARTED, NEEDS_ONLINE)."""
72
+ has_license_key: bool
73
+ """A license key is stored on this device, so a host offers Deactivate rather than Activate."""
74
+ message: str
75
+ """The status line: "Trial: 7 days left", "Trial ended — Buy license", "Licensed", ..."""
76
+
77
+
78
+ def status() -> LicenseStatus:
79
+ """The license state of this device and user. It never contacts the licensing service."""
80
+ result = native.LicenseStatus()
81
+ errors.check(library.load().deformable_license_status(ctypes.byref(result)))
82
+ return _converted(result)
83
+
84
+
85
+ def refresh(timeout: float = DEFAULT_TIMEOUT) -> LicenseStatus:
86
+ """Brings the license up to date with the licensing service when a contact is due, waiting at most `timeout`
87
+ seconds: starts the trial on a device that never started it, reports a running trial once a day, and renews a
88
+ stored license. Otherwise nothing is sent. Raises LicenseError when the service cannot be reached or refuses."""
89
+ result = native.LicenseStatus()
90
+ errors.check(library.load().deformable_license_refresh(timeout_milliseconds(timeout), ctypes.byref(result)))
91
+ return _converted(result)
92
+
93
+
94
+ def activate(license_key: str, timeout: float = DEFAULT_TIMEOUT) -> LicenseStatus:
95
+ """Activates a license key on this device, waiting at most `timeout` seconds; the state is then LICENSED. Raises
96
+ ValueError for an empty key, DeformableError (INVALID_ARGUMENT) for text that is no license key or while another
97
+ key is active here, and LicenseError when the service refuses the key or cannot be reached, with a message written
98
+ for the user."""
99
+ if not isinstance(license_key, str) or not license_key.strip():
100
+ raise ValueError("the license key must be a non-empty string")
101
+ if "\0" in license_key:
102
+ raise ValueError("the license key must not contain a NUL character")
103
+ result = native.LicenseStatus()
104
+ code = library.load().deformable_license_activate(
105
+ ctypes.create_string_buffer(license_key.encode("utf-8")), timeout_milliseconds(timeout), ctypes.byref(result)
106
+ )
107
+ errors.check(code)
108
+ return _converted(result)
109
+
110
+
111
+ def deactivate(timeout: float = DEFAULT_TIMEOUT) -> LicenseStatus:
112
+ """Frees this device's seat with the licensing service and forgets the key, waiting at most `timeout` seconds.
113
+ Without a stored key it does nothing. When the service cannot be reached nothing changes and LicenseError is
114
+ raised."""
115
+ result = native.LicenseStatus()
116
+ errors.check(library.load().deformable_license_deactivate(timeout_milliseconds(timeout), ctypes.byref(result)))
117
+ return _converted(result)
118
+
119
+
120
+ def set_online_access(allowed: bool) -> None:
121
+ """Allows (the default) or forbids every contact with the licensing service from this process, for a host whose
122
+ user turned online access off. Forbidden, nothing is sent: refresh(), activate() and deactivate() raise LicenseError
123
+ and change nothing, and engine calls neither start the trial nor renew the license on their own, so a state that
124
+ needs the service stays as it is. It grants nothing."""
125
+ errors.check(library.load().deformable_license_set_online_access(1 if allowed else 0))
126
+
127
+
128
+ def timeout_milliseconds(timeout: float) -> int:
129
+ """A timeout in seconds as the library takes it: whole milliseconds, rounded up, at least 1. The library counts
130
+ anything above 60 s as 60 s."""
131
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
132
+ raise TypeError("the timeout must be a number of seconds")
133
+ if math.isnan(timeout) or timeout <= 0:
134
+ raise ValueError("the timeout must be a positive number of seconds")
135
+ if math.isinf(timeout):
136
+ return _LARGEST_TIMEOUT_MILLISECONDS
137
+ return min(max(1, math.ceil(timeout * 1000)), _LARGEST_TIMEOUT_MILLISECONDS)
138
+
139
+
140
+ def _converted(result: native.LicenseStatus) -> LicenseStatus:
141
+ try:
142
+ state = LicenseState(result.state)
143
+ except ValueError:
144
+ # A newer library may report a state this package does not know yet; it is still reported, as a plain int.
145
+ state = result.state
146
+ return LicenseStatus(
147
+ state=state,
148
+ days_left=result.days_left,
149
+ tetrahedron_limit=result.tetrahedron_limit or None,
150
+ usable=bool(result.usable),
151
+ needs_online=bool(result.needs_online),
152
+ has_license_key=bool(result.has_license_key),
153
+ message=result.message.decode("utf-8", errors="replace"),
154
+ )
@@ -0,0 +1,48 @@
1
+ """Material Regions: which region, if any, holds each tetrahedron of a mesh."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import dataclasses
7
+ from collections.abc import Sequence
8
+ from typing import Any
9
+
10
+ from . import conversion, library, native
11
+ from .errors import check
12
+ from .handles import temporary_handle
13
+ from .meshes import polygon_mesh_view, tetrahedral_mesh_view
14
+ from .values import IDENTITY, AffineTransform
15
+
16
+
17
+ @dataclasses.dataclass
18
+ class MaterialRegion:
19
+ """A polygon mesh in its own object space and its map into the tetrahedral mesh's object space (the region's
20
+ world matrix times the inverse of the object's)."""
21
+
22
+ mesh: Any
23
+ """The region's mesh, normally a closed convex polyhedron with outward faces such as a unit cube."""
24
+ to_mesh_space: AffineTransform = IDENTITY
25
+ """Maps the region's points into the tetrahedral mesh's object space."""
26
+
27
+ def _native(self) -> native.MaterialRegion:
28
+ return native.MaterialRegion(polygon_mesh_view(self.mesh), conversion.affine_transform(self.to_mesh_space))
29
+
30
+
31
+ def material_region_indices(mesh: Any, regions: Sequence[MaterialRegion]) -> list[int]:
32
+ """Per tetrahedron of mesh: 0, or k + 1 for the first region k, in ascending signed volume of its untransformed
33
+ mesh, whose transformed face planes all hold n·c + d <= 0 at the tetrahedron's centroid c.
34
+
35
+ The host turns the result into materials: 0 is the object's material, k + 1 region k's.
36
+ """
37
+ functions = library.load()
38
+ region_array = conversion.structure_array(regions, native.MaterialRegion, MaterialRegion._native)
39
+ with temporary_handle("deformable_material_region_indices_destroy") as handle:
40
+ check(
41
+ functions.deformable_material_region_indices(
42
+ ctypes.byref(tetrahedral_mesh_view(mesh)), region_array, len(region_array), ctypes.byref(handle)
43
+ )
44
+ )
45
+ values = ctypes.POINTER(native.Index)()
46
+ count = native.Count()
47
+ check(functions.deformable_material_region_indices_values(handle, ctypes.byref(values), ctypes.byref(count)))
48
+ return conversion.read_scalars(values, ctypes.c_uint32, count.value)
@@ -0,0 +1,187 @@
1
+ """The mesh checks: verify_surface on a render surface and meshes_intersect on two tetrahedral meshes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import dataclasses
7
+ from collections.abc import Sequence
8
+ from typing import Any
9
+
10
+ from . import conversion, library, native
11
+ from .errors import check
12
+ from .handles import temporary_handle
13
+ from .meshes import FaceHole, SurfaceMesh, face_hole_array, surface_mesh_view, tetrahedral_mesh_view
14
+ from .values import MeshIssueKind, SelfIntersectionCheck
15
+
16
+
17
+ @dataclasses.dataclass(frozen=True)
18
+ class MeshChecks:
19
+ """The checks verify_surface runs."""
20
+
21
+ angle_threshold_degrees: float
22
+ """A face whose smallest corner angle is below this many degrees is a SMALL FACE ANGLE."""
23
+ gap_threshold: float
24
+ """Two corners of a face closer than this fraction of the mesh radius are NEAR VERTICES."""
25
+ planar_threshold: float
26
+ """A face deviating from its plane by more than this fraction of its radius is NON-PLANAR."""
27
+ uv_threshold: float | None
28
+ """INCONSISTENT UV when a corner lies farther than this fraction of the face radius from where the UV map puts
29
+ it; None skips the check."""
30
+ same_uv: bool
31
+ """SAME UV: two corners of a face at different places with identical UVs."""
32
+ convex: bool
33
+ """Reflex corners (NON-CONVEX)."""
34
+ watertight: bool
35
+ """NOT WATERTIGHT, and the inward-normal and nesting stage."""
36
+ self_intersection: SelfIntersectionCheck | int
37
+ """How far the self-intersection stage goes."""
38
+
39
+ @classmethod
40
+ def default(cls, **changes: Any) -> MeshChecks:
41
+ """The verify_surface defaults (angle 2°, gap 0.001, planar 0.05, no UV consistency, SAME UV, convexity or
42
+ watertightness check, self-intersection per island), with the given fields changed."""
43
+ structure = native.MeshChecks()
44
+ check(library.load().deformable_mesh_checks_default(ctypes.byref(structure)))
45
+ defaults = cls(
46
+ structure.angle_threshold_degrees,
47
+ structure.gap_threshold,
48
+ structure.planar_threshold,
49
+ structure.uv_threshold if structure.check_uv_consistency else None,
50
+ bool(structure.same_uv),
51
+ bool(structure.convex),
52
+ bool(structure.watertight),
53
+ SelfIntersectionCheck(structure.self_intersection),
54
+ )
55
+ return dataclasses.replace(defaults, **changes)
56
+
57
+ def _native(self) -> native.MeshChecks:
58
+ return native.MeshChecks(
59
+ self.angle_threshold_degrees,
60
+ self.gap_threshold,
61
+ self.planar_threshold,
62
+ self.uv_threshold is not None,
63
+ 0.0 if self.uv_threshold is None else self.uv_threshold,
64
+ bool(self.same_uv),
65
+ bool(self.convex),
66
+ bool(self.watertight),
67
+ int(self.self_intersection),
68
+ )
69
+
70
+
71
+ @dataclasses.dataclass(frozen=True)
72
+ class MeshIssue:
73
+ """One finding of verify_surface. kind says which fields it fills (deformable.h lists them); the others are 0 or
74
+ empty. Vertices are input vertex indices, an edge is a pair of vertex indices (smaller first), corners count
75
+ within their polygon, holes within their face, and islands are numbered by their lowest face."""
76
+
77
+ kind: MeshIssueKind
78
+ """What the issue is."""
79
+ vertex: int
80
+ """The vertex."""
81
+ face: int
82
+ """The face."""
83
+ hole: int
84
+ """The hole within the face."""
85
+ corner: int
86
+ """The corner within the face."""
87
+ uv_set: int
88
+ """The UV set, 0 or 1."""
89
+ island: int
90
+ """The island."""
91
+ container: int
92
+ """The innermost container island."""
93
+ use_count: int
94
+ """The face count of a non-manifold edge, or the loop-edge count for an edge of distinct positions."""
95
+ hole_count: int
96
+ """The hole count of the face."""
97
+ island_count: int
98
+ """The island count of the mesh."""
99
+ edge: tuple[int, int]
100
+ """The edge's two vertices."""
101
+ corners: tuple[int, int]
102
+ """The two corners within the face."""
103
+ inward: bool
104
+ """True when both nested islands have inward face normals."""
105
+ distance: float
106
+ """The distance between the two corners."""
107
+ relative_distance: float
108
+ """A distance over the mesh or face radius."""
109
+ degrees: float
110
+ """The smallest corner angle in degrees."""
111
+ relative_deviation: float
112
+ """The largest distance to the plane over the face radius."""
113
+ interior_angle_degrees: float
114
+ """The interior angle of the most reflex corner in degrees."""
115
+ vertices: list[int]
116
+ """The co-located vertices, ascending."""
117
+ edges: list[tuple[int, int]]
118
+ """The co-located edges, in order of first use."""
119
+ uncontained: list[int]
120
+ """The uncontained inward islands, ascending."""
121
+
122
+ @classmethod
123
+ def _from_native(cls, record: native.MeshIssue) -> MeshIssue:
124
+ return cls(
125
+ MeshIssueKind(record.kind),
126
+ record.vertex,
127
+ record.face,
128
+ record.hole,
129
+ record.corner,
130
+ record.uv_set,
131
+ record.island,
132
+ record.container,
133
+ record.use_count,
134
+ record.hole_count,
135
+ record.island_count,
136
+ tuple(record.edge),
137
+ tuple(record.corners),
138
+ bool(record.inward),
139
+ record.distance,
140
+ record.relative_distance,
141
+ record.degrees,
142
+ record.relative_deviation,
143
+ record.interior_angle_degrees,
144
+ conversion.read_scalars(record.vertices, ctypes.c_uint32, record.vertex_count),
145
+ conversion.read_groups(record.edges, ctypes.c_uint32, record.edge_count, 2),
146
+ conversion.read_scalars(record.uncontained, ctypes.c_uint32, record.uncontained_count),
147
+ )
148
+
149
+
150
+ def verify_surface(
151
+ surface: SurfaceMesh, holes: Sequence[FaceHole] = (), checks: MeshChecks | None = None
152
+ ) -> list[MeshIssue]:
153
+ """The issues checks finds on a surface, in check order (none = passes), from positions, holes and
154
+ the first two UV sets. checks defaults to MeshChecks.default()."""
155
+ checks = MeshChecks.default() if checks is None else checks
156
+ functions = library.load()
157
+ hole_array = face_hole_array(holes)
158
+ with temporary_handle("deformable_mesh_issue_list_destroy") as handle:
159
+ check(
160
+ functions.deformable_verify_surface(
161
+ ctypes.byref(surface_mesh_view(surface)),
162
+ hole_array,
163
+ len(hole_array),
164
+ ctypes.byref(checks._native()),
165
+ ctypes.byref(handle),
166
+ )
167
+ )
168
+ records = ctypes.POINTER(native.MeshIssue)()
169
+ count = native.Count()
170
+ check(functions.deformable_mesh_issue_list_issues(handle, ctypes.byref(records), ctypes.byref(count)))
171
+ return [
172
+ MeshIssue._from_native(record) for record in conversion.lent_array(records, native.MeshIssue, count.value)
173
+ ]
174
+
175
+
176
+ def meshes_intersect(first: Any, second: Any) -> bool:
177
+ """True when a tetrahedron pair, one from each mesh in world space, has overlapping closed boxes and
178
+ a clipped overlap volume above 1e-10 with a nonzero normal."""
179
+ intersect = native.Boolean()
180
+ check(
181
+ library.load().deformable_meshes_intersect(
182
+ ctypes.byref(tetrahedral_mesh_view(first)),
183
+ ctypes.byref(tetrahedral_mesh_view(second)),
184
+ ctypes.byref(intersect),
185
+ )
186
+ )
187
+ return bool(intersect.value)
deformable/meshes.py ADDED
@@ -0,0 +1,179 @@
1
+ """Mesh types, their borrowed C views, and the queries on a tetrahedral mesh.
2
+
3
+ The mesh classes hold whatever array form they were given (see deformable.conversion); meshes the library returns hold
4
+ lists of Vector3 and of index tuples. Any object with the same attributes can stand in for a mesh class.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ctypes
10
+ import dataclasses
11
+ from collections.abc import Sequence
12
+ from typing import Any, NamedTuple
13
+
14
+ from . import conversion, library, native
15
+ from .errors import check
16
+ from .handles import temporary_handle
17
+
18
+
19
+ @dataclasses.dataclass
20
+ class TetrahedralMesh:
21
+ """Node positions and four node indices per tetrahedron, positively oriented (left-handed)."""
22
+
23
+ positions: Any
24
+ """The node positions."""
25
+ tetrahedrons: Any
26
+ """Four node indices per tetrahedron."""
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class TriangleSurface:
31
+ """Vertex positions and three vertex indices per triangle."""
32
+
33
+ positions: Any
34
+ """The vertex positions."""
35
+ triangles: Any
36
+ """Three vertex indices per triangle."""
37
+
38
+
39
+ @dataclasses.dataclass
40
+ class PolygonMesh:
41
+ """Shared positions, the corner count of each polygon, and the vertex of every corner, polygon after polygon."""
42
+
43
+ positions: Any
44
+ """The vertex positions."""
45
+ polygon_sizes: Any
46
+ """The corner count of each polygon."""
47
+ polygon_vertices: Any
48
+ """The vertex index of each corner."""
49
+
50
+
51
+ @dataclasses.dataclass
52
+ class SurfaceMesh:
53
+ """A render surface: a polygon mesh with per-corner channels, each empty or with one entry per corner."""
54
+
55
+ mesh: PolygonMesh
56
+ """Positions and polygons."""
57
+ normals: Any = ()
58
+ """One normal per corner, or none."""
59
+ uv_sets: Sequence[Any] = ()
60
+ """UV sets, each with one coordinate pair per corner."""
61
+ colours: Any = ()
62
+ """One red, green, blue and alpha colour per corner, or none."""
63
+
64
+
65
+ class FaceHole(NamedTuple):
66
+ """A run of a face's corners that forms one hole loop; the corners before the face's first hole form its outer
67
+ loop."""
68
+
69
+ face: int
70
+ """The polygon holding the hole."""
71
+ first_corner: int
72
+ """Index of the run's first corner within the polygon."""
73
+ corner_count: int
74
+ """Number of corners in the run."""
75
+
76
+
77
+ def tetrahedral_mesh_view(mesh: Any) -> native.TetrahedralMeshView:
78
+ """The borrowed C view of a tetrahedral mesh."""
79
+ positions = conversion.vector3_array(mesh.positions)
80
+ tetrahedrons = conversion.index_group_array(mesh.tetrahedrons, 4, "tetrahedrons")
81
+ return native.TetrahedralMeshView(positions, len(positions), tetrahedrons, len(tetrahedrons))
82
+
83
+
84
+ def triangle_surface_view(surface: Any) -> native.TriangleSurfaceView:
85
+ """The borrowed C view of a triangle surface."""
86
+ positions = conversion.vector3_array(surface.positions)
87
+ triangles = conversion.index_group_array(surface.triangles, 3, "triangles")
88
+ return native.TriangleSurfaceView(positions, len(positions), triangles, len(triangles))
89
+
90
+
91
+ def polygon_mesh_view(mesh: Any) -> native.PolygonMeshView:
92
+ """The borrowed C view of a polygon mesh."""
93
+ positions = conversion.vector3_array(mesh.positions)
94
+ sizes = conversion.index_array(mesh.polygon_sizes, "polygon_sizes")
95
+ vertices = conversion.index_array(mesh.polygon_vertices, "polygon_vertices")
96
+ return native.PolygonMeshView(positions, len(positions), sizes, len(sizes), vertices, len(vertices))
97
+
98
+
99
+ def surface_mesh_view(surface: Any) -> native.SurfaceMeshView:
100
+ """The borrowed C view of a render surface; every UV set must have one pair per corner."""
101
+ mesh = polygon_mesh_view(surface.mesh)
102
+ normals = conversion.vector3_array(surface.normals, "normals")
103
+ uv_sets = [conversion.vector2_array(uv_set, "uv_sets") for uv_set in surface.uv_sets]
104
+ for uv_set in uv_sets:
105
+ # The library reads uv_set_count × corner_count pairs, so a short set would be read past its end.
106
+ if len(uv_set) != mesh.corner_count:
107
+ raise ValueError(f"a UV set holds {len(uv_set)} pairs for {mesh.corner_count} corners")
108
+ uv_type = native.Vector2 * (mesh.corner_count * len(uv_sets))
109
+ uvs = uv_type.from_buffer_copy(b"".join(bytes(uv_set) for uv_set in uv_sets))
110
+ colours = conversion.grouped_array(surface.colours, native.Colour, ctypes.c_double, 4, "colours")
111
+ return native.SurfaceMeshView(mesh, normals, len(normals), uvs, len(uv_sets), colours, len(colours))
112
+
113
+
114
+ def face_hole_array(holes: Sequence[Any]):
115
+ """The C array of face holes, each a FaceHole or a (face, first_corner, corner_count) triple."""
116
+ return conversion.structure_array(
117
+ holes,
118
+ native.FaceHole,
119
+ lambda hole: native.FaceHole(*(conversion.unsigned(value) for value in hole)),
120
+ )
121
+
122
+
123
+ def read_tetrahedral_mesh(view: native.TetrahedralMeshView) -> TetrahedralMesh:
124
+ """A copy of a lent tetrahedral mesh."""
125
+ return TetrahedralMesh(
126
+ conversion.read_vector3_list(view.positions, view.position_count),
127
+ conversion.read_groups(view.tetrahedrons, ctypes.c_uint32, view.tetrahedron_count, 4),
128
+ )
129
+
130
+
131
+ def read_triangle_surface(view: native.TriangleSurfaceView) -> TriangleSurface:
132
+ """A copy of a lent triangle surface."""
133
+ return TriangleSurface(
134
+ conversion.read_vector3_list(view.positions, view.position_count),
135
+ conversion.read_groups(view.triangles, ctypes.c_uint32, view.triangle_count, 3),
136
+ )
137
+
138
+
139
+ def read_polygon_mesh(view: native.PolygonMeshView) -> PolygonMesh:
140
+ """A copy of a lent polygon mesh."""
141
+ return PolygonMesh(
142
+ conversion.read_vector3_list(view.positions, view.position_count),
143
+ conversion.read_scalars(view.polygon_sizes, ctypes.c_uint32, view.polygon_count),
144
+ conversion.read_scalars(view.polygon_vertices, ctypes.c_uint32, view.corner_count),
145
+ )
146
+
147
+
148
+ def read_surface_mesh(view: native.SurfaceMeshView) -> SurfaceMesh:
149
+ """A copy of a lent render surface."""
150
+ corner_count = view.mesh.corner_count
151
+ uvs = conversion.read_vector2_list(view.uvs, view.uv_set_count * corner_count)
152
+ return SurfaceMesh(
153
+ read_polygon_mesh(view.mesh),
154
+ conversion.read_vector3_list(view.normals, view.normal_count),
155
+ [uvs[start : start + corner_count] for start in range(0, len(uvs), corner_count)],
156
+ conversion.read_groups(view.colours, ctypes.c_double, view.colour_count, 4),
157
+ )
158
+
159
+
160
+ def face_corners(face: int) -> tuple[int, int, int]:
161
+ """The three local corners (0-3) of tetrahedron face face (0-3, the face opposite corner face).
162
+
163
+ Seen from outside a positively oriented (left-handed) tetrahedron they run clockwise, so (b - a) × (c - a) points
164
+ inward.
165
+ """
166
+ corners = native.TriangleIndices()
167
+ check(library.load().deformable_face_corners(conversion.unsigned(face), ctypes.byref(corners)))
168
+ return tuple(corners)
169
+
170
+
171
+ def boundary_faces(mesh: Any) -> list[int]:
172
+ """The static outer-face mask of each tetrahedron: bit k is set when face k, opposite node k, has no neighbour."""
173
+ functions = library.load()
174
+ with temporary_handle("deformable_boundary_faces_destroy") as handle:
175
+ check(functions.deformable_boundary_faces(ctypes.byref(tetrahedral_mesh_view(mesh)), ctypes.byref(handle)))
176
+ masks = ctypes.POINTER(ctypes.c_uint8)()
177
+ count = native.Count()
178
+ check(functions.deformable_boundary_faces_masks(handle, ctypes.byref(masks), ctypes.byref(count)))
179
+ return conversion.read_scalars(masks, ctypes.c_uint8, count.value)