GE4 - Automating furniture planning

This guided exercise wants to show how it is possible to build a small prototype of an automatic furniture planner. Here’s a resumé of the entire gh script and python code but you can follow a step-by-step guide divided in the 3 major component classes of this nester:

🦗⬇️⬇️⬇️ Download the gh script file here ⬇️⬇️⬇️🦗

🦏⬇️⬇️⬇️ Download the rhino file here ⬇️⬇️⬇️🦏

just show me the code

The grasshopper file:

The script is composed by 3 main classes: Room, Furniture, Nester. The python code for the Room class:

import Rhino.Geometry as rg
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML
import Rhino as r

TOLERANCE = r.RhinoDoc.ActiveDoc.ModelAbsoluteTolerance

class Room:
    def __init__(self,
                 contour,
                 obstacles=[]
                 ):
        """
            __init__() is the constructor for the Room object.
            
            :param contour: (Rhino.Geometry.Curve) It is the closed contour of the room
            :param obstacles: (List(Rhino.Geometry.Curve)) A list of closed curves indicating
            openings and other obstacles in the room where furniture should not go.
            
            :return: the Room object
        """
        if not isinstance(contour, rg.Polyline):
            ghenv.Component.AddRuntimeMessage(RML.Error, "It is not a polyline")
        if not contour.IsClosed:
            ghenv.Component.AddRuntimeMessage(RML.Error, "The curve is not closed")
        self.contour = contour
        self.check_curve_orientation()
        
        self._off_contour = None
        
        self.obstacles = obstacles
     
    def check_curve_orientation(self):
         # check curve orientation for the correct offset and furniture orientation, all curves must be counterclockwise
        temp_poly_crv = self.contour.ToPolylineCurve()
        temp_crv_orientation = temp_poly_crv.ClosedCurveOrientation(rg.Vector3d.ZAxis)
        if temp_crv_orientation != rg.CurveOrientation.Clockwise:
            self.contour.Reverse()
            
    def add_obstacle(self, obstacle_crv):
        """
            The function add an obstacle to the list of obstacles in the room (door, window openings,
            tables, etc)
            
            :param obstacle_crv: (Rhino.Geometry.Curve) The curve of the obstacle
        """
        self.obstacles.append(obstacle_crv)
    
    def get_offset_contour(self):
        """
            The function calculate and returns the offset of the room contour.
            
            :return: The property of the contour now offset
        """
        if self._off_contour is None:
            temp_crv = self.contour.ToNurbsCurve()
            
            
            temp_crv_off = temp_crv.Offset(rg.Plane.WorldXY,
                                                                   -TOLERANCE*10,
                                                                   TOLERANCE,
                                                                   rg.CurveOffsetCornerStyle.Sharp)[0]
            _, self._off_contour = temp_crv_off.TryGetPolyline()
        return self._off_contour

    def Duplicate(self):
        """
            Copy all the properties inside the class. In C# for Rhino you need to do this for objects other
            than structs (e.g. Point3D, Vectors, etc). All classes need to be copied (e.g. Curve, Line).
            In (iron)python Rhino things are a bit more complicated. In fact you need to copy all objects (even
            structs, e.g. Point, Vector, Box), these types need to be copied with a new constructor. Class objects
            can be duplicated with the traditional .Duplicate() method. The reason why is that EVERYTHING is an OBJECT
            in Python and they are always passed as a REFERENCE.
            
            :return: (Room) a new object with duplicated properties (geometries) of the room.
        """
        room_copy = Room(contour=self.contour.Duplicate(),
                         obstacles=[o.DuplicateCurve() for o in self.obstacles])
        room_copy._off_contour = rg.Polyline(self.get_offset_contour())
        
        return room_copy


if __name__ == "__main__":
    room = Room(contour=i_contour,
                obstacles=i_obstacles)
    a = room

The python code for the Furniture class:

import Rhino.Geometry as rg

