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.
This commit is contained in:
Petru Conduraru
2026-07-16 19:03:37 +03:00
parent d9d1824886
commit 71f761aa6c
2 changed files with 32 additions and 10 deletions
@@ -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:
+20 -9
View File
@@ -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}