Repository: compas-dev/compas

Issue:
Character in `compas/scene/group.py` causes failure in Rhino 7
Something in the docstring in  `compas/scene/group.py` causes an error, causing compas.scene to not work. 

The Draw GH component gives the following error when adding to canvas:
```
Runtime error (SyntaxErrorException): Non-ASCII character '\xe2' in file C:\Users\xxx\AppData\Roaming\McNeel\Rhinoceros\7.0\scripts\compas\scene\group.py on line 24, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

Traceback:
  line 17, in <module>, "C:\Users\xxx\AppData\Roaming\McNeel\Rhinoceros\7.0\scripts\compas\scene\__init__.py"
  line 7, in script

```

**To Reproduce**
Steps to reproduce the behavior:
1. Open Rhino 7 and Grasshopper
2. Sdrop compas `Draw` component on canvas
3. component in error state
4. copy error
5. paste error here.

**Desktop (please complete the following information):**
 - OS: Windows 11
 - IronPython 
 
**Additional context**
Thanks!!

Discussion:
@Licini: thx! @obucklin this is now fixed in main branch, we will make a release soon

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 570fb07cbe6..f3dbfb7131e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,12 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * Added `compas.geometry.Brep.cap_planar_holes`.
 * Added `compas_rhino.geometry.RhinoBrep.cap_planar_holes`.
 * Added `compas.geometry.angle_vectors_projected`.
+* Added `compas.geometry.Brep.from_curves`.
+* Added `compas_rhino.geometry.RhinoBrep.from_curves`.
 
 ### Changed
 
 * 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.
 * 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.
 * Fixed call to `astar_shortest_path` in `Graph.shortest_path`.
+* Fixed a bug when printing an empty `Tree`.
+* Fixed a bug in `Group` for IronPython where the decoding declaration was missing.
+* Fixed a bug where a `Group` without name could not be added to the scene.
 
 ### Removed
 
diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py
index 458dfda9532..f8bb56a3e57 100644
--- a/src/compas/datastructures/tree/tree.py
+++ b/src/compas/datastructures/tree/tree.py
@@ -465,7 +465,8 @@ class Tree(Datastructure):
             for i, child in enumerate(node.children):
                 traverse(child, hierarchy, prefix, i == len(node.children) - 1, depth + 1)
 
-        traverse(self.root, hierarchy)
+        if self.root:
+            traverse(self.root, hierarchy)
 
         return "\n".join(hierarchy)
 
diff --git a/src/compas/geometry/brep/brep.py b/src/compas/geometry/brep/brep.py
index fc52571a215..ffa07063f44 100644
--- a/src/compas/geometry/brep/brep.py
+++ b/src/compas/geometry/brep/brep.py
@@ -358,7 +358,7 @@ class Brep(Geometry):
 
     @classmethod
     def from_curves(cls, curves):
-        """Construct a Brep from a set of curves.
+        """Construct a Brep from a set of closed, planar curves.
 
         Parameters
         ----------
@@ -366,7 +366,7 @@ class Brep(Geometry):
 
         Returns
         -------
