Skip to content

Scene Data Store #1461

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/compas/datastructures/tree/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ def get_node_by_name(self, name):

"""
for node in self.nodes:
if node.name == name:
if str(node.name) == str(name):
return node

def get_nodes_by_name(self, name):
Expand All @@ -436,7 +436,7 @@ def get_nodes_by_name(self, name):
nodes.append(node)
return nodes

def get_hierarchy_string(self, max_depth=None):
def get_hierarchy_string(self, max_depth=None, node_repr=None):
"""
Return string representation for the spatial hierarchy of the tree.

Expand All @@ -445,6 +445,10 @@ def get_hierarchy_string(self, max_depth=None):
max_depth : int, optional
The maximum depth of the hierarchy to print.
Default is ``None``, in which case the entire hierarchy is printed.
node_repr : callable, optional
A callable to represent the node string.
Default is ``None``, in which case the node.__repr__() is used.


Returns
-------
Expand All @@ -455,18 +459,23 @@ def get_hierarchy_string(self, max_depth=None):

hierarchy = []

def traverse(node, hierarchy, prefix="", last=True, depth=0):
def traverse(node, hierarchy, prefix="", last=True, depth=0, node_repr=None):
if max_depth is not None and depth > max_depth:
return

if node_repr is None:
node_string = node.__repr__()
else:
node_string = node_repr(node)

connector = "└── " if last else "├── "
hierarchy.append("{}{}{}".format(prefix, connector, node))
hierarchy.append("{}{}{}".format(prefix, connector, node_string))
prefix += " " if last else "│ "
for i, child in enumerate(node.children):
traverse(child, hierarchy, prefix, i == len(node.children) - 1, depth + 1)
traverse(child, hierarchy, prefix, i == len(node.children) - 1, depth + 1, node_repr)

if self.root:
traverse(self.root, hierarchy)
traverse(self.root, hierarchy, node_repr=node_repr)

return "\n".join(hierarchy)

