Skip to content

Commit

Permalink
fix(create): Improve Add Subface with new projection dist input
Browse files Browse the repository at this point in the history
The HB Add Subface component now has the following improvements:

* The component has a new input for projection distance, which can be used to handle aperture geometries that are not perfectly coplanar with the parent wall.
* The component runs much faster for large models as it uses a bounding box check before it tries to evaluate whether geometries can be added.
* The component can now accept an entire Model as input.
  • Loading branch information
chriswmackey committed Jan 31, 2025
1 parent f12340c commit 4ba7ccc
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 25 deletions.
Binary file modified honeybee_grasshopper_core/icon/HB Add Subface.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 10 additions & 3 deletions honeybee_grasshopper_core/json/HB_Add_Subface.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.8.0",
"version": "1.8.1",
"nickname": "AddSubface",
"outputs": [
[
Expand All @@ -16,7 +16,7 @@
{
"access": "list",
"name": "_hb_obj",
"description": "A Honeybee Face or a Room to which the _sub_faces should be added.",
"description": "A Honeybee Face or a Room to which the _sub_faces should be added.\nThis can also be an entire Honeybee Model in which case Apertures\nwill be added to all FAces of the Model (including both Room Faces\nand orphaned Faces).",
"type": "System.Object",
"default": null
},
Expand All @@ -26,10 +26,17 @@
"description": "A list of Honeybee Apertures and/or Doors that will be added\nto the input _hb_obj.",
"type": "System.Object",
"default": null
},
{
"access": "item",
"name": "project_dist_",
"description": "An optional number to be used to project the Aperture/Door geometry\nonto parent Faces. If specified, then Apertures within this distance\nof the parent Face will be projected and added. Otherwise,\nApertures/Doors will only be added if they are coplanar and fully\nbounded by a parent Face.",
"type": "double",
"default": null
}
],
"subcategory": "0 :: Create",
"code": "\ntry: # import the core honeybee dependencies\n from honeybee.room import Room\n from honeybee.face import Face\n from honeybee.aperture import Aperture\n from honeybee.boundarycondition import Surface, boundary_conditions\nexcept ImportError as e:\n raise ImportError('\\nFailed to import honeybee:\\n\\t{}'.format(e))\n\ntry: # import the ladybug_{{cad}} dependencies\n from ladybug_{{cad}}.config import tolerance, angle_tolerance\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, give_warning\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\nalready_added_ids = set() # track whether a given sub-face is already added\nindoor_faces = {}\n\n\ndef add_sub_face(face, sub_face):\n \"\"\"Add a sub-face (either Aperture or Door) to a parent Face.\"\"\"\n if isinstance(sub_face, Aperture): # the sub-face is an Aperture\n face.add_aperture(sub_face)\n else: # the sub-face is a Door\n face.add_door(sub_face)\n\n\ndef check_and_add_sub_face(face, sub_faces):\n \"\"\"Check whether a sub-face is valid for a face and, if so, add it.\"\"\"\n for i, sf in enumerate(sub_faces):\n if face.geometry.is_sub_face(sf.geometry, tolerance, angle_tolerance):\n if isinstance(face.boundary_condition, Surface):\n try:\n indoor_faces[face.identifier][1].append(sf)\n except KeyError: # the first time we're encountering the face\n indoor_faces[face.identifier] = [face, [sf]]\n sf_ids[i] = None\n else:\n if sf.identifier in already_added_ids:\n sf = sf.duplicate() # make sure the sub-face isn't added twice\n sf.add_prefix('Ajd')\n already_added_ids.add(sf.identifier)\n sf_ids[i] = None\n add_sub_face(face, sf)\n\n\nif all_required_inputs(ghenv.Component):\n # duplicate the initial objects\n hb_obj = [obj.duplicate() for obj in _hb_obj]\n sub_faces = [sf.duplicate() for sf in _sub_faces]\n sf_ids = [sf.identifier for sf in sub_faces]\n\n # check and add the sub-faces\n for obj in hb_obj:\n if isinstance(obj, Face):\n check_and_add_sub_face(obj, sub_faces)\n elif isinstance(obj, Room):\n for face in obj.faces:\n check_and_add_sub_face(face, sub_faces)\n else:\n raise TypeError('Expected Honeybee Face or Room. '\n 'Got {}.'.format(type(obj)))\n\n # for any Faces with a Surface boundary condition, add subfaces as a pair\n already_adj_ids = set()\n for in_face_id, in_face_props in indoor_faces.items():\n if in_face_id in already_adj_ids:\n continue\n face_1 = in_face_props[0]\n try:\n face_2 = indoor_faces[face_1.boundary_condition.boundary_condition_object][0]\n except KeyError as e:\n msg = 'Adding sub-faces to faces with interior (Surface) boundary ' \\\n 'conditions\\nis only possible when both adjacent faces are in ' \\\n 'the input _hb_obj.\\nFailed to find {}, which is adjacent ' \\\n 'to {}.'.format(e, in_face_id)\n print(msg)\n raise ValueError(msg)\n face_1.boundary_condition = boundary_conditions.outdoors\n face_2.boundary_condition = boundary_conditions.outdoors\n for sf in in_face_props[1]:\n add_sub_face(face_1, sf)\n sf2 = sf.duplicate() # make sure the sub-face isn't added twice\n sf2.add_prefix('Ajd')\n add_sub_face(face_2, sf2)\n face_1.set_adjacency(face_2)\n already_adj_ids.add(face_2.identifier)\n\n # if any of the sub-faces were not added, give a warning\n unmatched_ids = [sf_id for sf_id in sf_ids if sf_id is not None]\n msg = 'The following sub-faces were not matched with any parent Face:' \\\n '\\n{}'.format('\\n'.join(unmatched_ids))\n if len(unmatched_ids) != 0:\n print msg\n give_warning(ghenv.Component, msg)\n",
"code": "\nimport math\n\ntry: # import the core honeybee dependencies\n from ladybug_geometry.bounding import overlapping_bounding_boxes\n from ladybug_geometry.geometry3d.face import Face3D\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_geometry:\\n\\t{}'.format(e))\n\ntry: # import the core honeybee dependencies\n from honeybee.model import Model\n from honeybee.room import Room\n from honeybee.face import Face\n from honeybee.aperture import Aperture\n from honeybee.boundarycondition import Surface, boundary_conditions\nexcept ImportError as e:\n raise ImportError('\\nFailed to import honeybee:\\n\\t{}'.format(e))\n\ntry: # import the ladybug_{{cad}} dependencies\n from ladybug_{{cad}}.config import tolerance, angle_tolerance\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, give_warning\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\na_tol_min = math.radians(angle_tolerance) # min tolerance for projection\na_tol_max = math.pi - angle_tolerance # max tolerance for projection\nalready_added_ids = set() # track whether a given sub-face is already added\nindoor_faces = {}\n\n\ndef add_sub_face(face, sub_face):\n \"\"\"Add a sub-face (either Aperture or Door) to a parent Face.\"\"\"\n if isinstance(sub_face, Aperture): # the sub-face is an Aperture\n face.add_aperture(sub_face)\n else: # the sub-face is a Door\n face.add_door(sub_face)\n\n\ndef check_and_add_sub_face(face, sub_faces, dist):\n \"\"\"Check whether a sub-face is valid for a face and, if so, add it.\"\"\"\n for i, sf in enumerate(sub_faces):\n if overlapping_bounding_boxes(face.geometry, sf.geometry, dist):\n sf_to_add = None\n if project_dist_ is None: # just check if it is a valid subface\n if face.geometry.is_sub_face(sf.geometry, tolerance, angle_tolerance):\n sf_to_add = sf\n else:\n ang = sf.normal.angle(face.normal)\n if ang < a_tol_min or ang > a_tol_max:\n clean_pts = [face.geometry.plane.project_point(pt)\n for pt in sf.geometry.boundary]\n sf = sf.duplicate()\n sf._geometry = Face3D(clean_pts)\n sf_to_add = sf\n\n if sf_to_add is not None: # add the subface to the parent\n if isinstance(face.boundary_condition, Surface):\n try:\n indoor_faces[face.identifier][1].append(sf)\n except KeyError: # the first time we're encountering the face\n indoor_faces[face.identifier] = [face, [sf]]\n sf_ids[i] = None\n else:\n if sf.identifier in already_added_ids:\n sf = sf.duplicate() # make sure the sub-face isn't added twice\n sf.add_prefix('Ajd')\n already_added_ids.add(sf.identifier)\n sf_ids[i] = None\n add_sub_face(face, sf)\n\n\nif all_required_inputs(ghenv.Component):\n # duplicate the initial objects\n hb_obj = [obj.duplicate() for obj in _hb_obj]\n sub_faces = [sf.duplicate() for sf in _sub_faces]\n sf_ids = [sf.identifier for sf in sub_faces]\n\n # gather all of the parent Faces to be checked\n rel_faces = []\n for obj in hb_obj:\n if isinstance(obj, Face):\n rel_faces.append(obj)\n elif isinstance(obj, (Room, Model)):\n rel_faces.extend(obj.faces)\n else:\n raise TypeError('Expected Honeybee Face, Room or Model. '\n 'Got {}.'.format(type(obj)))\n dist = tolerance if project_dist_ is None else project_dist_\n for face in rel_faces:\n check_and_add_sub_face(face, sub_faces, dist)\n\n # for any Faces with a Surface boundary condition, add subfaces as a pair\n already_adj_ids = set()\n for in_face_id, in_face_props in indoor_faces.items():\n if in_face_id in already_adj_ids:\n continue\n face_1 = in_face_props[0]\n try:\n face_2 = indoor_faces[face_1.boundary_condition.boundary_condition_object][0]\n except KeyError as e:\n msg = 'Adding sub-faces to faces with interior (Surface) boundary ' \\\n 'conditions\\nis only possible when both adjacent faces are in ' \\\n 'the input _hb_obj.\\nFailed to find {}, which is adjacent ' \\\n 'to {}.'.format(e, in_face_id)\n print(msg)\n raise ValueError(msg)\n face_1.boundary_condition = boundary_conditions.outdoors\n face_2.boundary_condition = boundary_conditions.outdoors\n for sf in in_face_props[1]:\n add_sub_face(face_1, sf)\n sf2 = sf.duplicate() # make sure the sub-face isn't added twice\n sf2.add_prefix('Ajd')\n add_sub_face(face_2, sf2)\n face_1.set_adjacency(face_2)\n already_adj_ids.add(face_2.identifier)\n\n # if a project_dist_ was specified, trim the apertures with the Face geometry\n if project_dist_ is not None:\n for face in rel_faces:\n if face.has_sub_faces:\n face.fix_invalid_sub_faces(\n trim_with_parent=True, union_overlaps=False,\n offset_distance=tolerance * 5, tolerance=tolerance)\n\n # if any of the sub-faces were not added, give a warning\n unmatched_ids = [sf_id for sf_id in sf_ids if sf_id is not None]\n msg = 'The following sub-faces were not matched with any parent Face:' \\\n '\\n{}'.format('\\n'.join(unmatched_ids))\n if len(unmatched_ids) != 0:\n print msg\n give_warning(ghenv.Component, msg)\n",
"category": "Honeybee",
"name": "HB Add Subface",
"description": "Add a Honeybee Aperture or Door to a parent Face or Room.\n-"
Expand Down
88 changes: 66 additions & 22 deletions honeybee_grasshopper_core/src/HB Add Subface.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@
Args:
_hb_obj: A Honeybee Face or a Room to which the _sub_faces should be added.
This can also be an entire Honeybee Model in which case Apertures
will be added to all FAces of the Model (including both Room Faces
and orphaned Faces).
_sub_faces: A list of Honeybee Apertures and/or Doors that will be added
to the input _hb_obj.
project_dist_: An optional number to be used to project the Aperture/Door geometry
onto parent Faces. If specified, then Apertures within this distance
of the parent Face will be projected and added. Otherwise,
Apertures/Doors will only be added if they are coplanar and fully
bounded by a parent Face.
Returns:
report: Reports, errors, warnings, etc.
hb_obj: The input Honeybee Face or a Room with the input _sub_faces added
Expand All @@ -24,12 +32,21 @@

