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