Page MenuHomec4science

read_boundaries.py
No OneTemporary

File Metadata

Created
Sun, Apr 28, 13:53

read_boundaries.py

# coding: utf8
"""This module reads IfcRelSpaceBoundary from an IFC file and display them in FreeCAD"""
import os
import itertools
import ifcopenshell
import ifcopenshell.geom
import FreeCAD
import FreeCADGui
import Part
from bem_xml import BEMxml
WRITE_XML = True
GUI_UP = True
def ios_settings(brep):
"""Create ifcopenshell.geom.settings for various cases"""
settings = ifcopenshell.geom.settings()
settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
settings.set(settings.INCLUDE_CURVES, True)
if brep:
settings.set(settings.USE_BREP_DATA, True)
return settings
BREP_SETTINGS = ios_settings(brep=True)
MESH_SETTINGS = ios_settings(brep=False)
"""With IfcOpenShell 0.6.0a1 recreating face from wires seems to give more consistant results.
Especially when inner boundaries touch outer boundary"""
BREP = False
# IfcOpenShell/IFC default unit is m, FreeCAD internal unit is mm
SCALE = 1000
def display_boundaries(ifc_path, doc=FreeCAD.ActiveDocument):
"""Display IfcRelSpaceBoundaries from selected IFC file into FreeCAD documennt"""
# Create default groups
group = get_or_create_group(doc, "RelSpaceBoundary")
group.addProperty("App::PropertyString", "ApplicationIdentifier")
group.addProperty("App::PropertyString", "ApplicationVersion")
group.addProperty("App::PropertyString", "ApplicationFullName")
group_2nd = get_or_create_group(doc, "SecondLevel")
group.addObject(group_2nd)
elements_group = get_or_create_group(doc, "Elements")
ifc_file = ifcopenshell.open(ifc_path)
owning_application = ifc_file.by_type("IfcApplication")[0]
group.ApplicationIdentifier = owning_application.ApplicationIdentifier
group.ApplicationVersion = owning_application.Version
group.ApplicationFullName = owning_application.ApplicationFullName
# Generate projects
ifc_projects = ifc_file.by_type("IfcProject")
projects = get_or_create_group(doc, "Projects")
for ifc_project in ifc_projects:
projects.addObject(create_project_from_entity(ifc_project))
# Generate elements (Door, Window, Wall, Slab etc…) without their geometry
ifc_elements = (e for e in ifc_file.by_type("IfcElement") if e.ProvidesBoundaries)
for ifc_entity in ifc_elements:
elements_group.addObject(create_fc_object_from_entity(ifc_entity))
# Associate Host / Hosted elements
associate_host_element(ifc_file, elements_group)
# Generate boundaries
spaces = ifc_file.by_type("IfcSpace")
for space in spaces:
fc_space = create_space_from_entity(group_2nd, space)
# All boundaries have their placement relative to space placement
space_placement = get_placement(space)
for ifc_boundary in (b for b in space.BoundedBy if b.Name == "2ndLevel"):
face = make_relspaceboundary(ifc_boundary)
fc_space.addObject(face)
face.Placement = space_placement
element = get_related_element(doc, elements_group, ifc_boundary)
if element:
face.RelatedBuildingElement = element
append(element, "ProvidesBoundaries", face)
# face.ParentBoundary = get_parent_boundary(doc, elements_group, ifc_boundary)
face.RelatingSpace = fc_space
# Associate CorrespondingBoundary
associate_corresponding_boundaries(group_2nd)
# Associate hosted elements an fill gaps
for fc_space in group_2nd.Group:
fc_boundaries = fc_space.Group
# Minimal number of boundary is 5: 3 vertical faces, 2 horizontal faces
# If there is less than 5 boundaries there is an issue or a new case to analyse
if len(fc_boundaries) == 5:
continue
elif len(fc_boundaries) < 5:
assert ValueError, f"{fc_space.Label} has less than 5 boundaries"
# Find coplanar boundaries to identify hosted boundaries and fill gaps (2b)
associate_coplanar_boundaries(fc_boundaries)
# Associate hosted elements
associate_inner_boundaries(fc_boundaries)
# FIXME: Fill 2b gaps
# fill2b(fc_boundaries)
# for space_group in group_2nd.Group:
# find_coincident_in_space(space_group)
# # Find coincident points
# fc_boundaries = space_group.Group
# for fc_boundary_1 in fc_boundaries:
# for i, vertex_1 in enumerate(fc_boundary_1.Shape.Vertexes):
# if fc_boundary_1.Proxy.coincident_boundaries[i]:
# continue
# fc_boundary_1.Proxy.coincident_boundaries[i] = find_coincident(
# vertex_1.Point, fc_boundary_1, space_group
# )
# fc_boundary_1.CoincidentBoundaries = (
# fc_boundary_1.Proxy.coincident_boundaries
# )
create_geo_ext_boundaries(doc, group_2nd)
create_geo_int_boundaries(doc, group_2nd)
doc.recompute()
if WRITE_XML:
bem_xml = BEMxml()
for project in projects.Group:
bem_xml.write_project(project)
for space in group_2nd.Group:
bem_xml.write_space(space)
for boundary in space.Group:
bem_xml.write_boundary(boundary)
for building_element in elements_group.Group:
bem_xml.write_building_elements(building_element)
xml_path = "./output.xml" if os.name == "nt" else "/home/cyril/git/BIMxBEM/output.xml"
bem_xml.write_to_file(xml_path)
def associate_host_element(ifc_file, elements_group):
# Associate Host / Hosted elements
ifc_elements = (e for e in ifc_file.by_type("IfcElement") if e.ProvidesBoundaries)
for ifc_entity in ifc_elements:
if ifc_entity.FillsVoids:
host = get_element_by_guid(get_host_guid(ifc_entity), elements_group)
hosted = get_element_by_guid(ifc_entity.GlobalId, elements_group)
append(host, "HostedElements", hosted)
hosted.HostElement = host
def fill2b(fc_boundaries):
"""Modify boundary to include area of type 2b between boundaries"""
for fc_boundary in fc_boundaries:
non_hosted_shared_element = [
b for b in fc_boundary.ShareRelatedElementWith if not b.IsHosted
]
# FIXME : Simplification which doesn't handle case where 2b touch a corner
# FIXME : Currently assuming 2b boundaries are bounded by 4 vertices
if not non_hosted_shared_element:
continue
elif len(non_hosted_shared_element) == 1:
# Find side by side corresponding edges
fc_boundary1 = fc_boundary
fc_boundary2 = non_hosted_shared_element[0]
edges1 = fc_boundary1.Shape.OuterWire.Edges
edges2 = fc_boundary2.Shape.OuterWire.Edges
min_distance = 100000
closest_indexes = None
for (i, edge1), (j, edge2) in itertools.product(
enumerate(edges1), enumerate(edges2)
):
if edge1.Length <= edge2.Length:
mid_point = edge1.CenterOfMass
line_segment = (v.Point for v in edge2.Vertexes)
else:
mid_point = edge2.CenterOfMass
line_segment = (v.Point for v in edge1.Vertexes)
distance = mid_point.distanceToLineSegment(*line_segment).Length
if distance < min_distance:
min_distance = distance
closest_indexes = (i, j)
i, j = closest_indexes
# Compute centerline
vec1 = edges1[i].Vertexes[0].Point
vec2 = edges2[j].Vertexes[0].Point
line_segment = (v.Point for v in edges2[j].Vertexes)
vec2 = vec2 + vec2.distanceToLineSegment(*line_segment) / 2
mid_line = Part.Line(vec1, vec2)
# Compute intersection on center line between edges
vxs1_len = len(fc_boundary1.Shape.Vertexes)
vxs2_len = len(fc_boundary2.Shape.Vertexes)
b1_v1 = mid_line.intersectCC(edges1[i - 1].Curve)
b1_v2 = mid_line.intersectCC(edges1[(i + 1) % vxs1_len].Curve)
b2_v1 = mid_line.intersectCC(edges2[j - 1].Curve)
b2_v2 = mid_line.intersectCC(edges2[(j + 1) % vxs2_len].Curve)
vectors = [v.Point for v in fc_boundary1.Shape.OuterWire.Vertexes]
vectors[i] = b1_v1
vectors[(i + 1) % len(vectors)] = b1_v2
vectors.append(vectors[0])
wires = [Part.makePolygon(vectors)]
wires.extend(fc_boundary1.Shape.Wires[1:])
new_shape = Part.Face(wires)
fc_boundary1.Shape = new_shape
def associate_inner_boundaries(fc_boundaries):
"""Associate hosted elements like a window or a door in a wall"""
for fc_boundary in fc_boundaries:
if not fc_boundary.IsHosted:
continue
candidates = set(fc_boundaries).intersection(
fc_boundary.RelatedBuildingElement.HostElement.ProvidesBoundaries
)
# If there is more than 1 candidate it doesn't really matter
# as they share the same host element and space
host_element = candidates.pop()
fc_boundary.ParentBoundary = host_element
append(host_element, "InnerBoundaries", fc_boundary)
def associate_coplanar_boundaries(fc_boundaries):
""" Find coplanar boundaries to identify hosted boundaries and fill gaps (2b)
FIXME: Apparently in ArchiCAD, doors are not coplanar.
Idea: Make it coplanar with other boundaries sharing the same space+wall before"""
for fc_boundary_1, fc_boundary_2 in itertools.combinations(fc_boundaries, 2):
if is_coplanar(fc_boundary_1, fc_boundary_2):
append(fc_boundary_1, "ShareRelatedElementWith", fc_boundary_2)
append(fc_boundary_2, "ShareRelatedElementWith", fc_boundary_1)
def associate_corresponding_boundaries(group_2nd):
# Associate CorrespondingBoundary
for space_group in group_2nd.Group:
for fc_boundary in space_group.Group:
associate_corresponding_boundary(fc_boundary)
def is_coplanar(shape_1, shape_2):
"""Intended for RelSpaceBoundary use only
For some reason native Part.Shape.isCoplanar(Part.Shape) do not always work"""
return get_plane(shape_1).toShape().isCoplanar(get_plane(shape_2).toShape())
def get_plane(fc_boundary):
"""Intended for RelSpaceBoundary use only"""
return Part.Plane(
fc_boundary.Shape.Vertexes[0].Point, fc_boundary.Shape.normalAt(0, 0)
)
def append(doc_object, fc_property, value):
"""Intended to manipulate FreeCAD list like properties only"""
current_value = getattr(doc_object, fc_property)
current_value.append(value)
setattr(doc_object, fc_property, current_value)
def clean_corresponding_candidates(fc_boundary):
if fc_boundary.PhysicalOrVirtualBoundary == "VIRTUAL":
return []
other_boundaries = fc_boundary.RelatedBuildingElement.ProvidesBoundaries
other_boundaries.remove(fc_boundary)
return [
b
for b in other_boundaries
if not b.CorrespondingBoundary or b.RelatingSpace != fc_boundary.RelatingSpace
]
def associate_corresponding_boundary(fc_boundary):
"""Associate corresponding boundaries according to IFC definition.
Reference to the other space boundary of the pair of two space boundaries on either side of a space separating thermal boundary element.
https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcrelspaceboundary2ndlevel.htm
"""
if (
fc_boundary.InternalOrExternalBoundary != "INTERNAL"
or fc_boundary.CorrespondingBoundary
):
return
other_boundaries = clean_corresponding_candidates(fc_boundary)
if len(other_boundaries) == 1:
corresponding_boundary = other_boundaries[0]
else:
center_of_mass = fc_boundary.Shape.CenterOfMass
min_lenght = 10000 # No element has 10 m
for boundary in other_boundaries:
distance = center_of_mass.distanceToPoint(boundary.Shape.CenterOfMass)
if distance < min_lenght:
min_lenght = distance
corresponding_boundary = boundary
try:
fc_boundary.CorrespondingBoundary = corresponding_boundary
corresponding_boundary.CorrespondingBoundary = fc_boundary
except NameError:
# TODO: What to do with uncorrectly classified boundaries which have no corresponding boundary
FreeCAD.Console.PrintLog(
f"Boundary {fc_boundary.GlobalId} from space {fc_boundary}"
)
return
def find_coincident_in_space(space_group):
fc_boundaries = space_group.Group
for fc_boundary_1 in fc_boundaries:
for i, vertex_1 in enumerate(fc_boundary_1.Shape.Vertexes):
coincident_boundary = find_coincident(i, fc_boundary_1, fc_boundaries)
fc_boundary_1.CoincidentBoundaries = coincident_boundary["boundary"]
py_proxy.coincident_indexes[i] = coincident_boundary["index"]
fc_boundary_1.CoincidentBoundaries = py_proxy.coincident_boundaries
fc_boundary_1.CoincidentVertexIndexList = py_proxy.coincident_indexes
def find_coincident(index_1, fc_boundary_1, fc_boundaries):
point1 = fc_boundary_1.Shape.Vertexes[index_1].Point
for fc_boundary_2 in (b for b in fc_boundaries if b != fc_boundary_1):
for j, vertex_2 in enumerate(fc_boundary_2.Shape.Vertexes):
py_proxy = fc_boundary_2.Proxy
# Consider vector.isEqual(vertex.Point) if precision issue
if point1.isEqual(vertex_2.Point, 1):
py_proxy.coincident_boundaries[j] = fc_boundary_1
py_proxy.coincident_indexes[j] = index_1
return {"boundary": fc_boundary_2, "index": j}
else:
raise LookupError
def get_related_element(doc, group, ifc_entity):
elements = group.Group
if not ifc_entity.RelatedBuildingElement:
return
guid = ifc_entity.RelatedBuildingElement.GlobalId
for element in elements:
if element.GlobalId == guid:
return element
def get_host_guid(ifc_entity):
return (
ifc_entity.FillsVoids[0]
.RelatingOpeningElement.VoidsElements[0]
.RelatingBuildingElement.GlobalId
)
def get_element_by_guid(guid, elements_group):
for fc_element in elements_group.Group:
if fc_element.GlobalId == guid:
return fc_element
else:
FreeCAD.Console.PrintLog(f"Unable to get element by {guid}")
def get_thickness(ifc_entity):
thickness = 0
if ifc_entity.IsDecomposedBy:
ifc_entity = ifc_entity.IsDecomposedBy[0].RelatedObjects[0]
for association in ifc_entity.HasAssociations:
if not association.is_a("IfcRelAssociatesMaterial"):
continue
try:
material_layers = association.RelatingMaterial.ForLayerSet.MaterialLayers
except AttributeError:
try:
material_layers = association.RelatingMaterial.MaterialLayers
except AttributeError:
# TODO: Fallback method to handle materials with no layers. eg. association.RelatingMaterial.Materials
continue
for material_layer in material_layers:
thickness += material_layer.LayerThickness * SCALE
return thickness
def create_geo_ext_boundaries(doc, group_2nd):
"""Create boundaries necessary for SIA calculations"""
bem_group = doc.addObject("App::DocumentObjectGroup", "geoExt")
is_from_revit = group_2nd.getParentGroup().ApplicationIdentifier == "Revit"
is_from_archicad = group_2nd.getParentGroup().ApplicationFullName == "ARCHICAD-64"
for space in group_2nd.Group:
for boundary in space.Group:
if boundary.IsHosted or boundary.PhysicalOrVirtualBoundary == "VIRTUAL":
continue
bem_boundary = make_bem_boundary(boundary, "geoExt")
bem_group.addObject(bem_boundary)
thickness = boundary.RelatedBuildingElement.Thickness.Value
ifc_type = boundary.RelatedBuildingElement.IfcType
normal = boundary.Shape.normalAt(0, 0)
if is_from_archicad:
normal = -normal
if boundary.InternalOrExternalBoundary != "INTERNAL":
lenght = thickness
if is_from_revit and ifc_type.startswith("IfcWall"):
lenght /= 2
bem_boundary.Placement.move(normal * lenght)
else:
type1 = {"IfcSlab"}
if ifc_type in type1:
if normal.z > 0:
lenght = thickness
else:
continue
else:
if is_from_revit:
continue
lenght = thickness / 2
bem_boundary.Placement.move(normal * lenght)
def create_geo_int_boundaries(doc, group_2nd):
"""Create boundaries necessary for SIA calculations"""
bem_group = doc.addObject("App::DocumentObjectGroup", "geoInt")
is_from_revit = group_2nd.getParentGroup().ApplicationIdentifier == "Revit"
is_from_archicad = group_2nd.getParentGroup().ApplicationFullName == "ARCHICAD-64"
for fc_space in group_2nd.Group:
for boundary in fc_space.Group:
if boundary.IsHosted or boundary.PhysicalOrVirtualBoundary == "VIRTUAL":
continue
normal = boundary.Shape.normalAt(0, 0)
if is_from_archicad:
normal = -normal
bem_boundary = make_bem_boundary(boundary, "geoInt")
bem_group.addObject(bem_boundary)
ifc_type = boundary.RelatedBuildingElement.IfcType
if is_from_revit and ifc_type.startswith("IfcWall"):
thickness = boundary.RelatedBuildingElement.Thickness.Value
lenght = -thickness / 2
bem_boundary.Placement.move(normal * lenght)
def create_fc_shape(space_boundary):
""" Create Part shape from ifc geometry"""
if BREP:
try:
return _part_by_brep(
space_boundary.ConnectionGeometry.SurfaceOnRelatingElement
)
except RuntimeError:
print(f"Failed to generate brep from {space_boundary}")
fallback = True
if not BREP or fallback:
try:
return part_by_wires(
space_boundary.ConnectionGeometry.SurfaceOnRelatingElement
)
except RuntimeError:
print(f"Failed to generate mesh from {space_boundary}")
return _part_by_mesh(
space_boundary.ConnectionGeometry.SurfaceOnRelatingElement
)
def _part_by_brep(ifc_entity):
""" Create a Part Shape from brep generated by ifcopenshell from ifc geometry"""
ifc_shape = ifcopenshell.geom.create_shape(BREP_SETTINGS, ifc_entity)
fc_shape = Part.Shape()
fc_shape.importBrepFromString(ifc_shape.brep_data)
fc_shape.scale(SCALE)
return fc_shape
def _part_by_mesh(ifc_entity):
""" Create a Part Shape from mesh generated by ifcopenshell from ifc geometry"""
return Part.Face(_polygon_by_mesh(ifc_entity))
def _polygon_by_mesh(ifc_entity):
"""Create a Polygon from a compatible ifc entity"""
ifc_shape = ifcopenshell.geom.create_shape(MESH_SETTINGS, ifc_entity)
ifc_verts = ifc_shape.verts
fc_verts = [
FreeCAD.Vector(ifc_verts[i : i + 3]).scale(SCALE, SCALE, SCALE)
for i in range(0, len(ifc_verts), 3)
]
fc_verts = verts_clean(fc_verts)
return Part.makePolygon(fc_verts)
def verts_clean(vertices):
"""For some reason, vertices are not always clean and sometime a same vertex is repeated"""
new_verts = list()
for i in range(len(vertices) - 1):
if vertices[i] != vertices[i + 1]:
new_verts.append(vertices[i])
new_verts.append(vertices[-1])
return new_verts
def part_by_wires(ifc_entity):
""" Create a Part Shape from ifc geometry"""
boundaries = list()
boundaries.append(_polygon_by_mesh(ifc_entity.OuterBoundary))
try:
inner_boundaries = ifc_entity.InnerBoundaries
for inner_boundary in tuple(inner_boundaries) if inner_boundaries else tuple():
boundaries.append(_polygon_by_mesh(inner_boundary))
except RuntimeError:
pass
fc_shape = Part.makeFace(boundaries, "Part::FaceMakerBullseye")
matrix = get_matrix(ifc_entity.BasisSurface.Position)
fc_shape = fc_shape.transformGeometry(matrix)
return fc_shape
def get_matrix(position):
"""Transform position to FreeCAD.Matrix"""
location = FreeCAD.Vector(position.Location.Coordinates).scale(SCALE, SCALE, SCALE)
v_1 = FreeCAD.Vector(position.RefDirection.DirectionRatios)
v_3 = FreeCAD.Vector(position.Axis.DirectionRatios)
v_2 = v_3.cross(v_1)
# fmt: off
matrix = FreeCAD.Matrix(
v_1.x, v_2.x, v_3.x, location.x,
v_1.y, v_2.y, v_3.y, location.y,
v_1.z, v_2.z, v_3.z, location.z,
0, 0, 0, 1,
)
# fmt: on
return matrix
def get_placement(space):
"""Retrieve object placement"""
space_geom = ifcopenshell.geom.create_shape(BREP_SETTINGS, space)
# IfcOpenShell matrix values FreeCAD matrix values are transposed
ios_matrix = space_geom.transformation.matrix.data
m_l = list()
for i in range(3):
line = list(ios_matrix[i::3])
line[-1] *= SCALE
m_l.extend(line)
return FreeCAD.Matrix(*m_l)
def get_color(ifc_boundary):
"""Return a color depending on IfcClass given"""
product_colors = {
"IfcWall": (0.7, 0.3, 0.0),
"IfcWindow": (0.0, 0.7, 1.0),
"IfcSlab": (0.7, 0.7, 0.5),
"IfcRoof": (0.0, 0.3, 0.0),
"IfcDoor": (1.0, 1.0, 1.0),
}
if ifc_boundary.PhysicalOrVirtualBoundary == "VIRTUAL":
return (1.0, 0.0, 1.0)
ifc_product = ifc_boundary.RelatedBuildingElement
for product, color in product_colors.items():
# Not only test if IFC class is in dictionnary but it is a subclass
if ifc_product.is_a(product):
return color
else:
print(f"No color found for {ifc_product.is_a()}")
return (0.0, 0.0, 0.0)
def get_or_create_group(doc, name):
"""Get group by name or create one if not found"""
group = doc.findObjects("App::DocumentObjectGroup", name)
if group:
return group[0]
return doc.addObject("App::DocumentObjectGroup", name)
def make_relspaceboundary(ifc_entity):
"""Stantard FreeCAD FeaturePython Object creation method"""
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "RelSpaceBoundary")
# ViewProviderRelSpaceBoundary(obj.ViewObject)
RelSpaceBoundary(obj, ifc_entity)
try:
obj.ViewObject.Proxy = 0
except AttributeError:
FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui")
return obj
class Root:
"""Wrapping various IFC entity :
https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcroot.htm
"""
def __init__(self, obj, ifc_entity):
self.Type = self.__class__.__name__
obj.Proxy = self
ifc_attributes = "IFC Attributes"
obj.addProperty("App::PropertyString", "IfcType", "IFC")
obj.addProperty("App::PropertyInteger", "Id", ifc_attributes)
obj.addProperty("App::PropertyString", "GlobalId", ifc_attributes)
obj.addProperty("App::PropertyString", "Description", ifc_attributes)
obj.Id = ifc_entity.id()
obj.GlobalId = ifc_entity.GlobalId
obj.IfcType = ifc_entity.is_a()
self.set_label(obj, ifc_entity)
try:
obj.Description = ifc_entity.Description
except TypeError:
pass
def onChanged(self, obj, prop):
"""Do something when a property has changed"""
return
def execute(self, obj):
"""Do something when doing a recomputation, this method is mandatory"""
return
@staticmethod
def set_label(obj, ifc_entity):
"""Allow specific method for specific elements"""
obj.Label = "{} {}".format(ifc_entity.id(), ifc_entity.Name)
@classmethod
def create(cls, obj_name, ifc_entity=None):
"""Stantard FreeCAD FeaturePython Object creation method
ifc_entity : Optionnally provide a base entity.
"""
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name)
feature_python_object = cls(obj)
return obj
class RelSpaceBoundary(Root):
"""Wrapping IFC entity : 
https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcrelspaceboundary2ndlevel.htm"""
def __init__(self, obj, ifc_entity):
super().__init__(obj, ifc_entity)
obj.Proxy = self
bem_category = "BEM"
ifc_attributes = "IFC Attributes"
obj.addProperty("App::PropertyLink", "RelatingSpace", ifc_attributes)
obj.addProperty("App::PropertyLink", "RelatedBuildingElement", ifc_attributes)
obj.addProperty(
"App::PropertyEnumeration", "PhysicalOrVirtualBoundary", ifc_attributes
).PhysicalOrVirtualBoundary = ["PHYSICAL", "VIRTUAL", "NOTDEFINED"]
obj.addProperty(
"App::PropertyEnumeration", "InternalOrExternalBoundary", ifc_attributes
).InternalOrExternalBoundary = [
"INTERNAL",
"EXTERNAL",
"EXTERNAL_EARTH",
"EXTERNAL_WATER",
"EXTERNAL_FIRE",
"NOTDEFINED",
]
obj.addProperty("App::PropertyLink", "CorrespondingBoundary", ifc_attributes)
obj.addProperty("App::PropertyLink", "ParentBoundary", ifc_attributes)
obj.addProperty("App::PropertyLinkList", "InnerBoundaries", ifc_attributes)
obj.addProperty("App::PropertyLink", "ProcessedShape", bem_category)
obj.addProperty("App::PropertyLinkList", "CoincidentBoundaries", bem_category)
obj.addProperty(
"App::PropertyLinkList", "ShareRelatedElementWith", bem_category
)
obj.addProperty(
"App::PropertyIntegerList", "CoincidentVertexIndexList", bem_category
)
obj.addProperty("App::PropertyBool", "IsHosted", bem_category)
obj.addProperty("App::PropertyArea", "Area", bem_category)
obj.addProperty("App::PropertyArea", "AreaWithHosted", bem_category)
obj.addProperty("App::PropertyLink", "geoInt", bem_category)
obj.addProperty("App::PropertyLink", "geoExt", bem_category)
obj.ViewObject.ShapeColor = get_color(ifc_entity)
obj.GlobalId = ifc_entity.GlobalId
obj.InternalOrExternalBoundary = ifc_entity.InternalOrExternalBoundary
obj.PhysicalOrVirtualBoundary = ifc_entity.PhysicalOrVirtualBoundary
obj.Shape = create_fc_shape(ifc_entity)
obj.Area = obj.AreaWithHosted = obj.Shape.Area
self.set_label(obj, ifc_entity)
if not obj.PhysicalOrVirtualBoundary == "VIRTUAL":
obj.IsHosted = bool(ifc_entity.RelatedBuildingElement.FillsVoids)
self.coincident_boundaries = self.coincident_indexes = [None] * len(
obj.Shape.Vertexes
)
self.coplanar_with = []
def onChanged(self, obj, prop):
super().onChanged(obj, prop)
if prop == "InnerBoundaries":
obj.AreaWithHosted = self.recompute_area_with_hosted(obj)
@staticmethod
def recompute_area_with_hosted(obj):
"""Recompute area including inner boundaries"""
area = obj.Area
for boundary in obj.InnerBoundaries:
area = area + boundary.Area
return area
@staticmethod
def set_label(obj, ifc_entity):
try:
obj.Label = "{} {}".format(
ifc_entity.RelatedBuildingElement.id(),
ifc_entity.RelatedBuildingElement.Name,
)
except AttributeError:
FreeCAD.Console.PrintLog(
f"{ifc_entity.GlobalId} has no RelatedBuildingElement"
)
return
def create_fc_object_from_entity(ifc_entity):
"""Stantard FreeCAD FeaturePython Object creation method"""
obj_name = "Element"
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name)
Element(obj, ifc_entity)
try:
obj.ViewObject.Proxy = 0
except AttributeError:
FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui")
return obj
class Element(Root):
"""Wrapping various IFC entity :
https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/schema/ifcproductextension/lexical/ifcelement.htm
"""
def __init__(self, obj, ifc_entity):
super().__init__(obj, ifc_entity)
self.Type = "IfcRelSpaceBoundary"
obj.Proxy = self
ifc_attributes = "IFC Attributes"
bem_category = "BEM"
obj.addProperty("App::PropertyLinkList", "HasAssociations", ifc_attributes)
obj.addProperty("App::PropertyLinkList", "FillsVoids", ifc_attributes)
obj.addProperty("App::PropertyLinkList", "HasOpenings", ifc_attributes)
obj.addProperty("App::PropertyLinkList", "ProvidesBoundaries", ifc_attributes)
obj.addProperty("App::PropertyLinkList", "HostedElements", bem_category)
obj.addProperty("App::PropertyLink", "HostElement", bem_category)
obj.addProperty("App::PropertyLength", "Thickness", ifc_attributes)
ifc_walled_entities = {"IfcWall", "IfcSlab", "IfcRoof"}
for entity_class in ifc_walled_entities:
if ifc_entity.is_a(entity_class):
obj.Thickness = get_thickness(ifc_entity)
def make_bem_boundary(boundary, geo_type):
"""Stantard FreeCAD FeaturePython Object creation method"""
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "BEMBoundary")
# ViewProviderRelSpaceBoundary(obj.ViewObject)
BEMBoundary(obj, boundary)
setattr(boundary, geo_type, obj)
try:
obj.ViewObject.Proxy = 0
obj.ViewObject.ShapeColor = boundary.ViewObject.ShapeColor
except AttributeError:
FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui")
return obj
class BEMBoundary:
def __init__(self, obj, boundary):
self.Type = "BEMBoundary"
category_name = "BEM"
obj.addProperty("App::PropertyLink", "SourceBoundary", category_name)
obj.SourceBoundary = boundary
obj.addProperty("App::PropertyArea", "Area", category_name)
obj.addProperty("App::PropertyArea", "AreaWithHosted", category_name)
obj.Shape = boundary.Shape.copy()
obj.Area = obj.Shape.Area
obj.AreaWithHosted = self.recompute_area_with_hosted(obj)
self.set_label(obj)
@staticmethod
def recompute_area_with_hosted(obj):
"""Recompute area including inner boundaries"""
area = obj.Area
for boundary in obj.SourceBoundary.InnerBoundaries:
area = area + boundary.Area
return area
@staticmethod
def set_label(obj):
obj.Label = obj.SourceBoundary.Label
def create_project_from_entity(ifc_entity):
"""Stantard FreeCAD FeaturePython Object creation method"""
obj_name = "Project"
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name)
Project(obj, ifc_entity)
try:
obj.ViewObject.Proxy = 0
except AttributeError:
FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui")
return obj
class Project(Root):
"""Representation of an IfcProject:
https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcproject.htm"""
def __init__(self, obj, ifc_entity):
super().__init__(obj, ifc_entity)
ifc_attributes = "IFC Attributes"
obj.addProperty("App::PropertyString", "LongName", ifc_attributes)
obj.addProperty("App::PropertyVector", "TrueNorth", ifc_attributes)
obj.addProperty("App::PropertyVector", "WorldCoordinateSystem", ifc_attributes)
obj.LongName = ifc_entity.LongName
obj.TrueNorth = FreeCAD.Vector(
*ifc_entity.RepresentationContexts[0].TrueNorth.DirectionRatios
)
obj.WorldCoordinateSystem = FreeCAD.Vector(
ifc_entity.RepresentationContexts[
0
].WorldCoordinateSystem.Location.Coordinates
)
def create_space_from_entity(group_2nd, ifc_entity):
obj = group_2nd.newObject(
"App::DocumentObjectGroup", f"Space_{ifc_entity.Name}"
)
ifc_attributes = "IFC Attributes"
obj.addProperty("App::PropertyString", "IfcType", "IFC")
obj.addProperty("App::PropertyInteger", "Id", ifc_attributes)
obj.addProperty("App::PropertyString", "GlobalId", ifc_attributes)
obj.addProperty("App::PropertyString", "Description", ifc_attributes)
obj.Id = ifc_entity.id()
obj.GlobalId = ifc_entity.GlobalId
obj.IfcType = ifc_entity.is_a()
space_full_name = f"{ifc_entity.Name} {ifc_entity.LongName}"
obj.Label = space_full_name
try:
obj.Description = ifc_entity.Description
except TypeError:
pass
return obj
if __name__ == "__main__":
if os.name == 'nt':
TEST_FOLDER = r"C:\git\BIMxBEM\IfcTestFiles"
else:
TEST_FOLDER = "/home/cyril/git/BIMxBEM/IfcTestFiles/"
TEST_FILES = [
"Triangle_2x3_A22.ifc",
"Triangle_2x3_R19.ifc",
"2Storey_2x3_A22.ifc",
"2Storey_2x3_R19.ifc",
"0014_Vernier112D_ENE_ModèleÉnergétique_R20.ifc",
"Investigation_test_R19.ifc"
]
IFC_PATH = os.path.join(TEST_FOLDER, TEST_FILES[0])
DOC = FreeCAD.ActiveDocument
if DOC: # Remote debugging
import ptvsd
# Allow other computers to attach to ptvsd at this IP address and port.
ptvsd.enable_attach(address=("localhost", 5678), redirect_output=True)
# Pause the program until a remote debugger is attached
ptvsd.wait_for_attach()
# breakpoint()
display_boundaries(ifc_path=IFC_PATH, doc=DOC)
FreeCADGui.activeView().viewIsometric()
FreeCADGui.SendMsgToActiveView("ViewFit")
else:
FreeCADGui.showMainWindow()
DOC = FreeCAD.newDocument()
display_boundaries(ifc_path=IFC_PATH, doc=DOC)
FreeCADGui.activeView().viewIsometric()
FreeCADGui.SendMsgToActiveView("ViewFit")
FreeCADGui.exec_loop()
class box:
pass

Event Timeline