Skip to content

Commit b58de87

Browse files
authored
Merge pull request #887 from compas-dev/assembly
Basic Assembly data structure
2 parents cbe6bfa + 6168f81 commit b58de87

File tree

11 files changed

+516
-0
lines changed

11 files changed

+516
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
* Added `compas_rhino.DEFAULT_VERSION`.
1313
* Added `clean` option to `compas_rhino.install` to remove existing symlinks if they cannot be imported from the current environment.
14+
* Added basic implementation of `compas.datastructures.Assembly`.
1415
* Added `compas.is_grasshopper`.
1516
* Added `compas.GH`.
1617
* Added `compas.artists.Artist.CONTEXT`.
@@ -249,6 +250,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
249250
* Added `compas_rhino.geometry.RhinoCylinder`.
250251
* Added `compas_rhino.geometry.RhinoPolyline`.
251252
* Added `compas_rhino.geometry.RhinoSphere`.
253+
* Added basic implementation of `compas.datastructures.Assembly`.
252254
* Added `meshes` method to artists of `compas.robots.RobotModel`.
253255
* Added `FrameArtist` class to `compas_blender`.
254256

docs/tutorial/assembly.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
**********
2+
Assemblies
3+
**********
4+
5+
.. rst-class:: lead
6+
7+
The :class:`Assembly` data structure provides functionality for modelling and managing
8+
the connections between the individual components of an ... assembly.
9+
Each component is either a part defined by geometrical objects, or another assembly.
10+
11+
12+
Basic Usage
13+
===========
14+
15+
.. code-block:: python
16+
17+
from compas.datastructures import Part
18+
from compas.datastructures import Assembly
19+
from compas.geometry import Box
20+
21+
assembly = Assembly()
22+
23+
24+
Find Parts
25+
==========
26+
27+
.. code-block:: python
28+
29+
pass
30+
31+
32+
Access Data
33+
===========
34+
35+
.. code-block:: python
36+
37+
pass

docs/tutorial/assembly_blocks.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from math import radians
2+
3+
from compas.datastructures import Assembly
4+
from compas.datastructures import Part
5+
from compas.geometry import Box
6+
from compas.geometry import Cylinder
7+
from compas.geometry import Circle
8+
from compas.geometry import Plane
9+
from compas.geometry import Translation
10+
from compas.geometry import Rotation
11+
from compas.geometry import Frame
12+
13+
from compas_view2.app import App
14+
15+
assembly = Assembly()
16+
17+
a = Part(name='A',
18+
geometry=Box.from_width_height_depth(1, 1, 1))
19+
20+
b = Part(name='B',
21+
frame=Frame([0, 0, 1], [1, 0, 0], [0, 1, 0]),
22+
shape=Box.from_width_height_depth(1, 1, 1),
23+
features=[(Cylinder(Circle(Plane.worldXY(), 0.2), 1.0), 'difference')])
24+
25+
b.transform(Rotation.from_axis_and_angle([0, 0, 1], radians(45)))
26+
b.transform(Translation.from_vector([0, 0, 1]))
27+
28+
assembly.add_part(a)
29+
assembly.add_part(b)
30+
31+
assembly.add_connection(a, b)
32+
33+
viewer = App()
34+
viewer.add(b.geometry)
35+
viewer.add(b.frame)
36+
viewer.show()

docs/tutorial/assembly_robot.py

Whitespace-only changes.

docs/tutorial/assembly_splines.py

Whitespace-only changes.

