Skip to content

Commit f07bdbf

Browse files
committed
Merge branch 'main' into scene
2 parents 3d5e493 + c1ac434 commit f07bdbf

24 files changed

Lines changed: 420 additions & 18 deletions

File tree

AUTHORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@
4141
- Adam Anouar <<aanouar@student.ethz.ch>> [@AdamAnouar](https://github.com/AdamAnouar)
4242
- Joseph Kenny <<kenny@arch.ethz.ch>> [@jckenny59](https://github.com/jckenny59)
4343
- Panayiotis Papacharalambous <<papacharalambous@arch.ethz.ch>> [@papachap](https://github.com/papachap)
44+
- Oliver Bucklin <<obucklin@arch.ethz.ch>> [@obucklin](https://github.com/obucklin)

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added `inheritance` field to `__jsondump__` of `compas.datastructures.Datastructure` to allow for deserialization to closest available superclass of custom datastructures.
13+
14+
### Changed
15+
16+
### Removed
17+
18+
19+
## [2.11.0] 2025-04-22
20+
21+
### Added
22+
1223
* Added `Group` to `compas.scene`.
1324
* Added `compas.geometry.Brep.cap_planar_holes`.
1425
* Added `compas_rhino.geometry.RhinoBrep.cap_planar_holes`.
26+
* Added `compas.geometry.angle_vectors_projected`.
27+
* Added `compas.geometry.Brep.from_curves`.
28+
* Added `compas_rhino.geometry.RhinoBrep.from_curves`.
1529

1630
### Changed
1731

1832
* Changed `SceneObject.frame` to read-only result of `Frame.from_transformation(SceneObject.worldtransformation)`, representing the local coordinate system of the scene object in world coordinates.
1933
* Changed `SceneObject.worldtransformation` to the multiplication of all transformations from the scene object to the root of the scene tree, there will no longer be an additional transformation in relation to the object's frame.
2034
* Fixed call to `astar_shortest_path` in `Graph.shortest_path`.
35+
* Fixed a bug when printing an empty `Tree`.
36+
* Fixed a bug in `Group` for IronPython where the decoding declaration was missing.
37+
* Fixed a bug where a `Group` without name could not be added to the scene.
2138

2239
### Removed
2340

docs/userguide/advanced.serialisation.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,50 @@ which will help with IntelliSense and code completion.
149149
boxes: List[Box] = session['boxes']
150150
151151
152+
Inheritance
153+
===========
154+
155+
When working with custom classes that inherit from COMPAS datastructures, COMPAS will encode the inheritance chain in the serialized data. This allows the object to be reconstructed using the closest available superclass if the custom class is not available in the environment where the data is loaded.
156+
For example, a user can create a custom mesh class and serialize it to JSON:
157+
158+
.. code-block:: python
159+
160+
from compas.datastructures import Mesh
161+
from compas import json_dump
162+
163+
class CustomMesh(Mesh):
164+
def __init__(self, *args, **kwargs):
165+
super(CustomMesh, self).__init__(*args, **kwargs)
166+
self.custom_mesh_attr = "custom_mesh"
167+
168+
@property
169+
def __data__(self):
170+
data = super(CustomMesh, self).__data__
171+
data["custom_mesh_attr"] = self.custom_mesh_attr
172+
return data
173+
174+
@classmethod
175+
def __from_data__(cls, data):
176+
obj = super(CustomMesh, cls).__from_data__(data)
177+
obj.custom_mesh_attr = data.get("custom_mesh_attr", "")
178+
return obj
179+
180+
# Create and serialize a custom mesh
181+
custom_mesh = CustomMesh(name="test")
182+
json_dump(custom_mesh, 'custom_mesh.json')
183+
184+
If another user loads "custom_mesh.json" in an environment where the CustomMesh class is not available, COMPAS will reconstruct the object as an instance of its closest available superclass, which in this case is the regular Mesh class:
185+
186+
.. code-block:: python
187+
188+
from compas.datastructures import Mesh
189+
from compas import json_load
190+
191+
mesh = json_load('custom_mesh.json')
192+
assert isinstance(mesh, Mesh) # This will be True
193+
194+
195+
152196
Validation
153197
==========
154198

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ doctest_optionflags = [
8484
# ============================================================================
8585

8686
[tool.bumpversion]
87-
current_version = "2.10.0"
87+
current_version = "2.11.0"
8888
message = "Bump version to {new_version}"
8989
commit = true
9090
tag = true

src/compas/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
__copyright__ = "Copyright 2014-2022 - ETH Zurich, Copyright 2023 - COMPAS Association"
2121
__license__ = "MIT License"
2222
__email__ = "tom.v.mele@gmail.com"
23-
__version__ = "2.10.0"
23+
__version__ = "2.11.0"
2424

2525

2626
HERE = compas._os.realpath(os.path.dirname(__file__))

src/compas/data/data.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def __init__(self, name=None):
7575
def __dtype__(self):
7676
return "{}/{}".format(".".join(self.__class__.__module__.split(".")[:2]), self.__class__.__name__)
7777

78+
@classmethod
79+
def __clstype__(cls):
80+
return "{}/{}".format(".".join(cls.__module__.split(".")[:2]), cls.__name__)
81+
7882
@property
7983
def __data__(self):
8084
raise NotImplementedError

src/compas/data/encoders.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,16 @@
4040
numpy_support = False
4141

4242

43-
def cls_from_dtype(dtype): # type: (...) -> Type[Data]
43+
def cls_from_dtype(dtype, inheritance=None): # type: (...) -> Type[Data]
4444
"""Get the class object corresponding to a COMPAS data type specification.
4545
4646
Parameters
4747
----------
4848
dtype : str
4949
The data type of the COMPAS object in the following format:
5050
'{}/{}'.format(o.__class__.__module__, o.__class__.__name__).
51+
inheritance : list[str], optional
52+
The inheritance chain of this class, a list of superclasses that can be used if given dtype is not found.
5153
5254
Returns
5355
-------
@@ -63,9 +65,23 @@ def cls_from_dtype(dtype): # type: (...) -> Type[Data]
6365
If the module doesn't contain the specified data type.
6466
6567
"""
66-
mod_name, attr_name = dtype.split("/")
67-
module = __import__(mod_name, fromlist=[attr_name])
68-
return getattr(module, attr_name)
68+
69+
if inheritance is None:
70+
full_inheritance = [dtype]
71+
else:
72+
full_inheritance = [dtype] + inheritance
73+
74+
for dtype in full_inheritance:
75+
mod_name, attr_name = dtype.split("/")
76+
try:
77+
module = __import__(mod_name, fromlist=[attr_name])
78+
return getattr(module, attr_name)
79+
except ImportError:
80+
continue
81+
except AttributeError:
82+
continue
83+
84+
raise ValueError("No class found in inheritance chain: {}".format(full_inheritance))
6985

7086

7187
class DataEncoder(json.JSONEncoder):
@@ -220,7 +236,7 @@ def object_hook(self, o):
220236
return o
221237

222238
try:
223-
cls = cls_from_dtype(o["dtype"])
239+
cls = cls_from_dtype(o["dtype"], o.get("inheritance", None))
224240

225241
except ValueError:
226242
raise DecoderError(

src/compas/datastructures/datastructure.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,51 @@ def __init__(self, attributes=None, name=None):
2121
self._aabb = None
2222
self._obb = None
2323

24+
@property
25+
def __inheritance__(self):
26+
"""Get the inheritance chain of the datastructure.
27+
Until one level above the Datastructure class (eg. Mesh, Graph, ...).
28+
29+
Returns
30+
-------
31+
list[str]
32+
The inheritance chain of the datastructure.
33+
34+
"""
35+
inheritance = []
36+
for cls in self.__class__.__mro__:
37+
if cls == self.__class__:
38+
continue
39+
if cls == Datastructure:
40+
break
41+
inheritance.append(cls.__clstype__())
42+
return inheritance
43+
44+
def __jsondump__(self, minimal=False):
45+
"""Return the required information for serialization with the COMPAS JSON serializer.
46+
47+
Parameters
48+
----------
49+
minimal : bool, optional
50+
If True, exclude the GUID from the dump dict.
51+
52+
Returns
53+
-------
54+
dict
55+
56+
"""
57+
state = {
58+
"dtype": self.__dtype__,
59+
"data": self.__data__,
60+
"inheritance": self.__inheritance__,
61+
}
62+
if minimal:
63+
return state
64+
if self._name is not None:
65+
state["name"] = self._name
66+
state["guid"] = str(self.guid)
67+
return state
68+
2469
@property
2570
def aabb(self):
2671
if self._aabb is None:

src/compas/datastructures/tree/tree.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,8 @@ def traverse(node, hierarchy, prefix="", last=True, depth=0, node_repr=None):
474474
for i, child in enumerate(node.children):
475475
traverse(child, hierarchy, prefix, i == len(node.children) - 1, depth + 1, node_repr)
476476

477-
traverse(self.root, hierarchy, node_repr=node_repr)
477+
if self.root:
478+
traverse(self.root, hierarchy, node_repr=node_repr)
478479

479480
return "\n".join(hierarchy)
480481

src/compas/geometry/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
angle_points_xy,
100100
angle_vectors,
101101
angle_vectors_signed,
102+
angle_vectors_projected,
102103
angle_vectors_xy,
103104
angles_points,
104105
angles_points_xy,
@@ -488,6 +489,7 @@
488489
"angle_points_xy",
489490
"angle_vectors",
490491
"angle_vectors_signed",
492+
"angle_vectors_projected",
491493
"angle_vectors_xy",
492494
"angles_points",
493495
"angles_points_xy",

0 commit comments

Comments
 (0)