halfedge 0.2.0__tar.gz → 0.3.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.
- {halfedge-0.2.0 → halfedge-0.3.0}/.pre-commit-config.yaml +1 -0
- {halfedge-0.2.0/src/halfedge.egg-info → halfedge-0.3.0}/PKG-INFO +1 -1
- {halfedge-0.2.0 → halfedge-0.3.0}/pyproject.toml +2 -2
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge/__init__.py +4 -1
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge/half_edge_constructors.py +25 -1
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge/type_attrib.py +96 -1
- {halfedge-0.2.0 → halfedge-0.3.0/src/halfedge.egg-info}/PKG-INFO +1 -1
- {halfedge-0.2.0 → halfedge-0.3.0}/tests/test_classes.py +31 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/tests/test_constructors.py +0 -7
- {halfedge-0.2.0 → halfedge-0.3.0}/.github/workflows/pypi-project.yml +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/.gitignore +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/README.md +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/setup.cfg +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge/half_edge_elements.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge/half_edge_object.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge/half_edge_querries.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge/py.typed +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge/validations.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge.egg-info/SOURCES.txt +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge.egg-info/dependency_links.txt +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge.egg-info/requires.txt +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/src/halfedge.egg-info/top_level.txt +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/tests/__init__.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/tests/conftest.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/tests/test_elements.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/tests/test_object_pickups.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/tests/test_operations.py +0 -0
- {halfedge-0.2.0 → halfedge-0.3.0}/tests/test_validations.py +0 -0
|
@@ -110,6 +110,7 @@ repos:
|
|
|
110
110
|
- --disable=useless-return # conflicts with mypy
|
|
111
111
|
- --disable=assignment-from-no-return # prevents overloading Attrib methods to only raise an Exception
|
|
112
112
|
- --disable=useless-parent-delegation
|
|
113
|
+
- --disable=R0801 # Similar lines in multiple files
|
|
113
114
|
- --load-plugins=pylint.extensions.docparams
|
|
114
115
|
- --accept-no-param-doc=n
|
|
115
116
|
- --accept-no-raise-doc=n
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "halfedge"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "A typical half-edge data structure with some padding"
|
|
5
5
|
authors = [{ name = "Shay Hill", email = "shay_public@hotmail.com" }]
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -25,7 +25,7 @@ build-backend = "setuptools.build_meta"
|
|
|
25
25
|
|
|
26
26
|
[tool.commitizen]
|
|
27
27
|
name = "cz_conventional_commits"
|
|
28
|
-
version = "0.
|
|
28
|
+
version = "0.3.0"
|
|
29
29
|
tag_format = "$version"
|
|
30
30
|
major-version-zero = true
|
|
31
31
|
version_files = ["pyproject.toml:^version"]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Allow modules to be imported from top-level."""
|
|
2
2
|
|
|
3
|
-
from halfedge.
|
|
3
|
+
from halfedge.half_edge_constructors import BlindHalfEdges
|
|
4
|
+
from halfedge.half_edge_elements import Edge, Face, MeshElementBase, Vert
|
|
4
5
|
from halfedge.half_edge_object import HalfEdges
|
|
5
6
|
from halfedge.type_attrib import (
|
|
6
7
|
Attrib,
|
|
@@ -13,11 +14,13 @@ from halfedge.type_attrib import (
|
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"Attrib",
|
|
17
|
+
"BlindHalfEdges",
|
|
16
18
|
"ContagionAttrib",
|
|
17
19
|
"Edge",
|
|
18
20
|
"Face",
|
|
19
21
|
"HalfEdges",
|
|
20
22
|
"IncompatibleAttrib",
|
|
23
|
+
"MeshElementBase",
|
|
21
24
|
"NumericAttrib",
|
|
22
25
|
"Vector2Attrib",
|
|
23
26
|
"Vector3Attrib",
|
|
@@ -30,7 +30,10 @@ from paragraphs import par
|
|
|
30
30
|
from halfedge.half_edge_elements import Edge, Face, ManifoldMeshError, Vert
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
|
-
from halfedge.type_attrib import Attrib
|
|
33
|
+
from halfedge.type_attrib import Attrib, StaticAttrib
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_T = TypeVar("_T")
|
|
34
37
|
|
|
35
38
|
_TBlindHalfEdges = TypeVar("_TBlindHalfEdges", bound="BlindHalfEdges")
|
|
36
39
|
|
|
@@ -44,6 +47,27 @@ class BlindHalfEdges:
|
|
|
44
47
|
self.edges: set[Edge] = set()
|
|
45
48
|
else:
|
|
46
49
|
self.edges = edges
|
|
50
|
+
self.attrib: dict[str, StaticAttrib[Any]] = {}
|
|
51
|
+
|
|
52
|
+
def set_attrib(self, attrib: StaticAttrib[Any]) -> None:
|
|
53
|
+
"""Set an attribute.
|
|
54
|
+
|
|
55
|
+
:param attrib: StaticAttrib instance
|
|
56
|
+
"""
|
|
57
|
+
self.attrib[type(attrib).__name__] = attrib.copy_to_element(self)
|
|
58
|
+
|
|
59
|
+
def get_attrib(self, attrib: type[StaticAttrib[_T]]) -> StaticAttrib[_T]:
|
|
60
|
+
"""Get a StaticAttrib.
|
|
61
|
+
|
|
62
|
+
:param attrib: StaticAttrib class
|
|
63
|
+
:returns: StaticAttrib instance
|
|
64
|
+
:raise AttributeError: if StaticAttrib not found in self.attrib
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
return self.attrib[attrib.__name__]
|
|
68
|
+
except KeyError as e:
|
|
69
|
+
msg = f"{attrib.__name__} not found in {self.__class__.__name__}"
|
|
70
|
+
raise AttributeError(msg) from e
|
|
47
71
|
|
|
48
72
|
def new_vert(self, *attributes: Attrib[Any], edge: Edge | None = None) -> Vert:
|
|
49
73
|
"""Create a new Vert instance.
|
|
@@ -66,11 +66,106 @@ from typing import TYPE_CHECKING, Any, Generic, Literal, Tuple, TypeVar
|
|
|
66
66
|
from paragraphs import par
|
|
67
67
|
|
|
68
68
|
if TYPE_CHECKING:
|
|
69
|
+
from halfedge.half_edge_constructors import BlindHalfEdges
|
|
69
70
|
from halfedge.half_edge_elements import MeshElementBase
|
|
70
71
|
|
|
71
72
|
_T = TypeVar("_T")
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
class StaticAttrib(Generic[_T]):
|
|
76
|
+
"""Base class for storing a, potentially inferred, attribute value.
|
|
77
|
+
|
|
78
|
+
This is the equivalent to the Attrib class, but for meshes, which will never be
|
|
79
|
+
merged or split.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
__slots__ = ("_value", "mesh")
|
|
83
|
+
|
|
84
|
+
def __new__(
|
|
85
|
+
cls: type[_TStaticAttrib],
|
|
86
|
+
value: _T | None = None,
|
|
87
|
+
mesh: BlindHalfEdges | None = None,
|
|
88
|
+
) -> _TStaticAttrib:
|
|
89
|
+
"""Raise an exception if the attribute is not subclassed."""
|
|
90
|
+
del value
|
|
91
|
+
del mesh
|
|
92
|
+
if cls is StaticAttrib:
|
|
93
|
+
msg = "StaticAttrib is an abstract class and cannot be instantiated."
|
|
94
|
+
raise TypeError(msg)
|
|
95
|
+
return object.__new__(cls)
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self, value: _T | None = None, mesh: BlindHalfEdges | None = None
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Set value and mesh."""
|
|
101
|
+
self._value = value
|
|
102
|
+
self.mesh = mesh
|
|
103
|
+
|
|
104
|
+
def copy_to_element(
|
|
105
|
+
self: StaticAttrib[_T], mesh: BlindHalfEdges
|
|
106
|
+
) -> StaticAttrib[_T]:
|
|
107
|
+
"""Return a new instance with the same value, assigned to a new mesh.
|
|
108
|
+
|
|
109
|
+
:param mesh: BlindHalfEdges instance to which attrib will be assigned.
|
|
110
|
+
:return: Attrib instance
|
|
111
|
+
"""
|
|
112
|
+
return type(self)(self._value, mesh)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def value(self) -> _T:
|
|
116
|
+
"""Return value if set, else try to infer a value.
|
|
117
|
+
|
|
118
|
+
:return: Value of the attribute
|
|
119
|
+
:raises AttributeError: If no value is set and _infer_value fails
|
|
120
|
+
"""
|
|
121
|
+
if self._value is not None:
|
|
122
|
+
return self._value
|
|
123
|
+
with suppress(NotImplementedError, ValueError):
|
|
124
|
+
value = self._infer_value()
|
|
125
|
+
self._value = value
|
|
126
|
+
return self._value
|
|
127
|
+
msg = "no value set and failed to infer from 'self.mesh'"
|
|
128
|
+
raise AttributeError(msg)
|
|
129
|
+
|
|
130
|
+
def _infer_value(self) -> _T:
|
|
131
|
+
"""Get value of self from self._mesh.
|
|
132
|
+
|
|
133
|
+
Use the containing mesh to determine a value for self. If no value can be
|
|
134
|
+
determined, return None.
|
|
135
|
+
|
|
136
|
+
The purpose is to allow lazy attributes like edge norm and face area. Use
|
|
137
|
+
caution, however. These need to be calculated before merging since the method
|
|
138
|
+
may not support the new shape. For instance, this method might calculate the
|
|
139
|
+
area of a triangle, but would fail if two triangles were merged into a
|
|
140
|
+
square. To keep this safe, the _value is calculated *before* any merging. In
|
|
141
|
+
the "area of a triangle" example,
|
|
142
|
+
|
|
143
|
+
* The area calculation is deferred until the first merge.
|
|
144
|
+
* At the first merge, the area of each merged triangle is calculated. The
|
|
145
|
+
implication here is that calculation *cannot* be deferred till after a
|
|
146
|
+
merge.
|
|
147
|
+
* The merged method sums areas of the merged triangles at the first and
|
|
148
|
+
subsequent mergers, so further triangle area calculations (which
|
|
149
|
+
wouldn't work on the merged shapes anyway) are not required.
|
|
150
|
+
|
|
151
|
+
If you infer a value, cache it by setting self._value.
|
|
152
|
+
|
|
153
|
+
If you do not intend to infer values, raise an exception. This exception
|
|
154
|
+
should occur *before* an AttributeError is raised for a potentially missing
|
|
155
|
+
mesh attribute. It should be clear that _infer_value failed because there
|
|
156
|
+
is no provision for inferring this Attrib.value, *not* because the
|
|
157
|
+
user failed to set the Attrib property attribute.
|
|
158
|
+
"""
|
|
159
|
+
msg = par(
|
|
160
|
+
f"""'{type(self).__name__}' has no provision for inferring a value from
|
|
161
|
+
'self.mesh'"""
|
|
162
|
+
)
|
|
163
|
+
raise AttributeError(msg)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
_TStaticAttrib = TypeVar("_TStaticAttrib", bound=StaticAttrib[Any])
|
|
167
|
+
|
|
168
|
+
|
|
74
169
|
class Attrib(Generic[_T]):
|
|
75
170
|
"""Base class for element attributes.
|
|
76
171
|
|
|
@@ -121,7 +216,7 @@ class Attrib(Generic[_T]):
|
|
|
121
216
|
value = self._infer_value()
|
|
122
217
|
self._value = value
|
|
123
218
|
return self._value
|
|
124
|
-
msg =
|
|
219
|
+
msg = "no value set and failed to infer from 'self.element'"
|
|
125
220
|
raise AttributeError(msg)
|
|
126
221
|
|
|
127
222
|
def copy_to_element(self: Attrib[_T], element: MeshElementBase) -> Attrib[_T]:
|
|
@@ -12,6 +12,7 @@ from typing import Any, Tuple, TypeVar
|
|
|
12
12
|
|
|
13
13
|
import pytest
|
|
14
14
|
|
|
15
|
+
from halfedge.half_edge_constructors import BlindHalfEdges
|
|
15
16
|
from halfedge.half_edge_elements import (
|
|
16
17
|
Edge,
|
|
17
18
|
Face,
|
|
@@ -27,6 +28,7 @@ from halfedge.type_attrib import (
|
|
|
27
28
|
ContagionAttrib,
|
|
28
29
|
IncompatibleAttrib,
|
|
29
30
|
NumericAttrib,
|
|
31
|
+
StaticAttrib,
|
|
30
32
|
Vector2Attrib,
|
|
31
33
|
Vector3Attrib,
|
|
32
34
|
)
|
|
@@ -50,6 +52,10 @@ class MyAttrib(Attrib[int]):
|
|
|
50
52
|
"""An attribute with an integer value."""
|
|
51
53
|
|
|
52
54
|
|
|
55
|
+
class MyStaticAttrib(StaticAttrib[int]):
|
|
56
|
+
"""A static attribute with an integer value."""
|
|
57
|
+
|
|
58
|
+
|
|
53
59
|
class TestCannotInstantiateAbstractClasses:
|
|
54
60
|
def test_cannot_instantiate_abstract_class(self) -> None:
|
|
55
61
|
"""Raise TypeError when instantiating an abstract class."""
|
|
@@ -87,6 +93,31 @@ class TestCannotInstantiateAbstractClasses:
|
|
|
87
93
|
_ = Vector3Attrib()
|
|
88
94
|
assert "cannot be instantiated" in err.value.args[0]
|
|
89
95
|
|
|
96
|
+
def test_cannot_instantiate_static_attrib(self) -> None:
|
|
97
|
+
"""Raise TypeError when instantiating an abstract class."""
|
|
98
|
+
with pytest.raises(TypeError) as err:
|
|
99
|
+
_: StaticAttrib[Any] = StaticAttrib()
|
|
100
|
+
assert "cannot be instantiated" in err.value.args[0]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestStaticAttrib:
|
|
104
|
+
def test_attribute_error_if_no_value_set(self) -> None:
|
|
105
|
+
"""Raise AttributeError if no value set."""
|
|
106
|
+
attrib = MyStaticAttrib()
|
|
107
|
+
with pytest.raises(AttributeError):
|
|
108
|
+
_ = attrib.value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestBlindHalfEdgesAttribSettersAndGetters:
|
|
112
|
+
def test_set_attrib(self) -> None:
|
|
113
|
+
"""Set an attrib by passing a MeshElementBase instance"""
|
|
114
|
+
mesh = BlindHalfEdges()
|
|
115
|
+
attrib = MyStaticAttrib(7)
|
|
116
|
+
mesh.set_attrib(attrib)
|
|
117
|
+
stored_attrib = mesh.get_attrib(MyStaticAttrib)
|
|
118
|
+
assert stored_attrib.value == 7
|
|
119
|
+
assert stored_attrib.mesh is mesh
|
|
120
|
+
|
|
90
121
|
|
|
91
122
|
class TestAttribBaseClass:
|
|
92
123
|
def test_attribute_error_if_no_value_set(self) -> None:
|
|
@@ -5,8 +5,6 @@ created: 170204 14:22:23
|
|
|
5
5
|
|
|
6
6
|
# pyright: reportPrivateUsage=false
|
|
7
7
|
|
|
8
|
-
import itertools
|
|
9
|
-
import random
|
|
10
8
|
from typing import Any, Dict, Tuple
|
|
11
9
|
|
|
12
10
|
import pytest
|
|
@@ -22,11 +20,6 @@ from halfedge.half_edge_querries import StaticHalfEdges
|
|
|
22
20
|
from halfedge.type_attrib import IncompatibleAttrib, NumericAttrib
|
|
23
21
|
from tests.conftest import get_canonical_mesh
|
|
24
22
|
|
|
25
|
-
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
26
|
-
identifiers = (
|
|
27
|
-
"".join(random.choice(alphabet) for _ in range(10)) for _ in itertools.count()
|
|
28
|
-
)
|
|
29
|
-
|
|
30
23
|
|
|
31
24
|
class Coordinate(IncompatibleAttrib[Tuple[float, ...]]):
|
|
32
25
|
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|