New recipe (and essay) to fix ArchiCAD IFC4 door swings showing up in Revit

This commit is contained in:
Dion Moult
2021-10-13 16:39:16 +11:00
parent 0c6a3eafd2
commit 1d74306bc2
@@ -0,0 +1,177 @@
import os
import math
import ifcopenshell
import ifcopenshell.util.schema
import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
# ArchiCAD has the ability to store 2D data with objects like doors for
# door swings. ArchiCAD's implementation is not 100% correct (using
# footprint instead of annotation contexts), but otherwise not too
# shabby.
#
# Revit, however, is incapable of understanding this 2D representation.
# Revit users linking in IFCs produced by ArchiCAD may experience the
# following symptoms:
#
# A. Invisible doors, and difficulty selecting doors
# B. Invisible door swings, or only visible at particular view ranges
# C. Weird arc shapes around doors
# D. Extra lines around doors and walls
# E. Cannot easily change visibility graphics of 2D vs 3D elements
# F. Cannot view 2D data in a 3D view
#
# This is caused by the perfect storm of Revit IFC bugs, which we will
# work through methodically.
# Revit has the ability to switch between 3D representations and 2D
# representations (e.g. in plan view). It does this by detecting IFC
# representations that belong to either the Model Body representation
# for 3D and the "Annotation" representation for 2D. Note: this is not a real
# "Annotation" representation. It is an invalid representation that is
# hardcoded in Revit. Nevertheless, if you follow their incorrect
# context naming convention by renaming ArchiCAD's also incorrect
# FootPrint context (but less incorrect) to Annotation, Revit will show
# 2D door representations as desired and switch between 2D and 3D
# intelligently.
#
# See bug https://github.com/Autodesk/revit-ifc/issues/187
#for context in self.file.by_type("IfcGeometricRepresentationSubContext"):
# if context.ContextIdentifier == "FootPrint":
# context.ContextIdentifier = None
# context.ContextType = "Annotation"
# context.ParentContext = [
# c
# for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
# if c.ContextType == "Plan"
# ][0]
# However, we will not use this approach (hence it is commented out).
# This is because Revit will generate a single object with both a
# magically switching 2D and 3D representation - you cannot control both
# representations independently. Furthermore, the 2D representation is
# always generated at a Z value of 0. This means that the door swings
# are always invisible on any view range apart from those typically near
# an elevation of 0 which won't work for most projects. Because they are
# merged into a single object, you cannot fix the 2D elevation without
# breaking your 3D elevation. Even if we split it into 2D and 3D
# objects, the 2D object can only be moved up in 3D or section, but
# Revit makes the object invisible in 3D or section, so go fish.
#
# See symptom B.
# Purge axis representations from walls. Revit will draw the axis (a
# line through the wall) but doesn't give you control over its
# visibilty, resulting in very ugly walls and lines typically cutting
# across door openings.
#
# See bug https://github.com/Autodesk/revit-ifc/issues/360
#
# This fixes symptom D.
for wall in self.file.by_type("IfcWall"):
reps = list(wall.Representation.Representations)
reps_excluding_axis = [r for r in reps if r.RepresentationIdentifier != "Axis"]
wall.Representation.Representations = reps_excluding_axis
# We're going to split the 3D door body and the 2D door swing into two
# objects. Before we do that, we're going to cull the 2D representation
# from door types. If we didn't, this scenario may occur:
#
# 1. 3D IfcDoors with IfcDoorType import as Revit doors
# 2. 2D IfcBuildingElementProxy with no type import as Revit generic model
# 3. The Revit user turns off the visibilty of the door category.
# 4. The user is shocked to see that generic models are now invisible,
# even though it is clearly a separate category to doors.
#
# This should be a warning to all Revit users that unintuitively
# visibility graphics actually do _not_ affect the categories (e.g.
# Door, Window) for instances. Instead, they target categories for
# representation maps types, even if an instance is untyped. Yikes!
#
# See bug: https://github.com/Autodesk/revit-ifc/issues/362
#
# This fixes symptom E.
for door in self.file.by_type("IfcDoorType"):
rep_maps = list(door.RepresentationMaps or [])
door.RepresentationMaps = [
r
for r in rep_maps
if r.MappedRepresentation
if r.MappedRepresentation.RepresentationIdentifier != "FootPrint"
]
# Let's now duplicate each door. The original door shall only retain its
# non 2D representations. The copied door will be changed into a
# "IfcDiscreteAccessory" so it can be filtered separately in Revit.
# Then, the copied door will only retain 2D representations. However,
# we're going to change the 2D representation context into a Model Body
# 3D context. This tricks Revit into converting the 2D data into 3D
# model line geometry (or something similar to that).
#
# See bug https://github.com/Autodesk/revit-ifc/issues/358
#
# This fixes symptom A, B, and F.
for door in self.file.by_type("IfcDoor"):
reps = list(door.Representation.Representations)
reps_excluding_footprint = [r for r in reps if r.RepresentationIdentifier != "FootPrint"]
footprint_reps = [r for r in reps if r.RepresentationIdentifier == "FootPrint"]
if not footprint_reps:
continue
door_copy = ifcopenshell.util.element.copy(self.file, door)
door_copy = ifcopenshell.util.schema.reassign_class(self.file, door_copy, "IfcDiscreteAccessory")
door_copy.Representation = ifcopenshell.util.element.copy(self.file, door.Representation)
door_copy.Representation.Representations = footprint_reps
door.Representation.Representations = reps_excluding_footprint
related_elements = list(door.ContainedInStructure[0].RelatedElements)
related_elements.append(door_copy)
door.ContainedInStructure[0].RelatedElements = related_elements
body_context = [
c for c in self.file.by_type("IfcGeometricRepresentationSubContext") if c.ContextIdentifier == "Body"
][0]
for subelement in self.file.traverse(footprint_reps[0]):
if not subelement.is_a("IfcShapeRepresentation"):
continue
subelement.ContextOfItems = body_context
subelement.RepresentationIdentifier = "Body"
subelement.RepresentationType = "Curve3D"
# Revit has bugs with handling arc index, which is relevant in IFC4. Our
# solution is to facet the arcs and swap out the polycurves with new
# faceted polycurves.
#
# See bug https://github.com/Autodesk/revit-ifc/issues/359
#
# This fixes symptom C.
settings = ifcopenshell.geom.settings()
settings.set(settings.INCLUDE_CURVES, True)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
for curve in self.file.by_type("IfcIndexedPolyCurve"):
if True in [s.is_a("IfcArcIndex") for s in curve.Segments]:
shape = ifcopenshell.geom.create_shape(settings, curve)
e = shape.edges
v = shape.verts
is_2d = curve.Points.is_a("IfcCartesianPointList2D")
if is_2d:
vertices = [[v[i] / unit_scale, v[i + 1] / unit_scale] for i in range(0, len(v), 3)]
else:
vertices = [
[v[i] / unit_scale, v[i + 1] / unit_scale, v[i + 2] / unit_scale] for i in range(0, len(v), 3)
]
edges = [[e[i] + 1, e[i + 1] + 1] for i in range(0, len(e), 2)]
points = []
curve.Points = f.create_entity(curve.Points.is_a(), vertices)
curve.Segments = [f.createIfcLineIndex(e) for e in edges]