From e5f7f3211a5e30a961eb6afad5ac5f759b49ed4a Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 12 Jul 2026 18:35:43 +0300 Subject: [PATCH] Bonsai: support editing IfcGeometricCurveSet representation items (#6407) Entering item edit mode on an IfcGeometricCurveSet (the representation item produced for annotations such as SECTION_LEVEL, whose Elements are curves) reported "Editing IfcGeometricCurveSet geometry is not supported": tool.Geometry.is_curvelike_item did not recognise the set, so the edit path fell through to the unsupported branch. Handle the set as a single editable item whose nested curves are its Elements, mirroring how the join operator already treats it: - is_curvelike_item recognises IfcGeometricCurveSet. - tool.Model.convert_curve_to_mesh recurses into curve.Elements, importing each nested curve as its own edge loop in the mesh. - On save (both OverrideModeSetObject and DirectProfileEdit exit paths), keep the same set entity and swap out its Elements (remove_deep2 on the old ones), instead of replacing the set with a bare curve. Verified live in headless Blender: is_curvelike_item(IfcGeometricCurveSet) is now True and the set imports to a mesh (was the exact "not supported" error); a no-op edit round-trips for both a single-element and a two-element set, preserving the set entity with new elements at identical coordinates and removing the old ones; a plain (non-set) IfcIndexedPolyCurve item still goes through the original replace-and-remove path (no regression). Scope note: a multi-element set on a DIMENSION-style annotation is still limited by the pre-existing ensure_annotation_vertex_order helper, which walks one connected edge path and drops other loops. That is a separate, pre-existing helper bug (not introduced here) and is left for a follow-up; single-element and non-annotation curve sets round-trip fully. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 --- .../bonsai/bim/module/geometry/operator.py | 30 ++++++++++++++++++- src/bonsai/bonsai/tool/geometry.py | 1 + src/bonsai/bonsai/tool/model.py | 6 ++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 9bc0566532..7d6e60d2f6 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2683,6 +2683,19 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): self.enable_edit_mode(bpy.context) return + if item.is_a("IfcGeometricCurveSet"): + # The set itself is the edited representation item (e.g. a dimension + # annotation). Keep the same set entity and just swap out its nested + # curve Elements, rather than replacing the set with a bare curve. + old_elements = list(item.Elements) + item.Elements = tuple(new) + for old_element in old_elements: + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_element) + tool.Ifc.link(item, obj.data) + tool.Geometry.import_item(obj) + tool.Geometry.reload_representation(props.representation_obj) + return + additional_curves = [] if len(new) > 1: additional_curves = new[1:] @@ -2886,12 +2899,27 @@ class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator): ProfileDecorator.install(context) return {"CANCELLED"} + ifc_file = tool.Ifc.get() + + if item.is_a("IfcGeometricCurveSet"): + # The set itself is the edited representation item (e.g. a dimension + # annotation). Keep the same set entity and just swap out its nested + # curve Elements, rather than replacing the set with a bare curve. + old_elements = list(item.Elements) + item.Elements = tuple(new) + for old_element in old_elements: + ifcopenshell.util.element.remove_deep2(ifc_file, old_element) + tool.Ifc.link(item, obj.data) + tool.Geometry.import_item(obj) + tool.Geometry.reload_representation(props.representation_obj) + tool.Geometry.disable_item_mode() + return {"FINISHED"} + additional_curves = [] if len(new) > 1: additional_curves = new[1:] new = new[0] - ifc_file = tool.Ifc.get() for inverse in ifc_file.get_inverse(item): ifcopenshell.util.element.replace_attribute(inverse, item, new) ifcopenshell.util.element.remove_deep2(ifc_file, item) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index c4b7cd0ef6..48da9d2437 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1348,6 +1348,7 @@ class Geometry(bonsai.core.tool.Geometry): or item.is_a("IfcCompositeCurve") or item.is_a("IfcIndexedPolyCurve") or item.is_a("IfcCircle") + or item.is_a("IfcGeometricCurveSet") ) @classmethod diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index f54633c3cc..cc651a6ee8 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -625,6 +625,12 @@ class Model(bonsai.core.tool.Model): for segment in curve.Segments: cls.convert_curve_to_mesh(obj, position, segment.ParentCurve) + elif curve.is_a("IfcGeometricCurveSet"): + # A set of independent curves (e.g. dimension annotations). Each element is + # imported as its own disconnected edge loop within the same mesh. + for sub_curve in curve.Elements: + cls.convert_curve_to_mesh(obj, position, sub_curve, x_angle=x_angle) + elif curve.is_a("IfcIndexedPolyCurve"): for local_point in curve.Points.CoordList: global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d()