Skip to content

Commit

Permalink
Merge pull request #887 from compas-dev/assembly
Browse files Browse the repository at this point in the history
Basic Assembly data structure
  • Loading branch information
tomvanmele authored Dec 10, 2021
2 parents cbe6bfa + 6168f81 commit b58de87
Show file tree
Hide file tree
Showing 11 changed files with 516 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

Expand Down
37 changes: 37 additions & 0 deletions docs/tutorial/assembly.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
**********
Assemblies
**********

.. rst-class:: lead

The :class:`Assembly` data structure provides functionality for modelling and managing
the connections between the individual components of an ... assembly.
Each component is either a part defined by geometrical objects, or another assembly.


Basic Usage
===========

.. code-block:: python
from compas.datastructures import Part
from compas.datastructures import Assembly
from compas.geometry import Box
assembly = Assembly()
Find Parts
==========

.. code-block:: python
pass
Access Data
===========

.. code-block:: python
pass
36 changes: 36 additions & 0 deletions docs/tutorial/assembly_blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from math import radians

from compas.datastructures import Assembly
from compas.datastructures import Part
from compas.geometry import Box
from compas.geometry import Cylinder
from compas.geometry import Circle
from compas.geometry import Plane
from compas.geometry import Translation
from compas.geometry import Rotation
from compas.geometry import Frame

from compas_view2.app import App

assembly = Assembly()

a = Part(name='A',
geometry=Box.from_width_height_depth(1, 1, 1))

b = Part(name='B',
frame=Frame([0, 0, 1], [1, 0, 0], [0, 1, 0]),
shape=Box.from_width_height_depth(1, 1, 1),
features=[(Cylinder(Circle(Plane.worldXY(), 0.2), 1.0), 'difference')])

b.transform(Rotation.from_axis_and_angle([0, 0, 1], radians(45)))
b.transform(Translation.from_vector([0, 0, 1]))

assembly.add_part(a)
assembly.add_part(b)

assembly.add_connection(a, b)

viewer = App()
viewer.add(b.geometry)
viewer.add(b.frame)
viewer.show()
Empty file added docs/tutorial/assembly_robot.py
Empty file.
Empty file.
18 changes: 18 additions & 0 deletions src/compas/datastructures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
Mesh
Network
VolMesh
Assembly
Part
Functions
Expand Down Expand Up @@ -171,8 +173,16 @@
volmesh_transformed
Assembly
--------
.. autosummary::
:toctree: generated/
:nosignatures:
"""
from __future__ import absolute_import

import compas

from .datastructure import Datastructure
Expand Down Expand Up @@ -284,6 +294,11 @@
volmesh_transformed
)

from .assembly import (
Assembly,
Part
)

if not compas.IPY:
from .network import (
network_adjacency_matrix,
Expand Down Expand Up @@ -421,6 +436,9 @@
'volmesh_bounding_box',
'volmesh_transform',
'volmesh_transformed',
# Assemblies
'Assembly',
'Part',
]

if not compas.IPY:
Expand Down
6 changes: 6 additions & 0 deletions src/compas/datastructures/assembly/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from .assembly import Assembly # noqa: F401 F403
from .part import Part # noqa: F401 F403
166 changes: 166 additions & 0 deletions src/compas/datastructures/assembly/assembly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from ..datastructure import Datastructure
from ..graph import Graph
from .exceptions import AssemblyError


class Assembly(Datastructure):
"""A data structure for managing the connections between different parts of an assembly.
Attributes
----------
attributes: dict
General attributes of the assembly that will be included in the data dict.
graph: :class:`compas.datastructures.Graph`
The graph that is used under the hood to store the parts and their connections.
"""

@property
def DATASCHEMA(self):
import schema
return schema.Schema({
"attributes": dict,
"graph": Graph,
})

@property
def JSONSCHEMANAME(self):
return 'assembly'

def __init__(self, name=None, **kwargs):
super(Assembly, self).__init__()
self.attributes = {'name': name or 'Assembly'}
self.attributes.update(kwargs)
self.graph = Graph()
self._parts = {}

def __str__(self):
tpl = "<Assembly with {} parts and {} connections>"
return tpl.format(self.graph.number_of_nodes(), self.graph.number_of_edges())

@property
def name(self):
"""str : The name of the assembly."""
return self.attributes.get('name') or self.__class__.__name__

@name.setter
def name(self, value):
self.attributes['name'] = value

@property
def data(self):
"""dict : A data dict representing the assembly data structure for serialization.
"""
data = {
'attributes': self.attributes,
'graph': self.graph.data,
}
return data

@data.setter
def data(self, data):
self.attributes.update(data['attributes'] or {})
self.graph.data = data['graph']

def add_part(self, part, key=None, **kwargs):
"""Add a part to the assembly.
Parameters
----------
part: :class:`compas.datastructures.Part`
The part to add.
key: int or str, optional
The identifier of the part in the assembly.
Note that the key is unique only in the context of the current assembly.
Nested assemblies may have the same ``key`` value for one of their parts.
Default is ``None`` in which case the key will be automatically assigned integer value.
kwargs: dict
Additional named parameters collected in a dict.
Returns
-------
int or str
The identifier of the part in the current assembly graph.
"""
if part.guid in self._parts:
raise AssemblyError('Part already added to the assembly')
key = self.graph.add_node(key=key, part=part, **kwargs)
part.key = key
self._parts[part.guid] = part
return key

def add_connection(self, a, b, **kwargs):
"""Add a connection between two parts.
Parameters
----------
a: :class:`compas.datastructures.Part`
The "from" part.
b: :class:`compas.datastructures.Part`
The "to" part.
kwargs: dict
Additional named parameters collected in a dict.
Returns
-------
tuple of str or int
The tuple of node identifiers that identifies the connection.
Raises
------
:class:`AssemblyError`
If ``a`` and/or ``b`` are not in the assembly.
"""
if a.key is None or b.key is None:
raise AssemblyError('Both parts have to be added to the assembly before a connection can be created.')
if not self.graph.has_node(a.key) or not self.graph.has_node(b.key):
raise AssemblyError('Both parts have to be added to the assembly before a connection can be created.')
return self.graph.add_edge(a.key, b.key, **kwargs)

def parts(self):
"""The parts of the assembly.
Yields
------
:class:`compas.datastructures.Part`
The individual parts of the assembly.
"""
for node in self.graph.nodes():
yield self.graph.node_attribute(node, 'part')

def connections(self, data=False):
"""Iterate over the connections between the parts.
Parameters
----------
data : bool, optional
If ``True``, yield both the identifier and the attributes of each connection.
Yields
------
tuple
The next connection identifier (u, v), if ``data`` is ``False``.
Otherwise, the next connector identifier and its attributes as a ((u, v), attr) tuple.
"""
return self.graph.edges(data)

def find(self, guid):
"""Find a part in the assembly by its GUID.
Parameters
----------
guid: str
A globally unique identifier.
This identifier is automatically assigned when parts are created.
Returns
-------
:class:`compas.datastructures.Part` or None
The identified part, if any.
"""
return self._parts.get(guid)
10 changes: 10 additions & 0 deletions src/compas/datastructures/assembly/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class AssemblyError(Exception):
pass


class FrameError(Exception):
pass


class FeatureError(Exception):
pass
Loading

0 comments on commit b58de87

Please sign in to comment.