Skip to content

Commit

Permalink
fix(create): Expose an output for unmatched sub-faces
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey committed Feb 5, 2025
1 parent c5143ba commit 7d25e76
Show file tree
Hide file tree
Showing 6 changed files with 17 additions and 7 deletions.
11 changes: 9 additions & 2 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.1",
"version": "1.8.2",
"nickname": "AddSubface",
"outputs": [
[
Expand All @@ -9,6 +9,13 @@
"description": "The input Honeybee Face or a Room with the input _sub_faces added\nto it.",
"type": null,
"default": null
},
{
"access": "None",
"name": "unmatched",
"description": "A list of any Apertures or Doors that could not be matched and\nassigned to a parent Face.",
"type": null,
"default": null
}
]
],
Expand Down Expand Up @@ -36,7 +43,7 @@
}
],
"subcategory": "0 :: Create",
"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",
"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 unmatched_sfs[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 unmatched_sfs[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 unmatched_sfs = sub_faces[:] # copy the input list\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 = [sf for sf in unmatched_sfs if sf is not None]\n unmatched_ids = [sf.display_name for sf in unmatched]\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
13 changes: 8 additions & 5 deletions honeybee_grasshopper_core/src/HB Add Subface.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
report: Reports, errors, warnings, etc.
hb_obj: The input Honeybee Face or a Room with the input _sub_faces added
to it.
unmatched: A list of any Apertures or Doors that could not be matched and
assigned to a parent Face.
"""

ghenv.Component.Name = "HB Add Subface"
ghenv.Component.NickName = 'AddSubface'
ghenv.Component.Message = '1.8.1'
ghenv.Component.Message = '1.8.2'
ghenv.Component.Category = 'Honeybee'
ghenv.Component.SubCategory = '0 :: Create'
ghenv.Component.AdditionalHelpFromDocStrings = "4"
Expand Down Expand Up @@ -97,21 +99,21 @@ def check_and_add_sub_face(face, sub_faces, dist):
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
unmatched_sfs[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
unmatched_sfs[i] = None
add_sub_face(face, sf)


if all_required_inputs(ghenv.Component):
# duplicate the initial objects
hb_obj = [obj.duplicate() for obj in _hb_obj]
sub_faces = [sf.duplicate() for sf in _sub_faces]
sf_ids = [sf.identifier for sf in sub_faces]
unmatched_sfs = sub_faces[:] # copy the input list

# gather all of the parent Faces to be checked
rel_faces = []
Expand Down Expand Up @@ -161,7 +163,8 @@ def check_and_add_sub_face(face, sub_faces, dist):
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]
unmatched = [sf for sf in unmatched_sfs if sf is not None]
unmatched_ids = [sf.display_name for sf in unmatched]
msg = 'The following sub-faces were not matched with any parent Face:' \
'\n{}'.format('\n'.join(unmatched_ids))
if len(unmatched_ids) != 0:
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 7d25e76

Please # to comment.