class Furniture:
    def __init__(self,
                 name,
                 access_area,
                 geo4display=[]):
        """
            __init__(self, ...) is the constructor definition that you need to call for instantiate a class.
            
            :param name: (str) the name of the furniture
            :param vec_x: (Rhino.Geometry.Vector3d) the vector x
            :param vec_y: (Rhino.Geometry.Vector3d) the vector y
            :param origin: (Rhino.Geometry.Point3d) origin of the furniture
            :param access_area: (Rhino.Geometry.Curve) contour indicating the access area to the furniture
            :param geo4display: (List(Rhino.Geometry.GeometryBase)) the drawings of the object to be displayed
            
            :return: (Furniture) the furniture object
        """
        self.name = name
        self.plane = rg.Plane.WorldXY
        self.access_area = access_area
        self.geo4display = geo4display
    
    def Duplicate(self):
        """
            Copy all the properties inside the class. In C# for Rhino you need to do this for objects other
            than structs (e.g. Point3D, Vectors, etc). All classes need to be copied (e.g. Curve, Line).
            In (iron)python Rhino things are a bit more complicated. In fact you need to copy all objects (even
            structs, e.g. Point, Vector, Box), these types need to be copied with a new constructor. Class objects
            can be duplicated with the traditional .Duplicate() method. The reason why is that EVERYTHING is an OBJECT
            in Python and they are always passed as a REFERENCE.
            
            :return: (Furniture) a new object with duplicated properties (geometries).
        """
        furniture_copy = Furniture(name=self.name,
                                   access_area=self.access_area.DuplicateCurve()
                                   )
        furniture_copy.plane = rg.Plane(self.plane)
        furniture_copy.geo4display = [g.Duplicate() for g in self.geo4display]
        
        return furniture_copy
    
    def Transform(self, xform):
        """
            Transform all the properties (geometries) inside the class.
            
            :param xform: (Rhino.Geometry.Transform)the transformation to be applied
        """
        self.plane.Transform(xform)
        self.access_area.Transform(xform)
        for geo in self.geo4display:
            geo.Transform(xform)

    def __str__(self):
        """
            __str__ is a special method, like __init__ , that is supposed to return a string 
            representation of an object. This method returns the string representation of the 
            object. This method is called when print() or str() function is invoked on an object.

            :return: (str) the string you decided to return when the object is called
        """
        return self.name

if __name__ == "__main__":
    furniture = Furniture(name=i_name,
                          access_area=i_access_area,
                          geo4display=i_geo4display)
    a = furniture

The python code for the Nester class

import Rhino.Geometry as rg
import Rhino as r
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML
import Grasshopper as gh

TOLERANCE = r.RhinoDoc.ActiveDoc.ModelAbsoluteTolerance

