mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-30 03:59:55 +00:00
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.
This commit is contained in:
@@ -43,6 +43,8 @@ path.flush { stroke: blue; stroke-width: 0.1; stroke-opacity: 0.4; }
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.surface {fill: white; stroke-width: 0.1;}
|
.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; }
|
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
|
||||||
.IfcAnnotation { 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;} */
|
/* .IfcGeographicElement { fill: none; stroke: rgb(150, 150, 150); stroke-linecap: 'round'; stroke-dasharray: 1, 2;} */
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class profile:
|
|||||||
class LineworkContexts(NamedTuple):
|
class LineworkContexts(NamedTuple):
|
||||||
body: list[list[int]]
|
body: list[list[int]]
|
||||||
annotation: list[list[int]]
|
annotation: list[list[int]]
|
||||||
|
axis: list[list[int]]
|
||||||
|
|
||||||
|
|
||||||
class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
|
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_target_contexts: list[int] = []
|
||||||
model_annotation_model_contexts: list[int] = []
|
model_annotation_model_contexts: list[int] = []
|
||||||
|
|
||||||
|
axis_contexts: list[int] = []
|
||||||
|
|
||||||
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
|
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
|
||||||
if rep_context.is_a("IfcGeometricRepresentationSubContext"):
|
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.ContextType == "Plan":
|
||||||
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
|
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
|
||||||
if rep_context.TargetView == target_view:
|
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(
|
def serialize_contexts_elements(
|
||||||
self,
|
self,
|
||||||
@@ -723,6 +731,63 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
tree.add_element(elem)
|
tree.add_element(elem)
|
||||||
drawing_elements -= processed
|
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:
|
def generate_bisect_linework(self, context: bpy.types.Context, root) -> None:
|
||||||
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
||||||
|
|
||||||
@@ -1048,6 +1113,11 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
raycast_objs = set()
|
raycast_objs = set()
|
||||||
elements_with_faces = 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():
|
for ifc_path, (ifc, link_matrix) in files.items():
|
||||||
# Don't use draw.main() just whilst we're prototyping and experimenting
|
# Don't use draw.main() just whilst we're prototyping and experimenting
|
||||||
# TODO: hash paths are never used
|
# TODO: hash paths are never used
|
||||||
@@ -1075,6 +1145,8 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
self.serialize_contexts_elements(
|
self.serialize_contexts_elements(
|
||||||
ifc, tree, contexts, "annotation", drawing_elements, target_view, link_matrix
|
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:
|
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
|
||||||
with profile("Camera element"):
|
with profile("Camera element"):
|
||||||
@@ -1137,6 +1209,10 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
self.merge_linework_and_add_metadata(root)
|
self.merge_linework_and_add_metadata(root)
|
||||||
self.move_elements_to_top(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":
|
if self.cprops.fill_mode == "SHAPELY":
|
||||||
# shapely variant
|
# shapely variant
|
||||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||||
|
|||||||
@@ -536,6 +536,13 @@ class BIMCameraProperties(PropertyGroup):
|
|||||||
default=True,
|
default=True,
|
||||||
update=get_update_layer_callback("has_annotation", "HasAnnotation"),
|
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(
|
use_edge_classification: BoolProperty(
|
||||||
name="Use Edge Classification",
|
name="Use Edge Classification",
|
||||||
description="Classify projection edges into boundary/outline/sharp/crease/flush "
|
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 = col.row(align=True)
|
||||||
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
|
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
|
||||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
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.
|
# Drawing linked projects.
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ def add_drawing(
|
|||||||
"HasUnderlay": False,
|
"HasUnderlay": False,
|
||||||
"HasLinework": True,
|
"HasLinework": True,
|
||||||
"HasAnnotation": True,
|
"HasAnnotation": True,
|
||||||
|
"HasAxisLinework": False,
|
||||||
"GlobalReferencing": True,
|
"GlobalReferencing": True,
|
||||||
"Stylesheet": drawing.get_default_drawing_resource_path("Stylesheet"),
|
"Stylesheet": drawing.get_default_drawing_resource_path("Stylesheet"),
|
||||||
"Markers": drawing.get_default_drawing_resource_path("Markers"),
|
"Markers": drawing.get_default_drawing_resource_path("Markers"),
|
||||||
|
|||||||
@@ -1116,6 +1116,8 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
camera_props.has_linework = bool(pset["HasLinework"])
|
camera_props.has_linework = bool(pset["HasLinework"])
|
||||||
if "HasAnnotation" in pset:
|
if "HasAnnotation" in pset:
|
||||||
camera_props.has_annotation = bool(pset["HasAnnotation"])
|
camera_props.has_annotation = bool(pset["HasAnnotation"])
|
||||||
|
if "HasAxisLinework" in pset:
|
||||||
|
camera_props.has_axis_linework = bool(pset["HasAxisLinework"])
|
||||||
if "IsNTS" in pset:
|
if "IsNTS" in pset:
|
||||||
camera_props.is_nts = bool(pset["IsNTS"])
|
camera_props.is_nts = bool(pset["IsNTS"])
|
||||||
if "UseEdgeClassification" in pset:
|
if "UseEdgeClassification" in pset:
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ class TestAddDrawing:
|
|||||||
"HasUnderlay": False,
|
"HasUnderlay": False,
|
||||||
"HasLinework": True,
|
"HasLinework": True,
|
||||||
"HasAnnotation": True,
|
"HasAnnotation": True,
|
||||||
|
"HasAxisLinework": False,
|
||||||
"GlobalReferencing": True,
|
"GlobalReferencing": True,
|
||||||
"Stylesheet": "stylesheet.css",
|
"Stylesheet": "stylesheet.css",
|
||||||
"Markers": "markers.svg",
|
"Markers": "markers.svg",
|
||||||
|
|||||||
Reference in New Issue
Block a user