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/__init__.py +191 -0
- deformable/cancel_flag.py +43 -0
- deformable/conversion.py +176 -0
- deformable/deformable.dll +0 -0
- deformable/embedding.py +159 -0
- deformable/errors.py +150 -0
- deformable/handles.py +97 -0
- deformable/library.py +410 -0
- deformable/license.py +154 -0
- deformable/material_regions.py +48 -0
- deformable/mesh_verification.py +187 -0
- deformable/meshes.py +179 -0
- deformable/native.py +401 -0
- deformable/simulation.py +468 -0
- deformable/tools.py +340 -0
- deformable/values.py +116 -0
- deformable-0.1.0.dist-info/METADATA +14 -0
- deformable-0.1.0.dist-info/RECORD +19 -0
- deformable-0.1.0.dist-info/WHEEL +4 -0
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)
|