ghenv.Component.Name = "HB Add Subface"
ghenv.Component.NickName = 'AddSubface'
ghenv.Component.Message = '1.8.0'
ghenv.Component.Message = '1.8.1'
ghenv.Component.Category = 'Honeybee'
ghenv.Component.SubCategory = '0 :: Create'
ghenv.Component.AdditionalHelpFromDocStrings = "4"

import math

try: # import the core honeybee dependencies
from ladybug_geometry.bounding import overlapping_bounding_boxes
from ladybug_geometry.geometry3d.face import Face3D
except ImportError as e:
raise ImportError('\nFailed to import ladybug_geometry:\n\t{}'.format(e))

try: # import the core honeybee dependencies
from honeybee.model import Model
from honeybee.room import Room
from honeybee.face import Face
from honeybee.aperture import Aperture
Expand All @@ -43,6 +60,8 @@
except ImportError as e:
raise ImportError('\nFailed to import ladybug_rhino:\n\t{}'.format(e))

a_tol_min = math.radians(angle_tolerance) # min tolerance for projection
a_tol_max = math.pi - angle_tolerance # max tolerance for projection
already_added_ids = set() # track whether a given sub-face is already added
indoor_faces = {}

Expand All @@ -55,23 +74,37 @@ def add_sub_face(face, sub_face):
face.add_door(sub_face)