class Nester:
    def __init__(self,
                 room,
                 furniture_list,
                 step=100.  # mm
                 ):
        """
            __init__() is the constructor for the Nester class.
            
            :param room: (Room) The object of the room
            :param furniture_list: (List(Furniture)) The list of the furnitures
            :param step: (float) The length of the step to which the room is divided.
            It indicates the resolution/precision of the nesting operation.
            
            :return: the Nester object
        """
        # All the custom objects need to be duplicated to avoid to modify the previous objects in
        # the grasshopper flow.
        self.room = room.Duplicate()
        self.furniture_list = [f.Duplicate() for f in furniture_list]
        
        # This parameter represent the step for nesting, or how much you divide the room contours.
        # Implicitly this define the definition of the nesting operation.
        # we limit the division by 100 and the step indicates the unit length in millemeters.
        self.step = max(100, step)
        
        # This parameter is the outcome of the the nesting operation: the transformed furniture objects.
        self._furniture_nested_list = []
    
    
    def _compute_planes_pool(self, room_sides, step):
        """
            This function compute all the possible placement planes on the room contour
            
            :param room_sides: the sides of the room as Crv datatype
            :param step: the dividing step for the segments
            
            :return: (List(Planes)) a list of all the planes of the room's contour where furniture can be placed 
        """
        pool_planes = []
        for s_polyl in room_sides:
            s_crv = s_polyl.ToNurbsCurve()
            s_crv_params = s_crv.DivideByLength(step, True)
            s_crv_params = s_crv_params[:-1]
            
            axis_x = s_polyl.Direction
            axis_y = rg.Vector3d.CrossProduct(axis_x, rg.Vector3d.ZAxis)
            
            for s_crv_p in s_crv_params:
                plane = rg.Plane(s_crv.PointAt(s_crv_p), axis_x, axis_y)
                pool_planes.append(plane)
        return pool_planes
    
    
    def _nest_furnitures(self, furnitures, pool_planes):
        """
            This function compute all the possible placement planes on the room contour
            Here's the subset for this step:
                (b.0) iterate through planes
                (b.1) orient furniture
                (b.2) check collision between furniture bounding rectangle and ...
                (b.2.1) room contour
                (b.2.2) room obstacles (closed curve)
                (b.2.2.1) we need to check if the furniture is inside the obstacle
                (b.2.2.2) check if the obstacle is inside the furniture
                (b.2.2.3) we check for the furniture-obstacle intersection
                (b.2.3) add currently place furniture to the furniture list
                (b.3) break the loop when the correct placement is found and collect nested furniture
            
            :param room_sides: the list of furnitures to be placed
            :param pool_planes:  a list of all the planes of the room's contour where furniture can be placed
        """
        
        for idx, geo_t in enumerate(furnitures):
            
            # (b.0) iterate through planes
            for apln in pool_planes:
                
                # (b.1) orient furniture
                geo = geo_t.Duplicate()
                xform = rg.Transform.PlaneToPlane(geo_t.plane, apln)
                geo.Transform(xform)
                
                # (b.2) check collision between furniture bounding rectangle and ...
                obr = geo.access_area
                is_intersected_obs = False
                
                # (b.2.1) room contour
                ## check the furniture intersection with the room
                obs_intersect = rg.Intersect.Intersection.CurveCurve(obr,                                           # first curve
                                                                     self.room.get_offset_contour().ToNurbsCurve(), # second curve (casted polyline)
                                                                     TOLERANCE,                                     # interection tolerance
                                                                     TOLERANCE                                      # overlap tolerance
                                                                     )
                ## if there is intersection continue
                if obs_intersect.Count > 0:
                    continue
                
                for obs in self.room.obstacles:
                    
                    # (b.2.2) room obstacles (closed curve)
                    ## (b.2.2.1) we need to check if the furniture is inside the obstacle
                    if ( rg.PointContainment.Inside == obs.Contains(obr.PointAt(0),rg.Plane.WorldXY,TOLERANCE) ):
                        is_intersected_obs = True
                        break
                    
                    # (b.2.2.2) check if the obstacle is inside the furniture
                    if ( rg.PointContainment.Inside == obr.Contains(obs.PointAtStart,rg.Plane.WorldXY,TOLERANCE) ):
                        is_intersected_obs = True
                        break
                    
                    ## (b.2.2.3) we check for the furniture-obstacle intersection
                    obs_intersect = rg.Intersect.Intersection.CurveCurve(obr,       # first curve
                                                                         obs,       # second curve
                                                                         TOLERANCE, # interection tolerance
                                                                         TOLERANCE  # overlap tolerance
                                                                         )
                    if obs_intersect.Count > 0:
                        is_intersected_obs = True
                        break
                    
                # (b.2.3) add currently placed furniture to the furniture list
                if not is_intersected_obs:
                    self._furniture_nested_list.append(geo)
                    self.room.add_obstacle(obr)
                    break
        
    
    
    def _check_sanity_nesting(self):
        """
            This function checks if all the furnitures could be placed. If not it raise a warning message
        """
        if (len(self.furniture_list) != len(self._furniture_nested_list)):
            ghenv.Component.AddRuntimeMessage(RML.Warning, "Not all the furniture could be placed")
    
    def compute(self):
        """
            The definition is in charge of nesting the furniture. It's where the magic happens!
            This is how it works in order:
                (a) get furniture placement planes on the room walls by the given step size
                (b) iterate through the input list of furnitures
                (c) check if all the furnitures are placed
            
            :return: (List(Furniture)) The list of the nested furnitures
        """
        
        # (a) get furniture placement planes on the room walls by the given step size
        pool_planes = self._compute_planes_pool(self.room.contour.GetSegments(), self.step)
        
        # (b) iterate through the input list of furnitures and check collision between
        self._nest_furnitures(self.furniture_list, pool_planes)
        
        # (c) check if all the furnitures are placed
        self._check_sanity_nesting()


if __name__ == "__main__":
    # >>>> your code runs only here <<<<
    
    # instantiate the Nester class and run the compute function
    nester = Nester(room=i_room,
                    furniture_list=i_furniture_list,
                    step=i_step)
    nester.compute()
    
    # (optional) convert list of lists of furniture display curves to a datatree, because list of lists are not readable by grasshopper 
    datatree = gh.DataTree[rg.Curve]()
    for idx, g in enumerate(nester._furniture_nested_list):
        datatree.AddRange(g.geo4display, gh.Kernel.Data.GH_Path(idx))
    
    # output
    a  = datatree