Skip to content

Commit 10e4225

Browse files
committed
finish the work, update tests
1 parent f07bdbf commit 10e4225

4 files changed

Lines changed: 125 additions & 51 deletions

File tree

src/compas/scene/group.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,3 @@ class Group(SceneObject):
2727
└── <GeometryObject: Point>
2828
2929
"""
30-
31-
def __new__(cls, *args, **kwargs):
32-
# overwriting __new__ to revert to the default behavior of normal object, So an instance can be created directly without providing a registered item.
33-
return object.__new__(cls)
34-
35-
@property
36-
def __data__(self):
37-
# type: () -> dict
38-
data = {
39-
"settings": self.settings,
40-
"children": [child.__data__ for child in self.children],
41-
}
42-
return data

src/compas/scene/scene.py

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from .context import before_draw
1010
from .context import clear
1111
from .context import detect_current_context
12-
from .group import Group
1312
from .sceneobject import SceneObject
1413
from .sceneobject import SceneObjectFactory
1514

@@ -47,32 +46,33 @@ def __data__(self):
4746
# type: () -> dict
4847
return {
4948
"name": self.name,
50-
"items": list(self.items.values()),
51-
"objects": list(self.objects.values()),
49+
"attributes": self.attributes,
50+
"datastore": self.datastore,
51+
"objects": self.objects,
5252
"tree": self.tree,
5353
}
5454

55-
def __init__(self, name="Scene", context=None):
56-
# type: (str, str | None) -> None
57-
super(Scene, self).__init__(name=name)
55+
def __init__(self, context=None, datastore=None, objects=None, tree=None, **kwargs):
56+
# type: (str | None, dict | None, dict | None, Tree | None, **kwargs) -> None
57+
super(Scene, self).__init__(**kwargs)
5858

59-
# TODO: deal with context
6059
self.context = context or detect_current_context()
61-
62-
self.tree = Tree()
63-
self.tree.add(TreeNode(name="ROOT"))
64-
self.items = {}
65-
self.objects = {}
60+
self.datastore = datastore or {}
61+
self.objects = objects or {}
62+
self.tree = tree or Tree()
63+
if self.tree.root is None:
64+
self.tree.add(TreeNode(name=self.name))
6665

6766
def __repr__(self):
6867
# type: () -> str
6968

7069
def node_repr(node):
71-
if node.name == "ROOT":
72-
return self.name
73-
74-
sceneobject = self.objects[node.name]
75-
return str(sceneobject)
70+
# type: (TreeNode) -> str
71+
if node.is_root:
72+
return node.name
73+
else:
74+
sceneobject = self.objects[node.name]
75+
return str(sceneobject)
7676

7777
return self.tree.get_hierarchy_string(node_repr=node_repr)
7878

@@ -108,19 +108,39 @@ def add(self, item, parent=None, **kwargs):
108108

109109
# Add the scene object and item to the data store
110110
self.objects[str(sceneobject.guid)] = sceneobject
111-
self.items[str(item.guid)] = item
111+
self.datastore[str(item.guid)] = item
112112

113113
# Add the scene object to the hierarchical tree
114114
if parent is None:
115115
parent_node = self.tree.root
116116
else:
117-
print(parent.guid)
117+
if not isinstance(parent, SceneObject):
118+
raise ValueError("Parent is not a SceneObject.", parent)
118119
parent_node = self.tree.get_node_by_name(parent.guid)
120+
if parent_node is None:
121+
raise ValueError("Parent is not part of the scene.", parent)
119122

120123
self.tree.add(TreeNode(name=str(sceneobject.guid)), parent=parent_node)
121124

122125
return sceneobject
123126

127+
def remove(self, sceneobject):
128+
"""Remove a scene object along with all its descendants from the scene.
129+
130+
Parameters
131+
----------
132+
sceneobject : :class:`compas.scene.SceneObject`
133+
The scene object to remove.
134+
"""
135+
# type: (SceneObject) -> None
136+
guid = str(sceneobject.guid)
137+
self.objects.pop(guid, None)
138+
node = self.tree.get_node_by_name(guid)
139+
if node:
140+
for descendant in node.descendants:
141+
self.objects.pop(descendant.name, None)
142+
self.tree.remove(node)
143+
124144
def clear_context(self, guids=None):
125145
# type: (list | None) -> None
126146
"""Clear the visualisation context.
@@ -173,7 +193,7 @@ def clear(self, clear_scene=True, clear_context=True):
173193
"""
174194
guids = []
175195

176-
for sceneobject in self.objects:
196+
for sceneobject in list(self.objects.values()):
177197
guids += sceneobject.guids
178198
sceneobject._guids = None
179199

@@ -233,7 +253,7 @@ def find_by_name(self, name):
233253
return self.get_node_by_name(name=name)
234254

235255
def find_by_itemtype(self, itemtype):
236-
# type: (...) -> SceneObject | None
256+
# type: (type) -> SceneObject | None
237257
"""Find the first scene object with a data item of the given type.
238258
239259
Parameters
@@ -246,12 +266,12 @@ def find_by_itemtype(self, itemtype):
246266
:class:`SceneObject` or None
247267
248268
"""
249-
for obj in self.objects:
269+
for obj in self.objects.values():
250270
if isinstance(obj.item, itemtype):
251271
return obj
252272

253273
def find_all_by_itemtype(self, itemtype):
254-
# type: (...) -> list[SceneObject]
274+
# type: (type) -> list[SceneObject]
255275
"""Find all scene objects with a data item of the given type.
256276
257277
Parameters
@@ -265,7 +285,7 @@ def find_all_by_itemtype(self, itemtype):
265285
266286
"""
267287
sceneobjects = []
268-
for obj in self.objects:
288+
for obj in self.objects.values():
269289
if isinstance(obj.item, itemtype):
270290
sceneobjects.append(obj)
271291
return sceneobjects

src/compas/scene/sceneobject.py

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import compas.scene # noqa: F401
1313
from compas.colors import Color
1414
from compas.data import Data
15-
from compas.datastructures import TreeNode
1615
from compas.geometry import Frame
1716
from compas.geometry import Transformation
1817

@@ -55,8 +54,9 @@ def create(item=None, scene=None, **kwargs):
5554
if item is None:
5655
raise ValueError("Cannot create a scene object for None. Please ensure you pass an instance of a supported class.")
5756

58-
if scene is None:
59-
raise ValueError("Cannot create a scene object without a scene.")
57+
if isinstance(item, SceneObject):
58+
item._scene = scene
59+
return item
6060

6161
sceneobject_cls = get_sceneobject_cls(item, **kwargs)
6262

@@ -139,17 +139,24 @@ def __init__(
139139
**kwargs # type: dict
140140
): # fmt: skip
141141
# type: (...) -> None
142-
if item and not isinstance(item, Data):
143-
raise ValueError("The item assigned to this scene object should be a data object: {}".format(type(item)))
144142

145143
name = name or getattr(item, "name", None)
146144
super(SceneObject, self).__init__(name=name, **kwargs)
147145
# the scene object needs to store the context
148146
# because it has no access to the tree and/or the scene before it is added
149147
# which means that adding child objects will be added in context "None"
148+
149+
if isinstance(item, Data):
150+
self._item = str(item.guid)
151+
elif isinstance(item, str):
152+
self._item = item
153+
elif item is None:
154+
self._item = None
155+
else:
156+
raise ValueError("The item assigned to this scene object should be a data object or a str guid: {}".format(item))
157+
150158
self.context = context
151159
self._scene = scene
152-
self._item = item.guid
153160
self._guids = []
154161
self._node = None
155162
self._transformation = transformation
@@ -162,7 +169,7 @@ def __init__(
162169
def __data__(self):
163170
# type: () -> dict
164171
return {
165-
"item": str(self._item),
172+
"item": self._item,
166173
"name": self.name,
167174
"color": self.color,
168175
"opacity": self.opacity,
@@ -187,7 +194,44 @@ def item(self):
187194
@property
188195
def node(self):
189196
# type: () -> compas.datastructures.TreeNode
190-
return self.scene.tree.get_object_node(self.guid)
197+
return self.scene.tree.get_node_by_name(self.guid)
198+
199+
@property
200+
def is_root(self):
201+
# type: () -> bool
202+
return self.node.is_root
203+
204+
@property
205+
def is_leaf(self):
206+
# type: () -> bool
207+
return self.node.is_leaf
208+
209+
@property
210+
def is_branch(self):
211+
# type: () -> bool
212+
return self.node.is_branch
213+
214+
@property
215+
def parentnode(self):
216+
# type: () -> compas.datastructures.Node | None
217+
return self.node.parent
218+
219+
@property
220+
def childnodes(self):
221+
# type: () -> list[compas.datastructures.Node]
222+
return self.node.children
223+
224+
@property
225+
def parent(self):
226+
# type: () -> compas.scene.SceneObject | None
227+
if self.parentnode and not self.parentnode.is_root:
228+
return self.scene.objects[self.parentnode.name]
229+
return None
230+
231+
@property
232+
def children(self):
233+
# type: () -> list[compas.scene.SceneObject]
234+
return [self.scene.objects[child.name] for child in self.childnodes]
191235

192236
@property
193237
def guids(self):
@@ -236,6 +280,28 @@ def contrastcolor(self):
236280
self._contrastcolor = self.color.lightened(50)
237281
return self._contrastcolor
238282

283+
def add(self, item, **kwargs):
284+
"""Add a scene object to the scene.
285+
286+
Parameters
287+
----------
288+
item : :class:`compas.data.Data`
289+
The item to add to the scene.
290+
**kwargs : dict
291+
Additional keyword arguments to pass to the SceneObject constructor.
292+
293+
Returns
294+
-------
295+
:class:`compas.scene.SceneObject`
296+
The added scene object.
297+
"""
298+
# type: (compas.data.Data, dict) -> compas.scene.SceneObject
299+
return self.scene.add(item, parent=self, **kwargs)
300+
301+
def remove(self):
302+
"""Remove this scene object along with all its descendants from the scene."""
303+
self.scene.remove(self)
304+
239305
@contrastcolor.setter
240306
def contrastcolor(self, color):
241307
# type: (compas.colors.Color) -> None

tests/compas/scene/test_scene.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from compas.scene import register
77
from compas.scene import Scene
88
from compas.scene import SceneObject
9+
from compas.scene import SceneObjectFactory
910
from compas.scene import SceneObjectNotRegisteredError
1011
from compas.data import Data
1112
from compas.geometry import Box
@@ -46,29 +47,29 @@ def test_get_sceneobject_cls_with_orderly_registration():
4647
register(FakeItem, FakeSceneObject, context="fake")
4748
register(FakeSubItem, FakeSubSceneObject, context="fake")
4849
item = FakeItem()
49-
sceneobject = SceneObject(item, context="fake")
50+
sceneobject = SceneObjectFactory.create(item, context="fake")
5051
assert isinstance(sceneobject, FakeSceneObject)
5152

5253
item = FakeSubItem()
53-
sceneobject = SceneObject(item, context="fake")
54+
sceneobject = SceneObjectFactory.create(item, context="fake")
5455
assert isinstance(sceneobject, FakeSubSceneObject)
5556

5657
def test_get_sceneobject_cls_with_out_of_order_registration():
5758
register(FakeSubItem, FakeSubSceneObject, context="fake")
5859
register(FakeItem, FakeSceneObject, context="fake")
5960
item = FakeItem()
60-
sceneobject = SceneObject(item, context="fake")
61+
sceneobject = SceneObjectFactory.create(item, context="fake")
6162
assert isinstance(sceneobject, FakeSceneObject)
6263

6364
item = FakeSubItem()
64-
sceneobject = SceneObject(item, context="fake")
65+
sceneobject = SceneObjectFactory.create(item, context="fake")
6566
assert isinstance(sceneobject, FakeSubSceneObject)
6667

6768
def test_sceneobject_auto_context_discovery(mocker):
6869
register_fake_context()
6970

7071
item = FakeItem()
71-
sceneobject = SceneObject(item)
72+
sceneobject = SceneObjectFactory.create(item)
7273

7374
assert isinstance(sceneobject, FakeSceneObject)
7475

@@ -78,7 +79,7 @@ def test_sceneobject_auto_context_discovery_no_context(mocker):
7879

7980
with pytest.raises(SceneObjectNotRegisteredError):
8081
item = FakeSubItem()
81-
_ = SceneObject(item)
82+
_ = SceneObjectFactory.create(item)
8283

8384
def test_sceneobject_transform():
8485
scene = Scene()

0 commit comments

Comments
 (0)