def check_and_add_sub_face(face, sub_faces):
def check_and_add_sub_face(face, sub_faces, dist):
"""Check whether a sub-face is valid for a face and, if so, add it."""
for i, sf in enumerate(sub_faces):
if face.geometry.is_sub_face(sf.geometry, tolerance, angle_tolerance):
if isinstance(face.boundary_condition, Surface):
try:
indoor_faces[face.identifier][1].append(sf)
except KeyError: # the first time we're encountering the face
indoor_faces[face.identifier] = [face, [sf]]
sf_ids[i] = None
if overlapping_bounding_boxes(face.geometry, sf.geometry, dist):
sf_to_add = None
if project_dist_ is None: # just check if it is a valid subface
if face.geometry.is_sub_face(sf.geometry, tolerance, angle_tolerance):
sf_to_add = sf
else:
if sf.identifier in already_added_ids:
sf = sf.duplicate() # make sure the sub-face isn't added twice
sf.add_prefix('Ajd')
already_added_ids.add(sf.identifier)
sf_ids[i] = None
add_sub_face(face, sf)
ang = sf.normal.angle(face.normal)
if ang < a_tol_min or ang > a_tol_max:
clean_pts = [face.geometry.plane.project_point(pt)
for pt in sf.geometry.boundary]
sf = sf.duplicate()
sf._geometry = Face3D(clean_pts)
sf_to_add = sf

