halfedge 0.7.0__tar.gz → 0.8.1__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.7.0 → halfedge-0.8.1}/.github/workflows/pypi-project.yml +1 -1
- {halfedge-0.7.0 → halfedge-0.8.1}/.pre-commit-config.yaml +12 -15
- {halfedge-0.7.0/src/halfedge.egg-info → halfedge-0.8.1}/PKG-INFO +44 -28
- {halfedge-0.7.0 → halfedge-0.8.1}/README.md +41 -25
- {halfedge-0.7.0 → halfedge-0.8.1}/pyproject.toml +5 -5
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge/half_edge_constructors.py +3 -1
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge/half_edge_object.py +7 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge/type_attrib.py +5 -5
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge/validations.py +3 -1
- {halfedge-0.7.0 → halfedge-0.8.1/src/halfedge.egg-info}/PKG-INFO +44 -28
- {halfedge-0.7.0 → halfedge-0.8.1}/tests/conftest.py +25 -24
- {halfedge-0.7.0 → halfedge-0.8.1}/tests/test_classes.py +2 -4
- {halfedge-0.7.0 → halfedge-0.8.1}/tests/test_constructors.py +17 -17
- {halfedge-0.7.0 → halfedge-0.8.1}/tests/test_elements.py +3 -5
- {halfedge-0.7.0 → halfedge-0.8.1}/tests/test_operations.py +3 -3
- {halfedge-0.7.0 → halfedge-0.8.1}/tests/test_validations.py +1 -3
- {halfedge-0.7.0 → halfedge-0.8.1}/.gitignore +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/setup.cfg +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge/__init__.py +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge/half_edge_elements.py +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge/half_edge_querries.py +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge/py.typed +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge.egg-info/SOURCES.txt +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge.egg-info/dependency_links.txt +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge.egg-info/requires.txt +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/src/halfedge.egg-info/top_level.txt +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/tests/__init__.py +0 -0
- {halfedge-0.7.0 → halfedge-0.8.1}/tests/test_object_pickups.py +0 -0
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
strategy:
|
|
18
18
|
fail-fast: false
|
|
19
19
|
matrix:
|
|
20
|
-
python-version: ["3.
|
|
20
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
21
21
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
22
22
|
# if: startsWith(github.event.head_commit.message, 'bump:') == false
|
|
23
23
|
steps:
|
|
@@ -4,7 +4,7 @@ ci:
|
|
|
4
4
|
repos:
|
|
5
5
|
|
|
6
6
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
7
|
-
rev:
|
|
7
|
+
rev: v6.0.0
|
|
8
8
|
hooks:
|
|
9
9
|
- id: check-added-large-files
|
|
10
10
|
- id: check-ast
|
|
@@ -29,9 +29,6 @@ repos:
|
|
|
29
29
|
- id: mixed-line-ending
|
|
30
30
|
- id: requirements-txt-fixer
|
|
31
31
|
- id: trailing-whitespace
|
|
32
|
-
- id: fix-encoding-pragma
|
|
33
|
-
args:
|
|
34
|
-
- --remove
|
|
35
32
|
- id: name-tests-test
|
|
36
33
|
args:
|
|
37
34
|
- --pytest-test-first
|
|
@@ -42,12 +39,12 @@ repos:
|
|
|
42
39
|
# files: .pre-commit-config.yaml
|
|
43
40
|
|
|
44
41
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
45
|
-
rev: v1.
|
|
42
|
+
rev: v1.17.1
|
|
46
43
|
hooks:
|
|
47
44
|
- id: mypy
|
|
48
45
|
name: mypy
|
|
49
46
|
language: python
|
|
50
|
-
language_version: python 3.
|
|
47
|
+
language_version: python 3.9
|
|
51
48
|
types: [python]
|
|
52
49
|
require_serial: true
|
|
53
50
|
verbose: true
|
|
@@ -58,23 +55,23 @@ repos:
|
|
|
58
55
|
# files: ^(src/|tests/)
|
|
59
56
|
|
|
60
57
|
- repo: https://github.com/PyCQA/isort
|
|
61
|
-
rev:
|
|
58
|
+
rev: 6.0.1
|
|
62
59
|
hooks:
|
|
63
60
|
- id: isort
|
|
64
61
|
args: ["--profile", "black", "--filter-files", "--combine-as", "honor--noqa"]
|
|
65
62
|
|
|
66
63
|
- repo: https://github.com/psf/black
|
|
67
|
-
rev:
|
|
64
|
+
rev: 25.1.0
|
|
68
65
|
hooks:
|
|
69
66
|
- id: black
|
|
70
|
-
language_version: python3.
|
|
67
|
+
language_version: python3.9
|
|
71
68
|
args: ["--skip-magic-trailing-comma"]
|
|
72
69
|
|
|
73
70
|
- repo: https://github.com/asottile/pyupgrade
|
|
74
|
-
rev: v3.
|
|
71
|
+
rev: v3.20.0
|
|
75
72
|
hooks:
|
|
76
73
|
- args:
|
|
77
|
-
- --
|
|
74
|
+
- --py39-plus
|
|
78
75
|
id: pyupgrade
|
|
79
76
|
|
|
80
77
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
|
@@ -83,7 +80,7 @@ repos:
|
|
|
83
80
|
- id: remove-tabs
|
|
84
81
|
|
|
85
82
|
- repo: https://github.com/commitizen-tools/commitizen
|
|
86
|
-
rev:
|
|
83
|
+
rev: v4.8.3
|
|
87
84
|
hooks:
|
|
88
85
|
- id: commitizen
|
|
89
86
|
|
|
@@ -129,18 +126,18 @@ repos:
|
|
|
129
126
|
# PLR2004 magic value used in comparison
|
|
130
127
|
# D403 First word of the first line should be properly capitalized
|
|
131
128
|
# PLR0913 too namy arguments
|
|
132
|
-
rev: 'v0.
|
|
129
|
+
rev: 'v0.12.10'
|
|
133
130
|
hooks:
|
|
134
131
|
- id: ruff
|
|
135
132
|
exclude: "tests"
|
|
136
133
|
args:
|
|
137
|
-
- --target-version=
|
|
134
|
+
- --target-version=py39
|
|
138
135
|
- --select=ALL
|
|
139
136
|
- --ignore=COM812,D203,D213,ISC003,PYI019,A002,PLR2004,D403,PLR0913
|
|
140
137
|
# - --fix
|
|
141
138
|
|
|
142
139
|
# reads pyproject.toml for additional config
|
|
143
140
|
- repo: https://github.com/RobertCraigie/pyright-python
|
|
144
|
-
rev: v1.1.
|
|
141
|
+
rev: v1.1.404
|
|
145
142
|
hooks:
|
|
146
143
|
- id: pyright
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: halfedge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: A typical half-edge data structure with some padding
|
|
5
5
|
Author-email: Shay Hill <shay_public@hotmail.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: paragraphs
|
|
10
10
|
Provides-Extra: dev
|
|
@@ -35,11 +35,11 @@ mesh.remove_edge(edge)
|
|
|
35
35
|
|
|
36
36
|
## Particulars
|
|
37
37
|
|
|
38
|
-
The idea (for
|
|
38
|
+
The idea (for 19 years and counting) has been to create an interface that is neither too complex, too verbose, nor too magical. I've been all over the place as to where that line should be. This is my current thinking.
|
|
39
39
|
|
|
40
40
|
### Reflection
|
|
41
41
|
|
|
42
|
-
If you set `edge_a.orig = vert_a`, then the `Edge.vert` setter will *automagically* set `vert_a.edge = edge_a`. This is true for any setter that might otherwise break the mesh. Sometimes, this reflection will happen when it isn't strictly necessary. Imagine you have `face_a` with three edges: `edge_a`, `edge_b` and `edge_c`. `face_a` has a pointer to `edge_a`, but it could point to any of the three edges and still be correct per the requirements of the halfedge data structure. If you directly set `edge_b.face = face_a`, everything would still be correct (`face_a.edge` would still be `edge_a` and that would still be correct), but the `Edge.edge` setter will nevertheless set `face_a.edge = edge_b`.
|
|
42
|
+
If you set `edge_a.orig = vert_a`, then the `Edge.vert` setter will *automagically* set `vert_a.edge = edge_a`. This is true for any setter that might otherwise break the mesh. Sometimes, this reflection will happen when it isn't strictly necessary. Imagine you have `face_a` with three edges: `edge_a`, `edge_b` and `edge_c`. `face_a` has a pointer to `edge_a`, but it could point to any of the three edges and still be correct per the requirements of the halfedge data structure. If you directly set `edge_b.face = face_a`, everything would still be correct (`face_a.edge` would still be `edge_a` and that would still be correct), but the `Edge.edge` setter will nevertheless "reflectively" set `face_a.edge = edge_b`.
|
|
43
43
|
|
|
44
44
|
### id
|
|
45
45
|
|
|
@@ -64,50 +64,66 @@ Face(
|
|
|
64
64
|
|
|
65
65
|
The `is_hole` `__init__` kwarg is shorthand for
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
```python
|
|
68
|
+
class IsHole(ContagionAttribute):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
face = Face()
|
|
72
|
+
face.add_attrib(IsHole())
|
|
73
|
+
```
|
|
69
74
|
|
|
70
|
-
vert = Vert()
|
|
71
|
-
vert.add_attrib(IsHole())
|
|
72
75
|
|
|
73
76
|
The `face_instance.is_hole` property getter is shorthand for
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
```python
|
|
79
|
+
vert.get_attrib(IsHole())
|
|
80
|
+
```
|
|
76
81
|
|
|
77
82
|
More on `Attrib` classes below and in `type_attrib.py`.
|
|
78
83
|
|
|
79
84
|
### Element Attributes
|
|
80
85
|
|
|
81
|
-
By halfedge convention
|
|
86
|
+
By halfedge convention
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
- each Vert instance holds a pointer to one Edge instance;
|
|
89
|
+
- each Face instance holds a pointer to one Edge instance; and
|
|
90
|
+
- each Edge instance holds four pointers (orig, pair, face, next).
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
These describe the geometry of a mesh, but there may be other attributes you would like to assign to these instances. For example, each Face instance might have a color. There is no objectively correct way to define a face color, nor to merge colors when two faces are merged, nor to split a color when faces are split. Do a red and a blue face behave like paint and combine to make a purple face? Or do they behave like DNA to make a red *or* blue face depending on which is dominant? This library will not guess.
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
pass
|
|
94
|
+
For each such attribute, you will need to define `merge` and `split` methods to explicate how the attribute 1) combines when elements are merged; and 2) behaves when an element is split. Do this by creating a new descendent of `Attrib` or one of `Attrib`'s children defined in `type_attrib.py`. See the docstring in that file for more information.
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
```python
|
|
97
|
+
# `Vector2Attrib` is a child of `Attrib` defined in `type_attrib.py`.
|
|
98
|
+
# It defines suitable (YMMV) `merge` (average) and `split` (fail) methods for
|
|
99
|
+
# an (x, y) coordinate.
|
|
100
|
+
class Coordinate(Vector2Attrib):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
vert = Vert()
|
|
104
|
+
vert.add_attrib(Coordinate((1, 2)))
|
|
105
|
+
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
106
|
+
```
|
|
93
107
|
|
|
94
|
-
|
|
108
|
+
You cannot assign or access these attributes with `vert.attribute`. Instead assign with `vert.add_attrib(attrib_instance)`. Retrieve the value with `vert.get_attrib(attrib_class)`. Everything will be keyed to the class name, so you will need a new ElemAttribBase descendant for each attribute type.
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
110
|
+
These element attributes can also be passed at `__init__`
|
|
98
111
|
|
|
99
|
-
|
|
112
|
+
```python
|
|
113
|
+
vert = Vert(Coordinate(1, 2))
|
|
114
|
+
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
115
|
+
```
|
|
100
116
|
|
|
101
117
|
### You Should Know
|
|
102
118
|
|
|
103
119
|
A canonical half-edge data structure stores:
|
|
104
120
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
- a set of verts (redundant)
|
|
122
|
+
- for each vert, a pointer to an edge
|
|
123
|
+
- a set of edges
|
|
124
|
+
- for each edge, pointers to vert, pair, face, next
|
|
125
|
+
- a set of faces (redundant)
|
|
126
|
+
- for each face, a pointer to an edge
|
|
111
127
|
|
|
112
128
|
This implementation only stores a set of edges. Sets of verts and faces are generated by iterating through references in edge instances. This makes for slower code, but does not violate DRY and makes for dramatically cleaner code.
|
|
113
129
|
|
|
@@ -18,11 +18,11 @@ mesh.remove_edge(edge)
|
|
|
18
18
|
|
|
19
19
|
## Particulars
|
|
20
20
|
|
|
21
|
-
The idea (for
|
|
21
|
+
The idea (for 19 years and counting) has been to create an interface that is neither too complex, too verbose, nor too magical. I've been all over the place as to where that line should be. This is my current thinking.
|
|
22
22
|
|
|
23
23
|
### Reflection
|
|
24
24
|
|
|
25
|
-
If you set `edge_a.orig = vert_a`, then the `Edge.vert` setter will *automagically* set `vert_a.edge = edge_a`. This is true for any setter that might otherwise break the mesh. Sometimes, this reflection will happen when it isn't strictly necessary. Imagine you have `face_a` with three edges: `edge_a`, `edge_b` and `edge_c`. `face_a` has a pointer to `edge_a`, but it could point to any of the three edges and still be correct per the requirements of the halfedge data structure. If you directly set `edge_b.face = face_a`, everything would still be correct (`face_a.edge` would still be `edge_a` and that would still be correct), but the `Edge.edge` setter will nevertheless set `face_a.edge = edge_b`.
|
|
25
|
+
If you set `edge_a.orig = vert_a`, then the `Edge.vert` setter will *automagically* set `vert_a.edge = edge_a`. This is true for any setter that might otherwise break the mesh. Sometimes, this reflection will happen when it isn't strictly necessary. Imagine you have `face_a` with three edges: `edge_a`, `edge_b` and `edge_c`. `face_a` has a pointer to `edge_a`, but it could point to any of the three edges and still be correct per the requirements of the halfedge data structure. If you directly set `edge_b.face = face_a`, everything would still be correct (`face_a.edge` would still be `edge_a` and that would still be correct), but the `Edge.edge` setter will nevertheless "reflectively" set `face_a.edge = edge_b`.
|
|
26
26
|
|
|
27
27
|
### id
|
|
28
28
|
|
|
@@ -47,50 +47,66 @@ Face(
|
|
|
47
47
|
|
|
48
48
|
The `is_hole` `__init__` kwarg is shorthand for
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
```python
|
|
51
|
+
class IsHole(ContagionAttribute):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
face = Face()
|
|
55
|
+
face.add_attrib(IsHole())
|
|
56
|
+
```
|
|
52
57
|
|
|
53
|
-
vert = Vert()
|
|
54
|
-
vert.add_attrib(IsHole())
|
|
55
58
|
|
|
56
59
|
The `face_instance.is_hole` property getter is shorthand for
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
```python
|
|
62
|
+
vert.get_attrib(IsHole())
|
|
63
|
+
```
|
|
59
64
|
|
|
60
65
|
More on `Attrib` classes below and in `type_attrib.py`.
|
|
61
66
|
|
|
62
67
|
### Element Attributes
|
|
63
68
|
|
|
64
|
-
By halfedge convention
|
|
69
|
+
By halfedge convention
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
- each Vert instance holds a pointer to one Edge instance;
|
|
72
|
+
- each Face instance holds a pointer to one Edge instance; and
|
|
73
|
+
- each Edge instance holds four pointers (orig, pair, face, next).
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
These describe the geometry of a mesh, but there may be other attributes you would like to assign to these instances. For example, each Face instance might have a color. There is no objectively correct way to define a face color, nor to merge colors when two faces are merged, nor to split a color when faces are split. Do a red and a blue face behave like paint and combine to make a purple face? Or do they behave like DNA to make a red *or* blue face depending on which is dominant? This library will not guess.
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
pass
|
|
77
|
+
For each such attribute, you will need to define `merge` and `split` methods to explicate how the attribute 1) combines when elements are merged; and 2) behaves when an element is split. Do this by creating a new descendent of `Attrib` or one of `Attrib`'s children defined in `type_attrib.py`. See the docstring in that file for more information.
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
```python
|
|
80
|
+
# `Vector2Attrib` is a child of `Attrib` defined in `type_attrib.py`.
|
|
81
|
+
# It defines suitable (YMMV) `merge` (average) and `split` (fail) methods for
|
|
82
|
+
# an (x, y) coordinate.
|
|
83
|
+
class Coordinate(Vector2Attrib):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
vert = Vert()
|
|
87
|
+
vert.add_attrib(Coordinate((1, 2)))
|
|
88
|
+
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
89
|
+
```
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
You cannot assign or access these attributes with `vert.attribute`. Instead assign with `vert.add_attrib(attrib_instance)`. Retrieve the value with `vert.get_attrib(attrib_class)`. Everything will be keyed to the class name, so you will need a new ElemAttribBase descendant for each attribute type.
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
93
|
+
These element attributes can also be passed at `__init__`
|
|
81
94
|
|
|
82
|
-
|
|
95
|
+
```python
|
|
96
|
+
vert = Vert(Coordinate(1, 2))
|
|
97
|
+
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
98
|
+
```
|
|
83
99
|
|
|
84
100
|
### You Should Know
|
|
85
101
|
|
|
86
102
|
A canonical half-edge data structure stores:
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
- a set of verts (redundant)
|
|
105
|
+
- for each vert, a pointer to an edge
|
|
106
|
+
- a set of edges
|
|
107
|
+
- for each edge, pointers to vert, pair, face, next
|
|
108
|
+
- a set of faces (redundant)
|
|
109
|
+
- for each face, a pointer to an edge
|
|
94
110
|
|
|
95
111
|
This implementation only stores a set of edges. Sets of verts and faces are generated by iterating through references in edge instances. This makes for slower code, but does not violate DRY and makes for dramatically cleaner code.
|
|
96
112
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "halfedge"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.1"
|
|
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"}
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
requires-python = ">=3.
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
9
|
dependencies = ["paragraphs"]
|
|
10
10
|
|
|
11
11
|
[project.optional-dependencies]
|
|
@@ -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.8.1"
|
|
29
29
|
tag_format = "$version"
|
|
30
30
|
major-version-zero = true
|
|
31
31
|
version_files = ["pyproject.toml:^version"]
|
|
@@ -43,7 +43,7 @@ log_cli = 1
|
|
|
43
43
|
[tool.tox]
|
|
44
44
|
legacy_tox_ini = """
|
|
45
45
|
[tox]
|
|
46
|
-
envlist = py{
|
|
46
|
+
envlist = py{39,310,311,312,313}
|
|
47
47
|
|
|
48
48
|
[testenv]
|
|
49
49
|
deps = pytest
|
|
@@ -55,7 +55,7 @@ commands = pytest
|
|
|
55
55
|
include = ["src"]
|
|
56
56
|
exclude = ["**/__pycache__.py"]
|
|
57
57
|
|
|
58
|
-
pythonVersion = "3.
|
|
58
|
+
pythonVersion = "3.9"
|
|
59
59
|
pythonPlatform = "Any"
|
|
60
60
|
|
|
61
61
|
typeCheckingMode = "strict"
|
|
@@ -23,13 +23,15 @@ then passing that raw data to mesh_from_vr would create a mesh with 6 faces and
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
from contextlib import suppress
|
|
26
|
-
from typing import TYPE_CHECKING, Any,
|
|
26
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
27
27
|
|
|
28
28
|
from paragraphs import par
|
|
29
29
|
|
|
30
30
|
from halfedge.half_edge_elements import Edge, Face, ManifoldMeshError, Vert
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Iterable, Sequence
|
|
34
|
+
|
|
33
35
|
from halfedge.type_attrib import Attrib, StaticAttrib
|
|
34
36
|
|
|
35
37
|
|
|
@@ -79,6 +79,13 @@ class HalfEdges(StaticHalfEdges):
|
|
|
79
79
|
Able to infer from:
|
|
80
80
|
* both verts on same face: that face
|
|
81
81
|
* empty mesh: a new Hole
|
|
82
|
+
|
|
83
|
+
This is only called when inserting an edge into an existing face (or empty
|
|
84
|
+
mesh), so the orig and dest should always be connected to only one face (or
|
|
85
|
+
nothing, which is handled in this method).
|
|
86
|
+
|
|
87
|
+
Once the face is split, the two verts will form an edge that connects to two
|
|
88
|
+
faces.
|
|
82
89
|
"""
|
|
83
90
|
if not self.edges:
|
|
84
91
|
return self.new_hole()
|
|
@@ -61,7 +61,7 @@ When assigned to a Vert instance, these will be stored in the Vert instance's
|
|
|
61
61
|
from __future__ import annotations
|
|
62
62
|
|
|
63
63
|
from contextlib import suppress
|
|
64
|
-
from typing import TYPE_CHECKING, Any, Generic, Literal,
|
|
64
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar
|
|
65
65
|
|
|
66
66
|
from paragraphs import par
|
|
67
67
|
|
|
@@ -79,7 +79,7 @@ class StaticAttrib(Generic[_T]):
|
|
|
79
79
|
merged or split.
|
|
80
80
|
"""
|
|
81
81
|
|
|
82
|
-
__slots__ = ("
|
|
82
|
+
__slots__ = ("_mesh", "_value")
|
|
83
83
|
|
|
84
84
|
def __new__(
|
|
85
85
|
cls: type[_TStaticAttrib],
|
|
@@ -193,7 +193,7 @@ class Attrib(Generic[_T]):
|
|
|
193
193
|
every case.
|
|
194
194
|
"""
|
|
195
195
|
|
|
196
|
-
__slots__ = ("
|
|
196
|
+
__slots__ = ("_element", "_value")
|
|
197
197
|
|
|
198
198
|
def __new__(
|
|
199
199
|
cls: type[_TAttrib],
|
|
@@ -469,7 +469,7 @@ class NumericAttrib(Attrib[_T]):
|
|
|
469
469
|
return type(have_values[0])(sum(values) / len(values))
|
|
470
470
|
|
|
471
471
|
|
|
472
|
-
class Vector2Attrib(Attrib[
|
|
472
|
+
class Vector2Attrib(Attrib[tuple[float, float]]):
|
|
473
473
|
"""Average merge_from values as xy tuples."""
|
|
474
474
|
|
|
475
475
|
def __new__(
|
|
@@ -501,7 +501,7 @@ class Vector2Attrib(Attrib[Tuple[float, float]]):
|
|
|
501
501
|
return type(have_values[0])((sum_x / num, sum_y / num))
|
|
502
502
|
|
|
503
503
|
|
|
504
|
-
class Vector3Attrib(Attrib[
|
|
504
|
+
class Vector3Attrib(Attrib[tuple[float, float, float]]):
|
|
505
505
|
"""Average merge_from values as xyz tuples."""
|
|
506
506
|
|
|
507
507
|
def __new__(
|
|
@@ -10,11 +10,13 @@ created: 181127
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from itertools import chain
|
|
13
|
-
from typing import TYPE_CHECKING, Any, Callable,
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
|
14
14
|
|
|
15
15
|
from halfedge.half_edge_elements import Edge, Face, ManifoldMeshError
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
|
|
18
20
|
from halfedge.half_edge_querries import StaticHalfEdges
|
|
19
21
|
|
|
20
22
|
_T = TypeVar("_T")
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: halfedge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: A typical half-edge data structure with some padding
|
|
5
5
|
Author-email: Shay Hill <shay_public@hotmail.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: paragraphs
|
|
10
10
|
Provides-Extra: dev
|
|
@@ -35,11 +35,11 @@ mesh.remove_edge(edge)
|
|
|
35
35
|
|
|
36
36
|
## Particulars
|
|
37
37
|
|
|
38
|
-
The idea (for
|
|
38
|
+
The idea (for 19 years and counting) has been to create an interface that is neither too complex, too verbose, nor too magical. I've been all over the place as to where that line should be. This is my current thinking.
|
|
39
39
|
|
|
40
40
|
### Reflection
|
|
41
41
|
|
|
42
|
-
If you set `edge_a.orig = vert_a`, then the `Edge.vert` setter will *automagically* set `vert_a.edge = edge_a`. This is true for any setter that might otherwise break the mesh. Sometimes, this reflection will happen when it isn't strictly necessary. Imagine you have `face_a` with three edges: `edge_a`, `edge_b` and `edge_c`. `face_a` has a pointer to `edge_a`, but it could point to any of the three edges and still be correct per the requirements of the halfedge data structure. If you directly set `edge_b.face = face_a`, everything would still be correct (`face_a.edge` would still be `edge_a` and that would still be correct), but the `Edge.edge` setter will nevertheless set `face_a.edge = edge_b`.
|
|
42
|
+
If you set `edge_a.orig = vert_a`, then the `Edge.vert` setter will *automagically* set `vert_a.edge = edge_a`. This is true for any setter that might otherwise break the mesh. Sometimes, this reflection will happen when it isn't strictly necessary. Imagine you have `face_a` with three edges: `edge_a`, `edge_b` and `edge_c`. `face_a` has a pointer to `edge_a`, but it could point to any of the three edges and still be correct per the requirements of the halfedge data structure. If you directly set `edge_b.face = face_a`, everything would still be correct (`face_a.edge` would still be `edge_a` and that would still be correct), but the `Edge.edge` setter will nevertheless "reflectively" set `face_a.edge = edge_b`.
|
|
43
43
|
|
|
44
44
|
### id
|
|
45
45
|
|
|
@@ -64,50 +64,66 @@ Face(
|
|
|
64
64
|
|
|
65
65
|
The `is_hole` `__init__` kwarg is shorthand for
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
```python
|
|
68
|
+
class IsHole(ContagionAttribute):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
face = Face()
|
|
72
|
+
face.add_attrib(IsHole())
|
|
73
|
+
```
|
|
69
74
|
|
|
70
|
-
vert = Vert()
|
|
71
|
-
vert.add_attrib(IsHole())
|
|
72
75
|
|
|
73
76
|
The `face_instance.is_hole` property getter is shorthand for
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
```python
|
|
79
|
+
vert.get_attrib(IsHole())
|
|
80
|
+
```
|
|
76
81
|
|
|
77
82
|
More on `Attrib` classes below and in `type_attrib.py`.
|
|
78
83
|
|
|
79
84
|
### Element Attributes
|
|
80
85
|
|
|
81
|
-
By halfedge convention
|
|
86
|
+
By halfedge convention
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
- each Vert instance holds a pointer to one Edge instance;
|
|
89
|
+
- each Face instance holds a pointer to one Edge instance; and
|
|
90
|
+
- each Edge instance holds four pointers (orig, pair, face, next).
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
These describe the geometry of a mesh, but there may be other attributes you would like to assign to these instances. For example, each Face instance might have a color. There is no objectively correct way to define a face color, nor to merge colors when two faces are merged, nor to split a color when faces are split. Do a red and a blue face behave like paint and combine to make a purple face? Or do they behave like DNA to make a red *or* blue face depending on which is dominant? This library will not guess.
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
pass
|
|
94
|
+
For each such attribute, you will need to define `merge` and `split` methods to explicate how the attribute 1) combines when elements are merged; and 2) behaves when an element is split. Do this by creating a new descendent of `Attrib` or one of `Attrib`'s children defined in `type_attrib.py`. See the docstring in that file for more information.
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
```python
|
|
97
|
+
# `Vector2Attrib` is a child of `Attrib` defined in `type_attrib.py`.
|
|
98
|
+
# It defines suitable (YMMV) `merge` (average) and `split` (fail) methods for
|
|
99
|
+
# an (x, y) coordinate.
|
|
100
|
+
class Coordinate(Vector2Attrib):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
vert = Vert()
|
|
104
|
+
vert.add_attrib(Coordinate((1, 2)))
|
|
105
|
+
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
106
|
+
```
|
|
93
107
|
|
|
94
|
-
|
|
108
|
+
You cannot assign or access these attributes with `vert.attribute`. Instead assign with `vert.add_attrib(attrib_instance)`. Retrieve the value with `vert.get_attrib(attrib_class)`. Everything will be keyed to the class name, so you will need a new ElemAttribBase descendant for each attribute type.
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
110
|
+
These element attributes can also be passed at `__init__`
|
|
98
111
|
|
|
99
|
-
|
|
112
|
+
```python
|
|
113
|
+
vert = Vert(Coordinate(1, 2))
|
|
114
|
+
assert vert.get_attrib(Coordinate).value == (1, 2)
|
|
115
|
+
```
|
|
100
116
|
|
|
101
117
|
### You Should Know
|
|
102
118
|
|
|
103
119
|
A canonical half-edge data structure stores:
|
|
104
120
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
- a set of verts (redundant)
|
|
122
|
+
- for each vert, a pointer to an edge
|
|
123
|
+
- a set of edges
|
|
124
|
+
- for each edge, pointers to vert, pair, face, next
|
|
125
|
+
- a set of faces (redundant)
|
|
126
|
+
- for each face, a pointer to an edge
|
|
111
127
|
|
|
112
128
|
This implementation only stores a set of edges. Sets of verts and faces are generated by iterating through references in edge instances. This makes for slower code, but does not violate DRY and makes for dramatically cleaner code.
|
|
113
129
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""simple HalfEdges instances for testing
|
|
2
2
|
|
|
3
3
|
created: 181121 13:14:06
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from collections.abc import Iterable, Sequence
|
|
6
7
|
from itertools import product
|
|
7
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
10
11
|
|
|
@@ -14,12 +15,12 @@ from halfedge.half_edge_object import HalfEdges
|
|
|
14
15
|
from halfedge.type_attrib import IncompatibleAttrib
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
class Coordinate(IncompatibleAttrib[
|
|
18
|
+
class Coordinate(IncompatibleAttrib[tuple[int, ...]]):
|
|
18
19
|
pass
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@pytest.fixture
|
|
22
|
-
def he_triangle() ->
|
|
23
|
+
def he_triangle() -> dict[str, list[Any]]:
|
|
23
24
|
"""A simple triangle (inside and outside faces) for Mesh Element test"""
|
|
24
25
|
mesh = HalfEdges()
|
|
25
26
|
verts = [half_edge_elements.Vert(Coordinate(x)) for x in ((-1, 0), (1, 0), (0, 1))]
|
|
@@ -47,7 +48,7 @@ def he_triangle() -> Dict[str, List[Any]]:
|
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
@pytest.fixture(scope="module")
|
|
50
|
-
def meshes_vlvi() ->
|
|
51
|
+
def meshes_vlvi() -> dict[str, Any]:
|
|
51
52
|
"""A cube and a 3 x 3 grid"""
|
|
52
53
|
# fmt: off
|
|
53
54
|
cube_vl = [(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
|
|
@@ -73,13 +74,13 @@ def meshes_vlvi() -> Dict[str, Any]:
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
@pytest.fixture(scope="function")
|
|
76
|
-
def he_cube(meshes_vlvi:
|
|
77
|
+
def he_cube(meshes_vlvi: dict[str, Any]) -> HalfEdges:
|
|
77
78
|
vl = [Vert(Coordinate(x)) for x in meshes_vlvi["cube_vl"]]
|
|
78
79
|
return HalfEdges.from_vlfi(vl, meshes_vlvi["cube_vi"])
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
@pytest.fixture(scope="function")
|
|
82
|
-
def he_grid(meshes_vlvi:
|
|
83
|
+
def he_grid(meshes_vlvi: dict[str, Any]) -> HalfEdges:
|
|
83
84
|
vl = [Vert(Coordinate(x)) for x in meshes_vlvi["grid_vl"]]
|
|
84
85
|
return HalfEdges.from_vlfi(vl, meshes_vlvi["grid_vi"])
|
|
85
86
|
|
|
@@ -97,7 +98,7 @@ def he_mesh(
|
|
|
97
98
|
@pytest.fixture(scope="function", params=range(9))
|
|
98
99
|
def grid_faces(
|
|
99
100
|
request: pytest.FixtureRequest, he_grid: HalfEdges
|
|
100
|
-
) ->
|
|
101
|
+
) -> tuple[HalfEdges, Face]:
|
|
101
102
|
"""A cube and a 3 x 3 grid as HalfEdges instances"""
|
|
102
103
|
idx = cast(int, request.param)
|
|
103
104
|
return he_grid, sorted(he_grid.faces)[idx]
|
|
@@ -106,7 +107,7 @@ def grid_faces(
|
|
|
106
107
|
@pytest.fixture(scope="function", params=range(6))
|
|
107
108
|
def cube_faces(
|
|
108
109
|
request: pytest.FixtureRequest, he_cube: HalfEdges
|
|
109
|
-
) ->
|
|
110
|
+
) -> tuple[HalfEdges, Face]:
|
|
110
111
|
"""A cube and a 3 x 3 grid as HalfEdges instances"""
|
|
111
112
|
idx = cast(int, request.param)
|
|
112
113
|
return he_cube, sorted(he_cube.faces)[idx]
|
|
@@ -115,9 +116,9 @@ def cube_faces(
|
|
|
115
116
|
@pytest.fixture(params=range(2))
|
|
116
117
|
def mesh_faces(
|
|
117
118
|
request: pytest.FixtureRequest,
|
|
118
|
-
grid_faces:
|
|
119
|
-
cube_faces:
|
|
120
|
-
) ->
|
|
119
|
+
grid_faces: tuple[HalfEdges, Face],
|
|
120
|
+
cube_faces: tuple[HalfEdges, Face],
|
|
121
|
+
) -> tuple[HalfEdges, Face]:
|
|
121
122
|
"""A cube and a 3 x 3 grid as HalfEdges instances"""
|
|
122
123
|
if request.param == 0:
|
|
123
124
|
return grid_faces
|
|
@@ -127,7 +128,7 @@ def mesh_faces(
|
|
|
127
128
|
@pytest.fixture(scope="function", params=range(48))
|
|
128
129
|
def grid_edges(
|
|
129
130
|
request: pytest.FixtureRequest, he_grid: HalfEdges
|
|
130
|
-
) ->
|
|
131
|
+
) -> tuple[HalfEdges, Edge]:
|
|
131
132
|
"""A cube and a 3 x 3 grid as HalfEdges instances"""
|
|
132
133
|
idx = cast(int, request.param)
|
|
133
134
|
return he_grid, sorted(he_grid.edges)[idx]
|
|
@@ -136,7 +137,7 @@ def grid_edges(
|
|
|
136
137
|
@pytest.fixture(scope="function", params=range(24))
|
|
137
138
|
def cube_edges(
|
|
138
139
|
request: pytest.FixtureRequest, he_cube: HalfEdges
|
|
139
|
-
) ->
|
|
140
|
+
) -> tuple[HalfEdges, Edge]:
|
|
140
141
|
"""A cube and a 3 x 3 grid as HalfEdges instances"""
|
|
141
142
|
idx = cast(int, request.param)
|
|
142
143
|
return he_cube, sorted(he_cube.edges)[idx]
|
|
@@ -145,9 +146,9 @@ def cube_edges(
|
|
|
145
146
|
@pytest.fixture(params=range(2))
|
|
146
147
|
def mesh_edges(
|
|
147
148
|
request: pytest.FixtureRequest,
|
|
148
|
-
grid_edges:
|
|
149
|
-
cube_edges:
|
|
150
|
-
) ->
|
|
149
|
+
grid_edges: tuple[HalfEdges, Edge],
|
|
150
|
+
cube_edges: tuple[HalfEdges, Edge],
|
|
151
|
+
) -> tuple[HalfEdges, Edge]:
|
|
151
152
|
"""A cube and a 3 x 3 grid as HalfEdges instances"""
|
|
152
153
|
if request.param == 0:
|
|
153
154
|
return grid_edges
|
|
@@ -191,7 +192,7 @@ def compare_circular_2(
|
|
|
191
192
|
_TAxis = TypeVar("_TAxis")
|
|
192
193
|
|
|
193
194
|
|
|
194
|
-
def get_canonical_index_tuple(vals: Iterable[int]) ->
|
|
195
|
+
def get_canonical_index_tuple(vals: Iterable[int]) -> tuple[int, ...]:
|
|
195
196
|
"""Return a tuple with the lowest value first.
|
|
196
197
|
|
|
197
198
|
This is useful for comparing circular sequences. It will only be canonical when
|
|
@@ -204,13 +205,13 @@ def get_canonical_index_tuple(vals: Iterable[int]) -> Tuple[int, ...]:
|
|
|
204
205
|
|
|
205
206
|
|
|
206
207
|
def get_canonical_vr(
|
|
207
|
-
vr:
|
|
208
|
-
) ->
|
|
208
|
+
vr: set[tuple[tuple[_TAxis, ...], ...]],
|
|
209
|
+
) -> set[tuple[tuple[_TAxis, ...], ...]]:
|
|
209
210
|
"""Rotate each tuple in a set to start with its min item.
|
|
210
211
|
|
|
211
212
|
See docstring for canonical_mesh.
|
|
212
213
|
"""
|
|
213
|
-
vr_aligned:
|
|
214
|
+
vr_aligned: set[tuple[tuple[_TAxis, ...], ...]] = set()
|
|
214
215
|
for face_verts in vr:
|
|
215
216
|
min_item_idx = face_verts.index(min(face_verts))
|
|
216
217
|
vr_aligned.add(face_verts[min_item_idx:] + face_verts[:min_item_idx])
|
|
@@ -218,8 +219,8 @@ def get_canonical_vr(
|
|
|
218
219
|
|
|
219
220
|
|
|
220
221
|
def get_canonical_mesh(
|
|
221
|
-
vl: Sequence[
|
|
222
|
-
) ->
|
|
222
|
+
vl: Sequence[tuple[_TAxis, ...]], vi: Iterable[tuple[int, ...]]
|
|
223
|
+
) -> set[tuple[tuple[_TAxis, ...], ...]]:
|
|
223
224
|
"""Return a canonical mesh representation.
|
|
224
225
|
|
|
225
226
|
Methods in this library represent meshes as
|
|
@@ -264,7 +265,7 @@ def get_canonical_mesh(
|
|
|
264
265
|
This function produces an unambiguous representation so that such methods can be
|
|
265
266
|
tested.
|
|
266
267
|
"""
|
|
267
|
-
vr:
|
|
268
|
+
vr: set[tuple[tuple[_TAxis, ...], ...]] = set()
|
|
268
269
|
for face_indices in vi:
|
|
269
270
|
vr.add(tuple(vl[x] for x in face_indices))
|
|
270
271
|
return get_canonical_vr(vr)
|
|
@@ -8,7 +8,7 @@ created: 170204 14:22:23
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import random
|
|
11
|
-
from typing import Any,
|
|
11
|
+
from typing import Any, TypeVar
|
|
12
12
|
|
|
13
13
|
import pytest
|
|
14
14
|
|
|
@@ -333,11 +333,9 @@ def test_edge_lap_fails(he_triangle: dict[str, Any]) -> None:
|
|
|
333
333
|
assert "infinite" in err.value.args[0]
|
|
334
334
|
|
|
335
335
|
|
|
336
|
-
class Coordinate(IncompatibleAttrib[
|
|
336
|
+
class Coordinate(IncompatibleAttrib[tuple[int, int, int]]):
|
|
337
337
|
"""A subclass of IncompatibleAttrib."""
|
|
338
338
|
|
|
339
|
-
pass
|
|
340
|
-
|
|
341
339
|
|
|
342
340
|
class TestInitVert:
|
|
343
341
|
def setup_method(self) -> None:
|
|
@@ -5,7 +5,7 @@ created: 170204 14:22:23
|
|
|
5
5
|
|
|
6
6
|
# pyright: reportPrivateUsage=false
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ from halfedge.type_attrib import IncompatibleAttrib, NumericAttrib
|
|
|
21
21
|
from tests.conftest import get_canonical_mesh
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
class Coordinate(IncompatibleAttrib[
|
|
24
|
+
class Coordinate(IncompatibleAttrib[tuple[float, ...]]):
|
|
25
25
|
pass
|
|
26
26
|
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ class TestFromVlfi:
|
|
|
29
29
|
def test_raise_on_non_inferable_holes(self) -> None:
|
|
30
30
|
"""Raises if holes cannot be inferred."""
|
|
31
31
|
vl = [Vert() for _ in range(7)]
|
|
32
|
-
fi: list[
|
|
32
|
+
fi: list[tuple[int, ...]] = [(0, 2, 3, 1), (3, 5, 6, 4)]
|
|
33
33
|
with pytest.raises(ManifoldMeshError) as err:
|
|
34
34
|
_ = HalfEdges.from_vlfi(vl, fi)
|
|
35
35
|
assert "Ambiguous 'next'" in err.value.args[0]
|
|
@@ -54,7 +54,7 @@ class TestMeshElementBase:
|
|
|
54
54
|
assert flag1_defined.get_attrib(Flag2).value == 4
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def test_edge_lap_succeeds(he_triangle:
|
|
57
|
+
def test_edge_lap_succeeds(he_triangle: dict[str, Any]) -> None:
|
|
58
58
|
"""Returns to self when (func(func(func(....func(self))))) == self."""
|
|
59
59
|
for edge in he_triangle["edges"]:
|
|
60
60
|
assert _function_lap(lambda x: x.next, edge) == [
|
|
@@ -64,7 +64,7 @@ def test_edge_lap_succeeds(he_triangle: Dict[str, Any]) -> None:
|
|
|
64
64
|
]
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
def test_edge_lap_fails(he_triangle:
|
|
67
|
+
def test_edge_lap_fails(he_triangle: dict[str, Any]) -> None:
|
|
68
68
|
"""Fails when self intersects."""
|
|
69
69
|
edges = he_triangle["edges"]
|
|
70
70
|
with pytest.raises(ManifoldMeshError) as err:
|
|
@@ -75,55 +75,55 @@ def test_edge_lap_fails(he_triangle: Dict[str, Any]) -> None:
|
|
|
75
75
|
class TestElementSubclasses:
|
|
76
76
|
"""Test all three _MeshElementBase children."""
|
|
77
77
|
|
|
78
|
-
def test_edge_face_edges(self, he_triangle:
|
|
78
|
+
def test_edge_face_edges(self, he_triangle: dict[str, Any]) -> None:
|
|
79
79
|
"""Edge next around face."""
|
|
80
80
|
for edge in he_triangle["edges"]:
|
|
81
81
|
assert tuple(edge.face_edges) == (edge, edge.next, edge.next.next)
|
|
82
82
|
|
|
83
|
-
def test_face_edges(self, he_triangle:
|
|
83
|
+
def test_face_edges(self, he_triangle: dict[str, Any]) -> None:
|
|
84
84
|
"""Finds all edges, starting at face.edge."""
|
|
85
85
|
for face in he_triangle["faces"]:
|
|
86
86
|
assert tuple(face.edges) == tuple(face.edge.face_edges)
|
|
87
87
|
|
|
88
|
-
def test_edge_face_verts(self, he_triangle:
|
|
88
|
+
def test_edge_face_verts(self, he_triangle: dict[str, Any]) -> None:
|
|
89
89
|
"""Is equivalent to edge.pair.next around orig."""
|
|
90
90
|
for edge in he_triangle["edges"]:
|
|
91
91
|
assert tuple(edge.vert_edges) == (edge, edge.pair.next)
|
|
92
92
|
|
|
93
|
-
def test_vert_edges(self, he_triangle:
|
|
93
|
+
def test_vert_edges(self, he_triangle: dict[str, Any]) -> None:
|
|
94
94
|
"""Is equivalent to vert_edges for vert.edge."""
|
|
95
95
|
for vert in he_triangle["verts"]:
|
|
96
96
|
assert tuple(vert.edges) == tuple(vert.edge.vert_edges)
|
|
97
97
|
|
|
98
|
-
def test_vert_verts(self, he_triangle:
|
|
98
|
+
def test_vert_verts(self, he_triangle: dict[str, Any]) -> None:
|
|
99
99
|
"""Is equivalent to vert_edge.dest for vert.edge."""
|
|
100
100
|
for vert in he_triangle["verts"]:
|
|
101
101
|
assert vert.neighbors == [x.dest for x in vert.edge.vert_edges]
|
|
102
102
|
|
|
103
|
-
def test_vert_valence(self, he_triangle:
|
|
103
|
+
def test_vert_valence(self, he_triangle: dict[str, Any]) -> None:
|
|
104
104
|
"""Valence is two for every corner in a triangle."""
|
|
105
105
|
for vert in he_triangle["verts"]:
|
|
106
106
|
assert vert.valence == 2
|
|
107
107
|
|
|
108
|
-
def test_prev_by_face_edges(self, he_triangle:
|
|
108
|
+
def test_prev_by_face_edges(self, he_triangle: dict[str, Any]) -> None:
|
|
109
109
|
"""Previous edge will 'next' to self."""
|
|
110
110
|
for edge in he_triangle["edges"]:
|
|
111
111
|
assert edge.prev.next == edge
|
|
112
112
|
|
|
113
113
|
@staticmethod
|
|
114
|
-
def test_dest_is_next_orig(he_triangle:
|
|
114
|
+
def test_dest_is_next_orig(he_triangle: dict[str, Any]) -> None:
|
|
115
115
|
"""Finds orig of next or pair edge."""
|
|
116
116
|
for edge in he_triangle["edges"]:
|
|
117
117
|
assert edge.dest is edge.next.orig
|
|
118
118
|
|
|
119
119
|
@staticmethod
|
|
120
|
-
def test_face_verts(he_triangle:
|
|
120
|
+
def test_face_verts(he_triangle: dict[str, Any]) -> None:
|
|
121
121
|
"""Returns orig for every edge in face_verts."""
|
|
122
122
|
for face in he_triangle["faces"]:
|
|
123
123
|
assert tuple(face.verts) == tuple(face.edge.face_verts)
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
def test_half_edges_init(he_triangle:
|
|
126
|
+
def test_half_edges_init(he_triangle: dict[str, Any]) -> None:
|
|
127
127
|
"""Verts, edges, faces, and holes match hand-calculated coordinates."""
|
|
128
128
|
verts = set(he_triangle["verts"])
|
|
129
129
|
edges = set(he_triangle["edges"])
|
|
@@ -142,7 +142,7 @@ class TestHalfEdges:
|
|
|
142
142
|
"""Keep the linter happy."""
|
|
143
143
|
|
|
144
144
|
def test_vi(
|
|
145
|
-
self, meshes_vlvi:
|
|
145
|
+
self, meshes_vlvi: dict[str, Any], he_grid: HalfEdges, he_cube: HalfEdges
|
|
146
146
|
) -> None:
|
|
147
147
|
"""Convert unaltered mesh faces back to input vi."""
|
|
148
148
|
for mesh, key in ((he_grid, "grid"), (he_cube, "cube")):
|
|
@@ -153,7 +153,7 @@ class TestHalfEdges:
|
|
|
153
153
|
)
|
|
154
154
|
assert expect == result
|
|
155
155
|
|
|
156
|
-
def test_hi(self, meshes_vlvi:
|
|
156
|
+
def test_hi(self, meshes_vlvi: dict[str, Any], he_grid: HalfEdges) -> None:
|
|
157
157
|
"""Convert unaltered mesh holes back to input holes."""
|
|
158
158
|
input_vl, input_hi = meshes_vlvi["grid_vl"], meshes_vlvi["grid_hi"]
|
|
159
159
|
expect = get_canonical_mesh(input_vl, input_hi)
|
|
@@ -8,8 +8,6 @@ tested elsewhere.
|
|
|
8
8
|
:created: 2024-08-09
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import Tuple
|
|
12
|
-
|
|
13
11
|
import pytest
|
|
14
12
|
|
|
15
13
|
from halfedge.half_edge_elements import Face, Vert
|
|
@@ -24,7 +22,7 @@ class TestVert:
|
|
|
24
22
|
def test_holes(self) -> None:
|
|
25
23
|
"""Test that holes are correctly identified."""
|
|
26
24
|
vl = [Vert() for _ in range(3)]
|
|
27
|
-
fi: list[
|
|
25
|
+
fi: list[tuple[int, ...]] = [(0, 1, 2)]
|
|
28
26
|
mesh = HalfEdges.from_vlfi(vl, fi)
|
|
29
27
|
assert len(mesh.holes) == 1
|
|
30
28
|
|
|
@@ -53,7 +51,7 @@ class TestEdge:
|
|
|
53
51
|
def test_vert_faces(self, edge_index: int) -> None:
|
|
54
52
|
"""Test that vert_faces returns the correct faces."""
|
|
55
53
|
vl = [Vert() for _ in range(3)]
|
|
56
|
-
fi: list[
|
|
54
|
+
fi: list[tuple[int, ...]] = [(0, 1, 2)]
|
|
57
55
|
mesh = HalfEdges.from_vlfi(vl, fi)
|
|
58
56
|
assert len(mesh.faces) == 1
|
|
59
57
|
(face,) = mesh.faces
|
|
@@ -64,7 +62,7 @@ class TestEdge:
|
|
|
64
62
|
def test_vert_holes(self, edge_index: int) -> None:
|
|
65
63
|
"""Test that vert_holes returns the correct holes."""
|
|
66
64
|
vl = [Vert() for _ in range(3)]
|
|
67
|
-
fi: list[
|
|
65
|
+
fi: list[tuple[int, ...]] = [(0, 1, 2)]
|
|
68
66
|
mesh = HalfEdges.from_vlfi(vl, fi)
|
|
69
67
|
assert len(mesh.holes) == 1
|
|
70
68
|
(hole,) = mesh.holes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""test halfedge.operations
|
|
2
2
|
|
|
3
3
|
created: 181121 13:38:46
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ import random
|
|
|
10
10
|
from contextlib import suppress
|
|
11
11
|
from itertools import chain, combinations, permutations
|
|
12
12
|
from operator import attrgetter
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
14
14
|
|
|
15
15
|
import pytest
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ class NamedAttribute(IncompatibleAttrib[str]):
|
|
|
25
25
|
"""For color, flags. etc. to ensure attributes are passed"""
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
class Coordinate(IncompatibleAttrib[
|
|
28
|
+
class Coordinate(IncompatibleAttrib[tuple[float, ...]]):
|
|
29
29
|
"""Hold coordinates when creating a mesh from a list of vertices"""
|
|
30
30
|
|
|
31
31
|
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
created: 170204 14:22:23
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Tuple
|
|
7
|
-
|
|
8
6
|
import pytest
|
|
9
7
|
|
|
10
8
|
from halfedge.half_edge_elements import ManifoldMeshError, Vert
|
|
@@ -14,7 +12,7 @@ from halfedge.type_attrib import IncompatibleAttrib
|
|
|
14
12
|
from halfedge.validations import validate_mesh
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
class Coordinate(IncompatibleAttrib[
|
|
15
|
+
class Coordinate(IncompatibleAttrib[tuple[float, ...]]):
|
|
18
16
|
pass
|
|
19
17
|
|
|
20
18
|
|
|
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
|