Expand Down
2 changes: 2 additions & 0 deletions src/compas/scene/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from .exceptions import SceneObjectNotRegisteredError
from .sceneobject import SceneObject
from .sceneobject import SceneObjectFactory
from .meshobject import MeshObject
from .graphobject import GraphObject
from .geometryobject import GeometryObject
Expand Down Expand Up @@ -43,6 +44,7 @@ def register_scene_objects_base():
__all__ = [
"SceneObjectNotRegisteredError",
"SceneObject",
"SceneObjectFactory",
"MeshObject",
"GraphObject",
"GeometryObject",
Expand Down
13 changes: 0 additions & 13 deletions src/compas/scene/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,3 @@ class Group(SceneObject):
└── <GeometryObject: Point>

"""

def __new__(cls, *args, **kwargs):
# overwriting __new__ to revert to the default behavior of normal object, So an instance can be created directly without providing a registered item.
return object.__new__(cls)

@property
def __data__(self):
# type: () -> dict
data = {
"settings": self.settings,
"children": [child.__data__ for child in self.children],
}
return data
Comment on lines -30 to -42
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These customizations are no longer needed anymore, thanks to the more straight forward serialization/deserialization

119 changes: 77 additions & 42 deletions src/compas/scene/scene.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import compas.data # noqa: F401
import compas.datastructures # noqa: F401
import compas.geometry # noqa: F401
from compas.datastructures import Datastructure
from compas.datastructures import Tree
from compas.datastructures import TreeNode

from .context import after_draw
from .context import before_draw
from .context import clear
from .context import detect_current_context
from .group import Group
from .sceneobject import SceneObject
from .sceneobject import SceneObjectFactory


class Scene(Tree):
class Scene(Datastructure):
"""A scene is a container for hierarchical scene objects which are to be visualised in a given context.

Parameters
Expand Down Expand Up @@ -43,47 +44,51 @@ class Scene(Tree):
@property
def __data__(self):
# type: () -> dict
items = {str(object.item.guid): object.item for object in self.objects if object.item is not None}
return {
"name": self.name,
"root": self.root.__data__, # type: ignore
"items": list(items.values()),
"attributes": self.attributes,
"datastore": self.datastore,
"objectstore": self.objectstore,
"tree": self.tree,
}

@classmethod
def __from_data__(cls, data):
# type: (dict) -> Scene
scene = cls(data["name"])
items = {str(item.guid): item for item in data["items"]}

def add(node, parent, items):
for child_node in node.get("children", []):
settings = child_node["settings"]
if "item" in child_node:
guid = child_node["item"]
sceneobject = parent.add(items[guid], **settings)
else:
sceneobject = parent.add(Group(**settings))
add(child_node, sceneobject, items)

add(data["root"], scene, items)

return scene
Comment on lines 44 to -71
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We directly serialize datastore, objectstore and tree here. And during deserialization we can read them as they are without using a custom __from__data__ method.


def __init__(self, name="Scene", context=None):
# type: (str, str | None) -> None
super(Scene, self).__init__(name=name)
super(Scene, self).add(TreeNode(name="ROOT"))
def __init__(self, context=None, datastore=None, objectstore=None, tree=None, **kwargs):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users can passing their own customs stores and tree, if they know what they are doing.

# type: (str | None, dict | None, dict | None, Tree | None, **kwargs) -> None
super(Scene, self).__init__(**kwargs)

self.context = context or detect_current_context()
self.datastore = datastore or {}
self.objectstore = objectstore or {}
self.tree = tree or Tree()
if self.tree.root is None:
self.tree.add(TreeNode(name=self.name))

def __repr__(self):
# type: () -> str

def node_repr(node):
# type: (TreeNode) -> str
if node.is_root:
return node.name
else:
sceneobject = self.objectstore[node.name]
return str(sceneobject)

return self.tree.get_hierarchy_string(node_repr=node_repr)

@property
def items(self):
# type: () -> list[compas.data.Data]
return list(self.datastore.values())

@property
def objects(self):
# type: () -> list[SceneObject]
return [node for node in self.nodes if not node.is_root] # type: ignore
return list(self.objectstore.values())

@property
def context_objects(self):
# type: () -> list
# type: () -> list[SceneObject]
guids = []
for obj in self.objects:
guids += obj.guids
Expand All @@ -108,19 +113,49 @@ def add(self, item, parent=None, **kwargs):
The scene object associated with the item.
"""

parent = parent or self.root
if "context" in kwargs:
if kwargs["context"] != self.context:
raise Exception("Object context should be the same as scene context: {} != {}".format(kwargs["context"], self.context))
del kwargs["context"] # otherwist the SceneObject receives "context" twice, which results in an error

# Create a corresponding new scene object
sceneobject = SceneObjectFactory.create(item=item, context=self.context, scene=self, **kwargs)

if isinstance(item, SceneObject):
sceneobject = item
# Add the scene object and item to the data store
self.objectstore[str(sceneobject.guid)] = sceneobject
self.datastore[str(item.guid)] = item

# Add the scene object to the hierarchical tree
if parent is None:
parent_node = self.tree.root
else:
if "context" in kwargs:
if kwargs["context"] != self.context:
raise Exception("Object context should be the same as scene context: {} != {}".format(kwargs["context"], self.context))
del kwargs["context"] # otherwist the SceneObject receives "context" twice, which results in an error
sceneobject = SceneObject(item=item, context=self.context, **kwargs) # type: ignore
super(Scene, self).add(sceneobject, parent=parent)
if not isinstance(parent, SceneObject):
raise ValueError("Parent is not a SceneObject.", parent)
parent_node = self.tree.get_node_by_name(parent.guid)
if parent_node is None:
raise ValueError("Parent is not part of the scene.", parent)

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

return sceneobject

def remove(self, sceneobject):
"""Remove a scene object along with all its descendants from the scene.

Parameters
----------
sceneobject : :class:`compas.scene.SceneObject`
The scene object to remove.
"""
# type: (SceneObject) -> None
guid = str(sceneobject.guid)
self.objectstore.pop(guid, None)
node = self.tree.get_node_by_name(guid)
if node:
for descendant in node.descendants:
self.objectstore.pop(descendant.name, None)
self.tree.remove(node)

def clear_context(self, guids=None):
# type: (list | None) -> None
"""Clear the visualisation context.
Expand Down Expand Up @@ -233,7 +268,7 @@ def find_by_name(self, name):
return self.get_node_by_name(name=name)

def find_by_itemtype(self, itemtype):
# type: (...) -> SceneObject | None
# type: (type) -> SceneObject | None
"""Find the first scene object with a data item of the given type.

Parameters
Expand All @@ -251,7 +286,7 @@ def find_by_itemtype(self, itemtype):
return obj

def find_all_by_itemtype(self, itemtype):
# type: (...) -> list[SceneObject]
# type: (type) -> list[SceneObject]
"""Find all scene objects with a data item of the given type.

Parameters
Expand Down
Loading
Loading