From 71f761aa6c5a084837d565eeab71b7e55f9193c8 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Thu, 16 Jul 2026 19:03:37 +0300 Subject: [PATCH] Bonsai: preserve Z when editing FALL/slope annotation curves (#6557) Editing a FALL (or SLOPE_ANGLE/FRACTION/PERCENT) annotation's endpoint in item edit mode and tabbing out reverted the Z move: export_curves() always flattened curve points through auto_detect_curves()'s .to_2d(), even though a fall/slope annotation's whole meaning is the Z difference between its two points. That is fine for genuinely 2D profile/annotation curves, but wrong for these slope indicators. Add a preserve_z flag to export_curves()/auto_detect_curves() and set it only when the edited curve belongs to an IfcAnnotation whose predefined type is FALL/SLOPE_ANGLE/SLOPE_FRACTION/SLOPE_PERCENT, so the exported IfcIndexedPolyCurve/IfcPolyline keeps 3D points (IfcCartesianPointList3D) instead of being flattened to 2D. All other curve-like items (profiles, dimensions, leaders, etc.) are unaffected since preserve_z defaults to False. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/geometry/operator.py | 13 ++++++++- src/bonsai/bonsai/tool/model.py | 29 +++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 9bc0566532..e7e6bb8abb 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2671,7 +2671,18 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): tool.Geometry.import_item_attributes(obj) elif tool.Geometry.is_curvelike_item(item): ProfileDecorator.uninstall() - new = tool.Model.export_curves(obj) + # Fall / slope annotations encode their slope as a real Z difference + # between the curve's own points, so unlike ordinary 2D profile + # curves their Z must be preserved instead of being flattened. + preserve_z = False + if rep_obj and (rep_element := tool.Ifc.get_entity(rep_obj)) and rep_element.is_a("IfcAnnotation"): + preserve_z = ifcopenshell.util.element.get_predefined_type(rep_element) in ( + "FALL", + "SLOPE_ANGLE", + "SLOPE_FRACTION", + "SLOPE_PERCENT", + ) + new = tool.Model.export_curves(obj, preserve_z=preserve_z) if not new: diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index f54633c3cc..532df44499 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -289,13 +289,13 @@ class Model(bonsai.core.tool.Model): @classmethod def export_curves( - cls, obj: bpy.types.Object, position: Optional[Matrix] = None + cls, obj: bpy.types.Object, position: Optional[Matrix] = None, preserve_z: bool = False ) -> list[ifcopenshell.entity_instance] | None: if position is None: position = Matrix() results = [] - result = cls.auto_detect_curves(obj, obj.data, position) + result = cls.auto_detect_curves(obj, obj.data, position, preserve_z=preserve_z) if isinstance(result, dict) and result["curves"]: for curve in result["curves"]: results.append(tool.Ifc.get().add(curve)) @@ -2494,8 +2494,17 @@ class Model(bonsai.core.tool.Model): @classmethod def auto_detect_curves( - cls, obj: bpy.types.Object, mesh: bpy.types.Mesh, position: Matrix | None = None + cls, + obj: bpy.types.Object, + mesh: bpy.types.Mesh, + position: Matrix | None = None, + preserve_z: bool = False, ) -> Union[tuple, dict]: + """ + :param preserve_z: Keep each point's Z coordinate instead of flattening + to a 2D curve. Used for annotations whose geometry has a meaningful + Z difference between points, such as a FALL/slope indicator. + """ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if position is None: @@ -2651,18 +2660,20 @@ class Model(bonsai.core.tool.Model): segments[-1][0] = last_segment_indices curves.append(tmp.createIfcIndexedPolyCurve(points, segments)) elif tmp.schema == "IFC2X3": - points = [ - tmp.createIfcCartesianPoint(list(((position_i @ v.co) / unit_scale).to_2d())) - for v in loop_verts - ] + coords = [(position_i @ v.co) / unit_scale for v in loop_verts] + points = [tmp.createIfcCartesianPoint(list(co if preserve_z else co.to_2d())) for co in coords] if is_closed: points.append(points[0]) curves.append(tmp.createIfcPolyline(points)) else: # Pure straight polyline, no segments required - coord_list = [list(((position_i @ v.co) / unit_scale).to_2d()) for v in loop_verts] + coords = [(position_i @ v.co) / unit_scale for v in loop_verts] + coord_list = [list(co if preserve_z else co.to_2d()) for co in coords] if is_closed: coord_list.append(coord_list[0]) - points = tmp.createIfcCartesianPointList2D(coord_list) + if preserve_z: + points = tmp.createIfcCartesianPointList3D(coord_list) + else: + points = tmp.createIfcCartesianPointList2D(coord_list) curves.append(tmp.createIfcIndexedPolyCurve(points)) return {"ifc_file": tmp, "curves": curves}