if sf_to_add is not None: # add the subface to the parent
if isinstance(face.boundary_condition, Surface):
try:
indoor_faces[face.identifier][1].append(sf)
except KeyError: # the first time we're encountering the face
indoor_faces[face.identifier] = [face, [sf]]
sf_ids[i] = None
else:
if sf.identifier in already_added_ids:
sf = sf.duplicate() # make sure the sub-face isn't added twice
sf.add_prefix('Ajd')
already_added_ids.add(sf.identifier)
sf_ids[i] = None
add_sub_face(face, sf)


if all_required_inputs(ghenv.Component):
Expand All @@ -80,16 +113,19 @@ def check_and_add_sub_face(face, sub_faces):
sub_faces = [sf.duplicate() for sf in _sub_faces]
sf_ids = [sf.identifier for sf in sub_faces]

# check and add the sub-faces
# gather all of the parent Faces to be checked
rel_faces = []
for obj in hb_obj:
if isinstance(obj, Face):
check_and_add_sub_face(obj, sub_faces)
elif isinstance(obj, Room):
for face in obj.faces:
check_and_add_sub_face(face, sub_faces)
rel_faces.append(obj)
elif isinstance(obj, (Room, Model)):
rel_faces.extend(obj.faces)
else:
raise TypeError('Expected Honeybee Face or Room. '
raise TypeError('Expected Honeybee Face, Room or Model. '
'Got {}.'.format(type(obj)))
dist = tolerance if project_dist_ is None else project_dist_
for face in rel_faces:
check_and_add_sub_face(face, sub_faces, dist)

# for any Faces with a Surface boundary condition, add subfaces as a pair
already_adj_ids = set()
Expand All @@ -116,6 +152,14 @@ def check_and_add_sub_face(face, sub_faces):
face_1.set_adjacency(face_2)
already_adj_ids.add(face_2.identifier)

# if a project_dist_ was specified, trim the apertures with the Face geometry
if project_dist_ is not None:
for face in rel_faces:
if face.has_sub_faces:
face.fix_invalid_sub_faces(
trim_with_parent=True, union_overlaps=False,
offset_distance=tolerance * 5, tolerance=tolerance)

# if any of the sub-faces were not added, give a warning
unmatched_ids = [sf_id for sf_id in sf_ids if sf_id is not None]
msg = 'The following sub-faces were not matched with any parent Face:' \
Expand Down
Binary file modified honeybee_grasshopper_core/user_objects/HB Add Subface.ghuser
Binary file not shown.
Binary file modified samples/model_creation_workflows.gh
Binary file not shown.
Binary file modified samples/model_serialization.gh
Binary file not shown.
Binary file modified samples/model_with_shade_mesh.gh
Binary file not shown.

0 comments on commit 4ba7ccc

Please sign in to comment.