src/compas/datastructures/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
Mesh
2020
Network
2121
VolMesh
22+
Assembly
23+
Part
2224
2325
2426
Functions
@@ -171,8 +173,16 @@
171173
volmesh_transformed
172174
173175
176+
Assembly
177+
--------
178+
179+
.. autosummary::
180+
:toctree: generated/
181+
:nosignatures:
182+
174183
"""
175184
from __future__ import absolute_import
185+
176186
import compas
177187

178188
from .datastructure import Datastructure
@@ -284,6 +294,11 @@
284294
volmesh_transformed
285295
)
286296

297+
from .assembly import (
298+
Assembly,
299+
Part
300+
)
301+
287302
if not compas.IPY:
288303
from .network import (
289304
network_adjacency_matrix,
@@ -421,6 +436,9 @@
421436
'volmesh_bounding_box',
422437
'volmesh_transform',
423438
'volmesh_transformed',
439+
# Assemblies
440+
'Assembly',
441+
'Part',
424442
]
425443

426444
if not compas.IPY:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from __future__ import print_function
2+
from __future__ import absolute_import
3+
from __future__ import division
4+
5+
from .assembly import Assembly # noqa: F401 F403
6+
from .part import Part # noqa: F401 F403
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
from __future__ import print_function
2+
from __future__ import absolute_import
3+
from __future__ import division
4+
5+
from ..datastructure import Datastructure
6+
from ..graph import Graph
7+
from .exceptions import AssemblyError
8+
9+
10+
class Assembly(Datastructure):
11+
"""A data structure for managing the connections between different parts of an assembly.
12+
13+
Attributes
14+
----------
15+
attributes: dict
16+
General attributes of the assembly that will be included in the data dict.
17+
graph: :class:`compas.datastructures.Graph`
18+
The graph that is used under the hood to store the parts and their connections.
19+
"""
20+
21+
@property
22+
def DATASCHEMA(self):
23+
import schema
24+
return schema.Schema({
25+
"attributes": dict,
26+
"graph": Graph,
27+
})
28+
29+
@property
30+
def JSONSCHEMANAME(self):
31+
return 'assembly'
32+
33+
def __init__(self, name=None, **kwargs):
34+
super(Assembly, self).__init__()
35+
self.attributes = {'name': name or 'Assembly'}
36+
self.attributes.update(kwargs)
37+
self.graph = Graph()
38+
self._parts = {}
39+
40+
def __str__(self):
41+
tpl = "<Assembly with {} parts and {} connections>"
42+
return tpl.format(self.graph.number_of_nodes(), self.graph.number_of_edges())
43+
44+
@property
45+
def name(self):
46+
"""str : The name of the assembly."""
47+
return self.attributes.get('name') or self.__class__.__name__
48+
49+
@name.setter
50+
def name(self, value):
51+
self.attributes['name'] = value
52+
53+
@property
54+
def data(self):
55+
"""dict : A data dict representing the assembly data structure for serialization.
56+
"""
57+
data = {
58+
'attributes': self.attributes,
59+
'graph': self.graph.data,
60+
}
61+
return data
62+
63+
@data.setter
64+
def data(self, data):
65+
self.attributes.update(data['attributes'] or {})
66+
self.graph.data = data['graph']
67+
68+
def add_part(self, part, key=None, **kwargs):
69+
"""Add a part to the assembly.
70+
71+
Parameters
72+
----------
73+
part: :class:`compas.datastructures.Part`
74+
The part to add.
75+
key: int or str, optional
76+
The identifier of the part in the assembly.
77+
Note that the key is unique only in the context of the current assembly.
78+
Nested assemblies may have the same ``key`` value for one of their parts.
79+
Default is ``None`` in which case the key will be automatically assigned integer value.
80+
kwargs: dict
81+
Additional named parameters collected in a dict.
82+
83+
Returns
84+
-------
85+
int or str
86+
The identifier of the part in the current assembly graph.
87+
88+
"""
89+
if part.guid in self._parts:
90+
raise AssemblyError('Part already added to the assembly')
91+
key = self.graph.add_node(key=key, part=part, **kwargs)
92+
part.key = key
93+
self._parts[part.guid] = part
94+
return key
95+
96+
def add_connection(self, a, b, **kwargs):
97+
"""Add a connection between two parts.
98+
99+
Parameters
100+
----------
101+
a: :class:`compas.datastructures.Part`
102+
The "from" part.
103+
b: :class:`compas.datastructures.Part`
104+
The "to" part.
105+
kwargs: dict
106+
Additional named parameters collected in a dict.
107+
108+
Returns
109+
-------
110+
tuple of str or int
111+
The tuple of node identifiers that identifies the connection.
112+
113+
Raises
114+
------
115+
:class:`AssemblyError`
116+
If ``a`` and/or ``b`` are not in the assembly.
117+
"""
118+
if a.key is None or b.key is None:
119+
raise AssemblyError('Both parts have to be added to the assembly before a connection can be created.')
120+
if not self.graph.has_node(a.key) or not self.graph.has_node(b.key):
121+
raise AssemblyError('Both parts have to be added to the assembly before a connection can be created.')
122+
return self.graph.add_edge(a.key, b.key, **kwargs)
123+
124+
def parts(self):
125+
"""The parts of the assembly.
126+
127+
Yields
128+
------
129+
:class:`compas.datastructures.Part`
130+
The individual parts of the assembly.
131+
"""
132+
for node in self.graph.nodes():
133+
yield self.graph.node_attribute(node, 'part')
134+
135+
def connections(self, data=False):
136+
"""Iterate over the connections between the parts.
137+
138+
Parameters
139+
----------
140+
data : bool, optional
141+
If ``True``, yield both the identifier and the attributes of each connection.
142+
143+
Yields
144+
------
145+
tuple
146+
The next connection identifier (u, v), if ``data`` is ``False``.
147+
Otherwise, the next connector identifier and its attributes as a ((u, v), attr) tuple.
148+
"""
149+
return self.graph.edges(data)
150+
151+
def find(self, guid):
152+
"""Find a part in the assembly by its GUID.
153+
154+
Parameters
155+
----------
156+
guid: str
157+
A globally unique identifier.
158+
This identifier is automatically assigned when parts are created.
159+
160+
Returns
161+
-------
162+
:class:`compas.datastructures.Part` or None
163+
The identified part, if any.
164+
165+
"""
166+
return self._parts.get(guid)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class AssemblyError(Exception):
2+
pass
3+
4+
5+
class FrameError(Exception):
6+
pass
7+
8+
9+
class FeatureError(Exception):
10+
pass

0 commit comments

Comments
 (0)