-        :class:`compas.geometry.Brep`
+        list [:class:`compas.geometry.Brep`]
 
         """
         return from_curves(curves)
diff --git a/src/compas/scene/group.py b/src/compas/scene/group.py
index 16d9d2c6d9b..af10b42d514 100644
--- a/src/compas/scene/group.py
+++ b/src/compas/scene/group.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 from .sceneobject import SceneObject
 
 
diff --git a/src/compas/scene/sceneobject.py b/src/compas/scene/sceneobject.py
index dbfcd606463..371e00c8feb 100644
--- a/src/compas/scene/sceneobject.py
+++ b/src/compas/scene/sceneobject.py
@@ -104,7 +104,7 @@ class SceneObject(TreeNode):
         if item and not isinstance(item, Data):
             raise ValueError("The item assigned to this scene object should be a data object: {}".format(type(item)))
 
-        name = name or item.name
+        name = name or getattr(item, "name", None)
         super(SceneObject, self).__init__(name=name, **kwargs)
         # the scene object needs to store the context
         # because it has no access to the tree and/or the scene before it is added
diff --git a/src/compas_rhino/geometry/brep/__init__.py b/src/compas_rhino/geometry/brep/__init__.py
index f4ed1a7c70c..58dfd984b87 100644
--- a/src/compas_rhino/geometry/brep/__init__.py
+++ b/src/compas_rhino/geometry/brep/__init__.py
@@ -35,6 +35,11 @@ def from_extrusion(*args, **kwargs):
     return RhinoBrep.from_extrusion(*args, **kwargs)
 
 
+@plugin(category="factories", requires=["Rhino"])
+def from_curves(*args, **kwargs):
+    return RhinoBrep.from_curves(*args, **kwargs)
+
+
 @plugin(category="factories", requires=["Rhino"])
 def from_loft(*args, **kwargs):
     return RhinoBrep.from_loft(*args, **kwargs)
diff --git a/src/compas_rhino/geometry/brep/brep.py b/src/compas_rhino/geometry/brep/brep.py
index 09fc5618dc1..0e8697b4a0c 100644
--- a/src/compas_rhino/geometry/brep/brep.py
+++ b/src/compas_rhino/geometry/brep/brep.py
@@ -302,6 +302,39 @@ class RhinoBrep(Brep):
         rhino_cylinder = cylinder_to_rhino(cylinder)
         return cls.from_native(rhino_cylinder.ToBrep(True, True))
 
+    @classmethod
+    def from_curves(cls, curves):
+        """Create a RhinoBreps from a list of planar face boundary curves.
+
+        Parameters
+        ----------
+        curves : list of :class:`~compas.geometry.Curve` or :class:`~compas.geometry.Polyline`
+            The planar curves that make up the face borders of brep faces.
+
+        Returns
+        -------
+        list of :class:`~compas_rhino.geometry.RhinoBrep`
+
+        """
+        if not isinstance(curves, list):
+            curves = [curves]
+        faces = []
+        for curve in curves:
+            if isinstance(curve, Polyline):
+                rhino_curve = polyline_to_rhino_curve(curve)
+            else:
+                rhino_curve = curve_to_rhino(curve)
+            face = Rhino.Geometry.Brep.CreatePlanarBreps(rhino_curve, TOL.absolute)
+            if face is None:
+                raise BrepError("Failed to create face from curve: {} ".format(curve))
+            if len(face) > 1:
+                raise BrepError("Failed to create single face from curve: {} ".format(curve))
+            faces.append(face[0])
+        rhino_brep = Rhino.Geometry.Brep.JoinBreps(faces, TOL.absolute)
+        if rhino_brep is None:
+            raise BrepError("Failed to create Brep from faces: {} ".format(faces))
+        return [cls.from_native(brep) for brep in rhino_brep]
+
     @classmethod
     def from_extrusion(cls, curve, vector, cap_ends=True):
         """Create a RhinoBrep from an extrusion.


Test patch:
diff --git a/tests/compas/datastructures/test_tree.py b/tests/compas/datastructures/test_tree.py
index 3c12f7fd50f..b04a42fa566 100644
--- a/tests/compas/datastructures/test_tree.py
+++ b/tests/compas/datastructures/test_tree.py
@@ -51,6 +51,15 @@ def test_tree_initialization():
     assert tree.root is None
 
 
+def test_empty_tree():
+    tree = Tree()
+    assert tree.root is None
+    assert len(list(tree.nodes)) == 0
+    assert len(list(tree.leaves)) == 0
+    assert list(tree.traverse()) == []
+    assert tree.get_hierarchy_string() == ""
+
+
 # =============================================================================
 # TreeNode Properties
 # =============================================================================