Show IfcSpace in projection drawings

Previously IfcSpace was excluded from get_drawing_elements() and
handled via a separate special-case loop. This removes that special
case: spaces are now included in the standard element set and rendered
in generate_bisect_linework() alongside other elements.

For spaces not intersecting the cut plane (projection), visible edges
are projected using back-face culling and a crease-angle threshold to
filter triangulation edges, matching the HLR output for regular
elements.

CSS rules for .IfcSpace.projection are added to default.css and
sample.css to override the existing fill:none rule.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Ryan Schultz
2026-03-26 22:27:45 -05:00
parent 256d5a63f1
commit f5c22d97e9
4 changed files with 72 additions and 68 deletions
@@ -78,6 +78,7 @@ text.GRID, tspan.GRID { /* 5mm */ font-size: 8.25px; }
.material-sand { fill: url(#sand); }
.material-concrete { fill: url(#concrete); stroke-width: 0.5; }
.IfcSpace { fill: none; stroke: none; }
.IfcSpace.projection { fill: white; stroke: black; stroke-linecap: round; stroke-width: 0.25; }
.PredefinedType-STUD { stroke: black; stroke-width: 1; }
.PredefinedType-WOOD { fill: url(#wood); stroke: black; stroke-width: 0.5; }
.PredefinedType-STEEL { fill: url(#steel); stroke: black; stroke-width: 0.5; }
@@ -28,3 +28,4 @@
.break { fill: white; }
.breakline { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; marker-mid: url(#breakline-marker); }
.IfcSpace { fill: none; stroke: none; }
.IfcSpace.projection { fill: white; stroke: black; stroke-linecap: round; stroke-width: 0.25; }
@@ -653,26 +653,69 @@ class CreateDrawing(bpy.types.Operator):
continue
if not (element := tool.Ifc.get_entity(obj)):
continue
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
continue
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
if tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
lines = []
for edge in edges:
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
coords = [start, end]
d = " ".join(
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
)
d = "M{}".format(d[1:])
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = d
group.append(g)
for edge in edges:
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
coords = [start, end]
d = " ".join(
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
)
d = "M{}".format(d[1:])
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = d
group.append(g)
elif element.is_a("IfcSpatialElement"):
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.transform(obj.matrix_world)
bm.normal_update()
camera_direction = context.scene.camera.matrix_world.to_3x3() @ Vector((0, 0, -1))
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
g.set("class", "projection")
edge_count = 0
for edge in bm.edges:
link_faces = edge.link_faces
if len(link_faces) == 1:
should_draw = True
elif len(link_faces) == 2:
f0_front = link_faces[0].normal.dot(camera_direction) < 0
f1_front = link_faces[1].normal.dot(camera_direction) < 0
if f0_front != f1_front:
should_draw = True # silhouette edge
elif f0_front and f1_front:
# both front-facing: only draw real creases, skip flat triangulation edges
should_draw = link_faces[0].normal.dot(link_faces[1].normal) < 0.99
else:
should_draw = False # both back-facing
else:
should_draw = False
if not should_draw:
continue
p0 = camera_matrix_i @ edge.verts[0].co
p1 = camera_matrix_i @ edge.verts[1].co
d = "M{},{} L{},{}".format(
(x_offset + p0[0]) * svg_scale,
(y_offset - p0[1]) * svg_scale,
(x_offset + p1[0]) * svg_scale,
(y_offset - p1[1]) * svg_scale,
)
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = d
edge_count += 1
bm.free()
if edge_count:
group.insert(0, g)
else:
root.remove(g)
def generate_material_layers(self, context: bpy.types.Context, root) -> None:
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
@@ -1002,10 +1045,9 @@ class CreateDrawing(bpy.types.Operator):
group = root.find("{http://www.w3.org/2000/svg}g")
if group is None:
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
return svg_path
group = etree.Element("{http://www.w3.org/2000/svg}g")
group.tail = "\n"
root.insert(0, group)
# Add target_view and scale classes to the parent group from IFC data
if group is not None:
@@ -1257,49 +1299,6 @@ class CreateDrawing(bpy.types.Operator):
results = results.encode("ascii", "xmlcharrefreplace")
root = etree.fromstring(results)
# Spaces are handled as a special case, since they are often overlayed
# in addition to elements. For example, a space should not obscure
# other elements in projection. Spaces should also not override cut
# elements but instead be drawn in addition to cut elements.
spaces = tool.Drawing.get_drawing_spaces(self.camera_element)
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
x_offset = self.svg_writer.raw_width / 2
y_offset = self.svg_writer.raw_height / 2
for space in spaces:
obj = tool.Ifc.get_object(space)
if not obj or not tool.Drawing.is_intersecting_camera(obj, self.camera):
continue
verts, edges = tool.Drawing.bisect_mesh(obj, self.camera)
verts = [self.svg_writer.project_point_onto_camera(Vector(v)) for v in verts]
line_strings = [
shapely.LineString(
[
(
(x_offset + verts[e[0]][0]) * self.svg_writer.svg_scale,
(y_offset - verts[e[0]][1]) * self.svg_writer.svg_scale,
),
(
(x_offset + verts[e[1]][0]) * self.svg_writer.svg_scale,
(y_offset - verts[e[1]][1]) * self.svg_writer.svg_scale,
),
]
)
for e in edges
]
closed_polygons = shapely.polygonize(line_strings)
for polygon in closed_polygons.geoms:
classes = self.get_svg_classes(space)
path = etree.Element("path")
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
for interior in polygon.interiors:
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
path.attrib["d"] = d
path.set("class", " ".join(list(classes)))
group.append(path)
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
+6 -3
View File
@@ -2342,12 +2342,15 @@ class Drawing(bonsai.core.tool.Drawing):
elements = camera_view_elements
else:
# This can probably be smarter
elements = set(ifc_file.by_type("IfcElement"))
if ifc_file.schema == "IFC2X3":
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
else:
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
if ifc_file.schema == "IFC2X3":
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
else:
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
elements = elements & base_elements
updated_set = set()
for i in elements:
@@ -2543,7 +2546,7 @@ class Drawing(bonsai.core.tool.Drawing):
assert drawing and isinstance(camera.data, bpy.types.Camera)
cls.import_annotations_in_group(cls.get_drawing_group(drawing))
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
filtered_elements = cls.get_drawing_elements(drawing)
filtered_elements.add(drawing)
target_view = cls.get_drawing_target_view(drawing)