GE5 - nester
This is where the magic happens. The class Nester
is our engine where we encode our list of instructions to nest or pack our furnitures into a given room.
Basic class Nester’s structure
The Nester
class’s main ingredients to produce a layout are the following:
- a
Room
object: that we precedently created. It represents the room with its obstacles (openings, interior walls, etc). - a
list
ofFurniture
objects: the we also precedently created. This list contains a list of objects that we want to create. - other properties: these are the parameters that our Nester will need to tune some of its fonctionnalities. For now we are going to put only one: the nesting steps. This will define the precision to which furnitures are positioned: the bigger the rougher our placement will become.
Where,
the TypeHint
for the newly created class that we are feeding to the Nester
component will be of type ghdoc Object
⚠️!
The basic python code for the class is the followin:
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
):
# 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 = []
The structure is simple, besides the fact that we created an internal property self._furniture_nested_list
to store output our new furnitures. But why do we immediately duplicate the input list of furnitures? If we don’t we would apply the transformations to the same object and we would modify the original object, remember: when you apply transformation to geometries, in GHPython, everything needs to be copied 🐇🐇.
Furniture class’s methods
Concerning the methods, we will have one main method that we will expose to be called: the compute(self)
method. This is where the nesting take place! For better readability and organization is often convinient to split a bigger function such this one in muliple smaller ones wraped in the compute(self)
method. But before going into that let’s take a moment to design our nesting mechanism.
The compute(self)
method 🪑
Here we need to design and implement our own nesting algorithm, yes we used that word and you can too. An algorithm is a set of pre-defined operations involving calculations to solve a problem, in our case: placing authomatically a list of furnitures. So without further due, let’s list our operations in order to nest the furniture. Our algorithm can be divided in 3 big chunks:
- (a) we extract all the possible placement planes from the room for the furniture (this is our pool of possible choices to pick)
- (b) next, we place each furniture by checking collisions with the room elements (this is the broad logic with which we will place furnitures, but there others too!)
- (c) finally, we check that all furnitures are placed and there are no problems (this is a safety check, it’s a type of defensive coding which is also useful)
These 3 steps define exactly our 3 sub-methods wraped by compute(self)
:
def compute(self):
# (a) get furniture placement planes on the room walls by the given step size
pool_planes = self._compute_planes_pool(room_sides, step)
# (b) iterate through the input list of furnitures and check collision between
self._nest_furnitures(furnitures, pool_planes)
# (c) check if all the furnitures are placed
self._check_sanity_nesting()
Before going into coding it’s always good to try to define as much each intermediary steps. The more information you will add at this point to your design the easier will be to code it out.
(a) get furniture placement planes on the room walls by the given step size
(b) iterate through the input list of furnitures
(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
(c) check if all the furnitures are placed
It’s also useful to sketch a small dataflow which resume our design:
Enough planning, let’s dive into coding. We are going to see in details each of the 3 functions that compose our method compute(self)
.
(a) _compute_planes_pool(self, room_sides, step)
As said this function allows us to obtain all the possible planes out of the Room
’s contour to place furnitures.
def _compute_planes_pool(self, room_sides, step):
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
The implementation is rather simple. We iterate through the room’s sides and we divide each one of by the step length. Next, for each point on the curve we create a plane defined by:
origin
as the point on the room’s sideaxis_x
as the vector of the curve directionaxis_y
as the 90° rotation ofaxis_x
Finally we return the list of planes.
(b) _nest_furnitures(self, furnitures, pool_planes)
Here’s the core of our nesting method:
def _nest_furnitures(self, furnitures, pool_planes):
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
Although it might look complex the whole function is based on collision detection thanks to the Rhino method rg.Intersect.Intersection.CurveCurve()
. In fact, by simply checking bounding box collisions we are able to detect if the furniture can be placed or not, both for the room’s contours and obstacles. ⚠️ Note that if the furniture, it’s added to the list of collisions
, this allows our system to be aware of the placed furnitures during each loop ⚠️. Remeber: for
and while
loops together with break
conditions are ones of your most valuable friends in GHPython.
(c) _check_sanity_nesting(self)
This is our little safety check at the end that all furnitures are placed. If not a warning flag is raised for the component. This is just an example but you could add many other types of sanity checks at the end.
def _check_sanity_nesting(self):
if (len(self.furniture_list) != len(self._furniture_nested_list)):
ghenv.Component.AddRuntimeMessage(RML.Warning, "Not all the furniture could be placed")
The final code
And to resume everything contained in the GHPython 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
* The extra mile
🎉🎉🎉 That’s it! 🎉🎉🎉
Note that this is just a very basic implementation of what a furniture plan nester could be. Let’s not even talk about ML-based nesters.
If you are interested in this topic have a look at what exist already on the market such as PlanFinder or Finch.