Compare commits

...

1 Commits

Author SHA1 Message Date
Petru Conduraru d2a08d37a1 Bonsai: draw element Axis reference lines in drawings
Bonsai already writes an Axis representation for walls (Plan/Axis/GRAPH_VIEW)
and for every profile based member such as beams and columns
(Model/Axis/GRAPH_VIEW), but CreateDrawing.get_linework_contexts only ever
collected Body, Facetation and Annotation subcontexts, so that centreline
geometry could never reach a drawing. Users were tracing centrelines by hand
as separate annotations for every element.

Collect the Axis subcontexts into a third linework bucket and, when the
drawing opts in via EPset_Drawing.HasAxisLinework, project them into the
linework SVG as their own groups classed "axis" alongside the element's usual
classes. Styling then goes through the existing CSS channel, with a
centreline dash pattern added to default.css.

The pass is off by default, so existing drawings are unchanged.

Generated with the assistance of an AI coding tool.
2026-07-20 12:35:10 +03:00
7 changed files with 92 additions and 1 deletions
@@ -43,6 +43,8 @@ path.flush { stroke: blue; stroke-width: 0.1; stroke-opacity: 0.4; }
*/
.surface {fill: white; stroke-width: 0.1;}
/* Reference lines from an element's Axis representation (issue #6423). */
.axis { fill: none; stroke: black; stroke-width: 0.13; stroke-dasharray: 12, 3, 3, 3; }
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
/* .IfcGeographicElement { fill: none; stroke: rgb(150, 150, 150); stroke-linecap: 'round'; stroke-dasharray: 1, 2;} */
@@ -104,6 +104,7 @@ class profile:
class LineworkContexts(NamedTuple):
body: list[list[int]]
annotation: list[list[int]]
axis: list[list[int]]
class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
@@ -618,8 +619,15 @@ class CreateDrawing(bpy.types.Operator):
model_annotation_target_contexts: list[int] = []
model_annotation_model_contexts: list[int] = []
axis_contexts: list[int] = []
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
if rep_context.is_a("IfcGeometricRepresentationSubContext"):
if rep_context.ContextIdentifier == "Axis":
# Axis geometry is a view independent reference line, so unlike
# body and annotation it isn't bucketed by target view.
axis_contexts.append(rep_context.id())
continue
if rep_context.ContextType == "Plan":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
@@ -676,7 +684,7 @@ class CreateDrawing(bpy.types.Operator):
]
)
return LineworkContexts(body_contexts, annotation_contexts)
return LineworkContexts(body_contexts, annotation_contexts, [axis_contexts] if axis_contexts else [])
def serialize_contexts_elements(
self,
@@ -723,6 +731,63 @@ class CreateDrawing(bpy.types.Operator):
tree.add_element(elem)
drawing_elements -= processed
def collect_axis_linework(
self,
ifc: ifcopenshell.file,
contexts: LineworkContexts,
drawing_elements: set[ifcopenshell.entity_instance],
link_matrix: Optional[Matrix] = None,
) -> None:
context_ids = [i for context in contexts.axis for i in context]
if not context_ids or not drawing_elements:
return
with profile("Processing axis context"):
geom_settings = ifcopenshell.geom.settings()
geom_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES)
geom_settings.set("context-ids", context_ids)
if link_matrix is not None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc)
t = link_matrix.to_translation()
geom_settings.set("model-offset", (t.x / unit_scale, t.y / unit_scale, t.z / unit_scale))
q = link_matrix.to_quaternion()
geom_settings.set("model-rotation", (q.x, q.y, q.z, q.w))
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=list(drawing_elements)
)
for shape in it:
edges = shape.geometry.edges
if not len(edges):
continue
element = ifc.by_id(shape.id)
self.axis_linework.append((element, list(shape.geometry.verts), list(edges)))
def generate_axis_linework(self, context: bpy.types.Context, root) -> None:
if not self.axis_linework:
return
camera_matrix_i = context.scene.camera.matrix_world.inverted()
group = root.find("{http://www.w3.org/2000/svg}g")
if group is None:
return
raw_width, raw_height = self.get_camera_dimensions()
x_offset = raw_width / 2
y_offset = raw_height / 2
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
for element, verts, edges in self.axis_linework:
g = etree.SubElement(group, "{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 ""
classes = self.get_svg_classes(element)
classes.append("axis")
g.set("class", " ".join(classes))
for i in range(0, len(edges), 2):
coords = []
for vi in (edges[i], edges[i + 1]):
co = camera_matrix_i @ Vector((verts[vi * 3], verts[vi * 3 + 1], verts[vi * 3 + 2]))
coords.append(((x_offset + co.x) * svg_scale, (y_offset - co.y) * svg_scale))
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = "M{},{} L{},{}".format(*coords[0], *coords[1])
def generate_bisect_linework(self, context: bpy.types.Context, root) -> None:
camera_matrix_i = context.scene.camera.matrix_world.inverted()
@@ -1048,6 +1113,11 @@ class CreateDrawing(bpy.types.Operator):
raycast_objs = set()
elements_with_faces = set()
# Reference lines an element already carries in its Axis representation, drawn
# as their own layer so they can be styled independently. See #6423.
self.axis_linework: list[tuple[ifcopenshell.entity_instance, list[float], list[int]]] = []
has_axis_linework = ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasAxisLinework")
for ifc_path, (ifc, link_matrix) in files.items():
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
@@ -1075,6 +1145,8 @@ class CreateDrawing(bpy.types.Operator):
self.serialize_contexts_elements(
ifc, tree, contexts, "annotation", drawing_elements, target_view, link_matrix
)
if has_axis_linework:
self.collect_axis_linework(ifc, contexts, drawing_elements, link_matrix)
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
with profile("Camera element"):
@@ -1137,6 +1209,10 @@ class CreateDrawing(bpy.types.Operator):
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
# After merge_linework_and_add_metadata, which would otherwise reclassify these
# groups as cut or projection linework.
self.generate_axis_linework(context, root)
if self.cprops.fill_mode == "SHAPELY":
# shapely variant
group = root.find("{http://www.w3.org/2000/svg}g")
@@ -536,6 +536,13 @@ class BIMCameraProperties(PropertyGroup):
default=True,
update=get_update_layer_callback("has_annotation", "HasAnnotation"),
)
has_axis_linework: BoolProperty(
name="Axis Linework",
description="Draw the reference lines held in each element's Axis representation, such as "
"wall and beam centrelines. Style them with the .axis CSS class",
default=False,
update=get_update_layer_callback("has_axis_linework", "HasAxisLinework"),
)
use_edge_classification: BoolProperty(
name="Use Edge Classification",
description="Classify projection edges into boundary/outline/sharp/crease/flush "
@@ -73,6 +73,8 @@ class BIM_PT_camera(Panel):
row = col.row(align=True)
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = col.row(align=True)
row.prop(props, "has_axis_linework", icon="MOD_SIMPLIFY")
# Drawing linked projects.
row = col.row(align=True)
+1
View File
@@ -331,6 +331,7 @@ def add_drawing(
"HasUnderlay": False,
"HasLinework": True,
"HasAnnotation": True,
"HasAxisLinework": False,
"GlobalReferencing": True,
"Stylesheet": drawing.get_default_drawing_resource_path("Stylesheet"),
"Markers": drawing.get_default_drawing_resource_path("Markers"),
+2
View File
@@ -1116,6 +1116,8 @@ class Drawing(bonsai.core.tool.Drawing):
camera_props.has_linework = bool(pset["HasLinework"])
if "HasAnnotation" in pset:
camera_props.has_annotation = bool(pset["HasAnnotation"])
if "HasAxisLinework" in pset:
camera_props.has_axis_linework = bool(pset["HasAxisLinework"])
if "IsNTS" in pset:
camera_props.is_nts = bool(pset["IsNTS"])
if "UseEdgeClassification" in pset:
+1
View File
@@ -374,6 +374,7 @@ class TestAddDrawing:
"HasUnderlay": False,
"HasLinework": True,
"HasAnnotation": True,
"HasAxisLinework": False,
"GlobalReferencing": True,
"Stylesheet": "stylesheet.css",
"Markers": "markers.svg",